diff --git a/.bazelignore b/.bazelignore index 6670bec3..cee9ff57 100644 --- a/.bazelignore +++ b/.bazelignore @@ -1,7 +1,15 @@ commons-libs user-documentation -yaku-apps-python yaku-apps-typescript +yaku-apps-python/packages/autopilot-utils/tests-pex +yaku-apps-python/packages/autopilot-utils/tests/app_chained_multi_evaluator +yaku-apps-python/packages/autopilot-utils/tests/app_multi_command +yaku-apps-python/packages/autopilot-utils/tests/app_multi_evaluator +yaku-apps-python/packages/autopilot-utils/tests/app_single_command +yaku-apps-python/packages/autopilot-utils/tests/app_single_evaluator +yaku-apps-python/packages/autopilot-utils/tests/app_subprocess_steps +yaku-apps-python/packages/autopilot-utils/tests/app_super_simple_evaluator +yaku-apps-python/packages/autopilot-utils/tests/demo_app qg-api-service/qg-api-service/node_modules qg-api-service/node_modules node_modules \ No newline at end of file diff --git a/.bazelrc b/.bazelrc index bdc3acf7..280e47d7 100644 --- a/.bazelrc +++ b/.bazelrc @@ -1 +1,14 @@ -common --@aspect_rules_ts//ts:skipLibCheck=always \ No newline at end of file +common --@aspect_rules_ts//ts:skipLibCheck=always +build:ci --build_metadata=ROLE=CI +build:ci --bes_results_url=https://app.buildbuddy.io/invocation/ +build:ci --bes_backend=grpcs://remote.buildbuddy.io +build:ci --remote_cache=grpcs://yaku.buildbuddy.io +build:ci --remote_timeout=3600 +build:ci --experimental_remote_cache_compression +build:ci --experimental_remote_cache_compression_threshold=100 +build:ci --nolegacy_important_outputs +build:ci --remote_executor=grpcs://yaku.buildbuddy.io +build:ci --jobs=50 +build:ci --noslim_profile +build:ci --experimental_profile_include_target_label +build:ci --experimental_profile_include_primary_output \ No newline at end of file diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml index 1c7c3cdd..3d848fd3 100644 --- a/.github/workflows/build-all.yml +++ b/.github/workflows/build-all.yml @@ -10,6 +10,10 @@ on: env: REGISTRY: ghcr.io +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest @@ -26,11 +30,19 @@ jobs: disk-cache: ${{ github.workflow }} repository-cache: true - - name: Build with Bazel - run: bazel build //... + - name: Build + run: | + bazel build \ + --config=ci \ + --remote_header=x-buildbuddy-api-key=${{ secrets.BUILDBUDDY_ORG_API_KEY }} \ + //... - - name: Test with Bazel - run: bazel test //... + - name: Test + run: | + bazel test \ + --config=ci \ + --remote_header=x-buildbuddy-api-key=${{ secrets.BUILDBUDDY_ORG_API_KEY }} \ + //... #### ## only on push to main diff --git a/.gitignore b/.gitignore index 7a60bed5..b7686fd0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /.idea/ bazel-* node_modules +htmlcov/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..28fac4fe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,25 @@ +{ + "cSpell.words": [ + "bazel", + "genhtml", + "htmlcov", + "lcov", + "papsr", + "yaku" + ], + "coverage-gutters.coverageFileNames": [ + "bazel-out/_coverage/_coverage_report.dat" + ], + "coverage-gutters.ignoredPathGlobs": "**/{node_modules,venv,.venv,vendor,bazel-bin,bazel-out,bazel-testlogs}/**", + "[python]": { + "diffEditor.ignoreTrimWhitespace": false, + "gitlens.codeLens.symbolScopes": [ + "!Module" + ], + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnType": true, + "editor.formatOnSave": false, + "editor.wordBasedSuggestions": "off" + }, + "python.defaultInterpreterPath": "yaku-apps-python/3rdparty/.venv/bin/python" +} diff --git a/MODULE.bazel b/MODULE.bazel index f51ad3be..6ccd79a4 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -20,11 +20,36 @@ go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps") go_deps.from_file(go_mod = "//onyx:go.mod") use_repo(go_deps, "com_github_azure_azure_sdk_for_go_sdk_azcore", "com_github_azure_azure_sdk_for_go_sdk_azidentity", "com_github_azure_azure_sdk_for_go_sdk_storage_azblob", "com_github_chigopher_pathlib", "com_github_invopop_jsonschema", "com_github_invopop_yaml", "com_github_netflix_go_iomux", "com_github_pkg_errors", "com_github_spf13_afero", "com_github_spf13_cobra", "com_github_spf13_viper", "com_github_stretchr_testify", "com_github_xeipuuv_gojsonschema", "in_gopkg_natefinch_lumberjack_v2", "in_gopkg_yaml_v3", "org_uber_go_zap") +### +# Python +### + +PYTHON_VERSION = "3.11" + +# Update the version to the release found here: +# https://github.com/bazelbuild/rules_python/releases. +bazel_dep(name = "rules_python", dev_dependency = True, version = "0.36.0") + +bazel_dep(name = "aspect_rules_py", version = "1.0.0-rc0") + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain(python_version = PYTHON_VERSION, configure_coverage_tool = True) + +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") + +pip.parse( + hub_name = "pip", + python_version = PYTHON_VERSION, + requirements_lock = "//yaku-apps-python:3rdparty/requirements_lock.txt", +) + +use_repo(pip, "pip") + ### # core image ### -bazel_dep(name = "aspect_bazel_lib", version = "2.8.0") +bazel_dep(name = "aspect_bazel_lib", version = "2.9.1") bazel_dep(name = "rules_oci", version = "2.0.1") bazel_dep(name = "rules_pkg", version = "0.10.1") bazel_dep(name = "container_structure_test", version = "1.15.0") diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 75d7ca77..52156adc 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -4,18 +4,22 @@ "https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497", "https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2", "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589", - "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/source.json": "7e3a9adf473e9af076ae485ed649d5641ad50ec5c11718103f34de03170d94ad", + "https://bcr.bazel.build/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0", + "https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/MODULE.bazel": "1c8cec495288dccd14fdae6e3f95f772c1c91857047a098fad772034264cc8cb", + "https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/source.json": "14892cc698e02ffedf4967546e6bedb7245015906888d3465fcf27c90a26da10", "https://bcr.bazel.build/modules/apple_support/1.5.0/MODULE.bazel": "50341a62efbc483e8a2a6aec30994a58749bd7b885e18dd96aa8c33031e558ef", "https://bcr.bazel.build/modules/apple_support/1.5.0/source.json": "eb98a7627c0bc486b57f598ad8da50f6625d974c8f723e9ea71bd39f709c9862", "https://bcr.bazel.build/modules/aspect_bazel_lib/1.28.0/MODULE.bazel": "d793416e81c34d137d75ef84fe622df6c550826772a7f06e3b98a0d1c347fe1c", "https://bcr.bazel.build/modules/aspect_bazel_lib/1.36.0/MODULE.bazel": "710d3560d8891d209f7985f3e4223011c3fefed0cd4d23d3e7b77b0f8287ef64", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.7.2/MODULE.bazel": "780d1a6522b28f5edb7ea09630748720721dfe27690d65a2d33aa7509de77e07", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.7.7/MODULE.bazel": "491f8681205e31bb57892d67442ce448cda4f472a8e6b3dc062865e29a64f89c", - "https://bcr.bazel.build/modules/aspect_bazel_lib/2.8.0/MODULE.bazel": "d1170d33a4f6117ecd61aa7574d3d1f3f1a33ac0e2c1573b6bffa5a51c3753fc", - "https://bcr.bazel.build/modules/aspect_bazel_lib/2.8.0/source.json": "1562c277dfa738c29f6bc254228cc5689104a8b76b7c670ad4846066243322a1", + "https://bcr.bazel.build/modules/aspect_bazel_lib/2.9.1/MODULE.bazel": "39517c00a97118e7924786cd9b6fde80016386dee741d40fd9497b80d93c9b54", + "https://bcr.bazel.build/modules/aspect_bazel_lib/2.9.1/source.json": "5c98d61e7ed023a391c83e22e0a1c3576b84de57e75403f47fabc3ff62d05db4", "https://bcr.bazel.build/modules/aspect_rules_js/2.0.0/MODULE.bazel": "b45b507574aa60a92796e3e13c195cd5744b3b8aff516a9c0cb5ae6a048161c5", "https://bcr.bazel.build/modules/aspect_rules_js/2.1.0/MODULE.bazel": "f747a24e13bc3c35c712580fc4e30186c54d80d21997b9503e29705e4d864533", "https://bcr.bazel.build/modules/aspect_rules_js/2.1.0/source.json": "3a43843c6bd0ac65d118e72f504ff553a1ba1e65fec91938e5546effb4245104", + "https://bcr.bazel.build/modules/aspect_rules_py/1.0.0-rc0/MODULE.bazel": "32d7c3fc7dea1aef4a95475e91a9cdd79473c4bd6a198906a306bea2fbc4e601", + "https://bcr.bazel.build/modules/aspect_rules_py/1.0.0-rc0/source.json": "b8eaced4b9ab418f3498d9657f98c980e58f6fad397fc17081955dcf3813be3f", "https://bcr.bazel.build/modules/aspect_rules_swc/2.0.1/MODULE.bazel": "35e0c95698ecb59454ea2ca0de5dd4550afe23aaa0853c313024651ce3f1464e", "https://bcr.bazel.build/modules/aspect_rules_swc/2.0.1/source.json": "f52f91ab97db89b1e60067585104be37f05fa6f7c999e0156a1e6151d17c49b3", "https://bcr.bazel.build/modules/aspect_rules_ts/3.2.1/MODULE.bazel": "400959569a0755546d693aa5d05b7db7046ea697297afff6adea7155c34fe116", @@ -48,7 +52,8 @@ "https://bcr.bazel.build/modules/gazelle/0.37.0/MODULE.bazel": "d1327ba0907d0275ed5103bfbbb13518f6c04955b402213319d0d6c0ce9839d4", "https://bcr.bazel.build/modules/gazelle/0.37.0/source.json": "b3adc10e2394e7f63ea88fb1d622d4894bfe9ec6961c493ae9a887723ab16831", "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4", - "https://bcr.bazel.build/modules/googletest/1.11.0/source.json": "c73d9ef4268c91bd0c1cd88f1f9dfa08e814b1dbe89b5f594a9f08ba0244d206", + "https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f", + "https://bcr.bazel.build/modules/googletest/1.14.0/source.json": "2478949479000fdd7de9a3d0107ba2c85bb5f961c3ecb1aa448f52549ce310b5", "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", "https://bcr.bazel.build/modules/platforms/0.0.10/source.json": "f22828ff4cf021a6b577f1bf6341cb9dcd7965092a439f64fc1bb3b7a5ae4bd5", "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee", @@ -58,12 +63,15 @@ "https://bcr.bazel.build/modules/platforms/0.0.8/MODULE.bazel": "9f142c03e348f6d263719f5074b21ef3adf0b139ee4c5133e2aa35664da9eb2d", "https://bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel": "4a87a60c927b56ddd67db50c89acaa62f4ce2a1d2149ccb63ffd871d5ce29ebc", "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7", - "https://bcr.bazel.build/modules/protobuf/21.7/source.json": "bbe500720421e582ff2d18b0802464205138c06056f443184de39fbb8187b09b", + "https://bcr.bazel.build/modules/protobuf/23.1/MODULE.bazel": "88b393b3eb4101d18129e5db51847cd40a5517a53e81216144a8c32dfeeca52a", + "https://bcr.bazel.build/modules/protobuf/24.4/MODULE.bazel": "7bc7ce5f2abf36b3b7b7c8218d3acdebb9426aeb35c2257c96445756f970eb12", + "https://bcr.bazel.build/modules/protobuf/24.4/source.json": "ace4b8c65d4cfe64efe544f09fc5e5df77faf3a67fbb29c5341e0d755d9b15d6", "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0", "https://bcr.bazel.build/modules/protobuf/3.19.2/MODULE.bazel": "532ffe5f2186b69fdde039efe6df13ba726ff338c6bc82275ad433013fa10573", "https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858", "https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647", "https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c", + "https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f", "https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e", "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5", "https://bcr.bazel.build/modules/rules_cc/0.0.9/source.json": "1f1ba6fea244b616de4a554a0f4983c91a9301640c8fe0dd1d410254115c8430", @@ -78,9 +86,11 @@ "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", "https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86", "https://bcr.bazel.build/modules/rules_java/6.3.0/MODULE.bazel": "a97c7678c19f236a956ad260d59c86e10a463badb7eb2eda787490f4c969b963", + "https://bcr.bazel.build/modules/rules_java/7.1.0/MODULE.bazel": "30d9135a2b6561c761bd67bd4990da591e6bdc128790ce3e7afd6a3558b2fb64", "https://bcr.bazel.build/modules/rules_java/7.6.5/MODULE.bazel": "481164be5e02e4cab6e77a36927683263be56b7e36fef918b458d7a8a1ebadb1", "https://bcr.bazel.build/modules/rules_java/7.6.5/source.json": "a805b889531d1690e3c72a7a7e47a870d00323186a9904b36af83aa3d053ee8d", "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7", + "https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909", "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036", "https://bcr.bazel.build/modules/rules_jvm_external/5.2/source.json": "10572111995bc349ce31c78f74b3c147f6b3233975c7fa5eff9211f6db0d34d9", "https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0", @@ -99,12 +109,15 @@ "https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc", "https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06", "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7", + "https://bcr.bazel.build/modules/rules_proto/6.0.0-rc1/MODULE.bazel": "1e5b502e2e1a9e825eef74476a5a1ee524a92297085015a052510b09a1a09483", "https://bcr.bazel.build/modules/rules_proto/6.0.0/MODULE.bazel": "b531d7f09f58dce456cd61b4579ce8c86b38544da75184eadaf0a7cb7966453f", "https://bcr.bazel.build/modules/rules_proto/6.0.0/source.json": "de77e10ff0ab16acbf54e6b46eecd37a99c5b290468ea1aee6e95eb1affdaed7", "https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f", "https://bcr.bazel.build/modules/rules_python/0.22.1/MODULE.bazel": "26114f0c0b5e93018c0c066d6673f1a2c3737c7e90af95eff30cfee38d0bbac7", "https://bcr.bazel.build/modules/rules_python/0.24.0/MODULE.bazel": "4bff7f583653d0762cda21303da0643cc4c545ddfd9593337f18dad8d1787801", - "https://bcr.bazel.build/modules/rules_python/0.24.0/source.json": "c4bbbd6350883cfc0f4a805577ab94ed94c2f5dc588e84cf1851137c121d3887", + "https://bcr.bazel.build/modules/rules_python/0.29.0/MODULE.bazel": "2ac8cd70524b4b9ec49a0b8284c79e4cd86199296f82f6e0d5da3f783d660c82", + "https://bcr.bazel.build/modules/rules_python/0.36.0/MODULE.bazel": "a4ce1ccea92b9106c7d16ab9ee51c6183107e78ba4a37aa65055227b80cd480c", + "https://bcr.bazel.build/modules/rules_python/0.36.0/source.json": "b79cbb7b2ae1751949e2f6ee6692822e4ffd13ca1e959ce99abec4ac7666162a", "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c", "https://bcr.bazel.build/modules/stardoc/0.5.0/MODULE.bazel": "f9f1f46ba8d9c3362648eea571c6f9100680efc44913618811b58cc9c02cd678", "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8", @@ -113,7 +126,8 @@ "https://bcr.bazel.build/modules/stardoc/0.6.2/MODULE.bazel": "7060193196395f5dd668eda046ccbeacebfd98efc77fed418dbe2b82ffaa39fd", "https://bcr.bazel.build/modules/stardoc/0.6.2/source.json": "d2ff8063b63b4a85e65fe595c4290f99717434fa9f95b4748a79a7d04dfed349", "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", - "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/source.json": "f1ef7d3f9e0e26d4b23d1c39b5f5de71f584dd7d1b4ef83d9bbba6ec7a6a6459", + "https://bcr.bazel.build/modules/upb/0.0.0-20230516-61a97ef/MODULE.bazel": "c0df5e35ad55e264160417fd0875932ee3c9dda63d9fccace35ac62f45e1b6f9", + "https://bcr.bazel.build/modules/upb/0.0.0-20230516-61a97ef/source.json": "b2150404947339e8b947c6b16baa39fa75657f4ddec5e37272c7b11c7ab533bc", "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", "https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27", "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/MODULE.bazel": "af322bc08976524477c79d1e45e241b6efbeb918c497e8840b8ab116802dda79", @@ -151,8 +165,8 @@ }, "@@aspect_bazel_lib~//lib:extensions.bzl%toolchains": { "general": { - "bzlTransitiveDigest": "KYaT/FNBG41uHyRAAMvsl7AEIPayXS/gRWj1Z8snO4Y=", - "usagesDigest": "Vc4HVm494HVPPxpa8vKVkaMbSpm/i/ABYnDJf6xoIWQ=", + "bzlTransitiveDigest": "1PFvPNwXRu/vBjE9ZDyTb7kmOkzG1Hj+xn9cZSab2ec=", + "usagesDigest": "ByXw24Zh8VVLsxRU3+zFnLcZFtqSueJ2QLnjepH0dC4=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, @@ -380,7 +394,7 @@ "ruleClassName": "coreutils_platform_repo", "attributes": { "platform": "darwin_amd64", - "version": "0.0.26" + "version": "0.0.27" } }, "coreutils_darwin_arm64": { @@ -388,7 +402,7 @@ "ruleClassName": "coreutils_platform_repo", "attributes": { "platform": "darwin_arm64", - "version": "0.0.26" + "version": "0.0.27" } }, "coreutils_linux_amd64": { @@ -396,7 +410,7 @@ "ruleClassName": "coreutils_platform_repo", "attributes": { "platform": "linux_amd64", - "version": "0.0.26" + "version": "0.0.27" } }, "coreutils_linux_arm64": { @@ -404,7 +418,7 @@ "ruleClassName": "coreutils_platform_repo", "attributes": { "platform": "linux_arm64", - "version": "0.0.26" + "version": "0.0.27" } }, "coreutils_windows_amd64": { @@ -412,7 +426,7 @@ "ruleClassName": "coreutils_platform_repo", "attributes": { "platform": "windows_amd64", - "version": "0.0.26" + "version": "0.0.27" } }, "coreutils_toolchains": { @@ -721,6 +735,122 @@ ] } }, + "@@aspect_rules_py~//py:extensions.bzl%py_tools": { + "general": { + "bzlTransitiveDigest": "33DdvPAqRDZsBSZDYKS1h8CPOkvCdLcQT0Ty4RfQhpQ=", + "usagesDigest": "ZdtnhQ2UAc0ZBjc8RvT1/gyMajBc+xQwIcrkqLkdpn8=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "bsd_tar_darwin_amd64": { + "bzlFile": "@@aspect_bazel_lib~//lib/private:tar_toolchain.bzl", + "ruleClassName": "bsdtar_binary_repo", + "attributes": { + "platform": "darwin_amd64" + } + }, + "bsd_tar_darwin_arm64": { + "bzlFile": "@@aspect_bazel_lib~//lib/private:tar_toolchain.bzl", + "ruleClassName": "bsdtar_binary_repo", + "attributes": { + "platform": "darwin_arm64" + } + }, + "bsd_tar_linux_amd64": { + "bzlFile": "@@aspect_bazel_lib~//lib/private:tar_toolchain.bzl", + "ruleClassName": "bsdtar_binary_repo", + "attributes": { + "platform": "linux_amd64" + } + }, + "bsd_tar_linux_arm64": { + "bzlFile": "@@aspect_bazel_lib~//lib/private:tar_toolchain.bzl", + "ruleClassName": "bsdtar_binary_repo", + "attributes": { + "platform": "linux_arm64" + } + }, + "bsd_tar_windows_amd64": { + "bzlFile": "@@aspect_bazel_lib~//lib/private:tar_toolchain.bzl", + "ruleClassName": "bsdtar_binary_repo", + "attributes": { + "platform": "windows_amd64" + } + }, + "bsd_tar_toolchains": { + "bzlFile": "@@aspect_bazel_lib~//lib/private:tar_toolchain.bzl", + "ruleClassName": "tar_toolchains_repo", + "attributes": { + "user_repository_name": "bsd_tar" + } + }, + "rules_py_tools.darwin_amd64": { + "bzlFile": "@@aspect_rules_py~//py/private/toolchain:tools.bzl", + "ruleClassName": "prebuilt_tool_repo", + "attributes": { + "platform": "darwin_amd64" + } + }, + "rules_py_tools.darwin_arm64": { + "bzlFile": "@@aspect_rules_py~//py/private/toolchain:tools.bzl", + "ruleClassName": "prebuilt_tool_repo", + "attributes": { + "platform": "darwin_arm64" + } + }, + "rules_py_tools.linux_amd64": { + "bzlFile": "@@aspect_rules_py~//py/private/toolchain:tools.bzl", + "ruleClassName": "prebuilt_tool_repo", + "attributes": { + "platform": "linux_amd64" + } + }, + "rules_py_tools.linux_arm64": { + "bzlFile": "@@aspect_rules_py~//py/private/toolchain:tools.bzl", + "ruleClassName": "prebuilt_tool_repo", + "attributes": { + "platform": "linux_arm64" + } + }, + "rules_py_tools": { + "bzlFile": "@@aspect_rules_py~//py/private/toolchain:repo.bzl", + "ruleClassName": "toolchains_repo", + "attributes": { + "user_repository_name": "rules_py_tools" + } + }, + "rules_py_pex_2_3_1": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_file", + "attributes": { + "urls": [ + "https://files.pythonhosted.org/packages/e7/d0/fbda2a4d41d62d86ce53f5ae4fbaaee8c34070f75bb7ca009090510ae874/pex-2.3.1-py2.py3-none-any.whl" + ], + "sha256": "64692a5bf6f298403aab930d22f0d836ae4736c5bc820e262e9092fe8c56f830", + "downloaded_file_path": "pex-2.3.1-py2.py3-none-any.whl" + } + } + }, + "recordedRepoMappingEntries": [ + [ + "aspect_bazel_lib~", + "bazel_tools", + "bazel_tools" + ], + [ + "aspect_rules_py~", + "aspect_bazel_lib", + "aspect_bazel_lib~" + ], + [ + "aspect_rules_py~", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, "@@aspect_rules_swc~//swc:extensions.bzl%swc": { "general": { "bzlTransitiveDigest": "3apbvYWdvAuuh7WuuMR7QSBcZYajPR7jtSA8iESt50s=", @@ -910,7 +1040,7 @@ "@@platforms//host:extension.bzl%host_platform": { "general": { "bzlTransitiveDigest": "xelQcPZH8+tmuOHVjL9vDxMnnQNMlwj0SlvgoqBkm4U=", - "usagesDigest": "hgylFkgWSg0ulUwWZzEM1aIftlUnbmw2ynWLdEfHnZc=", + "usagesDigest": "LknWxmn/1mn06KjFGOFRB0A103Qo4Lpj0z30Xaq3WgA=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, @@ -926,7 +1056,7 @@ }, "@@rules_helm~//helm:extensions.bzl%helm": { "general": { - "bzlTransitiveDigest": "VUjn/jm0OKGsg664jQEQpujBkx0bxoyUtxSUk2kZK1M=", + "bzlTransitiveDigest": "7Eye4tQXiCdkmUTUucZY7IN5f7Jtn87ZK4F+1rBpuYQ=", "usagesDigest": "VFo7UX1TaDrAg+Td+5tDEUmFhO1s2AGNMvViDNcsXiw=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -1333,7 +1463,7 @@ }, "@@rules_oci~//oci:extensions.bzl%oci": { "general": { - "bzlTransitiveDigest": "hw5yoakUhNYa2t1S3ndCWQQbpMfgsZvwU9mzJ4JFcyo=", + "bzlTransitiveDigest": "KaVYg1bAMCqqwj5FDil+ImHHz9bn1ahpkqzsNUP/syU=", "usagesDigest": "ipMUI6hIMomgmQFgVfA5kiMxqCTdpuTYgpydWjylma4=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -1508,7 +1638,7 @@ "ruleClassName": "coreutils_platform_repo", "attributes": { "platform": "darwin_amd64", - "version": "0.0.26" + "version": "0.0.27" } }, "coreutils_darwin_arm64": { @@ -1516,7 +1646,7 @@ "ruleClassName": "coreutils_platform_repo", "attributes": { "platform": "darwin_arm64", - "version": "0.0.26" + "version": "0.0.27" } }, "coreutils_linux_amd64": { @@ -1524,7 +1654,7 @@ "ruleClassName": "coreutils_platform_repo", "attributes": { "platform": "linux_amd64", - "version": "0.0.26" + "version": "0.0.27" } }, "coreutils_linux_arm64": { @@ -1532,7 +1662,7 @@ "ruleClassName": "coreutils_platform_repo", "attributes": { "platform": "linux_arm64", - "version": "0.0.26" + "version": "0.0.27" } }, "coreutils_windows_amd64": { @@ -1540,7 +1670,7 @@ "ruleClassName": "coreutils_platform_repo", "attributes": { "platform": "windows_amd64", - "version": "0.0.26" + "version": "0.0.27" } }, "coreutils_toolchains": { @@ -1803,150 +1933,6 @@ ] ] } - }, - "@@rules_python~//python/extensions:python.bzl%python": { - "general": { - "bzlTransitiveDigest": "zULS07DfeL/QxvXtGw8sueRnkYX1iirpj2sXmGMSHsI=", - "usagesDigest": "/S2V9ZR748Of16ntqEXZJuN8MDYNIPaLcCSPVRrLHAU=", - "recordedFileInputs": {}, - "recordedDirentsInputs": {}, - "envVariables": {}, - "generatedRepoSpecs": { - "python_3_11_aarch64-apple-darwin": { - "bzlFile": "@@rules_python~//python:repositories.bzl", - "ruleClassName": "python_repository", - "attributes": { - "sha256": "4918cdf1cab742a90f85318f88b8122aeaa2d04705803c7b6e78e81a3dd40f80", - "patches": [], - "platform": "aarch64-apple-darwin", - "python_version": "3.11.1", - "release_filename": "20230116/cpython-3.11.1+20230116-aarch64-apple-darwin-install_only.tar.gz", - "urls": [ - "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1+20230116-aarch64-apple-darwin-install_only.tar.gz" - ], - "distutils_content": "", - "strip_prefix": "python", - "coverage_tool": "", - "ignore_root_user_error": false - } - }, - "python_3_11_aarch64-unknown-linux-gnu": { - "bzlFile": "@@rules_python~//python:repositories.bzl", - "ruleClassName": "python_repository", - "attributes": { - "sha256": "debf15783bdcb5530504f533d33fda75a7b905cec5361ae8f33da5ba6599f8b4", - "patches": [], - "platform": "aarch64-unknown-linux-gnu", - "python_version": "3.11.1", - "release_filename": "20230116/cpython-3.11.1+20230116-aarch64-unknown-linux-gnu-install_only.tar.gz", - "urls": [ - "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1+20230116-aarch64-unknown-linux-gnu-install_only.tar.gz" - ], - "distutils_content": "", - "strip_prefix": "python", - "coverage_tool": "", - "ignore_root_user_error": false - } - }, - "python_3_11_x86_64-apple-darwin": { - "bzlFile": "@@rules_python~//python:repositories.bzl", - "ruleClassName": "python_repository", - "attributes": { - "sha256": "20a4203d069dc9b710f70b09e7da2ce6f473d6b1110f9535fb6f4c469ed54733", - "patches": [], - "platform": "x86_64-apple-darwin", - "python_version": "3.11.1", - "release_filename": "20230116/cpython-3.11.1+20230116-x86_64-apple-darwin-install_only.tar.gz", - "urls": [ - "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1+20230116-x86_64-apple-darwin-install_only.tar.gz" - ], - "distutils_content": "", - "strip_prefix": "python", - "coverage_tool": "", - "ignore_root_user_error": false - } - }, - "python_3_11_x86_64-pc-windows-msvc": { - "bzlFile": "@@rules_python~//python:repositories.bzl", - "ruleClassName": "python_repository", - "attributes": { - "sha256": "edc08979cb0666a597466176511529c049a6f0bba8adf70df441708f766de5bf", - "patches": [], - "platform": "x86_64-pc-windows-msvc", - "python_version": "3.11.1", - "release_filename": "20230116/cpython-3.11.1+20230116-x86_64-pc-windows-msvc-shared-install_only.tar.gz", - "urls": [ - "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1+20230116-x86_64-pc-windows-msvc-shared-install_only.tar.gz" - ], - "distutils_content": "", - "strip_prefix": "python", - "coverage_tool": "", - "ignore_root_user_error": false - } - }, - "python_3_11_x86_64-unknown-linux-gnu": { - "bzlFile": "@@rules_python~//python:repositories.bzl", - "ruleClassName": "python_repository", - "attributes": { - "sha256": "02a551fefab3750effd0e156c25446547c238688a32fabde2995c941c03a6423", - "patches": [], - "platform": "x86_64-unknown-linux-gnu", - "python_version": "3.11.1", - "release_filename": "20230116/cpython-3.11.1+20230116-x86_64-unknown-linux-gnu-install_only.tar.gz", - "urls": [ - "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1+20230116-x86_64-unknown-linux-gnu-install_only.tar.gz" - ], - "distutils_content": "", - "strip_prefix": "python", - "coverage_tool": "", - "ignore_root_user_error": false - } - }, - "python_3_11": { - "bzlFile": "@@rules_python~//python/private:toolchains_repo.bzl", - "ruleClassName": "toolchain_aliases", - "attributes": { - "python_version": "3.11.1", - "user_repository_name": "python_3_11" - } - }, - "pythons_hub": { - "bzlFile": "@@rules_python~//python/extensions/private:pythons_hub.bzl", - "ruleClassName": "hub_repo", - "attributes": { - "default_python_version": "3.11", - "toolchain_prefixes": [ - "_0000_python_3_11_" - ], - "toolchain_python_versions": [ - "3.11" - ], - "toolchain_set_python_version_constraints": [ - "False" - ], - "toolchain_user_repository_names": [ - "python_3_11" - ] - } - }, - "python_versions": { - "bzlFile": "@@rules_python~//python/private:toolchains_repo.bzl", - "ruleClassName": "multi_toolchain_aliases", - "attributes": { - "python_versions": { - "3.11": "python_3_11" - } - } - } - }, - "recordedRepoMappingEntries": [ - [ - "rules_python~", - "bazel_tools", - "bazel_tools" - ] - ] - } } } } diff --git a/README.md b/README.md index 2c35c9c7..8cfb132f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -# yaku +# Yaku +[![Build all](https://github.com/B-S-F/yaku/actions/workflows/build-all.yml/badge.svg)](https://github.com/B-S-F/yaku/actions/workflows/build-all.yml) + + !! THIS PROJECT IS UNDER CONSTRUCTION !! Under Construction @@ -6,37 +9,76 @@ Foto from Mabel Amber from Pexels -## Bazel -[![Build all](https://github.com/B-S-F/yaku/actions/workflows/build-all.yml/badge.svg)](https://github.com/B-S-F/yaku/actions/workflows/build-all.yml) +## Locally build + +#### Prerequisites +In order to locally build, make user you have bazelisk installed: + +On macOS: +```bash +brew install bazelisk +```` -### Overview +On Windows: +```bash +choco install bazelisk. +``` +#### Build + +```bash +bazel build //... +``` -| Component | Build | Test | Artifact Upload | Comments | -|-------------|-----|------|-----------------|------------------------------| -| Onyx | ✔️ | ✔️ | | | -| API | | | | | -| Chart | ✔️ | ✔️ | ✔️ | | -| Core-image |(✔️) | | ✔️️ | build works but not complete | -| Python apps | ✔️ | ✔️ | ️ | | -| TS apps |(✔️) | | ️ | still some build issue | -| Frontend | | | ️ | | +#### Test + +```bash +bazel test //... +``` + + +## CI/CD +For the CI/CD pipeline, GitHub Actions is used. The workflow is defined in `.github/workflows/build-all.yml`. + +### Bazel Remote caching and remote execution +Remote caching and remote execution in Bazel are powerful features that enhance build efficiency and speed. Remote caching allows Bazel to store build outputs on a server, enabling reuse of these outputs across different builds and machines, which reduces redundant computations and speeds up the build process. Remote execution, on the other hand, offloads the execution of build actions to remote servers, distributing the workload and further accelerating the build process. + +### BuildBuddy +Incorporating BuildBuddy into your GitHub workflow leverages these capabilities by integrating a remote cache and execution service directly into your CI/CD pipeline. This setup ensures that builds are consistently fast and reliable, as BuildBuddy manages the caching and execution of build actions, optimizing resource usage and minimizing build times + +It is not require for contributers to have a BuildBuddy account. The BuildBuddy instance is hosted by the project owner. +However, if you want to follow the remote execution and caching, you can sign up for a free BuildBuddy account at [buildbuddy.io](https://buildbuddy.io/). +Afterwards you are able to [join our organization](https://yaku.buildbuddy.io/join/) and see the build results, stats and so on in [our BuildBuddy dashboard](https://yaku.buildbuddy.io). + +## Components + +| Component | Build | Test | Artifact Upload | Comments | +|-------------|-----|------|-----------------|----------------------------------------------------------------------------------| +| Onyx | ✔️ | ✔️ | | | +| API | | | | | +| Chart | ✔️ | ✔️ | ✔️ | | +| Core-image |(✔️) | | ✔️️ | build works but not complete | +| Python apps | ✔️ | ✔️ | ️ | | +| TS apps |(✔️) | | ️ | see [private test repo](https://github.com/bosch-grow-pat/bazel-typescript-test) | +| Frontend | | | ️ | | ✔️ works (✔) partially works -❌ does not work +❌ does not work -### Components - -#### Onyx +### Onyx - `BUILD` files created with `gazelle -go_prefix github.com/B-S-F/yaku` (from root) - derived from this tutorial: https://earthly.dev/blog/build-golang-bazel-gazelle/ -#### Chart +### Chart - see https://github.com/abrisco/rules_helm - chart is pushed to this OCI repo here: [ghcr.io/b-s-f/charts/yaku](https://github.com/B-S-F/yaku/pkgs/container/charts%2Fyaku) +### Python Apps +- see https://github.com/B-S-F/yaku/tree/main/yaku-apps-python + ... +## Misc ### Dependency graph Overview of the dependencies between the components (click to open in GraphvizOnline) [![](./misc/depgraph.svg)](https://dreampuf.github.io/GraphvizOnline/?compressed=CYSw5gTghgDgFgAgLYE9K0QbwFAIQOwHtgBTBAbQGc5YSBeAI0IA8BdAblwQCIB6XgMY0IAFwBcKKAGsArgH04JADZJuFBAMJLCEOt0gkS%2BbgBoElESiX1uAMxBLrwbhy59Bw8ZNkLlqhAC0AHw8-EJQomIiJEgwSlDRlLy%2BSjAkEJQAdCJxADr4YZ5iAG5QSjIkWZJISvmFEeIAwp6Z1bUFHg1iAhDASRFghAEA7jpSttrDlAE9fa1QNdxu9ZHe8ooqasE8AAIbSHKkMHIDhHKjEOOTSbxi%2B4ckx6fnYxOEU0t47uGr0ut%2BWxC3x0JACICQUDAJDEMBk1DBEKhn1CnV%2BPn2gN29yOciQIHwIEI-DufgexzxBMIyOBEFB4Mh0Nh8PpSLw5E02l0%2BlpRlM5ks1j09kcJGcri%2BYRBCIZMLhcGlrO2NLpiOhImg%2BEoIBEhPworkLJI1MltIVjLlZsxyrNYgsCzSzmWgilhttIntouNztNrrtsU9gSBJpVMrkfod2ViSi9mh9qqiGq1OsIeuABtVanZWh0egMvLMFisNmFThcnAl3pDUITUE12t1%2BsNVp2cQSth0SBuAlhYgiSAAbAAWGMu%2BPq2tJhtppuBlGxqvQptO%2Bc28d15Op9MM5utkTtiCdsI95gADn7ciHI7jMrXk5TjYzs%2B4Lfie47XZ7UAiQkvy9HN8Tet72nR8lRfNt334QhKDEJR8RkZgrwXGt1ynLdFSBHYIBkaxKDkQZ%2BAIkRCC0cJ8W6QY5EIWxbGpLCcMqfCiV4IiSKUMj8Aos5qNotx6NwpjCKJYjSJociBEonjm2wgSCJY4S2I4ri5E0TVx3xEQ6Jkxi5NY0SoHEyjVIsaANLo7FHlxfFCWJCzyWsqk%2BLsqzKVs0kcQpQlm2czzmLEXyYyKaIowSSpkg2NIMmyPIOh%2BcRSnKSp5hqOpUSaFo2lSuLul6foIEGEZXmuGZcuS6M%2BN3fdD14aDYPgxCKtfKqblquD8AQncmsgmqYOgujKu62q%2Bqc9zLOeC4rneG4SRUMkTnys4JreD4RtmnFxqKqa3LWsaFpeS5lsobzRqePaluuYlTkKg7rnMk75so86ttuK6npWr49nujabue3sFuuyaPifT6dtOx7NqmS7-re6YAAZMkHABGBHsjAAAvO7QYexaIem17cYCeGkZRkR0cxg4PIc4kAtWinLN86mHOOrGGduXyAgAVkyAAmTJ4dJjHabm1n-Iczmeb51HBYrFdXTDd1-WASM4iQm15Y9JWRCjZsGCgNHlDkESlBuSgIAEZ18FADcYLgaCRHOfFgCm1XXRnCgORzbgRBkCAAEcZEIEBKCNAsBWLBxS3FOd-2rGclWDG1YGOeIUHSNQ-2vWPQKDSs1cNc4IG1Eg5CMdUUBgQP8E0jPkLjzDQBMrRKjw3Xg%2BJBv1SbygW6gYO6I7iAu57tvbgHoe5Fbo0%2BLH3Dh5Idug872eJ97o1gYGg8PxkMRT3PX8Ppn5uV5HsRD%2B74%2B17A-jGMIAQQAAP3v2%2BH7Pue5DahD5oHQcF8b5fJ-fvVL%2BQ58g7GvnhZ%2Bj9IGvwvoA9qzAThIGAEOX%2BS8j4AI-gghYyDhxcHIPEBgyg9BgI3tVbs28%2BxDhIV1TeR5t67wvMOKOz4YGT1QYPf%2Bq9OoQVoYIT8B5967FIVvP639%2Bo0LIfwsR68JEiK-GbOAgjnzCLob2b8ijcEfRUXwih6jLwyJ4ZI7e5C6LgKonfKBFjWGrzgZ-ShP9R6Lw4egmxmDgGDlAWYyBT8rFOPHhgoB2CUGOL-i44OtisFIKUeBN8vDyE7zPIw8RhiREMP0WBbR8STGNRSao7JMsY7QnQsXYYhdoglyrhAculdq4FMzkU-OpSi4VLLhXMywNdb6yUIbNiJszYWytrqG2dsHaW2dnxTpBsjZ9PNqpQZKZhkWFGU7d6uxJndOmfwU2syUzzM1HcEZwxHbOw6XrKZvStn9LmUBfZtsllHLGVMdxKkFnqSrqA9ZPStAzIGTcxZ9sHkrLwswIcLy1KmSrsifBUBCFKGITsT5mzeDbN%2BdbA59zjlPPsWCkyBkq6Sktn80%2BJBbBQBwppZhCKzkbIuciq5uyiV3IBZivC2LjJvJEB86lXzjaXJ2YStFTLllTTkCCwcOKOUu3jEnd%2BUBU4QDUO7bMXJvZ%2BwDkHEO-IixCgjp6ZhCdXQypTmnYG2k8IwCkGAfgFqrUwELqUaIMJ7WhXVrEB40QBCco6CmFAzBBBIMEHBMQPqGofTNXIG11rLW8DtSAB1jJnXlPDO6kgnrUohv9cAQNIBg34F9YqrMnI9Cp0cO8PkhZBR2F1WKTgABfbAQA) diff --git a/WORKSPACE b/WORKSPACE index f4a2f97c..c302cf4d 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -23,4 +23,24 @@ helm_import_repository( version = "5.2.0", sha256 = "8ef4212d7d51be6c8192b3e91138a9ca918ca56142c42500028cfd3b80e0b2dd", chart_name = "minio", -) \ No newline at end of file +) + +### +# Buildbuddy, needed only for remote execution +### +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +http_archive( + name = "io_buildbuddy_buildbuddy_toolchain", + sha256 = "baa9af1b9fcc96d18ac90a4dd68ebd2046c8beb76ed89aea9aabca30959ad30c", + strip_prefix = "buildbuddy-toolchain-287d6042ad151be92de03c83ef48747ba832c4e2", + urls = ["https://github.com/buildbuddy-io/buildbuddy-toolchain/archive/287d6042ad151be92de03c83ef48747ba832c4e2.tar.gz"], +) + +load("@io_buildbuddy_buildbuddy_toolchain//:deps.bzl", "buildbuddy_deps") + +buildbuddy_deps() + +load("@io_buildbuddy_buildbuddy_toolchain//:rules.bzl", "buildbuddy") + +buildbuddy(name = "buildbuddy_toolchain") \ No newline at end of file diff --git a/misc/buildbuddy.png b/misc/buildbuddy.png new file mode 100644 index 00000000..5e7490b3 Binary files /dev/null and b/misc/buildbuddy.png differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..818d3688 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[tool.pyright] +include = ["yaku-apps-python"] +extraPaths = [ + "yaku-apps-python/packages/autopilot-utils/src", + "yaku-apps-python/packages/autopilot-utils/tests/app_multi_evaluator/src", + "yaku-apps-python/packages/autopilot-utils/tests/app_subprocess_steps/src", + "yaku-apps-python/packages/autopilot-utils/tests/app_single_evaluator/src", + "yaku-apps-python/packages/autopilot-utils/tests/app_multi_command/src", + "yaku-apps-python/packages/autopilot-utils/tests/app_chained_multi_evaluator/src", + "yaku-apps-python/packages/autopilot-utils/tests/demo_app/src", + "yaku-apps-python/packages/autopilot-utils/tests/app_super_simple_evaluator/src", + "yaku-apps-python/packages/autopilot-utils/tests/app_single_command/src", + "yaku-apps-python/apps/splunk-fetcher/src", + "yaku-apps-python/apps/sharepoint-fetcher/src", + "yaku-apps-python/apps/papsr/src", + "yaku-apps-python/apps/sharepoint/src", + "yaku-apps-python/apps/sharepoint-evaluator/src", + "yaku-apps-python/apps/pdf-signature-evaluator/src", + "yaku-apps-python/apps/filecheck/src", + "yaku-apps-python/apps/artifactory-fetcher/src", + "yaku-apps-python/apps/excel-tools/src", + "yaku-apps-python/apps/security-scanner/src", + "yaku-apps-python/apps/pex-tool/src", + +] diff --git a/tools/pytest-runner/BUILD.bazel b/tools/pytest-runner/BUILD.bazel new file mode 100644 index 00000000..d31f4694 --- /dev/null +++ b/tools/pytest-runner/BUILD.bazel @@ -0,0 +1 @@ +exports_files(["run_pytest.py"]) diff --git a/tools/pytest-runner/defs.bzl b/tools/pytest-runner/defs.bzl new file mode 100644 index 00000000..4ab495ca --- /dev/null +++ b/tools/pytest-runner/defs.bzl @@ -0,0 +1,19 @@ +load("@aspect_rules_py//py:defs.bzl", "py_test") + +def py_pytest_test(name, srcs, deps, data = None): + py_test( + name = name, + srcs = srcs + ["//tools/pytest-runner:run_pytest.py"], + # We have to tell pytest to only look into the `yaku-apps-python` subdirectory + # of the sandbox folder, as it also contains the installed Python + # packages which might contain also some tests. To prevent them + # from being picked up by pytest, we restrict pytest to our main + # folder, currently `yaku-apps-python`. + args = ["yaku-apps-python"], + main = "run_pytest.py", + data = data, + deps = deps + [ + "@pip//pytest", + "@pip//pytest_cov", + ], + ) diff --git a/tools/pytest-runner/run_pytest.py b/tools/pytest-runner/run_pytest.py new file mode 100644 index 00000000..cb5e82b7 --- /dev/null +++ b/tools/pytest-runner/run_pytest.py @@ -0,0 +1,18 @@ +import os +import sys + +import pytest + +if __name__ == "__main__": + sys.exit(pytest.main(sys.argv[1:])) + # sys.exit( + # pytest.main( + # sys.argv[1:] + # + [ + # "--cov=yaku", + # "--cov-report=lcov:" + os.getenv("COVERAGE_OUTPUT_FILE", ""), + # "--cov-report=term", + # "--cov-branch", + # ] + # ) + # ) diff --git a/yaku-apps-python/.bandit b/yaku-apps-python/.bandit deleted file mode 100644 index 777c418e..00000000 --- a/yaku-apps-python/.bandit +++ /dev/null @@ -1,2 +0,0 @@ -[bandit] -skips = B101,B404,B602,B603,B607 \ No newline at end of file diff --git a/yaku-apps-python/3rdparty/BUILD b/yaku-apps-python/3rdparty/BUILD deleted file mode 100644 index ffdeab3a..00000000 --- a/yaku-apps-python/3rdparty/BUILD +++ /dev/null @@ -1,9 +0,0 @@ -python_requirements( - name="reqs", - module_mapping={ - "dohq-artifactory": ["artifactory", "dohq_artifactory"], - "requests-ntlm": ["requests_ntlm"], - "cyclonedx-python-lib": ["cyclonedx"], - }, - resolve="python-default", # is also the default value for 'resolve' -) diff --git a/yaku-apps-python/3rdparty/python-lockfile.txt b/yaku-apps-python/3rdparty/python-lockfile.txt deleted file mode 100644 index 5f7adc8c..00000000 --- a/yaku-apps-python/3rdparty/python-lockfile.txt +++ /dev/null @@ -1,4817 +0,0 @@ -// This lockfile was autogenerated by Pants. To regenerate, run: -// -// pants generate-lockfiles --resolve=python-default -// -// --- BEGIN PANTS LOCKFILE METADATA: DO NOT EDIT OR REMOVE --- -// { -// "version": 3, -// "valid_for_interpreter_constraints": [ -// "CPython==3.10.*" -// ], -// "generated_with_requirements": [ -// "asn1crypto==1.5.1", -// "atlassian-python-api==3.41.3", -// "azure-devops==7.1.0b4", -// "beautifulsoup4==4.12.2", -// "certifi==2024.7.4", -// "click==8.1.3", -// "cryptography==43.0.1", -// "cyclonedx-python-lib[xml-validation]==5.1.1", -// "datetime", -// "defusedxml==0.7.1", -// "dohq-artifactory==0.8.3", -// "elmclient==0.26.2", -// "freezegun", -// "halo==0.0.31", -// "idna==3.7", -// "jira==3.8.0", -// "jsonschema==4.23.0", -// "langchain_ollama==0.1.1", -// "langchain_openai==0.1.20", -// "loguru==0.7.0", -// "mock", -// "mypy==1.4.1", -// "numpy==1.24.3", -// "ollama==0.3.1", -// "openai==1.40.*", -// "openpyxl<3.1.0", -// "oracledb==2.2.0", -// "packaging==23.2", -// "pandas==2.1.3", -// "py-jama-rest-client==1.17.1", -// "pydantic==1.10.13", -// "pyhanko==0.25.1", -// "pyhanko_certvalidator==0.26.3", -// "pypdf==3.17.3", -// "pytest-cov!=2.12.1,<3.1,>=2.12", -// "pytest-custom_exit_code==0.3.0", -// "pytest-mock>=3.10.0", -// "pytest-xdist<3,>=2.5", -// "pytest<8", -// "python-dateutil", -// "pytz==2023.3", -// "pyyaml==6.0", -// "requests", -// "requests-mock", -// "requests-ntlm==1.2.0", -// "splunk-sdk==1.7.3", -// "types-PyYAML", -// "types-beautifulsoup4", -// "types-jsonschema", -// "types-mock", -// "types-openpyxl", -// "types-python-dateutil", -// "types-pytz", -// "types-requests", -// "types-xmltodict", -// "urllib3==2.2.2", -// "validators==0.22.0", -// "xlrd==2.0.1", -// "xmltodict==0.13.0" -// ], -// "manylinux": "manylinux2014", -// "requirement_constraints": [], -// "only_binary": [], -// "no_binary": [] -// } -// --- END PANTS LOCKFILE METADATA --- - -{ - "allow_builds": true, - "allow_prereleases": false, - "allow_wheels": true, - "build_isolation": true, - "constraints": [], - "locked_resolves": [ - { - "locked_requirements": [ - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7", - "url": "https://files.pythonhosted.org/packages/7b/a2/10639a79341f6c019dedc95bd48a4928eed9f1d1197f4c04f546fc7ae0ff/anyio-4.4.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94", - "url": "https://files.pythonhosted.org/packages/e6/e3/c4c8d473d6780ef1853d630d581f70d655b4f8d7553c6997958c283039a2/anyio-4.4.0.tar.gz" - } - ], - "project_name": "anyio", - "requires_dists": [ - "Sphinx>=7; extra == \"doc\"", - "anyio[trio]; extra == \"test\"", - "coverage[toml]>=7; extra == \"test\"", - "exceptiongroup>=1.0.2; python_version < \"3.11\"", - "exceptiongroup>=1.2.0; extra == \"test\"", - "hypothesis>=4.0; extra == \"test\"", - "idna>=2.8", - "packaging; extra == \"doc\"", - "psutil>=5.9; extra == \"test\"", - "pytest-mock>=3.6.1; extra == \"test\"", - "pytest>=7.0; extra == \"test\"", - "sniffio>=1.1", - "sphinx-autodoc-typehints>=1.2.0; extra == \"doc\"", - "sphinx-rtd-theme; extra == \"doc\"", - "trio>=0.23; extra == \"trio\"", - "trustme; extra == \"test\"", - "typing-extensions>=4.1; python_version < \"3.11\"", - "uvloop>=0.17; (platform_python_implementation == \"CPython\" and platform_system != \"Windows\") and extra == \"test\"" - ], - "requires_python": ">=3.8", - "version": "4.4.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "5ea9e61caf96db1e5b3d0a914378d2cd83c269dfce1fb8242ce96589fa3382f0", - "url": "https://files.pythonhosted.org/packages/6a/fb/ff946843e6b55ae9fda84df3964d6c233cd2261dface789f5be02ab79bc5/anytree-2.12.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "244def434ccf31b668ed282954e5d315b4e066c4940b94aff4a7962d85947830", - "url": "https://files.pythonhosted.org/packages/f9/44/2dd9c5d0c3befe899738b930aa056e003b1441bfbf34aab8fce90b2b7dea/anytree-2.12.1.tar.gz" - } - ], - "project_name": "anytree", - "requires_dists": [ - "six" - ], - "requires_python": "<4,>=3.7.2", - "version": "2.12.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67", - "url": "https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c", - "url": "https://files.pythonhosted.org/packages/de/cf/d547feed25b5244fcb9392e288ff9fdc3280b10260362fc45d37a798a6ee/asn1crypto-1.5.1.tar.gz" - } - ], - "project_name": "asn1crypto", - "requires_dists": [], - "requires_python": null, - "version": "1.5.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "7661d3ce3c80e887a7e5ec1c61c1e37d3eaacb4857e377b38ef4084d0f067757", - "url": "https://files.pythonhosted.org/packages/2d/a4/8479331c0ce7867e0e3401c0d728dd187a969dc37aa34e75f482f672c4ab/atlassian_python_api-3.41.3-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "a29aae8f456babe125e3371a0355018e9c1d37190333efc312bd81163bd96ffd", - "url": "https://files.pythonhosted.org/packages/ac/20/9bbb0c15b73ddbef44828d9f86a4786b038541ef34e135f8340b9bf7837a/atlassian-python-api-3.41.3.tar.gz" - } - ], - "project_name": "atlassian-python-api", - "requires_dists": [ - "deprecated", - "oauthlib", - "requests", - "requests-kerberos; extra == \"kerberos\"", - "requests-oauthlib", - "six" - ], - "requires_python": null, - "version": "3.41.3" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", - "url": "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", - "url": "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz" - } - ], - "project_name": "attrs", - "requires_dists": [ - "cloudpickle; platform_python_implementation == \"CPython\" and extra == \"benchmark\"", - "cloudpickle; platform_python_implementation == \"CPython\" and extra == \"cov\"", - "cloudpickle; platform_python_implementation == \"CPython\" and extra == \"dev\"", - "cloudpickle; platform_python_implementation == \"CPython\" and extra == \"tests\"", - "cogapp; extra == \"docs\"", - "coverage[toml]>=5.3; extra == \"cov\"", - "furo; extra == \"docs\"", - "hypothesis; extra == \"benchmark\"", - "hypothesis; extra == \"cov\"", - "hypothesis; extra == \"dev\"", - "hypothesis; extra == \"tests\"", - "importlib-metadata; python_version < \"3.8\"", - "mypy>=1.11.1; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\") and extra == \"benchmark\"", - "mypy>=1.11.1; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\") and extra == \"cov\"", - "mypy>=1.11.1; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\") and extra == \"dev\"", - "mypy>=1.11.1; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\") and extra == \"tests\"", - "mypy>=1.11.1; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\") and extra == \"tests-mypy\"", - "myst-parser; extra == \"docs\"", - "pre-commit; extra == \"dev\"", - "pympler; extra == \"benchmark\"", - "pympler; extra == \"cov\"", - "pympler; extra == \"dev\"", - "pympler; extra == \"tests\"", - "pytest-codspeed; extra == \"benchmark\"", - "pytest-mypy-plugins; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\") and extra == \"benchmark\"", - "pytest-mypy-plugins; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\") and extra == \"cov\"", - "pytest-mypy-plugins; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\") and extra == \"dev\"", - "pytest-mypy-plugins; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\") and extra == \"tests\"", - "pytest-mypy-plugins; (platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\") and extra == \"tests-mypy\"", - "pytest-xdist[psutil]; extra == \"benchmark\"", - "pytest-xdist[psutil]; extra == \"cov\"", - "pytest-xdist[psutil]; extra == \"dev\"", - "pytest-xdist[psutil]; extra == \"tests\"", - "pytest>=4.3.0; extra == \"benchmark\"", - "pytest>=4.3.0; extra == \"cov\"", - "pytest>=4.3.0; extra == \"dev\"", - "pytest>=4.3.0; extra == \"tests\"", - "sphinx-notfound-page; extra == \"docs\"", - "sphinx; extra == \"docs\"", - "sphinxcontrib-towncrier; extra == \"docs\"", - "towncrier<24.7; extra == \"docs\"" - ], - "requires_python": ">=3.7", - "version": "24.2.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "22954de3777e0250029360ef31d80448ef1be13b80a459bff80ba7073379e2cd", - "url": "https://files.pythonhosted.org/packages/01/8e/fcb6a77d3029d2a7356f38dbc77cf7daa113b81ddab76b5593d23321e44c/azure_core-1.31.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "656a0dd61e1869b1506b7c6a3b31d62f15984b1a573d6326f6aa2f3e4123284b", - "url": "https://files.pythonhosted.org/packages/03/7a/f79ad135a276a37e61168495697c14ba1721a52c3eab4dae2941929c79f8/azure_core-1.31.0.tar.gz" - } - ], - "project_name": "azure-core", - "requires_dists": [ - "aiohttp>=3.0; extra == \"aio\"", - "requests>=2.21.0", - "six>=1.11.0", - "typing-extensions>=4.6.0" - ], - "requires_python": ">=3.8", - "version": "1.31.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "f827e9fbc7c77bc6f2aaee46e5717514e9fe7d676c87624eccd0ca640b54f122", - "url": "https://files.pythonhosted.org/packages/9a/30/067b9aba3cb146f4334afb737eb86c4f66e2b645fbca770377253550a9b3/azure_devops-7.1.0b4-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "f04ba939112579f3d530cfecc044a74ef9e9339ba23c9ee1ece248241f07ff85", - "url": "https://files.pythonhosted.org/packages/e1/f9/495982345252dc7a15ac632e038be1f975ca0d2f25abfe8f8d908569141d/azure-devops-7.1.0b4.tar.gz" - } - ], - "project_name": "azure-devops", - "requires_dists": [ - "msrest<0.8.0,>=0.7.1" - ], - "requires_python": ">=3.7", - "version": "7.1.0b4" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", - "url": "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", - "url": "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz" - } - ], - "project_name": "backports-tarfile", - "requires_dists": [ - "furo; extra == \"docs\"", - "jaraco.packaging>=9.3; extra == \"docs\"", - "jaraco.test; extra == \"testing\"", - "pytest!=8.0.*; extra == \"testing\"", - "pytest!=8.1.*,>=6; extra == \"testing\"", - "pytest-checkdocs>=2.4; extra == \"testing\"", - "pytest-cov; extra == \"testing\"", - "pytest-enabler>=2.2; extra == \"testing\"", - "rst.linker>=1.9; extra == \"docs\"", - "sphinx-lint; extra == \"docs\"", - "sphinx>=3.5; extra == \"docs\"" - ], - "requires_python": ">=3.8", - "version": "1.2.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a", - "url": "https://files.pythonhosted.org/packages/57/f4/a69c20ee4f660081a7dedb1ac57f29be9378e04edfcb90c526b923d4bebc/beautifulsoup4-4.12.2-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da", - "url": "https://files.pythonhosted.org/packages/af/0b/44c39cf3b18a9280950ad63a579ce395dda4c32193ee9da7ff0aed547094/beautifulsoup4-4.12.2.tar.gz" - } - ], - "project_name": "beautifulsoup4", - "requires_dists": [ - "html5lib; extra == \"html5lib\"", - "lxml; extra == \"lxml\"", - "soupsieve>1.2" - ], - "requires_python": ">=3.6.0", - "version": "4.12.2" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "2876f2051d7d6394a531d82dc6eb407faa0b01a0a0b3083817ccd7323b8d96bd", - "url": "https://files.pythonhosted.org/packages/3f/02/6389ef0529af6da0b913374dedb9bbde8eabfe45767ceec38cc37801b0bd/boolean.py-4.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "17b9a181630e43dde1851d42bef546d616d5d9b4480357514597e78b203d06e4", - "url": "https://files.pythonhosted.org/packages/a2/d9/b6e56a303d221fc0bdff2c775e4eef7fedd58194aa5a96fa89fb71634cc9/boolean.py-4.0.tar.gz" - } - ], - "project_name": "boolean-py", - "requires_dists": [], - "requires_python": null, - "version": "4.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410", - "url": "https://files.pythonhosted.org/packages/1d/e3/fa60c47d7c344533142eb3af0b73234ef8ea3fb2da742ab976b947e717df/bump2version-1.0.1-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6", - "url": "https://files.pythonhosted.org/packages/29/2a/688aca6eeebfe8941235be53f4da780c6edee05dbbea5d7abaa3aab6fad2/bump2version-1.0.1.tar.gz" - } - ], - "project_name": "bump2version", - "requires_dists": [], - "requires_python": ">=3.5", - "version": "1.0.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "f5bf3f0620c38db2e5122c0726bdebb0d16869de966ea6a2befe92470b740ea0", - "url": "https://files.pythonhosted.org/packages/a3/a9/7d331fec593a4b2953338df33e954aac6ff79eb5a073bca2783766bc7722/cachecontrol-0.14.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "7db1195b41c81f8274a7bbd97c956f44e8348265a1bc7641c37dfebc39f0c938", - "url": "https://files.pythonhosted.org/packages/06/55/edea9d90ee57ca54d34707607d15c20f72576b96cb9f3e7fc266cb06b426/cachecontrol-0.14.0.tar.gz" - } - ], - "project_name": "cachecontrol", - "requires_dists": [ - "CacheControl[filecache,redis]; extra == \"dev\"", - "black; extra == \"dev\"", - "build; extra == \"dev\"", - "cherrypy; extra == \"dev\"", - "filelock>=3.8.0; extra == \"filecache\"", - "furo; extra == \"dev\"", - "msgpack<2.0.0,>=0.5.2", - "mypy; extra == \"dev\"", - "pytest-cov; extra == \"dev\"", - "pytest; extra == \"dev\"", - "redis>=2.10.5; extra == \"redis\"", - "requests>=2.16.0", - "sphinx-copybutton; extra == \"dev\"", - "sphinx; extra == \"dev\"", - "tox; extra == \"dev\"", - "types-redis; extra == \"dev\"", - "types-requests; extra == \"dev\"" - ], - "requires_python": ">=3.7", - "version": "0.14.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90", - "url": "https://files.pythonhosted.org/packages/1c/d5/c84e1a17bf61d4df64ca866a1c9a913874b4e9bdc131ec689a0ad013fb36/certifi-2024.7.4-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", - "url": "https://files.pythonhosted.org/packages/c2/02/a95f2b11e207f68bc64d7aae9666fed2e2b3f307748d5123dffb72a1bbea/certifi-2024.7.4.tar.gz" - } - ], - "project_name": "certifi", - "requires_dists": [], - "requires_python": ">=3.6", - "version": "2024.7.4" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", - "url": "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", - "url": "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl" - }, - { - "algorithm": "sha256", - "hash": "6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", - "url": "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", - "url": "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl" - }, - { - "algorithm": "sha256", - "hash": "2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", - "url": "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", - "url": "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", - "url": "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", - "url": "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", - "url": "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" - }, - { - "algorithm": "sha256", - "hash": "edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", - "url": "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", - "url": "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz" - } - ], - "project_name": "cffi", - "requires_dists": [ - "pycparser" - ], - "requires_python": ">=3.8", - "version": "1.17.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "url": "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "url": "https://files.pythonhosted.org/packages/05/8c/eb854996d5fef5e4f33ad56927ad053d04dc820e4a3d39023f35cad72617/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" - }, - { - "algorithm": "sha256", - "hash": "25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "url": "https://files.pythonhosted.org/packages/2b/61/095a0aa1a84d1481998b534177c8566fdc50bb1233ea9a0478cd3cc075bd/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl" - }, - { - "algorithm": "sha256", - "hash": "beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "url": "https://files.pythonhosted.org/packages/33/c3/3b96a435c5109dd5b6adc8a59ba1d678b302a97938f032e3770cc84cd354/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "url": "https://files.pythonhosted.org/packages/3f/ba/3f5e7be00b215fa10e13d64b1f6237eb6ebea66676a41b2bcdd09fe74323/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "url": "https://files.pythonhosted.org/packages/43/05/3bf613e719efe68fb3a77f9c536a389f35b95d75424b96b426a47a45ef1d/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "url": "https://files.pythonhosted.org/packages/46/6a/d5c26c41c49b546860cc1acabdddf48b0b3fb2685f4f5617ac59261b44ae/charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl" - }, - { - "algorithm": "sha256", - "hash": "2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "url": "https://files.pythonhosted.org/packages/58/78/a0bc646900994df12e07b4ae5c713f2b3e5998f58b9d3720cce2aa45652f/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl" - }, - { - "algorithm": "sha256", - "hash": "f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "url": "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz" - }, - { - "algorithm": "sha256", - "hash": "fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "url": "https://files.pythonhosted.org/packages/a8/31/47d018ef89f95b8aded95c589a77c072c55e94b50a41aa99c0a2008a45a4/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "url": "https://files.pythonhosted.org/packages/b8/60/e2f67915a51be59d4539ed189eb0a2b0d292bf79270410746becb32bc2c3/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "url": "https://files.pythonhosted.org/packages/cc/94/f7cf5e5134175de79ad2059edf2adce18e0685ebdb9227ff0139975d0e93/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "url": "https://files.pythonhosted.org/packages/da/f1/3702ba2a7470666a62fd81c58a4c40be00670e5006a67f4d626e57f013ae/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "url": "https://files.pythonhosted.org/packages/eb/5c/97d97248af4920bc68687d9c3b3c0f47c910e21a8ff80af4565a576bd2f0/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl" - }, - { - "algorithm": "sha256", - "hash": "cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "url": "https://files.pythonhosted.org/packages/f6/93/bb6cbeec3bf9da9b2eba458c15966658d1daa8b982c642f81c93ad9b40e1/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl" - } - ], - "project_name": "charset-normalizer", - "requires_dists": [], - "requires_python": ">=3.7.0", - "version": "3.3.2" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48", - "url": "https://files.pythonhosted.org/packages/c2/f1/df59e28c642d583f7dacffb1e0965d0e00b218e0186d7858ac5233dce840/click-8.1.3-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", - "url": "https://files.pythonhosted.org/packages/59/87/84326af34517fca8c58418d148f2403df25303e02736832403587318e9e8/click-8.1.3.tar.gz" - } - ], - "project_name": "click", - "requires_dists": [ - "colorama; platform_system == \"Windows\"", - "importlib-metadata; python_version < \"3.8\"" - ], - "requires_python": ">=3.7", - "version": "8.1.3" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", - "url": "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", - "url": "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz" - } - ], - "project_name": "colorama", - "requires_dists": [], - "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7", - "version": "0.4.6" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", - "url": "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", - "url": "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", - "url": "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", - "url": "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", - "url": "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", - "url": "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl" - }, - { - "algorithm": "sha256", - "hash": "b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", - "url": "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", - "url": "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", - "url": "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", - "url": "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz" - } - ], - "project_name": "coverage", - "requires_dists": [ - "tomli; python_full_version <= \"3.11.0a6\" and extra == \"toml\"" - ], - "requires_python": ">=3.8", - "version": "7.6.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289", - "url": "https://files.pythonhosted.org/packages/df/e6/ccd29a1f9a6b71294e1e9f530c4d779d5dd37c8bb736c05d5fb6d98a971b/cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806", - "url": "https://files.pythonhosted.org/packages/00/0e/8217e348a1fa417ec4c78cd3cdf24154f5e76fd7597343a35bd403650dfd/cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034", - "url": "https://files.pythonhosted.org/packages/18/23/4175dcd935e1649865e1af7bd0b827cc9d9769a586dcc84f7cbe96839086/cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062", - "url": "https://files.pythonhosted.org/packages/33/13/1193774705783ba364121aa2a60132fa31a668b8ababd5edfa1662354ccd/cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85", - "url": "https://files.pythonhosted.org/packages/3d/ed/38b6be7254d8f7251fde8054af597ee8afa14f911da67a9410a45f602fc3/cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa", - "url": "https://files.pythonhosted.org/packages/43/f6/feebbd78a3e341e3913846a3bb2c29d0b09b1b3af1573c6baabc2533e147/cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d", - "url": "https://files.pythonhosted.org/packages/58/28/b92c98a04ba762f8cdeb54eba5c4c84e63cac037a7c5e70117d337b15ad6/cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl" - }, - { - "algorithm": "sha256", - "hash": "68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962", - "url": "https://files.pythonhosted.org/packages/5e/4b/39bb3c4c8cfb3e94e736b8d8859ce5c81536e91a1033b1d26770c4249000/cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c", - "url": "https://files.pythonhosted.org/packages/64/f3/b7946c3887cf7436f002f4cbb1e6aec77b8d299b86be48eeadfefb937c4b/cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d", - "url": "https://files.pythonhosted.org/packages/8a/b6/bc54b371f02cffd35ff8dc6baba88304d7cf8e83632566b4b42e00383e03/cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl" - }, - { - "algorithm": "sha256", - "hash": "014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494", - "url": "https://files.pythonhosted.org/packages/a4/65/430509e31700286ec02868a2457d2111d03ccefc20349d24e58d171ae0a7/cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1", - "url": "https://files.pythonhosted.org/packages/ac/7e/ebda4dd4ae098a0990753efbb4b50954f1d03003846b943ea85070782da7/cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a", - "url": "https://files.pythonhosted.org/packages/ad/43/7a9920135b0d5437cc2f8f529fa757431eb6a7736ddfadfdee1cc5890800/cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4", - "url": "https://files.pythonhosted.org/packages/bd/4c/ab0b9407d5247576290b4fd8abd06b7f51bd414f04eef0f2800675512d61/cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042", - "url": "https://files.pythonhosted.org/packages/cc/42/9ab8467af6c0b76f3d9b8f01d1cf25b9c9f3f2151f4acfab888d21c55a72/cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277", - "url": "https://files.pythonhosted.org/packages/ce/dc/1471d4d56608e1013237af334b8a4c35d53895694fbb73882d1c4fd3f55e/cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d", - "url": "https://files.pythonhosted.org/packages/de/ba/0664727028b37e249e73879348cc46d45c5c1a2a2e81e8166462953c5755/cryptography-43.0.1.tar.gz" - }, - { - "algorithm": "sha256", - "hash": "c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d", - "url": "https://files.pythonhosted.org/packages/ea/45/967da50269954b993d4484bf85026c7377bd551651ebdabba94905972556/cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl" - } - ], - "project_name": "cryptography", - "requires_dists": [ - "bcrypt>=3.1.5; extra == \"ssh\"", - "build; extra == \"sdist\"", - "certifi; extra == \"test\"", - "cffi>=1.12; platform_python_implementation != \"PyPy\"", - "check-sdist; extra == \"pep8test\"", - "click; extra == \"pep8test\"", - "cryptography-vectors==43.0.1; extra == \"test\"", - "mypy; extra == \"pep8test\"", - "nox; extra == \"nox\"", - "pretend; extra == \"test\"", - "pyenchant>=1.6.11; extra == \"docstest\"", - "pytest-benchmark; extra == \"test\"", - "pytest-cov; extra == \"test\"", - "pytest-randomly; extra == \"test-randomorder\"", - "pytest-xdist; extra == \"test\"", - "pytest>=6.2.0; extra == \"test\"", - "readme-renderer; extra == \"docstest\"", - "ruff; extra == \"pep8test\"", - "sphinx-rtd-theme>=1.1.1; extra == \"docs\"", - "sphinx>=5.3.0; extra == \"docs\"", - "sphinxcontrib-spelling>=4.0.1; extra == \"docstest\"" - ], - "requires_python": ">=3.7", - "version": "43.0.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "2989db0cd8bb4c0c442423d71ed7a84ae059e16a2d0f932cc4bf92da7385cdb3", - "url": "https://files.pythonhosted.org/packages/7a/54/16775bf32e9efa0be527b476c9e29cfdeb5af5892841aba4f425991533c7/cyclonedx_python_lib-5.1.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "215a636a4e77385d2cf4c6c9801c9bad4791849634f2c6daa45ab2c6cb0a85f6", - "url": "https://files.pythonhosted.org/packages/aa/f6/92a3274747c0cb65eec1e410c64c2fb45557963ec8d9b5eabd6361c37ec9/cyclonedx_python_lib-5.1.1.tar.gz" - } - ], - "project_name": "cyclonedx-python-lib", - "requires_dists": [ - "jsonschema[format]<5.0,>=4.18; extra == \"validation\" or extra == \"json-validation\"", - "license-expression<31,>=30", - "lxml<5,>=4; extra == \"validation\" or extra == \"xml-validation\"", - "packageurl-python>=0.11", - "py-serializable<0.16,>=0.15", - "sortedcontainers<3.0.0,>=2.4.0" - ], - "requires_python": "<4.0,>=3.8", - "version": "5.1.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "0abf6c51cb4ba7cee775ca46ccc727f3afdde463be28dbbe8803631fefd4a120", - "url": "https://files.pythonhosted.org/packages/f3/78/8e382b8cb4346119e2e04270b6eb4a01c5ee70b47a8a0244ecdb157204f7/DateTime-5.5-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "21ec6331f87a7fcb57bd7c59e8a68bfffe6fcbf5acdbbc7b356d6a9a020191d3", - "url": "https://files.pythonhosted.org/packages/2f/66/e284b9978fede35185e5d18fb3ae855b8f573d8c90a56de5f6d03e8ef99e/DateTime-5.5.tar.gz" - } - ], - "project_name": "datetime", - "requires_dists": [ - "pytz", - "zope.interface" - ], - "requires_python": ">=3.7", - "version": "5.5" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", - "url": "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", - "url": "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz" - } - ], - "project_name": "defusedxml", - "requires_dists": [], - "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7", - "version": "0.7.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c", - "url": "https://files.pythonhosted.org/packages/20/8d/778b7d51b981a96554f29136cd59ca7880bf58094338085bcf2a979a0e6a/Deprecated-1.2.14-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3", - "url": "https://files.pythonhosted.org/packages/92/14/1e41f504a246fc224d2ac264c227975427a85caf37c3979979edb9b1b232/Deprecated-1.2.14.tar.gz" - } - ], - "project_name": "deprecated", - "requires_dists": [ - "PyTest-Cov; extra == \"dev\"", - "PyTest; extra == \"dev\"", - "bump2version<1; extra == \"dev\"", - "sphinx<2; extra == \"dev\"", - "tox; extra == \"dev\"", - "wrapt<2,>=1.10" - ], - "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7", - "version": "1.2.14" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", - "url": "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", - "url": "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz" - } - ], - "project_name": "distro", - "requires_dists": [], - "requires_python": ">=3.6", - "version": "1.9.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", - "url": "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", - "url": "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz" - } - ], - "project_name": "docutils", - "requires_dists": [], - "requires_python": ">=3.9", - "version": "0.21.2" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "916a9fffa7293fecaf088a0f5723b71a2b6e57645b87c1f172fcc9c63432f3b7", - "url": "https://files.pythonhosted.org/packages/1e/4f/ce4c080f13b5171154c04eadb7291138ee047e7a3b838159eaa0ed523875/dohq_artifactory-0.8.3-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "bc13fabd0cdae34f620b9d229b5da96b6f3eb4d8dd8006e6095edbe1d84c36eb", - "url": "https://files.pythonhosted.org/packages/aa/ae/80cf7e44438ef3eb172013779cc73126587d336ea09abc9adefda7e696ad/dohq-artifactory-0.8.3.tar.gz" - } - ], - "project_name": "dohq-artifactory", - "requires_dists": [ - "PyJWT~=2.0", - "python-dateutil", - "requests" - ], - "requires_python": null, - "version": "0.8.3" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "3732814bbf20e9467b6d009fd47cb96fe12622976d4f3ee6a1126743664c6daa", - "url": "https://files.pythonhosted.org/packages/7c/c8/382b6a009cfcab3bf231004e595749d69ba8fedbc827997a6b4b87804b88/elmclient-0.26.2-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "def895bfe6587b748ca68cb0670e50c6a565a45e2ada68a3c7585470787f6d69", - "url": "https://files.pythonhosted.org/packages/d5/02/acd43a775c32accd63798493b68b2e3310439a7adc68a2a96df790255a2b/elmclient-0.26.2.tar.gz" - } - ], - "project_name": "elmclient", - "requires_dists": [ - "CacheControl", - "anytree", - "bump2version", - "colorama", - "cryptography", - "filelock", - "lark-parser", - "lockfile", - "lxml", - "openpyxl", - "python-dateutil", - "pytz", - "requests", - "requests-toolbelt", - "tqdm", - "twine", - "urllib3" - ], - "requires_python": null, - "version": "0.26.2" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada", - "url": "https://files.pythonhosted.org/packages/96/c2/3dd434b0108730014f1b96fd286040dc3bcb70066346f7e01ec2ac95865f/et_xmlfile-1.1.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c", - "url": "https://files.pythonhosted.org/packages/3d/5d/0413a31d184a20c763ad741cc7852a659bf15094c24840c5bdd1754765cd/et_xmlfile-1.1.0.tar.gz" - } - ], - "project_name": "et-xmlfile", - "requires_dists": [], - "requires_python": ">=3.6", - "version": "1.1.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", - "url": "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", - "url": "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz" - } - ], - "project_name": "exceptiongroup", - "requires_dists": [ - "pytest>=6; extra == \"test\"" - ], - "requires_python": ">=3.7", - "version": "1.2.2" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", - "url": "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", - "url": "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz" - } - ], - "project_name": "execnet", - "requires_dists": [ - "hatch; extra == \"testing\"", - "pre-commit; extra == \"testing\"", - "pytest; extra == \"testing\"", - "tox; extra == \"testing\"" - ], - "requires_python": ">=3.8", - "version": "2.1.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "f6ed4c963184f4c84dd5557ce8fece759a3724b37b80c6c4f20a2f63a4dc6609", - "url": "https://files.pythonhosted.org/packages/2f/95/f9310f35376024e1086c59cbb438d319fc9a4ef853289ce7c661539edbd4/filelock-3.16.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "81de9eb8453c769b63369f87f11131a7ab04e367f8d97ad39dc230daa07e3bec", - "url": "https://files.pythonhosted.org/packages/e6/76/3981447fd369539aba35797db99a8e2ff7ed01d9aa63e9344a31658b8d81/filelock-3.16.0.tar.gz" - } - ], - "project_name": "filelock", - "requires_dists": [ - "covdefaults>=2.3; extra == \"testing\"", - "coverage>=7.6.1; extra == \"testing\"", - "diff-cover>=9.1.1; extra == \"testing\"", - "furo>=2024.8.6; extra == \"docs\"", - "pytest-asyncio>=0.24; extra == \"testing\"", - "pytest-cov>=5; extra == \"testing\"", - "pytest-mock>=3.14; extra == \"testing\"", - "pytest-timeout>=2.3.1; extra == \"testing\"", - "pytest>=8.3.2; extra == \"testing\"", - "sphinx-autodoc-typehints!=1.23.4,>=2.4; extra == \"docs\"", - "sphinx>=8.0.2; extra == \"docs\"", - "typing-extensions>=4.12.2; python_version < \"3.11\" and extra == \"typing\"", - "virtualenv>=20.26.3; extra == \"testing\"" - ], - "requires_python": ">=3.8", - "version": "3.16.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1", - "url": "https://files.pythonhosted.org/packages/51/0b/0d7fee5919bccc1fdc1c2a7528b98f65c6f69b223a3fd8f809918c142c36/freezegun-1.5.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9", - "url": "https://files.pythonhosted.org/packages/2c/ef/722b8d71ddf4d48f25f6d78aa2533d505bf3eec000a7cacb8ccc8de61f2f/freezegun-1.5.1.tar.gz" - } - ], - "project_name": "freezegun", - "requires_dists": [ - "python-dateutil>=2.7" - ], - "requires_python": ">=3.7", - "version": "1.5.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", - "url": "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", - "url": "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz" - } - ], - "project_name": "h11", - "requires_dists": [ - "typing-extensions; python_version < \"3.8\"" - ], - "requires_python": ">=3.7", - "version": "0.14.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "7b67a3521ee91d53b7152d4ee3452811e1d2a6321975137762eb3d70063cc9d6", - "url": "https://files.pythonhosted.org/packages/ee/48/d53580d30b1fabf25d0d1fcc3f5b26d08d2ac75a1890ff6d262f9f027436/halo-0.0.31.tar.gz" - } - ], - "project_name": "halo", - "requires_dists": [ - "IPython==5.7.0; extra == \"ipython\"", - "backports.shutil-get-terminal-size>=1.0.0; python_version < \"3.3\"", - "colorama>=0.3.9", - "ipywidgets==7.1.0; extra == \"ipython\"", - "log-symbols>=0.0.14", - "six>=1.12.0", - "spinners>=0.0.24", - "termcolor>=1.1.0" - ], - "requires_python": ">=3.4", - "version": "0.0.31" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5", - "url": "https://files.pythonhosted.org/packages/78/d4/e5d7e4f2174f8a4d63c8897d79eb8fe2503f7ecc03282fee1fa2719c2704/httpcore-1.0.5-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61", - "url": "https://files.pythonhosted.org/packages/17/b0/5e8b8674f8d203335a62fdfcfa0d11ebe09e23613c3391033cbba35f7926/httpcore-1.0.5.tar.gz" - } - ], - "project_name": "httpcore", - "requires_dists": [ - "anyio<5.0,>=4.0; extra == \"asyncio\"", - "certifi", - "h11<0.15,>=0.13", - "h2<5,>=3; extra == \"http2\"", - "socksio==1.*; extra == \"socks\"", - "trio<0.26.0,>=0.22.0; extra == \"trio\"" - ], - "requires_python": ">=3.8", - "version": "1.0.5" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", - "url": "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", - "url": "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz" - } - ], - "project_name": "httpx", - "requires_dists": [ - "anyio", - "brotli; platform_python_implementation == \"CPython\" and extra == \"brotli\"", - "brotlicffi; platform_python_implementation != \"CPython\" and extra == \"brotli\"", - "certifi", - "click==8.*; extra == \"cli\"", - "h2<5,>=3; extra == \"http2\"", - "httpcore==1.*", - "idna", - "pygments==2.*; extra == \"cli\"", - "rich<14,>=10; extra == \"cli\"", - "sniffio", - "socksio==1.*; extra == \"socks\"", - "zstandard>=0.18.0; extra == \"zstd\"" - ], - "requires_python": ">=3.8", - "version": "0.27.2" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0", - "url": "https://files.pythonhosted.org/packages/e5/3e/741d8c82801c347547f8a2a06aa57dbb1992be9e948df2ea0eda2c8b79e8/idna-3.7-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", - "url": "https://files.pythonhosted.org/packages/21/ed/f86a79a07470cb07819390452f178b3bef1d375f2ec021ecfc709fc7cf07/idna-3.7.tar.gz" - } - ], - "project_name": "idna", - "requires_dists": [], - "requires_python": ">=3.5", - "version": "3.7" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", - "url": "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", - "url": "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz" - } - ], - "project_name": "importlib-metadata", - "requires_dists": [ - "flufl.flake8; extra == \"test\"", - "furo; extra == \"doc\"", - "importlib-resources>=1.3; python_version < \"3.9\" and extra == \"test\"", - "ipython; extra == \"perf\"", - "jaraco.packaging>=9.3; extra == \"doc\"", - "jaraco.test>=5.4; extra == \"test\"", - "jaraco.tidelift>=1.4; extra == \"doc\"", - "packaging; extra == \"test\"", - "pyfakefs; extra == \"test\"", - "pytest!=8.1.*,>=6; extra == \"test\"", - "pytest-checkdocs>=2.4; extra == \"check\"", - "pytest-cov; extra == \"cover\"", - "pytest-enabler>=2.2; extra == \"enabler\"", - "pytest-mypy; extra == \"type\"", - "pytest-perf>=0.9.2; extra == \"test\"", - "pytest-ruff>=0.2.1; sys_platform != \"cygwin\" and extra == \"check\"", - "rst.linker>=1.9; extra == \"doc\"", - "sphinx-lint; extra == \"doc\"", - "sphinx>=3.5; extra == \"doc\"", - "typing-extensions>=3.6.4; python_version < \"3.8\"", - "zipp>=3.20" - ], - "requires_python": ">=3.8", - "version": "8.5.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", - "url": "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", - "url": "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz" - } - ], - "project_name": "iniconfig", - "requires_dists": [], - "requires_python": ">=3.7", - "version": "2.0.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96", - "url": "https://files.pythonhosted.org/packages/b6/85/7882d311924cbcfc70b1890780763e36ff0b140c7e51c110fc59a532f087/isodate-0.6.1-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9", - "url": "https://files.pythonhosted.org/packages/db/7a/c0a56c7d56c7fa723988f122fa1f1ccf8c5c4ccc48efad0d214b49e5b1af/isodate-0.6.1.tar.gz" - } - ], - "project_name": "isodate", - "requires_dists": [ - "six" - ], - "requires_python": null, - "version": "0.6.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", - "url": "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", - "url": "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz" - } - ], - "project_name": "jaraco-classes", - "requires_dists": [ - "furo; extra == \"docs\"", - "jaraco.packaging>=9.3; extra == \"docs\"", - "jaraco.tidelift>=1.4; extra == \"docs\"", - "more-itertools", - "pytest-checkdocs>=2.4; extra == \"testing\"", - "pytest-cov; extra == \"testing\"", - "pytest-enabler>=2.2; extra == \"testing\"", - "pytest-mypy; extra == \"testing\"", - "pytest-ruff>=0.2.1; extra == \"testing\"", - "pytest>=6; extra == \"testing\"", - "rst.linker>=1.9; extra == \"docs\"", - "sphinx-lint; extra == \"docs\"", - "sphinx>=3.5; extra == \"docs\"" - ], - "requires_python": ">=3.8", - "version": "3.4.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", - "url": "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", - "url": "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz" - } - ], - "project_name": "jaraco-context", - "requires_dists": [ - "backports.tarfile; python_version < \"3.12\"", - "furo; extra == \"doc\"", - "jaraco.packaging>=9.3; extra == \"doc\"", - "jaraco.tidelift>=1.4; extra == \"doc\"", - "portend; extra == \"test\"", - "pytest!=8.1.*,>=6; extra == \"test\"", - "pytest-checkdocs>=2.4; extra == \"test\"", - "pytest-cov; extra == \"test\"", - "pytest-enabler>=2.2; extra == \"test\"", - "pytest-mypy; extra == \"test\"", - "pytest-ruff>=0.2.1; sys_platform != \"cygwin\" and extra == \"test\"", - "rst.linker>=1.9; extra == \"doc\"", - "sphinx-lint; extra == \"doc\"", - "sphinx>=3.5; extra == \"doc\"" - ], - "requires_python": ">=3.8", - "version": "6.0.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "c9d16a3ed4ccb5a889ad8e0b7a343401ee5b2a71cee6ed192d3f68bc351e94e3", - "url": "https://files.pythonhosted.org/packages/b1/54/7623e24ffc63730c3a619101361b08860c6b7c7cfc1aef6edb66d80ed708/jaraco.functools-4.0.2-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "3460c74cd0d32bf82b9576bbb3527c4364d5b27a21f5158a62aed6c4b42e23f5", - "url": "https://files.pythonhosted.org/packages/03/b1/6ca3c2052e584e9908a2c146f00378939b3c51b839304ab8ef4de067f042/jaraco_functools-4.0.2.tar.gz" - } - ], - "project_name": "jaraco-functools", - "requires_dists": [ - "furo; extra == \"doc\"", - "jaraco.classes; extra == \"test\"", - "jaraco.packaging>=9.3; extra == \"doc\"", - "jaraco.tidelift>=1.4; extra == \"doc\"", - "more-itertools", - "pytest!=8.1.*,>=6; extra == \"test\"", - "pytest-checkdocs>=2.4; extra == \"test\"", - "pytest-cov; extra == \"test\"", - "pytest-enabler>=2.2; extra == \"test\"", - "pytest-mypy; extra == \"test\"", - "pytest-ruff>=0.2.1; sys_platform != \"cygwin\" and extra == \"test\"", - "rst.linker>=1.9; extra == \"doc\"", - "sphinx-lint; extra == \"doc\"", - "sphinx>=3.5; extra == \"doc\"" - ], - "requires_python": ">=3.8", - "version": "4.0.2" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755", - "url": "https://files.pythonhosted.org/packages/ae/72/2a1e2290f1ab1e06f71f3d0f1646c9e4634e70e1d37491535e19266e8dc9/jeepney-0.8.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", - "url": "https://files.pythonhosted.org/packages/d6/f4/154cf374c2daf2020e05c3c6a03c91348d59b23c5366e968feb198306fdf/jeepney-0.8.0.tar.gz" - } - ], - "project_name": "jeepney", - "requires_dists": [ - "async-timeout; extra == \"test\"", - "async_generator; extra == \"trio\" and python_version == \"3.6\"", - "pytest-asyncio>=0.17; extra == \"test\"", - "pytest-trio; extra == \"test\"", - "pytest; extra == \"test\"", - "testpath; extra == \"test\"", - "trio; extra == \"test\"", - "trio; extra == \"trio\"" - ], - "requires_python": ">=3.7", - "version": "0.8.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "12190dc84dad00b8a6c0341f7e8a254b0f38785afdec022bd5941e1184a5a3fb", - "url": "https://files.pythonhosted.org/packages/4f/52/bb617020064261ba31cc965e932943458b7facfd9691ad7f76a2b631f44f/jira-3.8.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "63719c529a570aaa01c3373dbb5a104dab70381c5be447f6c27f997302fa335a", - "url": "https://files.pythonhosted.org/packages/78/b4/557e4c80c0ea12164ffeec0e29372c085bfb263faad53cef5e1455523bec/jira-3.8.0.tar.gz" - } - ], - "project_name": "jira", - "requires_dists": [ - "MarkupSafe>=0.23; extra == \"test\"", - "Pillow>=2.1.0", - "PyJWT; extra == \"opt\"", - "PyYAML>=5.1; extra == \"test\"", - "defusedxml", - "docutils>=0.12; extra == \"test\"", - "filemagic>=1.6; extra == \"opt\"", - "flaky; extra == \"test\"", - "furo; extra == \"docs\"", - "ipython>=4.0.0; extra == \"cli\"", - "keyring; extra == \"cli\"", - "oauthlib; extra == \"test\"", - "packaging", - "parameterized>=0.8.1; extra == \"test\"", - "pytest-cache; extra == \"test\"", - "pytest-cov; extra == \"test\"", - "pytest-instafail; extra == \"test\"", - "pytest-sugar; extra == \"test\"", - "pytest-timeout>=1.3.1; extra == \"test\"", - "pytest-xdist>=2.2; extra == \"test\"", - "pytest>=6.0.0; extra == \"test\"", - "requests-futures>=0.9.7; extra == \"async\"", - "requests-jwt; extra == \"opt\"", - "requests-kerberos; extra == \"opt\"", - "requests-mock; extra == \"test\"", - "requests-oauthlib>=1.1.0", - "requests-toolbelt", - "requests>=2.10.0", - "requires.io; extra == \"test\"", - "sphinx-copybutton; extra == \"docs\"", - "sphinx>=5.0.0; extra == \"docs\"", - "tenacity; extra == \"test\"", - "typing-extensions>=3.7.4.2", - "wheel>=0.24.0; extra == \"test\"", - "yanc>=0.3.3; extra == \"test\"" - ], - "requires_python": ">=3.8", - "version": "3.8.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "afa66939d834b0ce063f57d9895e8036ffc41c4bd90e4a99631e5f261d9b518e", - "url": "https://files.pythonhosted.org/packages/ff/33/135c0c33565b6d5c3010d047710837427dd24c9adbc9ca090f3f92df446e/jiter-0.5.0-cp310-cp310-musllinux_1_1_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "2a063f71c4b06225543dddadbe09d203dc0c95ba352d8b85f1221173480a71d5", - "url": "https://files.pythonhosted.org/packages/07/2d/5bdaddfefc44f91af0f3340e75ef327950d790c9f86490757ac8b395c074/jiter-0.5.0-cp310-cp310-macosx_11_0_arm64.whl" - }, - { - "algorithm": "sha256", - "hash": "503b2c27d87dfff5ab717a8200fbbcf4714516c9d85558048b1fc14d2de7d8dc", - "url": "https://files.pythonhosted.org/packages/41/6a/c038077509d67fe876c724bfe9ad15334593851a7def0d84518172bdd44a/jiter-0.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "c95980207b3998f2c3b3098f357994d3fd7661121f30669ca7cb945f09510a87", - "url": "https://files.pythonhosted.org/packages/56/9e/cbd8f6612346c38cc42e41e35cda19ce78f5b12e4106d1186e8e95ee839b/jiter-0.5.0-cp310-cp310-musllinux_1_1_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "6d1f3d27cce923713933a844872d213d244e09b53ec99b7a7fdf73d543529d6d", - "url": "https://files.pythonhosted.org/packages/67/0d/d82673814eb38c208b7881581df596e680f8c2c003e2b80c25ca58975ee4/jiter-0.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "a25fbd8a5a58061e433d6fae6d5298777c0814a8bcefa1e5ecfff20c594bd749", - "url": "https://files.pythonhosted.org/packages/73/a1/9ef99a279c72a031dbe8a4085db41e3521ae01ab0058651d6ccc809a5e93/jiter-0.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl" - }, - { - "algorithm": "sha256", - "hash": "acc0d5b8b3dd12e91dd184b87273f864b363dfabc90ef29a1092d269f18c7e28", - "url": "https://files.pythonhosted.org/packages/74/bd/964485231deaec8caa6599f3f27c8787a54e9f9373ae80dcfbda2ad79c02/jiter-0.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "63314832e302cc10d8dfbda0333a384bf4bcfce80d65fe99b0f3c0da8945a91a", - "url": "https://files.pythonhosted.org/packages/76/6f/21576071b8b056ef743129b9dacf9da65e328b58766f3d1ea265e966f000/jiter-0.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" - }, - { - "algorithm": "sha256", - "hash": "b599f4e89b3def9a94091e6ee52e1d7ad7bc33e238ebb9c4c63f211d74822c3f", - "url": "https://files.pythonhosted.org/packages/af/09/f659fc67d6aaa82c56432c4a7cc8365fff763acbf1c8f24121076617f207/jiter-0.5.0-cp310-cp310-macosx_10_12_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "c22541f0b672f4d741382a97c65609332a783501551445ab2df137ada01e019e", - "url": "https://files.pythonhosted.org/packages/cf/4f/6353179174db10254549bbf2eb2c7ea102e59e0460ee374adb12071c274d/jiter-0.5.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" - }, - { - "algorithm": "sha256", - "hash": "1d916ba875bcab5c5f7d927df998c4cb694d27dceddf3392e58beaf10563368a", - "url": "https://files.pythonhosted.org/packages/d7/1a/aa64be757afc614484b370a4d9fc1747dc9237b37ce464f7f9d9ca2a3d38/jiter-0.5.0.tar.gz" - } - ], - "project_name": "jiter", - "requires_dists": [], - "requires_python": ">=3.8", - "version": "0.5.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", - "url": "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", - "url": "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz" - } - ], - "project_name": "jsonpatch", - "requires_dists": [ - "jsonpointer>=1.9" - ], - "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7", - "version": "1.33" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", - "url": "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", - "url": "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz" - } - ], - "project_name": "jsonpointer", - "requires_dists": [], - "requires_python": ">=3.7", - "version": "3.0.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", - "url": "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", - "url": "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz" - } - ], - "project_name": "jsonschema", - "requires_dists": [ - "attrs>=22.2.0", - "fqdn; extra == \"format\"", - "fqdn; extra == \"format-nongpl\"", - "idna; extra == \"format\"", - "idna; extra == \"format-nongpl\"", - "importlib-resources>=1.4.0; python_version < \"3.9\"", - "isoduration; extra == \"format\"", - "isoduration; extra == \"format-nongpl\"", - "jsonpointer>1.13; extra == \"format\"", - "jsonpointer>1.13; extra == \"format-nongpl\"", - "jsonschema-specifications>=2023.03.6", - "pkgutil-resolve-name>=1.3.10; python_version < \"3.9\"", - "referencing>=0.28.4", - "rfc3339-validator; extra == \"format\"", - "rfc3339-validator; extra == \"format-nongpl\"", - "rfc3986-validator>0.1.0; extra == \"format-nongpl\"", - "rfc3987; extra == \"format\"", - "rpds-py>=0.7.1", - "uri-template; extra == \"format\"", - "uri-template; extra == \"format-nongpl\"", - "webcolors>=1.11; extra == \"format\"", - "webcolors>=24.6.0; extra == \"format-nongpl\"" - ], - "requires_python": ">=3.8", - "version": "4.23.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c", - "url": "https://files.pythonhosted.org/packages/ee/07/44bd408781594c4d0a027666ef27fab1e441b109dc3b76b4f836f8fd04fe/jsonschema_specifications-2023.12.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc", - "url": "https://files.pythonhosted.org/packages/f8/b9/cc0cc592e7c195fb8a650c1d5990b10175cf13b4c97465c72ec841de9e4b/jsonschema_specifications-2023.12.1.tar.gz" - } - ], - "project_name": "jsonschema-specifications", - "requires_dists": [ - "importlib-resources>=1.4.0; python_version < \"3.9\"", - "referencing>=0.31.0" - ], - "requires_python": ">=3.8", - "version": "2023.12.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "8d963da00ccdf06e356acd9bf3b743208878751032d8599c6cc89eb51310ffae", - "url": "https://files.pythonhosted.org/packages/63/42/ea8c9726e5ee5ff0731978aaf7cd5fa16674cf549c46279b279d7167c2b4/keyring-25.3.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "8d85a1ea5d6db8515b59e1c5d1d1678b03cf7fc8b8dcfb1651e8c4a524eb42ef", - "url": "https://files.pythonhosted.org/packages/32/30/bfdde7294ba6bb2f519950687471dc6a0996d4f77ab30d75c841fa4994ed/keyring-25.3.0.tar.gz" - } - ], - "project_name": "keyring", - "requires_dists": [ - "SecretStorage>=3.2; sys_platform == \"linux\"", - "furo; extra == \"doc\"", - "importlib-metadata>=4.11.4; python_version < \"3.12\"", - "importlib-resources; python_version < \"3.9\"", - "jaraco.classes", - "jaraco.context", - "jaraco.functools", - "jaraco.packaging>=9.3; extra == \"doc\"", - "jaraco.tidelift>=1.4; extra == \"doc\"", - "jeepney>=0.4.2; sys_platform == \"linux\"", - "pyfakefs; extra == \"test\"", - "pytest!=8.1.*,>=6; extra == \"test\"", - "pytest-checkdocs>=2.4; extra == \"test\"", - "pytest-cov; extra == \"test\"", - "pytest-enabler>=2.2; extra == \"test\"", - "pytest-mypy; extra == \"test\"", - "pytest-ruff>=0.2.1; sys_platform != \"cygwin\" and extra == \"test\"", - "pywin32-ctypes>=0.2.0; sys_platform == \"win32\"", - "rst.linker>=1.9; extra == \"doc\"", - "shtab>=1.1.0; extra == \"completion\"", - "sphinx-lint; extra == \"doc\"", - "sphinx>=3.5; extra == \"doc\"" - ], - "requires_python": ">=3.8", - "version": "25.3.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "71fff5cafa4b9c82a3a716e985f071383be452c35d8cc3169b3a393e6857fc99", - "url": "https://files.pythonhosted.org/packages/e9/20/4d9427db7e2c78b6be6d4300fc22da128c8ca07444d7e23ad72b8b7675b9/langchain_core-0.2.40-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "c838ea0c0b73475a8e58ced3e306b6d926ef063721abd164f237c8664916f502", - "url": "https://files.pythonhosted.org/packages/2a/27/17680c88690e895c2b7d08166c9aeff6ac734c5cf59eef9a9d828f5cf3c2/langchain_core-0.2.40.tar.gz" - } - ], - "project_name": "langchain-core", - "requires_dists": [ - "PyYAML>=5.3", - "jsonpatch<2.0,>=1.33", - "langsmith<0.2.0,>=0.1.112", - "packaging<25,>=23.2", - "pydantic<3,>=1; python_full_version < \"3.12.4\"", - "pydantic<3.0.0,>=2.7.4; python_full_version >= \"3.12.4\"", - "tenacity!=8.4.0,<9.0.0,>=8.1.0", - "typing-extensions>=4.7" - ], - "requires_python": "<4.0,>=3.8.1", - "version": "0.2.40" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "179b6f21e01fc72ebc034ec725f8c5dcef4a81709919278e6fa4f43605df5d82", - "url": "https://files.pythonhosted.org/packages/86/5f/8c2934abdfd860fec2b286dc0773df07815c7aed3ea90302c69508edc192/langchain_ollama-0.1.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "91b3b6cfcc90890c683995520d84210ebd2cee8c0f2cd0a5ffde9f1ffbee2f94", - "url": "https://files.pythonhosted.org/packages/2f/e1/8b3318d44eb3d997d097ceef7b7d955eb626933adb45f3267098286724a8/langchain_ollama-0.1.1.tar.gz" - } - ], - "project_name": "langchain-ollama", - "requires_dists": [ - "langchain-core<0.3.0,>=0.2.20", - "ollama<1,>=0.3.0" - ], - "requires_python": "<4.0,>=3.8.1", - "version": "0.1.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "232ebfe90b1898ef7cf181e364d45191edcf04bfc31b292ecaa1d2121942c28e", - "url": "https://files.pythonhosted.org/packages/aa/1c/aebc87392546cb90e143e299a3454b2fcecd8055ad73d6c09a43f5994f71/langchain_openai-0.1.20-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "2c91e9f771541076b138e65dd4c5427b26957a2272406a7f4ee747d7896f9b35", - "url": "https://files.pythonhosted.org/packages/42/d0/235841108357f0772255fffa64a74af1b4f5b331dcdbe053093e486494f4/langchain_openai-0.1.20.tar.gz" - } - ], - "project_name": "langchain-openai", - "requires_dists": [ - "langchain-core<0.3.0,>=0.2.26", - "openai<2.0.0,>=1.32.0", - "tiktoken<1,>=0.7" - ], - "requires_python": "<4.0,>=3.8.1", - "version": "0.1.20" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "fdb1ac8a671d3904201bfeea197d87bded46a10d08f1034af464211872e29893", - "url": "https://files.pythonhosted.org/packages/fd/37/8c3cec53dc27eafdb7af026fe49b528ff994550f23b355d68b4c0a7ab87d/langsmith-0.1.121-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "e9381b82a5bd484af9a51c3e96faea572746b8d617b070c1cda40cbbe48e33df", - "url": "https://files.pythonhosted.org/packages/7e/78/d5e31ed186b53cdabeca6fb4a01ce27c9b0cccda5ac2408d296dc22aeb11/langsmith-0.1.121.tar.gz" - } - ], - "project_name": "langsmith", - "requires_dists": [ - "httpx<1,>=0.23.0", - "orjson<4.0.0,>=3.9.14", - "pydantic<3,>=1; python_full_version < \"3.12.4\"", - "pydantic<3.0.0,>=2.7.4; python_full_version >= \"3.12.4\"", - "requests<3,>=2" - ], - "requires_python": "<4.0,>=3.8.1", - "version": "0.1.121" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "0eaf30cb5ba787fe404d73a7d6e61df97b21d5a63ac26c5008c78a494373c675", - "url": "https://files.pythonhosted.org/packages/76/00/90f05db333fe1aa6b6ffea83a35425b7d53ea95c8bba0b1597f226cf1d5f/lark_parser-0.12.0-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "15967db1f1214013dca65b1180745047b9be457d73da224fcda3d9dd4e96a138", - "url": "https://files.pythonhosted.org/packages/5a/ee/fd1192d7724419ddfe15b6f17d1c8742800d4de917c0adac3b6aaf22e921/lark-parser-0.12.0.tar.gz" - } - ], - "project_name": "lark-parser", - "requires_dists": [ - "atomicwrites; extra == \"atomic-cache\"", - "js2py; extra == \"nearley\"", - "regex; extra == \"regex\"" - ], - "requires_python": null, - "version": "0.12.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "97904b9185c7bbb1e98799606fa7424191c375e70ba63a524b6f7100e42ddc46", - "url": "https://files.pythonhosted.org/packages/91/84/a7cf5dfa141501a20cb63595f02edfe38e0db2e3cc34e4f3cd273cc285df/license_expression-30.3.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "60d5bec1f3364c256a92b9a08583d7ea933c7aa272c8d36d04144a89a3858c01", - "url": "https://files.pythonhosted.org/packages/57/8b/dbe230196eee2de208ba87dcfae69c46db9d7ed70e2f30f143bf994ee075/license_expression-30.3.1.tar.gz" - } - ], - "project_name": "license-expression", - "requires_dists": [ - "Sphinx>=5.0.2; extra == \"docs\"", - "black; extra == \"testing\"", - "boolean.py>=4.0", - "doc8>=0.11.2; extra == \"docs\"", - "isort; extra == \"testing\"", - "pytest!=7.0.0,>=6; extra == \"testing\"", - "pytest-xdist>=2; extra == \"testing\"", - "sphinx-autobuild; extra == \"docs\"", - "sphinx-copybutton; extra == \"docs\"", - "sphinx-reredirects>=0.1.2; extra == \"docs\"", - "sphinx-rtd-dark-mode>=1.3.0; extra == \"docs\"", - "sphinx-rtd-theme>=1.0.0; extra == \"docs\"", - "sphinxcontrib-apidoc>=0.4.0; extra == \"docs\"", - "twine; extra == \"testing\"" - ], - "requires_python": ">=3.8", - "version": "30.3.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa", - "url": "https://files.pythonhosted.org/packages/c8/22/9460e311f340cb62d26a38c419b1381b8593b0bb6b5d1f056938b086d362/lockfile-0.12.2-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799", - "url": "https://files.pythonhosted.org/packages/17/47/72cb04a58a35ec495f96984dddb48232b551aafb95bde614605b754fe6f7/lockfile-0.12.2.tar.gz" - } - ], - "project_name": "lockfile", - "requires_dists": [], - "requires_python": null, - "version": "0.12.2" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "4952106ff8b605ab7d5081dd2c7e6ca7374584eff7086f499c06edd1ce56dcca", - "url": "https://files.pythonhosted.org/packages/28/5d/d710c38be68b0fb54e645048fe359c3904cc3cb64b2de9d40e1712bf110c/log_symbols-0.0.14-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "cf0bbc6fe1a8e53f0d174a716bc625c4f87043cc21eb55dd8a740cfe22680556", - "url": "https://files.pythonhosted.org/packages/45/87/e86645d758a4401c8c81914b6a88470634d1785c9ad09823fa4a1bd89250/log_symbols-0.0.14.tar.gz" - } - ], - "project_name": "log-symbols", - "requires_dists": [ - "colorama>=0.3.9", - "enum34==1.1.6; python_version < \"3.4\"" - ], - "requires_python": null, - "version": "0.0.14" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "b93aa30099fa6860d4727f1b81f8718e965bb96253fa190fab2077aaad6d15d3", - "url": "https://files.pythonhosted.org/packages/71/bd/337f7a0cd2628c4c77512d78e26f93b13c327a2ddf2132001dd78c000bf4/loguru-0.7.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "1612053ced6ae84d7959dd7d5e431a0532642237ec21f7fd83ac73fe539e03e1", - "url": "https://files.pythonhosted.org/packages/0c/1d/697cbb4ae54217784c1c4805696efb2fd7a1cbbe4827264a80a49e52b828/loguru-0.7.0.tar.gz" - } - ], - "project_name": "loguru", - "requires_dists": [ - "Sphinx==5.3.0; python_version >= \"3.8\" and extra == \"dev\"", - "aiocontextvars>=0.2.0; python_version < \"3.7\"", - "colorama==0.4.5; python_version < \"3.8\" and extra == \"dev\"", - "colorama==0.4.6; python_version >= \"3.8\" and extra == \"dev\"", - "colorama>=0.3.4; sys_platform == \"win32\"", - "freezegun==1.1.0; python_version < \"3.8\" and extra == \"dev\"", - "freezegun==1.2.2; python_version >= \"3.8\" and extra == \"dev\"", - "mypy==v0.910; python_version < \"3.6\" and extra == \"dev\"", - "mypy==v0.971; (python_version >= \"3.6\" and python_version < \"3.7\") and extra == \"dev\"", - "mypy==v0.990; python_version >= \"3.7\" and extra == \"dev\"", - "pre-commit==3.2.1; python_version >= \"3.8\" and extra == \"dev\"", - "pytest-cov==2.12.1; python_version < \"3.8\" and extra == \"dev\"", - "pytest-cov==4.0.0; python_version >= \"3.8\" and extra == \"dev\"", - "pytest-mypy-plugins==1.10.1; python_version >= \"3.8\" and extra == \"dev\"", - "pytest-mypy-plugins==1.9.3; (python_version >= \"3.6\" and python_version < \"3.8\") and extra == \"dev\"", - "pytest==6.1.2; python_version < \"3.8\" and extra == \"dev\"", - "pytest==7.2.1; python_version >= \"3.8\" and extra == \"dev\"", - "sphinx-autobuild==2021.3.14; python_version >= \"3.8\" and extra == \"dev\"", - "sphinx-rtd-theme==1.2.0; python_version >= \"3.8\" and extra == \"dev\"", - "tox==3.27.1; python_version < \"3.8\" and extra == \"dev\"", - "tox==4.4.6; python_version >= \"3.8\" and extra == \"dev\"", - "win32-setctime>=1.0.0; sys_platform == \"win32\"" - ], - "requires_python": ">=3.5", - "version": "0.7.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "606d445feeb0856c2b424405236a01c71af7c97e5fe42fbc778634faef2b47e4", - "url": "https://files.pythonhosted.org/packages/60/8a/fc26540cc544a989277bdedeb098604ea7da998ebfd7bd0e94a3a936a817/lxml-4.9.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "bdd9abccd0927673cffe601d2c6cdad1c9321bf3437a2f507d6b037ef91ea307", - "url": "https://files.pythonhosted.org/packages/18/02/5e28cbbfff53d4e227114f507cb03d02973f0ba0e5ea3e11aea66fcfe471/lxml-4.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "056a17eaaf3da87a05523472ae84246f87ac2f29a53306466c22e60282e54ff8", - "url": "https://files.pythonhosted.org/packages/50/e4/e37f7f61ceaf0b29e7c5bf78fb1927818a52c986546459d33ccd742f2b8e/lxml-4.9.4-cp310-cp310-macosx_11_0_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "aaa5c173a26960fe67daa69aa93d6d6a1cd714a6eb13802d4e4bd1d24a530644", - "url": "https://files.pythonhosted.org/packages/71/35/a8c656eac628ba9148852fdb17ae4b0ac217619aef6d25a15137ab4939f7/lxml-4.9.4-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "b1541e50b78e15fa06a2670157a1962ef06591d4c998b998047fff5e3236880e", - "url": "https://files.pythonhosted.org/packages/84/14/c2070b5e37c650198de8328467dd3d1681e80986f81ba0fea04fc4ec9883/lxml-4.9.4.tar.gz" - }, - { - "algorithm": "sha256", - "hash": "de362ac8bc962408ad8fae28f3967ce1a262b5d63ab8cefb42662566737f1dc7", - "url": "https://files.pythonhosted.org/packages/9d/fd/16731c76b45117c9b9e0bc0ee3b6ffbf70d4d737549ce5fdc876e9e23064/lxml-4.9.4-cp310-cp310-musllinux_1_1_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "647459b23594f370c1c01768edaa0ba0959afc39caeeb793b43158bb9bb6a663", - "url": "https://files.pythonhosted.org/packages/a1/e8/665ea1d18f20a7c02d5561ea8500ded3148f3e9d8194efa2545e56fab059/lxml-4.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "a602ed9bd2c7d85bd58592c28e101bd9ff9c718fbde06545a70945ffd5d11868", - "url": "https://files.pythonhosted.org/packages/c5/ab/39f7110584a0f8357cef9f3c92c98320d25efc6d51506b770cf9a5f1da6c/lxml-4.9.4-cp310-cp310-musllinux_1_1_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "00e91573183ad273e242db5585b52670eddf92bacad095ce25c1e682da14ed91", - "url": "https://files.pythonhosted.org/packages/dd/3f/9bbd85d5baeae82b13f8e6b022f9bf34ffb5074f4f56d100854a5d66910d/lxml-4.9.4-cp310-cp310-manylinux_2_28_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "f6c35b2f87c004270fa2e703b872fcc984d714d430b305145c39d53074e1ffe0", - "url": "https://files.pythonhosted.org/packages/f6/54/e7cc9b0019209fc553d5cb4cb460df25513754666665d5cd0f0ec19685ed/lxml-4.9.4-pp310-pypy310_pp73-macosx_11_0_x86_64.whl" - } - ], - "project_name": "lxml", - "requires_dists": [ - "BeautifulSoup4; extra == \"htmlsoup\"", - "Cython==0.29.37; extra == \"source\"", - "cssselect>=0.7; extra == \"cssselect\"", - "html5lib; extra == \"html5\"" - ], - "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7", - "version": "4.9.4" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", - "url": "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", - "url": "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz" - } - ], - "project_name": "markdown-it-py", - "requires_dists": [ - "commonmark~=0.9; extra == \"compare\"", - "coverage; extra == \"testing\"", - "gprof2dot; extra == \"profiling\"", - "jupyter_sphinx; extra == \"rtd\"", - "linkify-it-py<3,>=1; extra == \"linkify\"", - "markdown~=3.4; extra == \"compare\"", - "mdit-py-plugins; extra == \"plugins\"", - "mdit-py-plugins; extra == \"rtd\"", - "mdurl~=0.1", - "mistletoe~=1.0; extra == \"compare\"", - "mistune~=2.0; extra == \"compare\"", - "myst-parser; extra == \"rtd\"", - "panflute~=2.3; extra == \"compare\"", - "pre-commit~=3.0; extra == \"code-style\"", - "psutil; extra == \"benchmarking\"", - "pytest-benchmark; extra == \"benchmarking\"", - "pytest-cov; extra == \"testing\"", - "pytest-regressions; extra == \"testing\"", - "pytest; extra == \"benchmarking\"", - "pytest; extra == \"testing\"", - "pyyaml; extra == \"rtd\"", - "sphinx-copybutton; extra == \"rtd\"", - "sphinx-design; extra == \"rtd\"", - "sphinx; extra == \"rtd\"", - "sphinx_book_theme; extra == \"rtd\"" - ], - "requires_python": ">=3.8", - "version": "3.0.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", - "url": "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", - "url": "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz" - } - ], - "project_name": "mdurl", - "requires_dists": [], - "requires_python": ">=3.7", - "version": "0.1.2" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744", - "url": "https://files.pythonhosted.org/packages/6b/20/471f41173930550f279ccb65596a5ac19b9ac974a8d93679bcd3e0c31498/mock-5.1.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d", - "url": "https://files.pythonhosted.org/packages/66/ab/41d09a46985ead5839d8be987acda54b5bb93f713b3969cc0be4f81c455b/mock-5.1.0.tar.gz" - } - ], - "project_name": "mock", - "requires_dists": [ - "blurb; extra == \"build\"", - "pytest-cov; extra == \"test\"", - "pytest; extra == \"test\"", - "sphinx; extra == \"docs\"", - "twine; extra == \"build\"", - "wheel; extra == \"build\"" - ], - "requires_python": ">=3.6", - "version": "5.1.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", - "url": "https://files.pythonhosted.org/packages/48/7e/3a64597054a70f7c86eb0a7d4fc315b8c1ab932f64883a297bdffeb5f967/more_itertools-10.5.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6", - "url": "https://files.pythonhosted.org/packages/51/78/65922308c4248e0eb08ebcbe67c95d48615cc6f27854b6f2e57143e9178f/more-itertools-10.5.0.tar.gz" - } - ], - "project_name": "more-itertools", - "requires_dists": [], - "requires_python": ">=3.8", - "version": "10.5.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b", - "url": "https://files.pythonhosted.org/packages/e4/13/7646f14f06838b406cf5a6ddbb7e8dc78b4996d891ab3b93c33d1ccc8678/msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5", - "url": "https://files.pythonhosted.org/packages/02/95/dc0044b439b518236aaf012da4677c1b8183ce388411ad1b1e63c32d8979/msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5", - "url": "https://files.pythonhosted.org/packages/08/52/bf4fbf72f897a23a56b822997a72c16de07d8d56d7bf273242f884055682/msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl" - }, - { - "algorithm": "sha256", - "hash": "73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b", - "url": "https://files.pythonhosted.org/packages/32/d3/c152e0c55fead87dd948d4b29879b0f14feeeec92ef1fd2ec21b107c3f49/msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd", - "url": "https://files.pythonhosted.org/packages/4b/f9/a892a6038c861fa849b11a2bb0502c07bc698ab6ea53359e5771397d883b/msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl" - }, - { - "algorithm": "sha256", - "hash": "64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68", - "url": "https://files.pythonhosted.org/packages/cb/a0/3d093b248837094220e1edc9ec4337de3443b1cfeeb6e0896af8ccc4cc7a/msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e", - "url": "https://files.pythonhosted.org/packages/cb/d0/7555686ae7ff5731205df1012ede15dd9d927f6227ea151e901c7406af4f/msgpack-1.1.0.tar.gz" - }, - { - "algorithm": "sha256", - "hash": "e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f", - "url": "https://files.pythonhosted.org/packages/d9/2c/82e73506dd55f9e43ac8aa007c9dd088c6f0de2aa19e8f7330e6a65879fc/msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d", - "url": "https://files.pythonhosted.org/packages/df/7a/d174cc6a3b6bb85556e6a046d3193294a92f9a8e583cdbd46dc8a1d7e7f4/msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e", - "url": "https://files.pythonhosted.org/packages/ff/75/09081792db60470bef19d9c2be89f024d366b1e1973c197bb59e6aabc647/msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - } - ], - "project_name": "msgpack", - "requires_dists": [], - "requires_python": ">=3.8", - "version": "1.1.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32", - "url": "https://files.pythonhosted.org/packages/15/cf/f2966a2638144491f8696c27320d5219f48a072715075d168b31d3237720/msrest-0.7.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9", - "url": "https://files.pythonhosted.org/packages/68/77/8397c8fb8fc257d8ea0fa66f8068e073278c65f05acb17dcb22a02bfdc42/msrest-0.7.1.zip" - } - ], - "project_name": "msrest", - "requires_dists": [ - "aiodns; python_version >= \"3.5\" and extra == \"async\"", - "aiohttp>=3.0; python_version >= \"3.5\" and extra == \"async\"", - "azure-core>=1.24.0", - "certifi>=2017.4.17", - "isodate>=0.6.0", - "requests-oauthlib>=0.5.0", - "requests~=2.16" - ], - "requires_python": ">=3.6", - "version": "0.7.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4", - "url": "https://files.pythonhosted.org/packages/3d/9a/e13addb8d652cb068f835ac2746d9d42f85b730092f581bb17e2059c28f1/mypy-1.4.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd", - "url": "https://files.pythonhosted.org/packages/04/5c/deeac94fcccd11aa621e6b350df333e1b809b11443774ea67582cc0205da/mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878", - "url": "https://files.pythonhosted.org/packages/a7/24/6f0df1874118839db1155fed62a4bd7e80c181367ff8ea07d40fbaffcfb4/mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl" - }, - { - "algorithm": "sha256", - "hash": "9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b", - "url": "https://files.pythonhosted.org/packages/b3/28/d8a8233ff167d06108e53b7aefb4a8d7350adbbf9d7abd980f17fdb7a3a6/mypy-1.4.1.tar.gz" - }, - { - "algorithm": "sha256", - "hash": "8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc", - "url": "https://files.pythonhosted.org/packages/e5/2f/de3c455c54e8cf5e37ea38705c1920f2df470389f8fc051084d2dd8c9c59/mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8", - "url": "https://files.pythonhosted.org/packages/fb/3b/1c7363863b56c059f60a1dfdca9ac774a22ba64b7a4da0ee58ee53e5243f/mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl" - } - ], - "project_name": "mypy", - "requires_dists": [ - "lxml; extra == \"reports\"", - "mypy-extensions>=1.0.0", - "pip; extra == \"install-types\"", - "psutil>=4.0; extra == \"dmypy\"", - "tomli>=1.1.0; python_version < \"3.11\"", - "typed-ast<2,>=1.4.0; extra == \"python2\"", - "typed-ast<2,>=1.4.0; python_version < \"3.8\"", - "typing-extensions>=4.1.0" - ], - "requires_python": ">=3.7", - "version": "1.4.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", - "url": "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", - "url": "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz" - } - ], - "project_name": "mypy-extensions", - "requires_dists": [], - "requires_python": ">=3.5", - "version": "1.0.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "36c95d4b70530b320b365659bb5034341316e6a9b30f0b25fa9c9eff4c27a204", - "url": "https://files.pythonhosted.org/packages/eb/61/73a007c74c37895fdf66e0edcd881f5eaa17a348ff02f4bb4bc906d61085/nh3-0.2.18-cp37-abi3-musllinux_1_2_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "0411beb0589eacb6734f28d5497ca2ed379eafab8ad8c84b31bb5c34072b7164", - "url": "https://files.pythonhosted.org/packages/05/2b/85977d9e11713b5747595ee61f381bc820749daf83f07b90b6c9964cf932/nh3-0.2.18-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" - }, - { - "algorithm": "sha256", - "hash": "de3ceed6e661954871d6cd78b410213bdcb136f79aafe22aa7182e028b8c7307", - "url": "https://files.pythonhosted.org/packages/1b/63/6ab90d0e5225ab9780f6c9fb52254fa36b52bb7c188df9201d05b647e5e1/nh3-0.2.18-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "7b7c2a3c9eb1a827d42539aa64091640bd275b81e097cd1d8d82ef91ffa2e811", - "url": "https://files.pythonhosted.org/packages/2c/b6/42fc3c69cabf86b6b81e4c051a9b6e249c5ba9f8155590222c2622961f58/nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "42c64511469005058cd17cc1537578eac40ae9f7200bedcfd1fc1a05f4f8c200", - "url": "https://files.pythonhosted.org/packages/45/b9/833f385403abaf0023c6547389ec7a7acf141ddd9d1f21573723a6eab39a/nh3-0.2.18-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4", - "url": "https://files.pythonhosted.org/packages/62/73/10df50b42ddb547a907deeb2f3c9823022580a7a47281e8eae8e003a9639/nh3-0.2.18.tar.gz" - }, - { - "algorithm": "sha256", - "hash": "c8b3a1cebcba9b3669ed1a84cc65bf005728d2f0bc1ed2a6594a992e817f3a50", - "url": "https://files.pythonhosted.org/packages/63/1d/842fed85cf66c973be0aed8770093d6a04741f65e2c388ddd4c07fd3296e/nh3-0.2.18-cp37-abi3-musllinux_1_2_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "5f36b271dae35c465ef5e9090e1fdaba4a60a56f0bb0ba03e0932a66f28b9189", - "url": "https://files.pythonhosted.org/packages/72/f2/5c894d5265ab80a97c68ca36f25c8f6f0308abac649aaf152b74e7e854a8/nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl" - }, - { - "algorithm": "sha256", - "hash": "f0eca9ca8628dbb4e916ae2491d72957fdd35f7a5d326b7032a345f111ac07fe", - "url": "https://files.pythonhosted.org/packages/a3/da/0c4e282bc3cff4a0adf37005fa1fb42257673fbc1bbf7d1ff639ec3d255a/nh3-0.2.18-cp37-abi3-musllinux_1_2_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "6955369e4d9f48f41e3f238a9e60f9410645db7e07435e62c6a9ea6135a4907f", - "url": "https://files.pythonhosted.org/packages/a4/17/59391c28580e2c32272761629893e761442fc7666da0b1cdb479f3b67b88/nh3-0.2.18-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "34c03fa78e328c691f982b7c03d4423bdfd7da69cd707fe572f544cf74ac23ad", - "url": "https://files.pythonhosted.org/packages/ab/a7/375afcc710dbe2d64cfbd69e31f82f3e423d43737258af01f6a56d844085/nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" - }, - { - "algorithm": "sha256", - "hash": "14c5a72e9fe82aea5fe3072116ad4661af5cf8e8ff8fc5ad3450f123e4925e86", - "url": "https://files.pythonhosted.org/packages/b3/89/1daff5d9ba5a95a157c092c7c5f39b8dd2b1ddb4559966f808d31cfb67e0/nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl" - }, - { - "algorithm": "sha256", - "hash": "19aaba96e0f795bd0a6c56291495ff59364f4300d4a39b29a0abc9cb3774a84b", - "url": "https://files.pythonhosted.org/packages/c2/a8/3bb02d0c60a03ad3a112b76c46971e9480efa98a8946677b5a59f60130ca/nh3-0.2.18-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl" - }, - { - "algorithm": "sha256", - "hash": "3a157ab149e591bb638a55c8c6bcb8cdb559c8b12c13a8affaba6cedfe51713a", - "url": "https://files.pythonhosted.org/packages/de/81/c291231463d21da5f8bba82c8167a6d6893cc5419b0639801ee5d3aeb8a9/nh3-0.2.18-cp37-abi3-musllinux_1_2_armv7l.whl" - } - ], - "project_name": "nh3", - "requires_dists": [], - "requires_python": null, - "version": "0.2.18" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "2d926b52ba1367f9acb76b0df6ed21f0b16a1ad87c6720a1121674e5cf63e2b6", - "url": "https://files.pythonhosted.org/packages/6f/72/38f9a536bdb5bfb1682f2520f133ec6e08dde8bcca1f632e347641d90763/numpy-1.24.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "ab344f1bf21f140adab8e47fdbc7c35a477dc01408791f8ba00d018dd0bc5155", - "url": "https://files.pythonhosted.org/packages/2c/d4/590ae7df5044465cc9fa2db152ae12468694d62d952b1528ecff328ef7fc/numpy-1.24.3.tar.gz" - }, - { - "algorithm": "sha256", - "hash": "8535303847b89aa6b0f00aa1dc62867b5a32923e4d1681a35b5eef2d9591a463", - "url": "https://files.pythonhosted.org/packages/62/e4/cd77d5f3d02c30d9ca8f2995df3cb3974c75cf1cc777fad445753475c4e4/numpy-1.24.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "3c1104d3c036fb81ab923f507536daedc718d0ad5a8707c6061cdfd6d184e570", - "url": "https://files.pythonhosted.org/packages/f3/23/7cc851bae09cf4db90d42a701dfe525780883ada86bece45e3da7a07e76b/numpy-1.24.3-cp310-cp310-macosx_10_9_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "202de8f38fc4a45a3eea4b63e2f376e5f2dc64ef0fa692838e31a808520efaf7", - "url": "https://files.pythonhosted.org/packages/fa/7d/8dfb40eecbb6bc83ca00ef979f5cdeca5909a250cb8b642dcf1fbd34c078/numpy-1.24.3-cp310-cp310-macosx_11_0_arm64.whl" - } - ], - "project_name": "numpy", - "requires_dists": [], - "requires_python": ">=3.8", - "version": "1.24.3" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", - "url": "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918", - "url": "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz" - } - ], - "project_name": "oauthlib", - "requires_dists": [ - "blinker>=1.4.0; extra == \"signals\"", - "cryptography>=3.0.0; extra == \"rsa\"", - "cryptography>=3.0.0; extra == \"signedtoken\"", - "pyjwt<3,>=2.0.0; extra == \"signedtoken\"" - ], - "requires_python": ">=3.6", - "version": "3.2.2" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "db50034c73d6350349bdfba19c3f0d54a3cea73eb97b35f9d7419b2fc7206454", - "url": "https://files.pythonhosted.org/packages/2f/25/c3442864bd77621809a208a483b0857f8d6444b7a67906b58b9dcddd1574/ollama-0.3.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "032572fb494a4fba200c65013fe937a65382c846b5f358d9e8918ecbc9ac44b5", - "url": "https://files.pythonhosted.org/packages/d7/4e/fc7ad9232c251b4885b1bf2e0f9ce35882e0f167a6ce7d3d15473dc07e7d/ollama-0.3.1.tar.gz" - } - ], - "project_name": "ollama", - "requires_dists": [ - "httpx<0.28.0,>=0.27.0" - ], - "requires_python": "<4.0,>=3.8", - "version": "0.3.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "3ed4ddad48e0dde059c9b4d3dc240e47781beca2811e52ba449ddc4a471a2fd4", - "url": "https://files.pythonhosted.org/packages/a6/ab/874c3dc9b266af17e6df5d2f555c029c2844922acd60ab7eeb6524c13a3d/openai-1.40.8-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "e225f830b946378e214c5b2cfa8df28ba2aeb7e9d44f738cb2a926fd971f5bc0", - "url": "https://files.pythonhosted.org/packages/80/a6/0e38f6eea04bf9f7e6df4acb122fc94f313ecae8a52a736d634db363b84d/openai-1.40.8.tar.gz" - } - ], - "project_name": "openai", - "requires_dists": [ - "anyio<5,>=3.5.0", - "cached-property; python_version < \"3.8\"", - "distro<2,>=1.7.0", - "httpx<1,>=0.23.0", - "jiter<1,>=0.4.0", - "numpy>=1; extra == \"datalib\"", - "pandas-stubs>=1.1.0.11; extra == \"datalib\"", - "pandas>=1.2.3; extra == \"datalib\"", - "pydantic<3,>=1.9.0", - "sniffio", - "tqdm>4", - "typing-extensions<5,>=4.11" - ], - "requires_python": ">=3.7.1", - "version": "1.40.8" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "0ab6d25d01799f97a9464630abacbb34aafecdcaa0ef3cba6d6b3499867d0355", - "url": "https://files.pythonhosted.org/packages/7b/60/9afac4fd6feee0ac09339de4101ee452ea643d26e9ce44c7708a0023f503/openpyxl-3.0.10-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "e47805627aebcf860edb4edf7987b1309c1b3632f3750538ed962bbcc3bd7449", - "url": "https://files.pythonhosted.org/packages/2c/b8/ff77a718173fd73e49f883b4fda88f11af1fc51edb9252af3785b0cad987/openpyxl-3.0.10.tar.gz" - } - ], - "project_name": "openpyxl", - "requires_dists": [ - "et-xmlfile" - ], - "requires_python": ">=3.6", - "version": "3.0.10" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "19408844bd4af5b4d40f06c3e5b88c6bfce4a749f61ab766f41b22c4070c5c15", - "url": "https://files.pythonhosted.org/packages/09/15/81bf7c6f802e66dd2e47013168a4dd6c5ca938e94f6422cfaca51dc93625/oracledb-2.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "253a85eef53d97815b4d838e5275d0a99e33ec340eb4b945cd2371e2bcede46b", - "url": "https://files.pythonhosted.org/packages/2f/49/7c807201d48f796a1f1b126a9395a7dc416b1b0b3f008063b92b9cee2e0f/oracledb-2.2.0-cp310-cp310-macosx_10_9_universal2.whl" - }, - { - "algorithm": "sha256", - "hash": "fa5c2982076366f59dade28b554b43a257ad426e55359124bc37f191f51c2d46", - "url": "https://files.pythonhosted.org/packages/82/b8/a3e66b1eefeef2baff1eac1c7611380b84d0b89af8aab51b98e17ab18efa/oracledb-2.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "f52c7df38b13243b5ce583457b80748a34682b9bb8370da2497868b71976798b", - "url": "https://files.pythonhosted.org/packages/99/40/e401b2d582a1b47df87b6dc9e7a7d4c30be9d44cc760d7e97be7600bea76/oracledb-2.2.0.tar.gz" - } - ], - "project_name": "oracledb", - "requires_dists": [ - "cryptography>=3.2.1" - ], - "requires_python": ">=3.7", - "version": "2.2.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "fdf5197a21dd660cf19dfd2a3ce79574588f8f5e2dbf21bda9ee2d2b46924d84", - "url": "https://files.pythonhosted.org/packages/a6/d8/eee30caba21a8d6a9df06d2519bb0ecd0adbcd57f2e79d360de5570031cf/orjson-3.10.7-cp310-cp310-musllinux_1_2_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "ac7cf6222b29fbda9e3a472b41e6a5538b48f2c8f99261eecd60aafbdb60690c", - "url": "https://files.pythonhosted.org/packages/04/da/d03d72b54bdd60d05de372114abfbd9f05050946895140c6ff5f27ab8f49/orjson-3.10.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" - }, - { - "algorithm": "sha256", - "hash": "74f4544f5a6405b90da8ea724d15ac9c36da4d72a738c64685003337401f5c12", - "url": "https://files.pythonhosted.org/packages/49/12/60931cf808b9334f26210ab496442f4a7a3d66e29d1cf12e0a01857e756f/orjson-3.10.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl" - }, - { - "algorithm": "sha256", - "hash": "de817e2f5fc75a9e7dd350c4b0f54617b280e26d1631811a43e7e968fa71e3e9", - "url": "https://files.pythonhosted.org/packages/7f/7e/ef8522dbba112af6cc52227dcc746dd3447c7d53ea8cea35740239b547ee/orjson-3.10.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl" - }, - { - "algorithm": "sha256", - "hash": "479fd0844ddc3ca77e0fd99644c7fe2de8e8be1efcd57705b5c92e5186e8a250", - "url": "https://files.pythonhosted.org/packages/82/0a/1f09c12d15b1e83156b7f3f621561d38650fe5b8f39f38f04a64de1a87fc/orjson-3.10.7-cp310-cp310-musllinux_1_2_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "75ef0640403f945f3a1f9f6400686560dbfb0fb5b16589ad62cd477043c4eee3", - "url": "https://files.pythonhosted.org/packages/9e/03/821c8197d0515e46ea19439f5c5d5fd9a9889f76800613cfac947b5d7845/orjson-3.10.7.tar.gz" - }, - { - "algorithm": "sha256", - "hash": "348bdd16b32556cf8d7257b17cf2bdb7ab7976af4af41ebe79f9796c218f7e91", - "url": "https://files.pythonhosted.org/packages/b6/bc/fbd345d771a73cacc5b0e774d034cd081590b336754c511f4ead9fdc4cf1/orjson-3.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "bf6ba8ebc8ef5792e2337fb0419f8009729335bb400ece005606336b7fd7bab7", - "url": "https://files.pythonhosted.org/packages/dd/47/1ddff6e23fe5f4aeaaed996a3cde422b3eaac4558c03751723e106184c68/orjson-3.10.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" - }, - { - "algorithm": "sha256", - "hash": "34a566f22c28222b08875b18b0dfbf8a947e69df21a9ed5c51a6bf91cfb944ac", - "url": "https://files.pythonhosted.org/packages/fe/0e/efbd0a2d25f8e82b230eb20b6b8424be6dd95b6811b669be9af16234b6db/orjson-3.10.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - } - ], - "project_name": "orjson", - "requires_dists": [], - "requires_python": ">=3.8", - "version": "3.10.7" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "2b2f1d2d42ec152ca90ccb5682f3e051fb55986e1b170ebde472b133713e7085", - "url": "https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "6f5fef59cb5b3708321db7cca56aed8ad7e662853351e7991fcf60ec606d47a4", - "url": "https://files.pythonhosted.org/packages/06/81/a7654e654a4b30eda06ef9ad8c1b45d1534bfd10b5c045d0c0f6b16fecd2/oscrypto-1.3.0.tar.gz" - } - ], - "project_name": "oscrypto", - "requires_dists": [ - "asn1crypto>=1.5.1" - ], - "requires_python": null, - "version": "1.3.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "a40210652c89022772a6c8340d6066f7d5dc67132141e5284a4db7a27d0a8ab0", - "url": "https://files.pythonhosted.org/packages/4b/ca/b598e18eb0820a0116690a960d85625aae50dae8ba58195e254e35c2289a/packageurl_python-0.15.6-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "cbc89afd15d5f4d05db4f1b61297e5b97a43f61f28799f6d282aff467ed2ee96", - "url": "https://files.pythonhosted.org/packages/56/c5/c0f3ac14fd44f9b344069397fbe79aad1fd2c69220d145447c6c29cb541d/packageurl_python-0.15.6.tar.gz" - } - ], - "project_name": "packageurl-python", - "requires_dists": [ - "black; extra == \"lint\"", - "isort; extra == \"lint\"", - "mypy; extra == \"lint\"", - "pytest; extra == \"test\"", - "setuptools; extra == \"build\"", - "sqlalchemy>=2.0.0; extra == \"sqlalchemy\"", - "wheel; extra == \"build\"" - ], - "requires_python": ">=3.7", - "version": "0.15.6" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7", - "url": "https://files.pythonhosted.org/packages/ec/1a/610693ac4ee14fcdf2d9bf3c493370e4f2ef7ae2e19217d7a237ff42367d/packaging-23.2-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "url": "https://files.pythonhosted.org/packages/fb/2b/9b9c33ffed44ee921d0967086d653047286054117d584f1b1a7c22ceaf7b/packaging-23.2.tar.gz" - } - ], - "project_name": "packaging", - "requires_dists": [], - "requires_python": ">=3.7", - "version": "23.2" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "0296a66200dee556850d99b24c54c7dfa53a3264b1ca6f440e42bad424caea03", - "url": "https://files.pythonhosted.org/packages/49/73/5fe14264e2c9e53f40f946782f0e38240f0a6d577609aca440f7223facd6/pandas-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "59dfe0e65a2f3988e940224e2a70932edc964df79f3356e5f2997c7d63e758b4", - "url": "https://files.pythonhosted.org/packages/1b/fa/4e5d054549faf1524230ffcd57ca98bb7350a4ed62ef722daabde4cb7632/pandas-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "acf08a73b5022b479c1be155d4988b72f3020f308f7a87c527702c5f8966d34f", - "url": "https://files.pythonhosted.org/packages/6a/81/711b32480f508dcf29cb501f61c4f1c3c72409e9168c6625145440c1c320/pandas-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "22929f84bca106921917eb73c1521317ddd0a4c71b395bcf767a106e3494209f", - "url": "https://files.pythonhosted.org/packages/86/ff/662dde2193fc93b8547b073db20472b9676f944d907247a46c9c5bc45bfc/pandas-2.1.3.tar.gz" - }, - { - "algorithm": "sha256", - "hash": "35172bff95f598cc5866c047f43c7f4df2c893acd8e10e6653a4b792ed7f19bb", - "url": "https://files.pythonhosted.org/packages/8e/e4/29e1fa38caba5d5d6db1807ff0f4152b51b7fb3af3a8446095b0a7619d54/pandas-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "3cc4469ff0cf9aa3a005870cb49ab8969942b7156e0a46cc3f5abd6b11051dfb", - "url": "https://files.pythonhosted.org/packages/fc/85/ec986ea64f55013d8c669da657f0da86383a15668f9814be2001e08a4807/pandas-2.1.3-cp310-cp310-macosx_11_0_arm64.whl" - } - ], - "project_name": "pandas", - "requires_dists": [ - "PyQt5>=5.15.6; extra == \"all\"", - "PyQt5>=5.15.6; extra == \"clipboard\"", - "SQLAlchemy>=1.4.36; extra == \"all\"", - "SQLAlchemy>=1.4.36; extra == \"mysql\"", - "SQLAlchemy>=1.4.36; extra == \"postgresql\"", - "SQLAlchemy>=1.4.36; extra == \"sql-other\"", - "beautifulsoup4>=4.11.1; extra == \"all\"", - "beautifulsoup4>=4.11.1; extra == \"html\"", - "bottleneck>=1.3.4; extra == \"all\"", - "bottleneck>=1.3.4; extra == \"performance\"", - "dataframe-api-compat>=0.1.7; extra == \"all\"", - "dataframe-api-compat>=0.1.7; extra == \"consortium-standard\"", - "fastparquet>=0.8.1; extra == \"all\"", - "fsspec>=2022.05.0; extra == \"all\"", - "fsspec>=2022.05.0; extra == \"fss\"", - "gcsfs>=2022.05.0; extra == \"all\"", - "gcsfs>=2022.05.0; extra == \"gcp\"", - "html5lib>=1.1; extra == \"all\"", - "html5lib>=1.1; extra == \"html\"", - "hypothesis>=6.46.1; extra == \"all\"", - "hypothesis>=6.46.1; extra == \"test\"", - "jinja2>=3.1.2; extra == \"all\"", - "jinja2>=3.1.2; extra == \"output-formatting\"", - "lxml>=4.8.0; extra == \"all\"", - "lxml>=4.8.0; extra == \"html\"", - "lxml>=4.8.0; extra == \"xml\"", - "matplotlib>=3.6.1; extra == \"all\"", - "matplotlib>=3.6.1; extra == \"plot\"", - "numba>=0.55.2; extra == \"all\"", - "numba>=0.55.2; extra == \"performance\"", - "numexpr>=2.8.0; extra == \"all\"", - "numexpr>=2.8.0; extra == \"performance\"", - "numpy<2,>=1.22.4; python_version < \"3.11\"", - "numpy<2,>=1.23.2; python_version == \"3.11\"", - "numpy<2,>=1.26.0; python_version >= \"3.12\"", - "odfpy>=1.4.1; extra == \"all\"", - "odfpy>=1.4.1; extra == \"excel\"", - "openpyxl>=3.0.10; extra == \"all\"", - "openpyxl>=3.0.10; extra == \"excel\"", - "pandas-gbq>=0.17.5; extra == \"all\"", - "pandas-gbq>=0.17.5; extra == \"gcp\"", - "psycopg2>=2.9.3; extra == \"all\"", - "psycopg2>=2.9.3; extra == \"postgresql\"", - "pyarrow>=7.0.0; extra == \"all\"", - "pyarrow>=7.0.0; extra == \"feather\"", - "pyarrow>=7.0.0; extra == \"parquet\"", - "pymysql>=1.0.2; extra == \"all\"", - "pymysql>=1.0.2; extra == \"mysql\"", - "pyreadstat>=1.1.5; extra == \"all\"", - "pyreadstat>=1.1.5; extra == \"spss\"", - "pytest-xdist>=2.2.0; extra == \"all\"", - "pytest-xdist>=2.2.0; extra == \"test\"", - "pytest>=7.3.2; extra == \"all\"", - "pytest>=7.3.2; extra == \"test\"", - "python-dateutil>=2.8.2", - "pytz>=2020.1", - "pyxlsb>=1.0.9; extra == \"all\"", - "pyxlsb>=1.0.9; extra == \"excel\"", - "qtpy>=2.2.0; extra == \"all\"", - "qtpy>=2.2.0; extra == \"clipboard\"", - "s3fs>=2022.05.0; extra == \"all\"", - "s3fs>=2022.05.0; extra == \"aws\"", - "scipy>=1.8.1; extra == \"all\"", - "scipy>=1.8.1; extra == \"computation\"", - "tables>=3.7.0; extra == \"all\"", - "tables>=3.7.0; extra == \"hdf5\"", - "tabulate>=0.8.10; extra == \"all\"", - "tabulate>=0.8.10; extra == \"output-formatting\"", - "tzdata>=2022.1", - "xarray>=2022.03.0; extra == \"all\"", - "xarray>=2022.03.0; extra == \"computation\"", - "xlrd>=2.0.1; extra == \"all\"", - "xlrd>=2.0.1; extra == \"excel\"", - "xlsxwriter>=3.0.3; extra == \"all\"", - "xlsxwriter>=3.0.3; extra == \"excel\"", - "zstandard>=0.17.0; extra == \"all\"", - "zstandard>=0.17.0; extra == \"compression\"" - ], - "requires_python": ">=3.9", - "version": "2.1.3" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", - "url": "https://files.pythonhosted.org/packages/96/b9/c0362c54290a31866c3526848583a2f45a535aa9d725fd31e25d318c805f/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", - "url": "https://files.pythonhosted.org/packages/07/8b/34854bf11a83c248505c8cb0fcf8d3d0b459a2246c8809b967963b6b12ae/pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", - "url": "https://files.pythonhosted.org/packages/0e/69/a31cccd538ca0b5272be2a38347f8839b97a14be104ea08b0db92f749c74/pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", - "url": "https://files.pythonhosted.org/packages/38/30/095d4f55f3a053392f75e2eae45eba3228452783bab3d9a920b951ac495c/pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", - "url": "https://files.pythonhosted.org/packages/5e/dd/425b95d0151e1d6c951f45051112394f130df3da67363b6bc75dc4c27aba/pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", - "url": "https://files.pythonhosted.org/packages/78/63/0632aee4e82476d9cbe5200c0cdf9ba41ee04ed77887432845264d81116d/pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", - "url": "https://files.pythonhosted.org/packages/8a/25/1fc45761955f9359b1169aa75e241551e74ac01a09f487adaaf4c3472d11/pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", - "url": "https://files.pythonhosted.org/packages/9a/9e/4143b907be8ea0bce215f2ae4f7480027473f8b61fcedfda9d851082a5d2/pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl" - }, - { - "algorithm": "sha256", - "hash": "6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", - "url": "https://files.pythonhosted.org/packages/b1/84/9a15cc5726cbbfe7f9f90bfb11f5d028586595907cd093815ca6644932e3/pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", - "url": "https://files.pythonhosted.org/packages/b5/5b/6651c288b08df3b8c1e2f8c1152201e0b25d240e22ddade0f1e242fc9fa0/pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", - "url": "https://files.pythonhosted.org/packages/b6/f5/f71fe1888b96083b3f6dfa0709101f61fc9e972c0c8d04e9d93ccef2a045/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", - "url": "https://files.pythonhosted.org/packages/cd/74/ad3d526f3bf7b6d3f408b73fde271ec69dfac8b81341a318ce825f2b3812/pillow-10.4.0.tar.gz" - }, - { - "algorithm": "sha256", - "hash": "9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", - "url": "https://files.pythonhosted.org/packages/d7/ac/4184edd511b14f760c73f5bb8a5d6fd85c591c8aff7c2229677a355c4179/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", - "url": "https://files.pythonhosted.org/packages/da/21/1749cd09160149c0a246a81d646e05f35041619ce76f6493d6a96e8d1103/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", - "url": "https://files.pythonhosted.org/packages/f3/e8/4ff79788803a5fcd5dc35efdc9386af153569853767bff74540725b45863/pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl" - } - ], - "project_name": "pillow", - "requires_dists": [ - "check-manifest; extra == \"tests\"", - "coverage; extra == \"tests\"", - "defusedxml; extra == \"tests\"", - "defusedxml; extra == \"xmp\"", - "furo; extra == \"docs\"", - "markdown2; extra == \"tests\"", - "olefile; extra == \"docs\"", - "olefile; extra == \"fpx\"", - "olefile; extra == \"mic\"", - "olefile; extra == \"tests\"", - "packaging; extra == \"tests\"", - "pyroma; extra == \"tests\"", - "pytest-cov; extra == \"tests\"", - "pytest-timeout; extra == \"tests\"", - "pytest; extra == \"tests\"", - "sphinx-copybutton; extra == \"docs\"", - "sphinx-inline-tabs; extra == \"docs\"", - "sphinx>=7.3; extra == \"docs\"", - "sphinxext-opengraph; extra == \"docs\"", - "typing-extensions; python_version < \"3.10\" and extra == \"typing\"" - ], - "requires_python": ">=3.8", - "version": "10.4.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097", - "url": "https://files.pythonhosted.org/packages/56/09/054aea9b7534a15ad38a363a2bd974c20646ab1582a387a95b8df1bfea1c/pkginfo-1.10.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297", - "url": "https://files.pythonhosted.org/packages/2f/72/347ec5be4adc85c182ed2823d8d1c7b51e13b9a6b0c1aae59582eca652df/pkginfo-1.10.0.tar.gz" - } - ], - "project_name": "pkginfo", - "requires_dists": [ - "pytest-cov; extra == \"testing\"", - "pytest; extra == \"testing\"", - "wheel; extra == \"testing\"" - ], - "requires_python": ">=3.6", - "version": "1.10.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", - "url": "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", - "url": "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz" - } - ], - "project_name": "pluggy", - "requires_dists": [ - "pre-commit; extra == \"dev\"", - "pytest-benchmark; extra == \"testing\"", - "pytest; extra == \"testing\"", - "tox; extra == \"dev\"" - ], - "requires_python": ">=3.8", - "version": "1.5.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", - "url": "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", - "url": "https://files.pythonhosted.org/packages/98/ff/fec109ceb715d2a6b4c4a85a61af3b40c723a961e8828319fbcb15b868dc/py-1.11.0.tar.gz" - } - ], - "project_name": "py", - "requires_dists": [], - "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7", - "version": "1.11.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "f3eee95c4608ff0db1be57b432bf996c6bcc298005286280a150b5a049e63638", - "url": "https://files.pythonhosted.org/packages/a0/08/4d5e85a4ec1c8ce7d8f21bd56ca4341e1408357df2b83982342ec7e4a5c6/py_jama_rest_client-1.17.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "ceed4e99fdc133ac595bcb76101f3d9ac4e07294b6fc80c9e2e2ab054f7bce73", - "url": "https://files.pythonhosted.org/packages/fa/43/efe5aaecd8db28c43eeddb1680e2f9121a353263ceb57bebb9a4bffa06fc/py-jama-rest-client-1.17.1.tar.gz" - } - ], - "project_name": "py-jama-rest-client", - "requires_dists": [ - "requests" - ], - "requires_python": null, - "version": "1.17.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "d3f1201b33420c481aa83f7860c7bf2c2f036ba3ea82b6e15a96696457c36cd2", - "url": "https://files.pythonhosted.org/packages/1c/3e/430be46b4381b4768aa6b5f1d322db8bb48c0655763639b6d14979764ad2/py_serializable-0.15.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "8fc41457d8ee5f5c5a12f41fd87bf1a4f2ecf9da39fee92059b728e78f320771", - "url": "https://files.pythonhosted.org/packages/66/bb/477b7d60381d97a4ba45ae1bcedd6eeb1f689bea82034f80bbdc9634d639/py-serializable-0.15.0.tar.gz" - } - ], - "project_name": "py-serializable", - "requires_dists": [ - "defusedxml<0.8.0,>=0.7.1" - ], - "requires_python": "<4.0,>=3.7", - "version": "0.15.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", - "url": "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", - "url": "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz" - } - ], - "project_name": "pycparser", - "requires_dists": [], - "requires_python": ">=3.8", - "version": "2.22" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687", - "url": "https://files.pythonhosted.org/packages/39/9f/ab6d19c5d3fccc1e3e0d835ac773031388802b31d93937daf878465c2ecf/pydantic-1.10.13-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "bc0898c12f8e9c97f6cd44c0ed70d55749eaf783716896960b4ecce2edfd2d69", - "url": "https://files.pythonhosted.org/packages/13/7c/4540e678a202273d667285309d46f09682e11e7a28c39a49670b17f96dd5/pydantic-1.10.13-cp310-cp310-musllinux_1_1_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340", - "url": "https://files.pythonhosted.org/packages/51/cd/721eb771f3f09f60de0807e240c3acf44c38828d0ced869fe8df7e79801b/pydantic-1.10.13.tar.gz" - }, - { - "algorithm": "sha256", - "hash": "3ecea2b9d80e5333303eeb77e180b90e95eea8f765d08c3d278cd56b00345d01", - "url": "https://files.pythonhosted.org/packages/60/8f/1c75810777a93859c4ac54179ecdd83c14d02706a648f87c51f6798e7e85/pydantic-1.10.13-cp310-cp310-macosx_11_0_arm64.whl" - }, - { - "algorithm": "sha256", - "hash": "84bafe2e60b5e78bc64a2941b4c071a4b7404c5c907f5f5a99b0139781e69ed8", - "url": "https://files.pythonhosted.org/packages/6d/35/2063581cd58d31c4da4891cc42d7cad1dccd805013a5534acd51593f735d/pydantic-1.10.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "654db58ae399fe6434e55325a2c3e959836bd17a6f6a0b6ca8107ea0571d2e17", - "url": "https://files.pythonhosted.org/packages/8e/45/89c01c7ed39072bf79f286e0694394d06ab07cc5084c0cd31e4364cf2000/pydantic-1.10.13-cp310-cp310-musllinux_1_1_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "1740068fd8e2ef6eb27a20e5651df000978edce6da6803c2bef0bc74540f9548", - "url": "https://files.pythonhosted.org/packages/e0/2f/d6f17f8385d718233bcae893d27525443d41201c938b68a4af3d591a33e4/pydantic-1.10.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "efff03cc7a4f29d9009d1c96ceb1e7a70a65cfe86e89d34e4a5f2ab1e5693737", - "url": "https://files.pythonhosted.org/packages/e3/39/c866689ef73ca39c4efc314fa751397ae15ee085632086383fefec8600c0/pydantic-1.10.13-cp310-cp310-macosx_10_9_x86_64.whl" - } - ], - "project_name": "pydantic", - "requires_dists": [ - "email-validator>=1.0.3; extra == \"email\"", - "python-dotenv>=0.10.4; extra == \"dotenv\"", - "typing-extensions>=4.2.0" - ], - "requires_python": ">=3.7", - "version": "1.10.13" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", - "url": "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", - "url": "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz" - } - ], - "project_name": "pygments", - "requires_dists": [ - "colorama>=0.4.6; extra == \"windows-terminal\"" - ], - "requires_python": ">=3.8", - "version": "2.18.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "045a0999c5e3b22caad86e4fa11ef488c3fd7f5b5886c045ca11ffa24254c33c", - "url": "https://files.pythonhosted.org/packages/bc/96/3eb50a09d2ac73780abfbaaa7ec23f2ef5a4f9059118c3e393afb390e047/pyHanko-0.25.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "8718d9046d442589eef6dd6973110fa5e385555cc4a6b2b1aeca3c2f3b6742e9", - "url": "https://files.pythonhosted.org/packages/99/59/d165478e7cef237cb7d22ac80e062829fcb13736c3edc71e5f7a1e814163/pyhanko-0.25.1.tar.gz" - } - ], - "project_name": "pyhanko", - "requires_dists": [ - "Pillow>=7.2.0; extra == \"image-support\"", - "aiohttp~=3.9.0; extra == \"async-http\"", - "asn1crypto>=1.5.1", - "backports.zoneinfo[tzdata]; python_version < \"3.9\" and extra == \"testing-basic\"", - "certomancer-csc-dummy==0.3.0; extra == \"live-test\"", - "certomancer-csc-dummy==0.3.0; extra == \"testing\"", - "certomancer==0.12.*; extra == \"testing-basic\"", - "certomancer[web-api]==0.12.*; extra == \"live-test\"", - "click>=8.1.3", - "cryptography>=42.0.1", - "defusedxml~=0.7.1; extra == \"xmp\"", - "fonttools>=4.33.3; extra == \"opentype\"", - "freezegun>=1.1.0; extra == \"testing-basic\"", - "oscrypto>=1.2.1; extra == \"extra-pubkey-algs\"", - "pyHanko[async-http,etsi,extra-pubkey-algs,image-support,opentype,pkcs11,xmp]; extra == \"mypy\"", - "pyHanko[async-http,extra-pubkey-algs,image-support,opentype,pkcs11,testing-basic,xmp]; extra == \"testing\"", - "pyHanko[async-http,extra-pubkey-algs,testing-basic,xmp]; extra == \"live-test\"", - "pyHanko[etsi]; extra == \"testing\"", - "pyhanko-certvalidator<0.27,>=0.26.2", - "pytest-aiohttp~=1.0.4; extra == \"live-test\"", - "pytest-aiohttp~=1.0.4; extra == \"testing\"", - "pytest-asyncio==0.23.8; extra == \"testing-basic\"", - "pytest-cov<5.1,>=4.0; extra == \"live-test\"", - "pytest-cov<5.1,>=4.0; extra == \"testing-basic\"", - "pytest>=6.1.1; extra == \"testing-basic\"", - "python-barcode==0.15.1; extra == \"image-support\"", - "python-pkcs11~=0.7.0; extra == \"pkcs11\"", - "pyyaml>=6.0", - "qrcode>=7.3.1", - "requests-mock>=1.8.0; extra == \"testing-basic\"", - "requests>=2.31.0", - "sphinx-rtd-theme; extra == \"docs\"", - "sphinx; extra == \"docs\"", - "types-PyYAML; extra == \"mypy\"", - "types-python-dateutil; extra == \"mypy\"", - "types-requests; extra == \"mypy\"", - "types-tzlocal; extra == \"mypy\"", - "tzlocal>=4.3", - "uharfbuzz<0.40.0,>=0.25.0; extra == \"opentype\"", - "xsdata<25.0,>=24.4; extra == \"etsi\"" - ], - "requires_python": ">=3.8", - "version": "0.25.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "e386c87e202ff1caacf5fd941da6c3509e79db54dbd7b43c6550ceebe5e67077", - "url": "https://files.pythonhosted.org/packages/51/9c/8332e1a42c8476aa48b0ca10dee9d5513e6398f296b90194c8e8ba444690/pyhanko_certvalidator-0.26.3-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "47fba8e9dbf846d766f2e0a453572dd4b25b2f1397847a31fe892c8eb00391f5", - "url": "https://files.pythonhosted.org/packages/d0/82/66c6d0cda723b58cb01839cfa7d2d81b4328e275be45caaefe3425f3c995/pyhanko-certvalidator-0.26.3.tar.gz" - } - ], - "project_name": "pyhanko-certvalidator", - "requires_dists": [ - "aiohttp<3.10,>=3.8; extra == \"async-http\"", - "aiohttp<3.10,>=3.8; extra == \"testing\"", - "asn1crypto>=1.5.1", - "cryptography>=41.0.5", - "freezegun>=1.1.0; extra == \"testing\"", - "oscrypto>=1.1.0", - "pyhanko-certvalidator[async-http]; extra == \"testing\"", - "pyhanko-certvalidator[testing]; extra == \"mypy\"", - "pytest-aiohttp~=1.0.4; extra == \"testing\"", - "pytest-cov<4.2,>=4.0; extra == \"testing\"", - "pytest>=6.1.1; extra == \"testing\"", - "requests>=2.31.0", - "types-requests; extra == \"mypy\"", - "uritools>=3.0.1" - ], - "requires_python": ">=3.7", - "version": "0.26.3" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850", - "url": "https://files.pythonhosted.org/packages/79/84/0fdf9b18ba31d69877bd39c9cd6052b47f3761e9910c15de788e519f079f/PyJWT-2.9.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c", - "url": "https://files.pythonhosted.org/packages/fb/68/ce067f09fca4abeca8771fe667d89cc347d1e99da3e093112ac329c6020e/pyjwt-2.9.0.tar.gz" - } - ], - "project_name": "pyjwt", - "requires_dists": [ - "coverage[toml]==5.0.4; extra == \"dev\"", - "coverage[toml]==5.0.4; extra == \"tests\"", - "cryptography>=3.4.0; extra == \"crypto\"", - "cryptography>=3.4.0; extra == \"dev\"", - "pre-commit; extra == \"dev\"", - "pytest<7.0.0,>=6.0.0; extra == \"dev\"", - "pytest<7.0.0,>=6.0.0; extra == \"tests\"", - "sphinx-rtd-theme; extra == \"dev\"", - "sphinx-rtd-theme; extra == \"docs\"", - "sphinx; extra == \"dev\"", - "sphinx; extra == \"docs\"", - "zope.interface; extra == \"dev\"", - "zope.interface; extra == \"docs\"" - ], - "requires_python": ">=3.8", - "version": "2.9.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "1f77682d95dc6308517caa96e267628f747100634ec43476166763a226320817", - "url": "https://files.pythonhosted.org/packages/20/1f/88b9d422df9f37363cb8f0dd2ea58fa9c50a15560a6f45379486d393df01/pypdf-3.17.3-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "70c072218e3729218676bdf107e921fa49d1838c2f46056ce26d495c7e58f641", - "url": "https://files.pythonhosted.org/packages/23/38/5e123ad1b071f6aa068d6c5a01ae384d1794fce93b42ad22da152232f878/pypdf-3.17.3.tar.gz" - } - ], - "project_name": "pypdf", - "requires_dists": [ - "Pillow>=8.0.0; extra == \"full\"", - "Pillow>=8.0.0; extra == \"image\"", - "PyCryptodome; extra == \"crypto\" and python_version == \"3.6\"", - "PyCryptodome; extra == \"full\" and python_version == \"3.6\"", - "black; extra == \"dev\"", - "cryptography; extra == \"crypto\" and python_version >= \"3.7\"", - "cryptography; extra == \"full\" and python_version >= \"3.7\"", - "dataclasses; python_version < \"3.7\"", - "flit; extra == \"dev\"", - "myst_parser; extra == \"docs\"", - "pip-tools; extra == \"dev\"", - "pre-commit<2.18.0; extra == \"dev\"", - "pytest-cov; extra == \"dev\"", - "pytest-socket; extra == \"dev\"", - "pytest-timeout; extra == \"dev\"", - "pytest-xdist; extra == \"dev\"", - "sphinx; extra == \"docs\"", - "sphinx_rtd_theme; extra == \"docs\"", - "typing_extensions>=3.7.4.3; python_version < \"3.10\"", - "wheel; extra == \"dev\"" - ], - "requires_python": ">=3.6", - "version": "3.17.3" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "4a43e969b8f5aaafb2a415536c1a8ec7e341cd6a3f957fd5b5f32a4cfeed902c", - "url": "https://files.pythonhosted.org/packages/3e/b9/3766cc361d93edb2ce81e2e1f87dd98f314d7d513877a342d31b30741680/pypng-0.20220715.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "739c433ba96f078315de54c0db975aee537cbc3e1d0ae4ed9aab0ca1e427e2c1", - "url": "https://files.pythonhosted.org/packages/93/cd/112f092ec27cca83e0516de0a3368dbd9128c187fb6b52aaaa7cde39c96d/pypng-0.20220715.0.tar.gz" - } - ], - "project_name": "pypng", - "requires_dists": [], - "requires_python": null, - "version": "0.20220715.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "129a4294f2c4d681d5875240ef87accc6f1d921e8983737fb0b59642b397951e", - "url": "https://files.pythonhosted.org/packages/43/c3/4dc3d1d029e14bf065f1df9e98e3e503e622de34706a06ab6c3731377e85/pyspnego-0.11.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "e92ed8b0a62765b9d6abbb86a48cf871228ddb97678598dc01c9c39a626823f6", - "url": "https://files.pythonhosted.org/packages/46/f5/1f938a781742d18475ac43a101ec8a9499e1655da0984e08b59e20012c04/pyspnego-0.11.1.tar.gz" - } - ], - "project_name": "pyspnego", - "requires_dists": [ - "cryptography", - "gssapi>=1.6.0; sys_platform != \"win32\" and extra == \"kerberos\"", - "krb5>=0.3.0; sys_platform != \"win32\" and extra == \"kerberos\"", - "ruamel.yaml; extra == \"yaml\"", - "sspilib>=0.1.0; sys_platform == \"win32\"" - ], - "requires_python": ">=3.8", - "version": "0.11.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", - "url": "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", - "url": "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz" - } - ], - "project_name": "pytest", - "requires_dists": [ - "argcomplete; extra == \"testing\"", - "attrs>=19.2.0; extra == \"testing\"", - "colorama; sys_platform == \"win32\"", - "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", - "hypothesis>=3.56; extra == \"testing\"", - "importlib-metadata>=0.12; python_version < \"3.8\"", - "iniconfig", - "mock; extra == \"testing\"", - "nose; extra == \"testing\"", - "packaging", - "pluggy<2.0,>=0.12", - "pygments>=2.7.2; extra == \"testing\"", - "requests; extra == \"testing\"", - "setuptools; extra == \"testing\"", - "tomli>=1.0.0; python_version < \"3.11\"", - "xmlschema; extra == \"testing\"" - ], - "requires_python": ">=3.7", - "version": "7.4.4" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", - "url": "https://files.pythonhosted.org/packages/20/49/b3e0edec68d81846f519c602ac38af9db86e1e71275528b3e814ae236063/pytest_cov-3.0.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470", - "url": "https://files.pythonhosted.org/packages/61/41/e046526849972555928a6d31c2068410e47a31fb5ab0a77f868596811329/pytest-cov-3.0.0.tar.gz" - } - ], - "project_name": "pytest-cov", - "requires_dists": [ - "coverage[toml]>=5.2.1", - "fields; extra == \"testing\"", - "hunter; extra == \"testing\"", - "process-tests; extra == \"testing\"", - "pytest-xdist; extra == \"testing\"", - "pytest>=4.6", - "six; extra == \"testing\"", - "virtualenv; extra == \"testing\"" - ], - "requires_python": ">=3.6", - "version": "3.0.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "6e0ce6e57ce3a583cb7e5023f7d1021e19dfec22be41d9ad345bae2fc61caf3b", - "url": "https://files.pythonhosted.org/packages/35/a0/effb6cbbccfd1c106c572d3d619b3418d71093afb4cd4f91f51e6a1799d2/pytest_custom_exit_code-0.3.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "51ffff0ee2c1ddcc1242e2ddb2a5fd02482717e33a2326ef330e3aa430244635", - "url": "https://files.pythonhosted.org/packages/92/9d/e1eb0af5e96a5c34f59b9aa69dfb680764420fe60f2ec28cfbc5339f99f8/pytest-custom_exit_code-0.3.0.tar.gz" - } - ], - "project_name": "pytest-custom-exit-code", - "requires_dists": [ - "pytest>=4.0.2" - ], - "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>2.7", - "version": "0.3.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "810958f66a91afb1a1e2ae83089d8dc1cd2437ac96b12963042fbb9fb4d16af0", - "url": "https://files.pythonhosted.org/packages/f4/af/9c0bda43e486a3c9bf1e0f876d0f241bc3f229d7d65d09331a0868db9629/pytest_forked-1.6.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "4dafd46a9a600f65d822b8f605133ecf5b3e1941ebb3588e943b4e3eb71a5a3f", - "url": "https://files.pythonhosted.org/packages/8c/c9/93ad2ba2413057ee694884b88cf7467a46c50c438977720aeac26e73fdb7/pytest-forked-1.6.0.tar.gz" - } - ], - "project_name": "pytest-forked", - "requires_dists": [ - "py", - "pytest>=3.10" - ], - "requires_python": ">=3.7", - "version": "1.6.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", - "url": "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", - "url": "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz" - } - ], - "project_name": "pytest-mock", - "requires_dists": [ - "pre-commit; extra == \"dev\"", - "pytest-asyncio; extra == \"dev\"", - "pytest>=6.2.5", - "tox; extra == \"dev\"" - ], - "requires_python": ">=3.8", - "version": "3.14.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65", - "url": "https://files.pythonhosted.org/packages/21/08/b1945d4b4986eb1aa10cf84efc5293bba39da80a2f95db3573dd90678408/pytest_xdist-2.5.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf", - "url": "https://files.pythonhosted.org/packages/5d/43/9dbc32d297d6eae85d6c05dc8e8d3371061bd6cbe56a2f645d9ea4b53d9b/pytest-xdist-2.5.0.tar.gz" - } - ], - "project_name": "pytest-xdist", - "requires_dists": [ - "execnet>=1.1", - "filelock; extra == \"testing\"", - "psutil>=3.0; extra == \"psutil\"", - "pytest-forked", - "pytest>=6.2.0", - "setproctitle; extra == \"setproctitle\"" - ], - "requires_python": ">=3.6", - "version": "2.5.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", - "url": "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", - "url": "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz" - } - ], - "project_name": "python-dateutil", - "requires_dists": [ - "six>=1.5" - ], - "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7", - "version": "2.9.0.post0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb", - "url": "https://files.pythonhosted.org/packages/7f/99/ad6bd37e748257dd70d6f85d916cafe79c0b0f5e2e95b11f7fbc82bf3110/pytz-2023.3-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588", - "url": "https://files.pythonhosted.org/packages/5e/32/12032aa8c673ee16707a9b6cdda2b09c0089131f35af55d443b6a9c69c1d/pytz-2023.3.tar.gz" - } - ], - "project_name": "pytz", - "requires_dists": [], - "requires_python": null, - "version": "2023.3" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5", - "url": "https://files.pythonhosted.org/packages/02/25/6ba9f6bb50a3d4fbe22c1a02554dc670682a07c8701d1716d19ddea2c940/PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "url": "https://files.pythonhosted.org/packages/36/2b/61d51a2c4f25ef062ae3f74576b01638bebad5e045f747ff12643df63844/PyYAML-6.0.tar.gz" - }, - { - "algorithm": "sha256", - "hash": "d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "url": "https://files.pythonhosted.org/packages/44/e5/4fea13230bcebf24b28c0efd774a2dd65a0937a2d39e94a4503438b078ed/PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "url": "https://files.pythonhosted.org/packages/5e/f4/7b4bb01873be78fc9fde307f38f62e380b7111862c165372cf094ca2b093/PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "url": "https://files.pythonhosted.org/packages/91/49/d46d7b15cddfa98533e89f3832f391aedf7e31f37b4d4df3a7a7855a7073/PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl" - }, - { - "algorithm": "sha256", - "hash": "a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "url": "https://files.pythonhosted.org/packages/ef/ad/b443cce94539e57e1a745a845f95c100ad7b97593d7e104051e43f730ecd/PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl" - } - ], - "project_name": "pyyaml", - "requires_dists": [], - "requires_python": ">=3.6", - "version": "6.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "581dca7a029bcb2deef5d01068e39093e80ef00b4a61098a2182eac59d01643a", - "url": "https://files.pythonhosted.org/packages/24/79/aaf0c1c7214f2632badb2771d770b1500d3d7cbdf2590ae62e721ec50584/qrcode-7.4.2-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "9dd969454827e127dbd93696b20747239e6d540e082937c90f14ac95b30f5845", - "url": "https://files.pythonhosted.org/packages/30/35/ad6d4c5a547fe9a5baf85a9edbafff93fc6394b014fab30595877305fa59/qrcode-7.4.2.tar.gz" - } - ], - "project_name": "qrcode", - "requires_dists": [ - "colorama; platform_system == \"Windows\"", - "coverage; extra == \"test\"", - "pillow>=9.1.0; extra == \"all\"", - "pillow>=9.1.0; extra == \"pil\"", - "pypng", - "pytest-cov; extra == \"all\"", - "pytest-cov; extra == \"dev\"", - "pytest; extra == \"all\"", - "pytest; extra == \"dev\"", - "pytest; extra == \"test\"", - "tox; extra == \"all\"", - "tox; extra == \"dev\"", - "typing-extensions", - "zest.releaser[recommended]; extra == \"all\"", - "zest.releaser[recommended]; extra == \"maintainer\"" - ], - "requires_python": ">=3.7", - "version": "7.4.2" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", - "url": "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", - "url": "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz" - } - ], - "project_name": "readme-renderer", - "requires_dists": [ - "Pygments>=2.5.1", - "cmarkgfm>=0.8.0; extra == \"md\"", - "docutils>=0.21.2", - "nh3>=0.2.14" - ], - "requires_python": ">=3.9", - "version": "44.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de", - "url": "https://files.pythonhosted.org/packages/b7/59/2056f61236782a2c86b33906c025d4f4a0b17be0161b63b70fd9e8775d36/referencing-0.35.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c", - "url": "https://files.pythonhosted.org/packages/99/5b/73ca1f8e72fff6fa52119dbd185f73a907b1989428917b24cff660129b6d/referencing-0.35.1.tar.gz" - } - ], - "project_name": "referencing", - "requires_dists": [ - "attrs>=22.2.0", - "rpds-py>=0.7.0" - ], - "requires_python": ">=3.8", - "version": "0.35.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a", - "url": "https://files.pythonhosted.org/packages/b7/99/38434984d912edbd2e1969d116257e869578f67461bd7462b894c45ed874/regex-2024.9.11-cp310-cp310-musllinux_1_2_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35", - "url": "https://files.pythonhosted.org/packages/22/91/8339dd3abce101204d246e31bc26cdd7ec07c9f91598472459a3a902aa41/regex-2024.9.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl" - }, - { - "algorithm": "sha256", - "hash": "a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8", - "url": "https://files.pythonhosted.org/packages/3c/65/b9f002ab32f7b68e7d1dcabb67926f3f47325b8dbc22cc50b6a043e1d07c/regex-2024.9.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" - }, - { - "algorithm": "sha256", - "hash": "09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca", - "url": "https://files.pythonhosted.org/packages/40/b8/3e9484c6230b8b6e8f816ab7c9a080e631124991a4ae2c27a81631777db0/regex-2024.9.11-cp310-cp310-musllinux_1_2_s390x.whl" - }, - { - "algorithm": "sha256", - "hash": "1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408", - "url": "https://files.pythonhosted.org/packages/63/12/497bd6599ce8a239ade68678132296aec5ee25ebea45fc8ba91aa60fceec/regex-2024.9.11-cp310-cp310-macosx_10_9_universal2.whl" - }, - { - "algorithm": "sha256", - "hash": "3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6", - "url": "https://files.pythonhosted.org/packages/65/7b/953075723dd5ab00780043ac2f9de667306ff9e2a85332975e9f19279174/regex-2024.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl" - }, - { - "algorithm": "sha256", - "hash": "16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5", - "url": "https://files.pythonhosted.org/packages/69/a8/b2fb45d9715b1469383a0da7968f8cacc2f83e9fbbcd6b8713752dd980a6/regex-2024.9.11-cp310-cp310-macosx_11_0_arm64.whl" - }, - { - "algorithm": "sha256", - "hash": "635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d", - "url": "https://files.pythonhosted.org/packages/71/3a/52ff61054d15a4722605f5872ad03962b319a04c1ebaebe570b8b9b7dde1/regex-2024.9.11-cp310-cp310-musllinux_1_2_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c", - "url": "https://files.pythonhosted.org/packages/88/87/1ce4a5357216b19b7055e7d3b0efc75a6e426133bf1e7d094321df514257/regex-2024.9.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137", - "url": "https://files.pythonhosted.org/packages/97/07/37e460ab5ca84be8e1e197c3b526c5c86993dcc9e13cbc805c35fc2463c1/regex-2024.9.11-cp310-cp310-musllinux_1_2_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d", - "url": "https://files.pythonhosted.org/packages/c1/24/595ddb9bec2a9b151cdaf9565b0c9f3da9f0cb1dca6c158bc5175332ddf8/regex-2024.9.11-cp310-cp310-macosx_10_9_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71", - "url": "https://files.pythonhosted.org/packages/cb/19/556638aa11c2ec9968a1da998f07f27ec0abb9bf3c647d7c7985ca0b8eea/regex-2024.9.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8", - "url": "https://files.pythonhosted.org/packages/d1/e9/7a5bc4c6ef8d9cd2bdd83a667888fc35320da96a4cc4da5fa084330f53db/regex-2024.9.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a", - "url": "https://files.pythonhosted.org/packages/f1/0b/29f2105bfac3ed08e704914c38e93b07c784a6655f8a015297ee7173e95b/regex-2024.9.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd", - "url": "https://files.pythonhosted.org/packages/f9/38/148df33b4dbca3bd069b963acab5e0fa1a9dbd6820f8c322d0dd6faeff96/regex-2024.9.11.tar.gz" - } - ], - "project_name": "regex", - "requires_dists": [], - "requires_python": ">=3.8", - "version": "2024.9.11" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", - "url": "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", - "url": "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz" - } - ], - "project_name": "requests", - "requires_dists": [ - "PySocks!=1.5.7,>=1.5.6; extra == \"socks\"", - "certifi>=2017.4.17", - "chardet<6,>=3.0.2; extra == \"use-chardet-on-py3\"", - "charset-normalizer<4,>=2", - "idna<4,>=2.5", - "urllib3<3,>=1.21.1" - ], - "requires_python": ">=3.8", - "version": "2.32.3" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563", - "url": "https://files.pythonhosted.org/packages/97/ec/889fbc557727da0c34a33850950310240f2040f3b1955175fdb2b36a8910/requests_mock-1.12.1-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401", - "url": "https://files.pythonhosted.org/packages/92/32/587625f91f9a0a3d84688bf9cfc4b2480a7e8ec327cefd0ff2ac891fd2cf/requests-mock-1.12.1.tar.gz" - } - ], - "project_name": "requests-mock", - "requires_dists": [ - "fixtures; extra == \"fixture\"", - "requests<3,>=2.22" - ], - "requires_python": ">=3.5", - "version": "1.12.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "b7781090c647308a88b55fb530c7b3705cef45349e70a83b8d6731e7889272a6", - "url": "https://files.pythonhosted.org/packages/b6/0b/84787a85a4aee9860a510747e9a0cffd08ebfa32d9c728b0db6306883ad1/requests_ntlm-1.2.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "33c285f5074e317cbdd338d199afa46a7c01132e5c111d36bd415534e9b916a8", - "url": "https://files.pythonhosted.org/packages/7a/ad/486a6ca1879cf1bb181e3e4af4d816d23ec538a220ef75ca925ccb7dd31d/requests_ntlm-1.2.0.tar.gz" - } - ], - "project_name": "requests-ntlm", - "requires_dists": [ - "cryptography>=1.3", - "pyspnego>=0.1.6", - "requests>=2.0.0" - ], - "requires_python": ">=3.7", - "version": "1.2.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", - "url": "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", - "url": "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz" - } - ], - "project_name": "requests-oauthlib", - "requires_dists": [ - "oauthlib>=3.0.0", - "oauthlib[signedtoken]>=3.0.0; extra == \"rsa\"", - "requests>=2.0.0" - ], - "requires_python": ">=3.4", - "version": "2.0.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", - "url": "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", - "url": "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz" - } - ], - "project_name": "requests-toolbelt", - "requires_dists": [ - "requests<3.0.0,>=2.0.1" - ], - "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7", - "version": "1.0.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", - "url": "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", - "url": "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz" - } - ], - "project_name": "rfc3986", - "requires_dists": [ - "idna; extra == \"idna2008\"" - ], - "requires_python": ">=3.7", - "version": "2.0.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06", - "url": "https://files.pythonhosted.org/packages/b0/11/dadb85e2bd6b1f1ae56669c3e1f0410797f9605d752d68fb47b77f525b31/rich-13.8.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a", - "url": "https://files.pythonhosted.org/packages/92/76/40f084cb7db51c9d1fa29a7120717892aeda9a7711f6225692c957a93535/rich-13.8.1.tar.gz" - } - ], - "project_name": "rich", - "requires_dists": [ - "ipywidgets<9,>=7.5.1; extra == \"jupyter\"", - "markdown-it-py>=2.2.0", - "pygments<3.0.0,>=2.13.0", - "typing-extensions<5.0,>=4.0.0; python_version < \"3.9\"" - ], - "requires_python": ">=3.7.0", - "version": "13.8.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344", - "url": "https://files.pythonhosted.org/packages/bf/d6/4b2fad4898154365f0f2bd72ffd190349274a4c1d6a6f94f02a83bb2b8f1/rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60", - "url": "https://files.pythonhosted.org/packages/01/9e/d68fba289625b5d3c9d1925825d7da716fbf812bda2133ac409021d5db13/rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92", - "url": "https://files.pythonhosted.org/packages/04/b6/02a54c47c178d180395b3c9a8bfb3b93906e08f9acf7b4a1067d27c3fae0/rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" - }, - { - "algorithm": "sha256", - "hash": "617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045", - "url": "https://files.pythonhosted.org/packages/06/39/bf1f664c347c946ef56cecaa896e3693d91acc741afa78ebb3fdb7aba08b/rpds_py-0.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150", - "url": "https://files.pythonhosted.org/packages/36/10/3f4e490fe6eb069c07c22357d0b4804cd94cb9f8d01345ef9b1d93482b9d/rpds_py-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f", - "url": "https://files.pythonhosted.org/packages/3b/d3/822a28152a1e7e2ba0dc5d06cf8736f4cd64b191bb6ec47fb51d1c3c5ccf/rpds_py-0.20.0-cp310-cp310-musllinux_1_2_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf", - "url": "https://files.pythonhosted.org/packages/4a/6d/1166a157b227f2333f8e8ae320b6b7ea2a6a38fbe7a3563ad76dffc8608d/rpds_py-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121", - "url": "https://files.pythonhosted.org/packages/55/64/b693f262791b818880d17268f3f8181ef799b0d187f6f731b1772e05a29a/rpds_py-0.20.0.tar.gz" - }, - { - "algorithm": "sha256", - "hash": "1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2", - "url": "https://files.pythonhosted.org/packages/60/5e/642a44fda6dda90b5237af7a0ef1d088159c30a504852b94b0396eb62125/rpds_py-0.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" - }, - { - "algorithm": "sha256", - "hash": "d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855", - "url": "https://files.pythonhosted.org/packages/6f/d9/7ff03ff3642c600f27ff94512bb158a8d815fea5ed4162c75a7e850d6003/rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl" - }, - { - "algorithm": "sha256", - "hash": "df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140", - "url": "https://files.pythonhosted.org/packages/70/a4/70ea49863ea09ae4c2971f2eef58e80b757e3c0f2f618c5815bb751f7847/rpds_py-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2", - "url": "https://files.pythonhosted.org/packages/71/2d/a7e60483b72b91909e18f29a5c5ae847bac4e2ae95b77bb77e1f41819a58/rpds_py-0.20.0-cp310-cp310-macosx_10_12_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94", - "url": "https://files.pythonhosted.org/packages/72/f8/d5625ee05c4e5c478954a16d9359069c66fe8ac8cd5ddf28f80d3b321837/rpds_py-0.20.0-cp310-cp310-musllinux_1_2_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3", - "url": "https://files.pythonhosted.org/packages/7c/b5/ff18c093c9e72630f6d6242e5ccb0728ef8265ba0a154b5972f89d23790a/rpds_py-0.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl" - }, - { - "algorithm": "sha256", - "hash": "d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d", - "url": "https://files.pythonhosted.org/packages/a7/64/df4966743aa4def8727dc13d06527c8b13eb7412c1429def2d4701bee520/rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" - }, - { - "algorithm": "sha256", - "hash": "9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f", - "url": "https://files.pythonhosted.org/packages/b5/b4/f15b0c55a6d880ce74170e7e28c3ed6c5acdbbd118df50b91d1dabf86008/rpds_py-0.20.0-cp310-cp310-macosx_11_0_arm64.whl" - }, - { - "algorithm": "sha256", - "hash": "c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511", - "url": "https://files.pythonhosted.org/packages/b8/c6/e1b886f7277b3454e55e85332e165091c19114eecb5377b88d892fd36ccf/rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc", - "url": "https://files.pythonhosted.org/packages/c1/71/876135d3cb90d62468540b84e8e83ff4dc92052ab309bfdea7ea0b9221ad/rpds_py-0.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl" - }, - { - "algorithm": "sha256", - "hash": "4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075", - "url": "https://files.pythonhosted.org/packages/c3/92/93c5a530898d3a5d1ce087455071ba714b77806ed9ffee4070d0c7a53b7e/rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce", - "url": "https://files.pythonhosted.org/packages/c6/a5/6ef91e4425dc8b3445ff77d292fc4c5e37046462434a0423c4e0a596a8bd/rpds_py-0.20.0-cp310-cp310-musllinux_1_2_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51", - "url": "https://files.pythonhosted.org/packages/e2/62/e26bd5b944e547c7bfd0b6ca7e306bfa430f8bd298ab72a1217976a7ca8d/rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e", - "url": "https://files.pythonhosted.org/packages/f5/c8/cd6ab31b4424c7fab3b17e153b6ea7d1bb0d7cabea5c1ef683cc8adb8bc2/rpds_py-0.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" - }, - { - "algorithm": "sha256", - "hash": "deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02", - "url": "https://files.pythonhosted.org/packages/f7/da/8ccaeba6a3dda7467aebaf893de9eafd56275e2c90773c83bf15fb0b8374/rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - } - ], - "project_name": "rpds-py", - "requires_dists": [], - "requires_python": ">=3.8", - "version": "0.20.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", - "url": "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", - "url": "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz" - } - ], - "project_name": "secretstorage", - "requires_dists": [ - "cryptography>=2.0", - "jeepney>=0.6" - ], - "requires_python": ">=3.6", - "version": "3.3.3" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2", - "url": "https://files.pythonhosted.org/packages/ff/ae/f19306b5a221f6a436d8f2238d5b80925004093fa3edea59835b514d9057/setuptools-75.1.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538", - "url": "https://files.pythonhosted.org/packages/27/b8/f21073fde99492b33ca357876430822e4800cdf522011f18041351dfa74b/setuptools-75.1.0.tar.gz" - } - ], - "project_name": "setuptools", - "requires_dists": [ - "build[virtualenv]>=1.0.3; extra == \"test\"", - "filelock>=3.4.0; extra == \"test\"", - "furo; extra == \"doc\"", - "importlib-metadata>=6; python_version < \"3.10\" and extra == \"core\"", - "importlib-metadata>=7.0.2; python_version < \"3.10\" and extra == \"type\"", - "importlib-resources>=5.10.2; python_version < \"3.9\" and extra == \"core\"", - "ini2toml[lite]>=0.14; extra == \"test\"", - "jaraco.collections; extra == \"core\"", - "jaraco.develop>=7.21; (python_version >= \"3.9\" and sys_platform != \"cygwin\") and extra == \"test\"", - "jaraco.develop>=7.21; sys_platform != \"cygwin\" and extra == \"type\"", - "jaraco.envs>=2.2; extra == \"test\"", - "jaraco.functools; extra == \"core\"", - "jaraco.packaging>=9.3; extra == \"doc\"", - "jaraco.path>=3.2.0; extra == \"test\"", - "jaraco.test; extra == \"test\"", - "jaraco.text>=3.7; extra == \"core\"", - "jaraco.tidelift>=1.4; extra == \"doc\"", - "more-itertools; extra == \"core\"", - "more-itertools>=8.8; extra == \"core\"", - "mypy==1.11.*; extra == \"type\"", - "packaging; extra == \"core\"", - "packaging>=23.2; extra == \"test\"", - "packaging>=24; extra == \"core\"", - "pip>=19.1; extra == \"test\"", - "platformdirs>=2.6.2; extra == \"core\"", - "pygments-github-lexers==0.0.5; extra == \"doc\"", - "pyproject-hooks!=1.1; extra == \"doc\"", - "pyproject-hooks!=1.1; extra == \"test\"", - "pytest!=8.1.*,>=6; extra == \"test\"", - "pytest-checkdocs>=2.4; extra == \"check\"", - "pytest-cov; extra == \"cover\"", - "pytest-enabler>=2.2; extra == \"enabler\"", - "pytest-home>=0.5; extra == \"test\"", - "pytest-mypy; extra == \"type\"", - "pytest-perf; sys_platform != \"cygwin\" and extra == \"test\"", - "pytest-ruff>=0.2.1; sys_platform != \"cygwin\" and extra == \"check\"", - "pytest-subprocess; extra == \"test\"", - "pytest-timeout; extra == \"test\"", - "pytest-xdist>=3; extra == \"test\"", - "rst.linker>=1.9; extra == \"doc\"", - "ruff>=0.5.2; sys_platform != \"cygwin\" and extra == \"check\"", - "sphinx-favicon; extra == \"doc\"", - "sphinx-inline-tabs; extra == \"doc\"", - "sphinx-lint; extra == \"doc\"", - "sphinx-notfound-page<2,>=1; extra == \"doc\"", - "sphinx-reredirects; extra == \"doc\"", - "sphinx>=3.5; extra == \"doc\"", - "sphinxcontrib-towncrier; extra == \"doc\"", - "tomli-w>=1.0.0; extra == \"test\"", - "tomli>=2.0.1; python_version < \"3.11\" and extra == \"core\"", - "towncrier<24.7; extra == \"doc\"", - "virtualenv>=13.0.0; extra == \"test\"", - "wheel>=0.43.0; extra == \"core\"", - "wheel>=0.44.0; extra == \"test\"" - ], - "requires_python": ">=3.8", - "version": "75.1.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", - "url": "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "url": "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz" - } - ], - "project_name": "six", - "requires_dists": [], - "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7", - "version": "1.16.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", - "url": "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", - "url": "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz" - } - ], - "project_name": "sniffio", - "requires_dists": [], - "requires_python": ">=3.7", - "version": "1.3.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", - "url": "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", - "url": "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz" - } - ], - "project_name": "sortedcontainers", - "requires_dists": [], - "requires_python": null, - "version": "2.4.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", - "url": "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", - "url": "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz" - } - ], - "project_name": "soupsieve", - "requires_dists": [], - "requires_python": ">=3.8", - "version": "2.6" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "2fa30d0b72c9650ad12bbe031c9943b8d441e41b4f5602b0ec977a19f3290e98", - "url": "https://files.pythonhosted.org/packages/9f/8e/3310207a68118000ca27ac878b8386123628b335ecb3d4bec4743357f0d1/spinners-0.0.24-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "1eb6aeb4781d72ab42ed8a01dcf20f3002bf50740d7154d12fb8c9769bf9e27f", - "url": "https://files.pythonhosted.org/packages/d3/91/bb331f0a43e04d950a710f402a0986a54147a35818df0e1658551c8d12e1/spinners-0.0.24.tar.gz" - } - ], - "project_name": "spinners", - "requires_dists": [ - "enum34==1.1.6; python_version < \"3.4\"" - ], - "requires_python": null, - "version": "0.0.24" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "21c4564095e04857b8c206db8197cdda64708bf7b6cefddf2fd9ac88cfd45196", - "url": "https://files.pythonhosted.org/packages/99/ac/c7287f153e65c28ef4f04fdc2483d5dee7e0c80b9f4b46d0398ab2512446/splunk-sdk-1.7.3.tar.gz" - } - ], - "project_name": "splunk-sdk", - "requires_dists": [], - "requires_python": null, - "version": "1.7.3" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687", - "url": "https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78", - "url": "https://files.pythonhosted.org/packages/a3/4d/6a19536c50b849338fcbe9290d562b52cbdcf30d8963d3588a68a4107df1/tenacity-8.5.0.tar.gz" - } - ], - "project_name": "tenacity", - "requires_dists": [ - "pytest; extra == \"test\"", - "reno; extra == \"doc\"", - "sphinx; extra == \"doc\"", - "tornado>=4.5; extra == \"test\"", - "typeguard; extra == \"test\"" - ], - "requires_python": ">=3.8", - "version": "8.5.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63", - "url": "https://files.pythonhosted.org/packages/d9/5f/8c716e47b3a50cbd7c146f45881e11d9414def768b7cd9c5e6650ec2a80a/termcolor-2.4.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a", - "url": "https://files.pythonhosted.org/packages/10/56/d7d66a84f96d804155f6ff2873d065368b25a07222a6fd51c4f24ef6d764/termcolor-2.4.0.tar.gz" - } - ], - "project_name": "termcolor", - "requires_dists": [ - "pytest-cov; extra == \"tests\"", - "pytest; extra == \"tests\"" - ], - "requires_python": ">=3.8", - "version": "2.4.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "8e58c7eb29d2ab35a7a8929cbeea60216a4ccdf42efa8974d8e176d50c9a3df5", - "url": "https://files.pythonhosted.org/packages/f2/6c/83ca40527d072739f0704b9f59b325786c444ca63672a77cb69adc8181f7/tiktoken-0.7.0-cp310-cp310-musllinux_1_2_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "13c94efacdd3de9aff824a788353aa5749c0faee1fbe3816df365ea450b82311", - "url": "https://files.pythonhosted.org/packages/72/40/61d6354cb64a563fce475a2907039be9fe809ca5f801213856353b01a35b/tiktoken-0.7.0-cp310-cp310-musllinux_1_2_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "485f3cc6aba7c6b6ce388ba634fbba656d9ee27f766216f45146beb4ac18b25f", - "url": "https://files.pythonhosted.org/packages/96/10/28d59d43d72a0ebd4211371d0bf10c935cdecbb62b812ae04c58bfc37d96/tiktoken-0.7.0-cp310-cp310-macosx_10_9_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "79383a6e2c654c6040e5f8506f3750db9ddd71b550c724e673203b4f6b4b4590", - "url": "https://files.pythonhosted.org/packages/b9/ab/f9c7675747f259d133d66065106cf732a7c2bef6043062fbca8e011f7f4d/tiktoken-0.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "1077266e949c24e0291f6c350433c6f0971365ece2b173a23bc3b9f9defef6b6", - "url": "https://files.pythonhosted.org/packages/c4/4a/abaec53e93e3ef37224a4dd9e2fc6bb871e7a538c2b6b9d2a6397271daf4/tiktoken-0.7.0.tar.gz" - }, - { - "algorithm": "sha256", - "hash": "5d4511c52caacf3c4981d1ae2df85908bd31853f33d30b345c8b6830763f769c", - "url": "https://files.pythonhosted.org/packages/e7/8c/7d1007557b343d5cf18349802e94d3a14397121e9105b4661f8cd753f9bf/tiktoken-0.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "e54be9a2cd2f6d6ffa3517b064983fb695c9a9d8aa7d574d1ef3c3f931a99225", - "url": "https://files.pythonhosted.org/packages/f8/0c/d4125348dedd1f8f38e3f85245e7fc38858ffc77c9b7edfb762a8191ba0b/tiktoken-0.7.0-cp310-cp310-macosx_11_0_arm64.whl" - } - ], - "project_name": "tiktoken", - "requires_dists": [ - "blobfile>=2; extra == \"blobfile\"", - "regex>=2022.1.18", - "requests>=2.26.0" - ], - "requires_python": ">=3.8", - "version": "0.7.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "url": "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", - "url": "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz" - } - ], - "project_name": "tomli", - "requires_dists": [], - "requires_python": ">=3.7", - "version": "2.0.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd", - "url": "https://files.pythonhosted.org/packages/48/5d/acf5905c36149bbaec41ccf7f2b68814647347b72075ac0b1fe3022fdc73/tqdm-4.66.5-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad", - "url": "https://files.pythonhosted.org/packages/58/83/6ba9844a41128c62e810fddddd72473201f3eacde02046066142a2d96cc5/tqdm-4.66.5.tar.gz" - } - ], - "project_name": "tqdm", - "requires_dists": [ - "colorama; platform_system == \"Windows\"", - "ipywidgets>=6; extra == \"notebook\"", - "pytest-cov; extra == \"dev\"", - "pytest-timeout; extra == \"dev\"", - "pytest-xdist; extra == \"dev\"", - "pytest>=6; extra == \"dev\"", - "requests; extra == \"telegram\"", - "slack-sdk; extra == \"slack\"" - ], - "requires_python": ">=3.7", - "version": "4.66.5" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997", - "url": "https://files.pythonhosted.org/packages/5d/ec/00f9d5fd040ae29867355e559a94e9a8429225a0284a3f5f091a3878bfc0/twine-5.1.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db", - "url": "https://files.pythonhosted.org/packages/77/68/bd982e5e949ef8334e6f7dcf76ae40922a8750aa2e347291ae1477a4782b/twine-5.1.1.tar.gz" - } - ], - "project_name": "twine", - "requires_dists": [ - "importlib-metadata>=3.6", - "keyring>=15.1", - "pkginfo<1.11", - "pkginfo>=1.8.1", - "readme-renderer>=35.0", - "requests-toolbelt!=0.9.0,>=0.8.0", - "requests>=2.20", - "rfc3986>=1.4.0", - "rich>=12.0.0", - "urllib3>=1.26.0" - ], - "requires_python": ">=3.8", - "version": "5.1.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "32f5ac48514b488f15241afdd7d2f73f0baf3c54e874e23b66708503dd288489", - "url": "https://files.pythonhosted.org/packages/78/ce/bea8ea21414287b7fc0ad2fccdb571ad8e2f438d1a1d486d146e906ea041/types_beautifulsoup4-4.12.0.20240907-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "8d023b86530922070417a1d4c4d91678ab0ff2439b3b2b2cffa3b628b49ebab1", - "url": "https://files.pythonhosted.org/packages/3e/e3/138900d35df4e673935239f0259f4b984f11db32c99ceb4f6819c47a7cfc/types-beautifulsoup4-4.12.0.20240907.tar.gz" - } - ], - "project_name": "types-beautifulsoup4", - "requires_dists": [ - "types-html5lib" - ], - "requires_python": ">=3.8", - "version": "4.12.0.20240907" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "575c4fd84ba8eeeaa8520c7e4c7042b7791f5ec3e9c0a5d5c418124c42d9e7e4", - "url": "https://files.pythonhosted.org/packages/d9/df/ee52df5c2cb7f40f6b9d45fc11cc9256d3e237e04d57e2d797b448815fc7/types_html5lib-1.1.11.20240806-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "8060dc98baf63d6796a765bbbc809fff9f7a383f6e3a9add526f814c086545ef", - "url": "https://files.pythonhosted.org/packages/21/ac/a2ca5366f0337ae9c947d611c19116bd56e845976782aaa35247e2a699e8/types-html5lib-1.1.11.20240806.tar.gz" - } - ], - "project_name": "types-html5lib", - "requires_dists": [], - "requires_python": ">=3.8", - "version": "1.1.11.20240806" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "be283e23f0b87547316c2ee6b0fd36d95ea30e921db06478029e10b5b6aa6ac3", - "url": "https://files.pythonhosted.org/packages/3c/6f/8acab5c921096a52bd26888d5155a36cd2ff7f820b4fbb4e36c662e9a32e/types_jsonschema-4.23.0.20240813-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "c93f48206f209a5bc4608d295ac39f172fb98b9e24159ce577dbd25ddb79a1c0", - "url": "https://files.pythonhosted.org/packages/c7/97/4f1880bfeb87a70c78d87089073ad0d0361747d5c36b2e3fafa5b2edb263/types-jsonschema-4.23.0.20240813.tar.gz" - } - ], - "project_name": "types-jsonschema", - "requires_dists": [ - "referencing" - ], - "requires_python": ">=3.8", - "version": "4.23.0.20240813" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "d586a01d39ad919d3ddcd73de6cde73ca7f3c69707219f722d1b8d7733641ad7", - "url": "https://files.pythonhosted.org/packages/02/6d/7710612643616654ca0094234bce0f0448f4aa9d6f3057e4681143f73e73/types_mock-5.1.0.20240425-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "5281a645d72e827d70043e3cc144fe33b1c003db084f789dc203aa90e812a5a4", - "url": "https://files.pythonhosted.org/packages/36/40/1ed5b983c97161ad1605de42932143bcb24f5e435cc660de4487f78f6a4c/types-mock-5.1.0.20240425.tar.gz" - } - ], - "project_name": "types-mock", - "requires_dists": [], - "requires_python": ">=3.8", - "version": "5.1.0.20240425" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "ead901b87e80f9fce1ff3d624c9ae279281d69d6584014f42105513f5cbc68dd", - "url": "https://files.pythonhosted.org/packages/86/75/fbb177ee5fdee817c31191cb777c43f7b285556b12377eedb1403d276a27/types_openpyxl-3.1.5.20240822-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "08b0b771c6721bde35bea5df9c61ca8317ff1e1ee782164c274e69a869e49db4", - "url": "https://files.pythonhosted.org/packages/30/e8/c58b6c2938994efe01e52176c421ed5463824fcc5d4b9cb56743a9cb7b0b/types-openpyxl-3.1.5.20240822.tar.gz" - } - ], - "project_name": "types-openpyxl", - "requires_dists": [], - "requires_python": ">=3.8", - "version": "3.1.5.20240822" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "27c8cc2d058ccb14946eebcaaa503088f4f6dbc4fb6093d3d456a49aef2753f6", - "url": "https://files.pythonhosted.org/packages/aa/4c/5c684b333135a6fb085bb5a5bdfd962937f80bec06745a88fd551e29f4d9/types_python_dateutil-2.9.0.20240906-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "9706c3b68284c25adffc47319ecc7947e5bb86b3773f843c73906fd598bc176e", - "url": "https://files.pythonhosted.org/packages/3e/d9/9c9ec2d870af7aa9b722ce4fd5890bb55b1d18898df7f1d069cab194bb2a/types-python-dateutil-2.9.0.20240906.tar.gz" - } - ], - "project_name": "types-python-dateutil", - "requires_dists": [], - "requires_python": ">=3.8", - "version": "2.9.0.20240906" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "a1eebf57ebc6e127a99d2fa2ba0a88d2b173784ef9b3defcc2004ab6855a44df", - "url": "https://files.pythonhosted.org/packages/b6/a6/8846372f55c6bb470ff7207e4dc601017e264e5fe7d79a441ece3545b36c/types_pytz-2024.2.0.20240913-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "4433b5df4a6fc587bbed41716d86a5ba5d832b4378e506f40d34bc9c81df2c24", - "url": "https://files.pythonhosted.org/packages/da/cf/a4811b07d7309d9eecf0f383ca5747ce90f8a0d860acb2050bc57f3c9379/types-pytz-2024.2.0.20240913.tar.gz" - } - ], - "project_name": "types-pytz", - "requires_dists": [], - "requires_python": ">=3.8", - "version": "2024.2.0.20240913" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "392b267f1c0fe6022952462bf5d6523f31e37f6cea49b14cee7ad634b6301570", - "url": "https://files.pythonhosted.org/packages/9e/2c/c1d81d680997d24b0542aa336f0a65bd7835e5224b7670f33a7d617da379/types_PyYAML-6.0.12.20240917-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "d1405a86f9576682234ef83bcb4e6fff7c9305c8b1fbad5e0bcd4f7dbdc9c587", - "url": "https://files.pythonhosted.org/packages/92/7d/a95df0a11f95c8f48d7683f03e4aed1a2c0fc73e9de15cca4d38034bea1a/types-PyYAML-6.0.12.20240917.tar.gz" - } - ], - "project_name": "types-pyyaml", - "requires_dists": [], - "requires_python": ">=3.8", - "version": "6.0.12.20240917" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "59c2f673eb55f32a99b2894faf6020e1a9f4a402ad0f192bfee0b64469054310", - "url": "https://files.pythonhosted.org/packages/8f/55/ea44dad71b9d92f86198f7448f5ba46ac919355f4f69bb1c0fa1af02b1b4/types_requests-2.32.0.20240914-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "2850e178db3919d9bf809e434eef65ba49d0e7e33ac92d588f4a5e295fffd405", - "url": "https://files.pythonhosted.org/packages/9b/9e/aea33405c230cc3984c9f1065012d3a2003cef910730c367a0e91e7a4901/types-requests-2.32.0.20240914.tar.gz" - } - ], - "project_name": "types-requests", - "requires_dists": [ - "urllib3>=2" - ], - "requires_python": ">=3.8", - "version": "2.32.0.20240914" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "cb251c59e838986d8402b10d804225ade9fd6c9f66d01dc45cd6cfdf43640128", - "url": "https://files.pythonhosted.org/packages/9a/31/8046c84acfc397e60afe39ac5cdaf1ba5cf95eef11a1e995c3e951f08ba5/types_xmltodict-0.13.0.3-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "8884534bab0364c4b22d5973f3c8153ff40d413a801d9e70eb893e676909f1fc", - "url": "https://files.pythonhosted.org/packages/4f/bc/191d95f4e18ee8ffe626046574805fad7734827146e6ad39d56149fb1f62/types-xmltodict-0.13.0.3.tar.gz" - } - ], - "project_name": "types-xmltodict", - "requires_dists": [], - "requires_python": null, - "version": "0.13.0.3" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", - "url": "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", - "url": "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz" - } - ], - "project_name": "typing-extensions", - "requires_dists": [], - "requires_python": ">=3.8", - "version": "4.12.2" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252", - "url": "https://files.pythonhosted.org/packages/65/58/f9c9e6be752e9fcb8b6a0ee9fb87e6e7a1f6bcab2cdc73f02bb7ba91ada0/tzdata-2024.1-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", - "url": "https://files.pythonhosted.org/packages/74/5b/e025d02cb3b66b7b76093404392d4b44343c69101cc85f4d180dd5784717/tzdata-2024.1.tar.gz" - } - ], - "project_name": "tzdata", - "requires_dists": [], - "requires_python": ">=2", - "version": "2024.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8", - "url": "https://files.pythonhosted.org/packages/97/3f/c4c51c55ff8487f2e6d0e618dba917e3c3ee2caae6cf0fbb59c9b1876f2e/tzlocal-5.2-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e", - "url": "https://files.pythonhosted.org/packages/04/d3/c19d65ae67636fe63953b20c2e4a8ced4497ea232c43ff8d01db16de8dc0/tzlocal-5.2.tar.gz" - } - ], - "project_name": "tzlocal", - "requires_dists": [ - "backports.zoneinfo; python_version < \"3.9\"", - "check-manifest; extra == \"devenv\"", - "pytest-cov; extra == \"devenv\"", - "pytest-mock>=3.3; extra == \"devenv\"", - "pytest>=4.3; extra == \"devenv\"", - "tzdata; platform_system == \"Windows\"", - "zest.releaser; extra == \"devenv\"" - ], - "requires_python": ">=3.8", - "version": "5.2" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "bae297d090e69a0451130ffba6f2f1c9477244aa0a5543d66aed2d9f77d0dd9c", - "url": "https://files.pythonhosted.org/packages/e6/17/5a4510d9ca9cc8be217ce359eb54e693dca81cf4d442308b282d5131b17d/uritools-4.0.3-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "ee06a182a9c849464ce9d5fa917539aacc8edd2a4924d1b7aabeeecabcae3bc2", - "url": "https://files.pythonhosted.org/packages/d3/43/4182fb2a03145e6d38698e38b49114ce59bc8c79063452eb585a58f8ce78/uritools-4.0.3.tar.gz" - } - ], - "project_name": "uritools", - "requires_dists": [], - "requires_python": ">=3.7", - "version": "4.0.3" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", - "url": "https://files.pythonhosted.org/packages/ca/1c/89ffc63a9605b583d5df2be791a27bc1a42b7c32bab68d3c8f2f73a98cd4/urllib3-2.2.2-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168", - "url": "https://files.pythonhosted.org/packages/43/6d/fa469ae21497ddc8bc93e5877702dca7cb8f911e337aca7452b5724f1bb6/urllib3-2.2.2.tar.gz" - } - ], - "project_name": "urllib3", - "requires_dists": [ - "brotli>=1.0.9; platform_python_implementation == \"CPython\" and extra == \"brotli\"", - "brotlicffi>=0.8.0; platform_python_implementation != \"CPython\" and extra == \"brotli\"", - "h2<5,>=4; extra == \"h2\"", - "pysocks!=1.5.7,<2.0,>=1.5.6; extra == \"socks\"", - "zstandard>=0.18.0; extra == \"zstd\"" - ], - "requires_python": ">=3.8", - "version": "2.2.2" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "61cf7d4a62bbae559f2e54aed3b000cea9ff3e2fdbe463f51179b92c58c9585a", - "url": "https://files.pythonhosted.org/packages/3a/0c/785d317eea99c3739821718f118c70537639aa43f96bfa1d83a71f68eaf6/validators-0.22.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "77b2689b172eeeb600d9605ab86194641670cdb73b60afd577142a9397873370", - "url": "https://files.pythonhosted.org/packages/9b/21/40a249498eee5a244a017582c06c0af01851179e2617928063a3d628bc8f/validators-0.22.0.tar.gz" - } - ], - "project_name": "validators", - "requires_dists": [ - "bandit[toml]>=1.7.5; extra == \"sast\"", - "black>=23.7.0; extra == \"tooling\"", - "build>=1.0.0; extra == \"package\"", - "mkdocs-git-revision-date-localized-plugin>=1.2.0; extra == \"docs-online\"", - "mkdocs-material>=9.2.6; extra == \"docs-online\"", - "mkdocs>=1.5.2; extra == \"docs-online\"", - "mkdocstrings[python]>=0.22.0; extra == \"docs-online\"", - "myst-parser>=2.0.0; extra == \"docs-offline\"", - "pre-commit>=3.3.3; extra == \"hooks\"", - "pyaml>=23.7.0; extra == \"docs-online\"", - "pyaml>=23.7.0; extra == \"tooling-extras\"", - "pypandoc-binary>=1.11; extra == \"docs-offline\"", - "pypandoc-binary>=1.11; extra == \"tooling-extras\"", - "pyright>=1.1.325; extra == \"tooling\"", - "pytest>=7.4.0; extra == \"testing\"", - "pytest>=7.4.0; extra == \"tooling-extras\"", - "ruff>=0.0.287; extra == \"tooling\"", - "sphinx>=7.1.1; extra == \"docs-offline\"", - "tox>=4.11.1; extra == \"runner\"", - "twine>=4.0.2; extra == \"package\"" - ], - "requires_python": ">=3.8", - "version": "0.22.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", - "url": "https://files.pythonhosted.org/packages/ff/21/abdedb4cdf6ff41ebf01a74087740a709e2edb146490e4d9beea054b0b7a/wrapt-1.16.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136", - "url": "https://files.pythonhosted.org/packages/07/44/359e4724a92369b88dbf09878a7cde7393cf3da885567ea898e5904049a3/wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72", - "url": "https://files.pythonhosted.org/packages/19/d4/cd33d3a82df73a064c9b6401d14f346e1d2fb372885f0295516ec08ed2ee/wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020", - "url": "https://files.pythonhosted.org/packages/32/12/e11adfde33444986135d8881b401e4de6cbb4cced046edc6b464e6ad7547/wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl" - }, - { - "algorithm": "sha256", - "hash": "ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf", - "url": "https://files.pythonhosted.org/packages/49/83/b40bc1ad04a868b5b5bcec86349f06c1ee1ea7afe51dc3e46131e4f39308/wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440", - "url": "https://files.pythonhosted.org/packages/70/7d/3dcc4a7e96f8d3e398450ec7703db384413f79bd6c0196e0e139055ce00f/wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d", - "url": "https://files.pythonhosted.org/packages/95/4c/063a912e20bcef7124e0df97282a8af3ff3e4b603ce84c481d6d7346be0a/wrapt-1.16.0.tar.gz" - }, - { - "algorithm": "sha256", - "hash": "ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4", - "url": "https://files.pythonhosted.org/packages/a8/c6/5375258add3777494671d8cec27cdf5402abd91016dee24aa2972c61fedf/wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487", - "url": "https://files.pythonhosted.org/packages/d1/c4/8dfdc3c2f0b38be85c8d9fdf0011ebad2f54e40897f9549a356bebb63a97/wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0", - "url": "https://files.pythonhosted.org/packages/ef/58/2fde309415b5fa98fd8f5f4a11886cbf276824c4c64d45a39da342fff6fe/wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl" - } - ], - "project_name": "wrapt", - "requires_dists": [], - "requires_python": ">=3.6", - "version": "1.16.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd", - "url": "https://files.pythonhosted.org/packages/a6/0c/c2a72d51fe56e08a08acc85d13013558a2d793028ae7385448a6ccdfae64/xlrd-2.0.1-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88", - "url": "https://files.pythonhosted.org/packages/a6/b3/19a2540d21dea5f908304375bd43f5ed7a4c28a370dc9122c565423e6b44/xlrd-2.0.1.tar.gz" - } - ], - "project_name": "xlrd", - "requires_dists": [ - "pytest-cov; extra == \"test\"", - "pytest; extra == \"test\"", - "sphinx; extra == \"docs\"", - "twine; extra == \"build\"", - "wheel; extra == \"build\"" - ], - "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7", - "version": "2.0.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852", - "url": "https://files.pythonhosted.org/packages/94/db/fd0326e331726f07ff7f40675cd86aa804bfd2e5016c727fa761c934990e/xmltodict-0.13.0-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56", - "url": "https://files.pythonhosted.org/packages/39/0d/40df5be1e684bbaecdb9d1e0e40d5d482465de6b00cbb92b84ee5d243c7f/xmltodict-0.13.0.tar.gz" - } - ], - "project_name": "xmltodict", - "requires_dists": [], - "requires_python": ">=3.4", - "version": "0.13.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", - "url": "https://files.pythonhosted.org/packages/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", - "url": "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz" - } - ], - "project_name": "zipp", - "requires_dists": [ - "big-O; extra == \"test\"", - "furo; extra == \"doc\"", - "importlib-resources; python_version < \"3.9\" and extra == \"test\"", - "jaraco.functools; extra == \"test\"", - "jaraco.itertools; extra == \"test\"", - "jaraco.packaging>=9.3; extra == \"doc\"", - "jaraco.test; extra == \"test\"", - "jaraco.tidelift>=1.4; extra == \"doc\"", - "more-itertools; extra == \"test\"", - "pytest!=8.1.*,>=6; extra == \"test\"", - "pytest-checkdocs>=2.4; extra == \"check\"", - "pytest-cov; extra == \"cover\"", - "pytest-enabler>=2.2; extra == \"enabler\"", - "pytest-ignore-flaky; extra == \"test\"", - "pytest-mypy; extra == \"type\"", - "pytest-ruff>=0.2.1; sys_platform != \"cygwin\" and extra == \"check\"", - "rst.linker>=1.9; extra == \"doc\"", - "sphinx-lint; extra == \"doc\"", - "sphinx>=3.5; extra == \"doc\"" - ], - "requires_python": ">=3.8", - "version": "3.20.2" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "6195c3c03fef9f87c0dbee0b3b6451df6e056322463cf35bca9a088e564a3c58", - "url": "https://files.pythonhosted.org/packages/45/58/890cf943c9a7dd82d096a11872c7efb3f0e97e86f71b886018044fb01972/zope.interface-7.0.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "3aa8fcbb0d3c2be1bfd013a0f0acd636f6ed570c287743ae2bbd467ee967154d", - "url": "https://files.pythonhosted.org/packages/5a/a9/9665ba3aa7c6173ea2c3249c85546139119eaf3146f280cea8053e0047b9/zope.interface-7.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "db6237e8fa91ea4f34d7e2d16d74741187e9105a63bbb5686c61fea04cdbacca", - "url": "https://files.pythonhosted.org/packages/6b/c3/7d18af6971634087a4ddc436e37fc47988c31635cd01948ff668d11c96c4/zope.interface-7.0.3-cp310-cp310-macosx_11_0_arm64.whl" - }, - { - "algorithm": "sha256", - "hash": "53d678bb1c3b784edbfb0adeebfeea6bf479f54da082854406a8f295d36f8386", - "url": "https://files.pythonhosted.org/packages/8a/64/2922134a93978b6a8b823f3e784d6af3d5d165fad1f66388b0f89b5695fc/zope.interface-7.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "cd2690d4b08ec9eaf47a85914fe513062b20da78d10d6d789a792c0b20307fb1", - "url": "https://files.pythonhosted.org/packages/c8/83/7de03efae7fc9a4ec64301d86e29a324f32fe395022e3a5b1a79e376668e/zope.interface-7.0.3.tar.gz" - }, - { - "algorithm": "sha256", - "hash": "9b9369671a20b8d039b8e5a1a33abd12e089e319a3383b4cc0bf5c67bd05fe7b", - "url": "https://files.pythonhosted.org/packages/e9/33/a55311169d3d41b61da7c5b7d528ebb0469263252a71d9510849c0d66201/zope.interface-7.0.3-cp310-cp310-macosx_10_9_x86_64.whl" - } - ], - "project_name": "zope-interface", - "requires_dists": [ - "Sphinx; extra == \"docs\"", - "coverage>=5.0.3; extra == \"test\"", - "coverage>=5.0.3; extra == \"testing\"", - "repoze.sphinx.autointerface; extra == \"docs\"", - "setuptools", - "sphinx-rtd-theme; extra == \"docs\"", - "zope.event; extra == \"test\"", - "zope.event; extra == \"testing\"", - "zope.testing; extra == \"test\"", - "zope.testing; extra == \"testing\"" - ], - "requires_python": ">=3.8", - "version": "7.0.3" - } - ], - "platform_tag": null - } - ], - "only_builds": [], - "only_wheels": [], - "path_mappings": {}, - "pex_version": "2.4.0", - "pip_version": "23.1.2", - "prefer_older_binary": false, - "requirements": [ - "asn1crypto==1.5.1", - "atlassian-python-api==3.41.3", - "azure-devops==7.1.0b4", - "beautifulsoup4==4.12.2", - "certifi==2024.7.4", - "click==8.1.3", - "cryptography==43.0.1", - "cyclonedx-python-lib[xml-validation]==5.1.1", - "datetime", - "defusedxml==0.7.1", - "dohq-artifactory==0.8.3", - "elmclient==0.26.2", - "freezegun", - "halo==0.0.31", - "idna==3.7", - "jira==3.8.0", - "jsonschema==4.23.0", - "langchain_ollama==0.1.1", - "langchain_openai==0.1.20", - "loguru==0.7.0", - "mock", - "mypy==1.4.1", - "numpy==1.24.3", - "ollama==0.3.1", - "openai==1.40.*", - "openpyxl<3.1.0", - "oracledb==2.2.0", - "packaging==23.2", - "pandas==2.1.3", - "py-jama-rest-client==1.17.1", - "pydantic==1.10.13", - "pyhanko==0.25.1", - "pyhanko_certvalidator==0.26.3", - "pypdf==3.17.3", - "pytest-cov!=2.12.1,<3.1,>=2.12", - "pytest-custom_exit_code==0.3.0", - "pytest-mock>=3.10.0", - "pytest-xdist<3,>=2.5", - "pytest<8", - "python-dateutil", - "pytz==2023.3", - "pyyaml==6.0", - "requests", - "requests-mock", - "requests-ntlm==1.2.0", - "splunk-sdk==1.7.3", - "types-PyYAML", - "types-beautifulsoup4", - "types-jsonschema", - "types-mock", - "types-openpyxl", - "types-python-dateutil", - "types-pytz", - "types-requests", - "types-xmltodict", - "urllib3==2.2.2", - "validators==0.22.0", - "xlrd==2.0.1", - "xmltodict==0.13.0" - ], - "requires_python": [ - "==3.10.*" - ], - "resolver_version": "pip-2020-resolver", - "style": "universal", - "target_systems": [ - "linux", - "mac" - ], - "transitive": true, - "use_pep517": null -} diff --git a/yaku-apps-python/3rdparty/requirements.txt b/yaku-apps-python/3rdparty/requirements.txt index a3e7329a..6ab34901 100644 --- a/yaku-apps-python/3rdparty/requirements.txt +++ b/yaku-apps-python/3rdparty/requirements.txt @@ -28,8 +28,8 @@ pydantic==1.10.13 pyhanko==0.25.1 pyhanko_certvalidator==0.26.3 pypdf==3.17.3 -pytest<8 -pytest-cov>=2.12,!=2.12.1,<3.1 +pytest +pytest-cov==5.0.0 pytest-custom_exit_code==0.3.0 pytest-mock>=3.10.0 pytest-xdist>=2.5,<3 @@ -38,7 +38,8 @@ pytz==2023.3 pyyaml==6.0 requests-mock requests-ntlm==1.2.0 -requests==2.32.0 +requests==2.32.3 +splunk-sdk urllib3==2.2.2 xlrd==2.0.1 xmltodict==0.13.0 diff --git a/yaku-apps-python/3rdparty/requirements_lock.txt b/yaku-apps-python/3rdparty/requirements_lock.txt new file mode 100644 index 00000000..d2b32693 --- /dev/null +++ b/yaku-apps-python/3rdparty/requirements_lock.txt @@ -0,0 +1,1744 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# bazel run //yaku-apps-python:requirements.update +# +anyio==4.6.2.post1 \ + --hash=sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c \ + --hash=sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d + # via + # httpx + # openai +anytree==2.12.1 \ + --hash=sha256:244def434ccf31b668ed282954e5d315b4e066c4940b94aff4a7962d85947830 \ + --hash=sha256:5ea9e61caf96db1e5b3d0a914378d2cd83c269dfce1fb8242ce96589fa3382f0 + # via elmclient +asn1crypto==1.5.1 \ + --hash=sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c \ + --hash=sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67 + # via + # -r yaku-apps-python/3rdparty/requirements.txt + # oscrypto + # pyhanko + # pyhanko-certvalidator +attrs==24.2.0 \ + --hash=sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346 \ + --hash=sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2 + # via + # jsonschema + # referencing +azure-core==1.31.0 \ + --hash=sha256:22954de3777e0250029360ef31d80448ef1be13b80a459bff80ba7073379e2cd \ + --hash=sha256:656a0dd61e1869b1506b7c6a3b31d62f15984b1a573d6326f6aa2f3e4123284b + # via msrest +azure-devops==7.1.0b4 \ + --hash=sha256:f04ba939112579f3d530cfecc044a74ef9e9339ba23c9ee1ece248241f07ff85 \ + --hash=sha256:f827e9fbc7c77bc6f2aaee46e5717514e9fe7d676c87624eccd0ca640b54f122 + # via -r yaku-apps-python/3rdparty/requirements.txt +backports-tarfile==1.2.0 \ + --hash=sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 \ + --hash=sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991 + # via jaraco-context +beautifulsoup4==4.12.2 \ + --hash=sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da \ + --hash=sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a + # via -r yaku-apps-python/3rdparty/requirements.txt +boolean-py==4.0 \ + --hash=sha256:17b9a181630e43dde1851d42bef546d616d5d9b4480357514597e78b203d06e4 \ + --hash=sha256:2876f2051d7d6394a531d82dc6eb407faa0b01a0a0b3083817ccd7323b8d96bd + # via license-expression +bump2version==1.0.1 \ + --hash=sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410 \ + --hash=sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6 + # via elmclient +cachecontrol==0.14.0 \ + --hash=sha256:7db1195b41c81f8274a7bbd97c956f44e8348265a1bc7641c37dfebc39f0c938 \ + --hash=sha256:f5bf3f0620c38db2e5122c0726bdebb0d16869de966ea6a2befe92470b740ea0 + # via elmclient +certifi==2024.7.4 \ + --hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \ + --hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90 + # via + # -r yaku-apps-python/3rdparty/requirements.txt + # httpcore + # httpx + # msrest + # requests +cffi==1.17.1 \ + --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ + --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \ + --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \ + --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \ + --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \ + --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \ + --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \ + --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \ + --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \ + --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \ + --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \ + --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \ + --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \ + --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \ + --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \ + --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \ + --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \ + --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \ + --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \ + --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \ + --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \ + --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \ + --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \ + --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \ + --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \ + --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \ + --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \ + --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \ + --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \ + --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \ + --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \ + --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \ + --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \ + --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \ + --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \ + --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \ + --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \ + --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \ + --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \ + --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \ + --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \ + --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \ + --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \ + --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \ + --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \ + --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \ + --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \ + --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \ + --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \ + --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \ + --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \ + --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \ + --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \ + --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \ + --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \ + --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \ + --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \ + --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \ + --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \ + --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \ + --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \ + --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \ + --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \ + --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \ + --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \ + --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ + --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b + # via cryptography +charset-normalizer==3.4.0 \ + --hash=sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621 \ + --hash=sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6 \ + --hash=sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8 \ + --hash=sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912 \ + --hash=sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c \ + --hash=sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b \ + --hash=sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d \ + --hash=sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d \ + --hash=sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95 \ + --hash=sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e \ + --hash=sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565 \ + --hash=sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64 \ + --hash=sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab \ + --hash=sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be \ + --hash=sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e \ + --hash=sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907 \ + --hash=sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0 \ + --hash=sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2 \ + --hash=sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62 \ + --hash=sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62 \ + --hash=sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23 \ + --hash=sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc \ + --hash=sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284 \ + --hash=sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca \ + --hash=sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455 \ + --hash=sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858 \ + --hash=sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b \ + --hash=sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594 \ + --hash=sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc \ + --hash=sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db \ + --hash=sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b \ + --hash=sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea \ + --hash=sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6 \ + --hash=sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920 \ + --hash=sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749 \ + --hash=sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7 \ + --hash=sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd \ + --hash=sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99 \ + --hash=sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242 \ + --hash=sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee \ + --hash=sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129 \ + --hash=sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2 \ + --hash=sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51 \ + --hash=sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee \ + --hash=sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8 \ + --hash=sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b \ + --hash=sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613 \ + --hash=sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742 \ + --hash=sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe \ + --hash=sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3 \ + --hash=sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5 \ + --hash=sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631 \ + --hash=sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7 \ + --hash=sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15 \ + --hash=sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c \ + --hash=sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea \ + --hash=sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417 \ + --hash=sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250 \ + --hash=sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88 \ + --hash=sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca \ + --hash=sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa \ + --hash=sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99 \ + --hash=sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149 \ + --hash=sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41 \ + --hash=sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574 \ + --hash=sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0 \ + --hash=sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f \ + --hash=sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d \ + --hash=sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654 \ + --hash=sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3 \ + --hash=sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19 \ + --hash=sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90 \ + --hash=sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578 \ + --hash=sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9 \ + --hash=sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1 \ + --hash=sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51 \ + --hash=sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719 \ + --hash=sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236 \ + --hash=sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a \ + --hash=sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c \ + --hash=sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade \ + --hash=sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944 \ + --hash=sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc \ + --hash=sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6 \ + --hash=sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6 \ + --hash=sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27 \ + --hash=sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6 \ + --hash=sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2 \ + --hash=sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12 \ + --hash=sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf \ + --hash=sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114 \ + --hash=sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7 \ + --hash=sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf \ + --hash=sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d \ + --hash=sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b \ + --hash=sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed \ + --hash=sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03 \ + --hash=sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4 \ + --hash=sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67 \ + --hash=sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365 \ + --hash=sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a \ + --hash=sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748 \ + --hash=sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b \ + --hash=sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079 \ + --hash=sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482 + # via requests +click==8.1.3 \ + --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \ + --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48 + # via + # -r yaku-apps-python/3rdparty/requirements.txt + # pyhanko +colorama==0.4.6 \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via elmclient +coverage[toml]==7.6.3 \ + --hash=sha256:04f2189716e85ec9192df307f7c255f90e78b6e9863a03223c3b998d24a3c6c6 \ + --hash=sha256:0c6c0f4d53ef603397fc894a895b960ecd7d44c727df42a8d500031716d4e8d2 \ + --hash=sha256:0ca37993206402c6c35dc717f90d4c8f53568a8b80f0bf1a1b2b334f4d488fba \ + --hash=sha256:12f9515d875859faedb4144fd38694a761cd2a61ef9603bf887b13956d0bbfbb \ + --hash=sha256:1990b1f4e2c402beb317840030bb9f1b6a363f86e14e21b4212e618acdfce7f6 \ + --hash=sha256:2341a78ae3a5ed454d524206a3fcb3cec408c2a0c7c2752cd78b606a2ff15af4 \ + --hash=sha256:23bb63ae3f4c645d2d82fa22697364b0046fbafb6261b258a58587441c5f7bd0 \ + --hash=sha256:27bd5f18d8f2879e45724b0ce74f61811639a846ff0e5c0395b7818fae87aec6 \ + --hash=sha256:2dc7d6b380ca76f5e817ac9eef0c3686e7834c8346bef30b041a4ad286449990 \ + --hash=sha256:331b200ad03dbaa44151d74daeb7da2cf382db424ab923574f6ecca7d3b30de3 \ + --hash=sha256:365defc257c687ce3e7d275f39738dcd230777424117a6c76043459db131dd43 \ + --hash=sha256:37be7b5ea3ff5b7c4a9db16074dc94523b5f10dd1f3b362a827af66a55198175 \ + --hash=sha256:3c2e6fa98032fec8282f6b27e3f3986c6e05702828380618776ad794e938f53a \ + --hash=sha256:40e8b1983080439d4802d80b951f4a93d991ef3261f69e81095a66f86cf3c3c6 \ + --hash=sha256:43517e1f6b19f610a93d8227e47790722c8bf7422e46b365e0469fc3d3563d97 \ + --hash=sha256:43b32a06c47539fe275106b376658638b418c7cfdfff0e0259fbf877e845f14b \ + --hash=sha256:43d6a66e33b1455b98fc7312b124296dad97a2e191c80320587234a77b1b736e \ + --hash=sha256:4c59d6a4a4633fad297f943c03d0d2569867bd5372eb5684befdff8df8522e39 \ + --hash=sha256:52ac29cc72ee7e25ace7807249638f94c9b6a862c56b1df015d2b2e388e51dbd \ + --hash=sha256:54356a76b67cf8a3085818026bb556545ebb8353951923b88292556dfa9f812d \ + --hash=sha256:583049c63106c0555e3ae3931edab5669668bbef84c15861421b94e121878d3f \ + --hash=sha256:6d99198203f0b9cb0b5d1c0393859555bc26b548223a769baf7e321a627ed4fc \ + --hash=sha256:6da42bbcec130b188169107ecb6ee7bd7b4c849d24c9370a0c884cf728d8e976 \ + --hash=sha256:6e484e479860e00da1f005cd19d1c5d4a813324e5951319ac3f3eefb497cc549 \ + --hash=sha256:70a6756ce66cd6fe8486c775b30889f0dc4cb20c157aa8c35b45fd7868255c5c \ + --hash=sha256:70d24936ca6c15a3bbc91ee9c7fc661132c6f4c9d42a23b31b6686c05073bde5 \ + --hash=sha256:71967c35828c9ff94e8c7d405469a1fb68257f686bca7c1ed85ed34e7c2529c4 \ + --hash=sha256:79644f68a6ff23b251cae1c82b01a0b51bc40c8468ca9585c6c4b1aeee570e0b \ + --hash=sha256:87cd2e29067ea397a47e352efb13f976eb1b03e18c999270bb50589323294c6e \ + --hash=sha256:8d4c6ea0f498c7c79111033a290d060c517853a7bcb2f46516f591dab628ddd3 \ + --hash=sha256:9134032f5aa445ae591c2ba6991d10136a1f533b1d2fa8f8c21126468c5025c6 \ + --hash=sha256:921fbe13492caf6a69528f09d5d7c7d518c8d0e7b9f6701b7719715f29a71e6e \ + --hash=sha256:99670790f21a96665a35849990b1df447993880bb6463a0a1d757897f30da929 \ + --hash=sha256:9975442f2e7a5cfcf87299c26b5a45266ab0696348420049b9b94b2ad3d40234 \ + --hash=sha256:99ded130555c021d99729fabd4ddb91a6f4cc0707df4b1daf912c7850c373b13 \ + --hash=sha256:a3328c3e64ea4ab12b85999eb0779e6139295bbf5485f69d42cf794309e3d007 \ + --hash=sha256:a4fb91d5f72b7e06a14ff4ae5be625a81cd7e5f869d7a54578fc271d08d58ae3 \ + --hash=sha256:aa23ce39661a3e90eea5f99ec59b763b7d655c2cada10729ed920a38bfc2b167 \ + --hash=sha256:aac7501ae73d4a02f4b7ac8fcb9dc55342ca98ffb9ed9f2dfb8a25d53eda0e4d \ + --hash=sha256:ab84a8b698ad5a6c365b08061920138e7a7dd9a04b6feb09ba1bfae68346ce6d \ + --hash=sha256:b4adeb878a374126f1e5cf03b87f66279f479e01af0e9a654cf6d1509af46c40 \ + --hash=sha256:b9853509b4bf57ba7b1f99b9d866c422c9c5248799ab20e652bbb8a184a38181 \ + --hash=sha256:bb7d5fe92bd0dc235f63ebe9f8c6e0884f7360f88f3411bfed1350c872ef2054 \ + --hash=sha256:bca4c8abc50d38f9773c1ec80d43f3768df2e8576807d1656016b9d3eeaa96fd \ + --hash=sha256:c222958f59b0ae091f4535851cbb24eb57fc0baea07ba675af718fb5302dddb2 \ + --hash=sha256:c30e42ea11badb147f0d2e387115b15e2bd8205a5ad70d6ad79cf37f6ac08c91 \ + --hash=sha256:c3a79f56dee9136084cf84a6c7c4341427ef36e05ae6415bf7d787c96ff5eaa3 \ + --hash=sha256:c51ef82302386d686feea1c44dbeef744585da16fcf97deea2a8d6c1556f519b \ + --hash=sha256:c77326300b839c44c3e5a8fe26c15b7e87b2f32dfd2fc9fee1d13604347c9b38 \ + --hash=sha256:d33a785ea8354c480515e781554d3be582a86297e41ccbea627a5c632647f2cd \ + --hash=sha256:d546cfa78844b8b9c1c0533de1851569a13f87449897bbc95d698d1d3cb2a30f \ + --hash=sha256:da29ceabe3025a1e5a5aeeb331c5b1af686daab4ff0fb4f83df18b1180ea83e2 \ + --hash=sha256:df8c05a0f574d480947cba11b947dc41b1265d721c3777881da2fb8d3a1ddfba \ + --hash=sha256:e266af4da2c1a4cbc6135a570c64577fd3e6eb204607eaff99d8e9b710003c6f \ + --hash=sha256:e279f3db904e3b55f520f11f983cc8dc8a4ce9b65f11692d4718ed021ec58b83 \ + --hash=sha256:ea52bd218d4ba260399a8ae4bb6b577d82adfc4518b93566ce1fddd4a49d1dce \ + --hash=sha256:ebec65f5068e7df2d49466aab9128510c4867e532e07cb6960075b27658dca38 \ + --hash=sha256:ec1e3b40b82236d100d259854840555469fad4db64f669ab817279eb95cd535c \ + --hash=sha256:ee77c7bef0724165e795b6b7bf9c4c22a9b8468a6bdb9c6b4281293c6b22a90f \ + --hash=sha256:f263b18692f8ed52c8de7f40a0751e79015983dbd77b16906e5b310a39d3ca21 \ + --hash=sha256:f7b26757b22faf88fcf232f5f0e62f6e0fd9e22a8a5d0d5016888cdfe1f6c1c4 \ + --hash=sha256:f7ddb920106bbbbcaf2a274d56f46956bf56ecbde210d88061824a95bdd94e92 + # via pytest-cov +cryptography==43.0.1 \ + --hash=sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494 \ + --hash=sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806 \ + --hash=sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d \ + --hash=sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062 \ + --hash=sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2 \ + --hash=sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4 \ + --hash=sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1 \ + --hash=sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85 \ + --hash=sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84 \ + --hash=sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042 \ + --hash=sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d \ + --hash=sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962 \ + --hash=sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2 \ + --hash=sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa \ + --hash=sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d \ + --hash=sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365 \ + --hash=sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96 \ + --hash=sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47 \ + --hash=sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d \ + --hash=sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d \ + --hash=sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c \ + --hash=sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb \ + --hash=sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277 \ + --hash=sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172 \ + --hash=sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034 \ + --hash=sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a \ + --hash=sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289 + # via + # -r yaku-apps-python/3rdparty/requirements.txt + # elmclient + # oracledb + # pyhanko + # pyhanko-certvalidator + # pyspnego + # requests-ntlm +cyclonedx-python-lib[xml-validation]==5.1.1 \ + --hash=sha256:215a636a4e77385d2cf4c6c9801c9bad4791849634f2c6daa45ab2c6cb0a85f6 \ + --hash=sha256:2989db0cd8bb4c0c442423d71ed7a84ae059e16a2d0f932cc4bf92da7385cdb3 + # via -r yaku-apps-python/3rdparty/requirements.txt +datetime==5.5 \ + --hash=sha256:0abf6c51cb4ba7cee775ca46ccc727f3afdde463be28dbbe8803631fefd4a120 \ + --hash=sha256:21ec6331f87a7fcb57bd7c59e8a68bfffe6fcbf5acdbbc7b356d6a9a020191d3 + # via -r yaku-apps-python/3rdparty/requirements.txt +defusedxml==0.7.1 \ + --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ + --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 + # via + # -r yaku-apps-python/3rdparty/requirements.txt + # jira + # py-serializable +deprecation==2.1.0 \ + --hash=sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff \ + --hash=sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a + # via splunk-sdk +distro==1.9.0 \ + --hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \ + --hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2 + # via openai +docutils==0.21.2 \ + --hash=sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f \ + --hash=sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2 + # via readme-renderer +dohq-artifactory==0.8.3 \ + --hash=sha256:916a9fffa7293fecaf088a0f5723b71a2b6e57645b87c1f172fcc9c63432f3b7 \ + --hash=sha256:bc13fabd0cdae34f620b9d229b5da96b6f3eb4d8dd8006e6095edbe1d84c36eb + # via -r yaku-apps-python/3rdparty/requirements.txt +elmclient==0.26.2 \ + --hash=sha256:3732814bbf20e9467b6d009fd47cb96fe12622976d4f3ee6a1126743664c6daa \ + --hash=sha256:def895bfe6587b748ca68cb0670e50c6a565a45e2ada68a3c7585470787f6d69 + # via -r yaku-apps-python/3rdparty/requirements.txt +et-xmlfile==1.1.0 \ + --hash=sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c \ + --hash=sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada + # via openpyxl +execnet==2.1.1 \ + --hash=sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc \ + --hash=sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3 + # via pytest-xdist +filelock==3.16.1 \ + --hash=sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0 \ + --hash=sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435 + # via elmclient +freezegun==1.5.1 \ + --hash=sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9 \ + --hash=sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1 + # via -r yaku-apps-python/3rdparty/requirements.txt +h11==0.14.0 \ + --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \ + --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761 + # via httpcore +httpcore==1.0.6 \ + --hash=sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f \ + --hash=sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f + # via httpx +httpx==0.27.2 \ + --hash=sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0 \ + --hash=sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2 + # via + # langsmith + # ollama + # openai +idna==3.7 \ + --hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \ + --hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0 + # via + # -r yaku-apps-python/3rdparty/requirements.txt + # anyio + # httpx + # requests +importlib-metadata==8.5.0 \ + --hash=sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b \ + --hash=sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7 + # via + # keyring + # twine +iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 + # via pytest +isodate==0.7.2 \ + --hash=sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15 \ + --hash=sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6 + # via msrest +jaraco-classes==3.4.0 \ + --hash=sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd \ + --hash=sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790 + # via keyring +jaraco-context==6.0.1 \ + --hash=sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3 \ + --hash=sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4 + # via keyring +jaraco-functools==4.1.0 \ + --hash=sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d \ + --hash=sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649 + # via keyring +jira==3.8.0 \ + --hash=sha256:12190dc84dad00b8a6c0341f7e8a254b0f38785afdec022bd5941e1184a5a3fb \ + --hash=sha256:63719c529a570aaa01c3373dbb5a104dab70381c5be447f6c27f997302fa335a + # via -r yaku-apps-python/3rdparty/requirements.txt +jiter==0.6.1 \ + --hash=sha256:03a025b52009f47e53ea619175d17e4ded7c035c6fbd44935cb3ada11e1fd592 \ + --hash=sha256:08be33db6dcc374c9cc19d3633af5e47961a7b10d4c61710bd39e48d52a35824 \ + --hash=sha256:0b809e39e342c346df454b29bfcc7bca3d957f5d7b60e33dae42b0e5ec13e027 \ + --hash=sha256:13f9084e3e871a7c0b6e710db54444088b1dd9fbefa54d449b630d5e73bb95d0 \ + --hash=sha256:15f8395e835cf561c85c1adee72d899abf2733d9df72e9798e6d667c9b5c1f30 \ + --hash=sha256:18aa9d1626b61c0734b973ed7088f8a3d690d0b7f5384a5270cd04f4d9f26c86 \ + --hash=sha256:1fad93654d5a7dcce0809aff66e883c98e2618b86656aeb2129db2cd6f26f867 \ + --hash=sha256:220e0963b4fb507c525c8f58cde3da6b1be0bfddb7ffd6798fb8f2531226cdb1 \ + --hash=sha256:25f0d2f6e01a8a0fb0eab6d0e469058dab2be46ff3139ed2d1543475b5a1d8e7 \ + --hash=sha256:26d2bdd5da097e624081c6b5d416d3ee73e5b13f1703bcdadbb1881f0caa1933 \ + --hash=sha256:31d8e00e1fb4c277df8ab6f31a671f509ebc791a80e5c61fdc6bc8696aaa297c \ + --hash=sha256:3343d4706a2b7140e8bd49b6c8b0a82abf9194b3f0f5925a78fc69359f8fc33c \ + --hash=sha256:33af2b7d2bf310fdfec2da0177eab2fedab8679d1538d5b86a633ebfbbac4edd \ + --hash=sha256:352cd24121e80d3d053fab1cc9806258cad27c53cad99b7a3cac57cf934b12e4 \ + --hash=sha256:36c0b51a285b68311e207a76c385650322734c8717d16c2eb8af75c9d69506e7 \ + --hash=sha256:3c843e7c1633470708a3987e8ce617ee2979ee18542d6eb25ae92861af3f1d62 \ + --hash=sha256:3cbc1a66b4e41511209e97a2866898733c0110b7245791ac604117b7fb3fedb7 \ + --hash=sha256:3e36a320634f33a07794bb15b8da995dccb94f944d298c8cfe2bd99b1b8a574a \ + --hash=sha256:40b03b75f903975f68199fc4ec73d546150919cb7e534f3b51e727c4d6ccca5a \ + --hash=sha256:47fee1be677b25d0ef79d687e238dc6ac91a8e553e1a68d0839f38c69e0ee491 \ + --hash=sha256:4e6e340e8cd92edab7f6a3a904dbbc8137e7f4b347c49a27da9814015cc0420c \ + --hash=sha256:51b58f7a0d9e084a43b28b23da2b09fc5e8df6aa2b6a27de43f991293cab85fd \ + --hash=sha256:540fcb224d7dc1bcf82f90f2ffb652df96f2851c031adca3c8741cb91877143b \ + --hash=sha256:59e2b37f3b9401fc9e619f4d4badcab2e8643a721838bcf695c2318a0475ae42 \ + --hash=sha256:5a99d4e0b5fc3b05ea732d67eb2092fe894e95a90e6e413f2ea91387e228a307 \ + --hash=sha256:5f79ce15099154c90ef900d69c6b4c686b64dfe23b0114e0971f2fecd306ec6c \ + --hash=sha256:67723a011964971864e0b484b0ecfee6a14de1533cff7ffd71189e92103b38a8 \ + --hash=sha256:677be9550004f5e010d673d3b2a2b815a8ea07a71484a57d3f85dde7f14cf132 \ + --hash=sha256:691352e5653af84ed71763c3c427cff05e4d658c508172e01e9c956dfe004aba \ + --hash=sha256:77c296d65003cd7ee5d7b0965f6acbe6cffaf9d1fa420ea751f60ef24e85fed5 \ + --hash=sha256:7a3567c8228afa5ddcce950631c6b17397ed178003dc9ee7e567c4c4dcae9fa0 \ + --hash=sha256:7cea41c4c673353799906d940eee8f2d8fd1d9561d734aa921ae0f75cb9732f4 \ + --hash=sha256:7d72fc86474862c9c6d1f87b921b70c362f2b7e8b2e3c798bb7d58e419a6bc0f \ + --hash=sha256:81116a6c272a11347b199f0e16b6bd63f4c9d9b52bc108991397dd80d3c78aba \ + --hash=sha256:82521000d18c71e41c96960cb36e915a357bc83d63a8bed63154b89d95d05ad1 \ + --hash=sha256:825651a3f04cf92a661d22cad61fc913400e33aa89b3e3ad9a6aa9dc8a1f5a71 \ + --hash=sha256:852508a54fe3228432e56019da8b69208ea622a3069458252f725d634e955b31 \ + --hash=sha256:883d2ced7c21bf06874fdeecab15014c1c6d82216765ca6deef08e335fa719e0 \ + --hash=sha256:8c97e90fec2da1d5f68ef121444c2c4fa72eabf3240829ad95cf6bbeca42a301 \ + --hash=sha256:91e63273563401aadc6c52cca64a7921c50b29372441adc104127b910e98a5b6 \ + --hash=sha256:928bf25eb69ddb292ab8177fe69d3fbf76c7feab5fce1c09265a7dccf25d3991 \ + --hash=sha256:9df588e9c830b72d8db1dd7d0175af6706b0904f682ea9b1ca8b46028e54d6e9 \ + --hash=sha256:a2e861658c3fe849efc39b06ebb98d042e4a4c51a8d7d1c3ddc3b1ea091d0784 \ + --hash=sha256:a311df1fa6be0ccd64c12abcd85458383d96e542531bafbfc0a16ff6feda588f \ + --hash=sha256:a31c6fcbe7d6c25d6f1cc6bb1cba576251d32795d09c09961174fe461a1fb5bd \ + --hash=sha256:aa25c7a9bf7875a141182b9c95aed487add635da01942ef7ca726e42a0c09058 \ + --hash=sha256:adef59d5e2394ebbad13b7ed5e0306cceb1df92e2de688824232a91588e77aa7 \ + --hash=sha256:aeeb0c0325ef96c12a48ea7e23e2e86fe4838e6e0a995f464cf4c79fa791ceeb \ + --hash=sha256:b03c24e7da7e75b170c7b2b172d9c5e463aa4b5c95696a368d52c295b3f6847f \ + --hash=sha256:b2019d966e98f7c6df24b3b8363998575f47d26471bfb14aade37630fae836a1 \ + --hash=sha256:b3e02f7a27f2bcc15b7d455c9df05df8ffffcc596a2a541eeda9a3110326e7a3 \ + --hash=sha256:bae5ae4853cb9644144e9d0755854ce5108d470d31541d83f70ca7ecdc2d1637 \ + --hash=sha256:bd95375ce3609ec079a97c5d165afdd25693302c071ca60c7ae1cf826eb32022 \ + --hash=sha256:be7503dd6f4bf02c2a9bacb5cc9335bc59132e7eee9d3e931b13d76fd80d7fda \ + --hash=sha256:c74a8d93718137c021d9295248a87c2f9fdc0dcafead12d2930bc459ad40f885 \ + --hash=sha256:cc56c8f0b2a28ad4d8047f3ae62d25d0e9ae01b99940ec0283263a04724de1f3 \ + --hash=sha256:d08510593cb57296851080018006dfc394070178d238b767b1879dc1013b106c \ + --hash=sha256:d465db62d2d10b489b7e7a33027c4ae3a64374425d757e963f86df5b5f2e7fc5 \ + --hash=sha256:d71c962f0971347bd552940ab96aa42ceefcd51b88c4ced8a27398182efa8d80 \ + --hash=sha256:db459ed22d0208940d87f614e1f0ea5a946d29a3cfef71f7e1aab59b6c6b2afb \ + --hash=sha256:defee3949313c1f5b55e18be45089970cdb936eb2a0063f5020c4185db1b63c9 \ + --hash=sha256:e19cd21221fc139fb032e4112986656cb2739e9fe6d84c13956ab30ccc7d4449 \ + --hash=sha256:e4e85f9e12cd8418ab10e1fcf0e335ae5bb3da26c4d13a0fd9e6a17a674783b6 \ + --hash=sha256:e51a2d80d5fe0ffb10ed2c82b6004458be4a3f2b9c7d09ed85baa2fbf033f54b \ + --hash=sha256:e5c0507131c922defe3f04c527d6838932fcdfd69facebafd7d3574fa3395314 \ + --hash=sha256:e7b75436d4fa2032b2530ad989e4cb0ca74c655975e3ff49f91a1a3d7f4e1df2 \ + --hash=sha256:e8bd065be46c2eecc328e419d6557bbc37844c88bb07b7a8d2d6c91c7c4dedc9 \ + --hash=sha256:e90552109ca8ccd07f47ca99c8a1509ced93920d271bb81780a973279974c5ab \ + --hash=sha256:e9ac7c2f092f231f5620bef23ce2e530bd218fc046098747cc390b21b8738a7a \ + --hash=sha256:ed69a7971d67b08f152c17c638f0e8c2aa207e9dd3a5fcd3cba294d39b5a8d2d \ + --hash=sha256:f1c53615fcfec3b11527c08d19cff6bc870da567ce4e57676c059a3102d3a082 \ + --hash=sha256:f491cc69ff44e5a1e8bc6bf2b94c1f98d179e1aaf4a554493c171a5b2316b701 \ + --hash=sha256:f791b6a4da23238c17a81f44f5b55d08a420c5692c1fda84e301a4b036744eb1 + # via openai +jsonpatch==1.33 \ + --hash=sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade \ + --hash=sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c + # via langchain-core +jsonpointer==3.0.0 \ + --hash=sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942 \ + --hash=sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef + # via jsonpatch +jsonschema==4.23.0 \ + --hash=sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4 \ + --hash=sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566 + # via -r yaku-apps-python/3rdparty/requirements.txt +jsonschema-specifications==2024.10.1 \ + --hash=sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272 \ + --hash=sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf + # via jsonschema +keyring==25.4.1 \ + --hash=sha256:5426f817cf7f6f007ba5ec722b1bcad95a75b27d780343772ad76b17cb47b0bf \ + --hash=sha256:b07ebc55f3e8ed86ac81dd31ef14e81ace9dd9c3d4b5d77a6e9a2016d0d71a1b + # via twine +langchain-core==0.2.41 \ + --hash=sha256:3278fda5ba9a05defae8bb19f1226032add6aab21917db7b3bc74e750e263e84 \ + --hash=sha256:bc12032c5a298d85be754ccb129bc13ea21ccb1d6e22f8d7ba18b8da64315bb5 + # via + # langchain-ollama + # langchain-openai +langchain-ollama==0.1.1 \ + --hash=sha256:179b6f21e01fc72ebc034ec725f8c5dcef4a81709919278e6fa4f43605df5d82 \ + --hash=sha256:91b3b6cfcc90890c683995520d84210ebd2cee8c0f2cd0a5ffde9f1ffbee2f94 + # via -r yaku-apps-python/3rdparty/requirements.txt +langchain-openai==0.1.20 \ + --hash=sha256:232ebfe90b1898ef7cf181e364d45191edcf04bfc31b292ecaa1d2121942c28e \ + --hash=sha256:2c91e9f771541076b138e65dd4c5427b26957a2272406a7f4ee747d7896f9b35 + # via -r yaku-apps-python/3rdparty/requirements.txt +langsmith==0.1.135 \ + --hash=sha256:7abed7e141386af99a2177f0b3600b124ae3ad1b482879ba0724ce92ef998a11 \ + --hash=sha256:b1d1ca3bad483a4239745c57e9b9157b4d099fbf3149be21e3d112c94ede06ac + # via langchain-core +lark-parser==0.12.0 \ + --hash=sha256:0eaf30cb5ba787fe404d73a7d6e61df97b21d5a63ac26c5008c78a494373c675 \ + --hash=sha256:15967db1f1214013dca65b1180745047b9be457d73da224fcda3d9dd4e96a138 + # via elmclient +license-expression==30.3.1 \ + --hash=sha256:60d5bec1f3364c256a92b9a08583d7ea933c7aa272c8d36d04144a89a3858c01 \ + --hash=sha256:97904b9185c7bbb1e98799606fa7424191c375e70ba63a524b6f7100e42ddc46 + # via cyclonedx-python-lib +lockfile==0.12.2 \ + --hash=sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799 \ + --hash=sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa + # via elmclient +loguru==0.7.0 \ + --hash=sha256:1612053ced6ae84d7959dd7d5e431a0532642237ec21f7fd83ac73fe539e03e1 \ + --hash=sha256:b93aa30099fa6860d4727f1b81f8718e965bb96253fa190fab2077aaad6d15d3 + # via -r yaku-apps-python/3rdparty/requirements.txt +lxml==4.9.4 \ + --hash=sha256:00e91573183ad273e242db5585b52670eddf92bacad095ce25c1e682da14ed91 \ + --hash=sha256:01bf1df1db327e748dcb152d17389cf6d0a8c5d533ef9bab781e9d5037619229 \ + --hash=sha256:056a17eaaf3da87a05523472ae84246f87ac2f29a53306466c22e60282e54ff8 \ + --hash=sha256:0a08c89b23117049ba171bf51d2f9c5f3abf507d65d016d6e0fa2f37e18c0fc5 \ + --hash=sha256:1343df4e2e6e51182aad12162b23b0a4b3fd77f17527a78c53f0f23573663545 \ + --hash=sha256:1449f9451cd53e0fd0a7ec2ff5ede4686add13ac7a7bfa6988ff6d75cff3ebe2 \ + --hash=sha256:16b9ec51cc2feab009e800f2c6327338d6ee4e752c76e95a35c4465e80390ccd \ + --hash=sha256:1f10f250430a4caf84115b1e0f23f3615566ca2369d1962f82bef40dd99cd81a \ + --hash=sha256:231142459d32779b209aa4b4d460b175cadd604fed856f25c1571a9d78114771 \ + --hash=sha256:232fd30903d3123be4c435fb5159938c6225ee8607b635a4d3fca847003134ba \ + --hash=sha256:23d891e5bdc12e2e506e7d225d6aa929e0a0368c9916c1fddefab88166e98b20 \ + --hash=sha256:266f655d1baff9c47b52f529b5f6bec33f66042f65f7c56adde3fcf2ed62ae8b \ + --hash=sha256:273473d34462ae6e97c0f4e517bd1bf9588aa67a1d47d93f760a1282640e24ac \ + --hash=sha256:2bd9ac6e44f2db368ef8986f3989a4cad3de4cd55dbdda536e253000c801bcc7 \ + --hash=sha256:33714fcf5af4ff7e70a49731a7cc8fd9ce910b9ac194f66eaa18c3cc0a4c02be \ + --hash=sha256:359a8b09d712df27849e0bcb62c6a3404e780b274b0b7e4c39a88826d1926c28 \ + --hash=sha256:365005e8b0718ea6d64b374423e870648ab47c3a905356ab6e5a5ff03962b9a9 \ + --hash=sha256:389d2b2e543b27962990ab529ac6720c3dded588cc6d0f6557eec153305a3622 \ + --hash=sha256:3b505f2bbff50d261176e67be24e8909e54b5d9d08b12d4946344066d66b3e43 \ + --hash=sha256:3d74d4a3c4b8f7a1f676cedf8e84bcc57705a6d7925e6daef7a1e54ae543a197 \ + --hash=sha256:3f3f00a9061605725df1816f5713d10cd94636347ed651abdbc75828df302b20 \ + --hash=sha256:43498ea734ccdfb92e1886dfedaebeb81178a241d39a79d5351ba2b671bff2b2 \ + --hash=sha256:4855161013dfb2b762e02b3f4d4a21cc7c6aec13c69e3bffbf5022b3e708dd97 \ + --hash=sha256:4d973729ce04784906a19108054e1fd476bc85279a403ea1a72fdb051c76fa48 \ + --hash=sha256:4ece9cca4cd1c8ba889bfa67eae7f21d0d1a2e715b4d5045395113361e8c533d \ + --hash=sha256:506becdf2ecaebaf7f7995f776394fcc8bd8a78022772de66677c84fb02dd33d \ + --hash=sha256:520486f27f1d4ce9654154b4494cf9307b495527f3a2908ad4cb48e4f7ed7ef7 \ + --hash=sha256:5557461f83bb7cc718bc9ee1f7156d50e31747e5b38d79cf40f79ab1447afd2d \ + --hash=sha256:562778586949be7e0d7435fcb24aca4810913771f845d99145a6cee64d5b67ca \ + --hash=sha256:59bb5979f9941c61e907ee571732219fa4774d5a18f3fa5ff2df963f5dfaa6bc \ + --hash=sha256:606d445feeb0856c2b424405236a01c71af7c97e5fe42fbc778634faef2b47e4 \ + --hash=sha256:6197c3f3c0b960ad033b9b7d611db11285bb461fc6b802c1dd50d04ad715c225 \ + --hash=sha256:647459b23594f370c1c01768edaa0ba0959afc39caeeb793b43158bb9bb6a663 \ + --hash=sha256:647bfe88b1997d7ae8d45dabc7c868d8cb0c8412a6e730a7651050b8c7289cf2 \ + --hash=sha256:6bee9c2e501d835f91460b2c904bc359f8433e96799f5c2ff20feebd9bb1e590 \ + --hash=sha256:6dbdacf5752fbd78ccdb434698230c4f0f95df7dd956d5f205b5ed6911a1367c \ + --hash=sha256:701847a7aaefef121c5c0d855b2affa5f9bd45196ef00266724a80e439220e46 \ + --hash=sha256:786d6b57026e7e04d184313c1359ac3d68002c33e4b1042ca58c362f1d09ff58 \ + --hash=sha256:7b378847a09d6bd46047f5f3599cdc64fcb4cc5a5a2dd0a2af610361fbe77b16 \ + --hash=sha256:7d1d6c9e74c70ddf524e3c09d9dc0522aba9370708c2cb58680ea40174800013 \ + --hash=sha256:857d6565f9aa3464764c2cb6a2e3c2e75e1970e877c188f4aeae45954a314e0c \ + --hash=sha256:8671622256a0859f5089cbe0ce4693c2af407bc053dcc99aadff7f5310b4aa02 \ + --hash=sha256:88f7c383071981c74ec1998ba9b437659e4fd02a3c4a4d3efc16774eb108d0ec \ + --hash=sha256:8aecb5a7f6f7f8fe9cac0bcadd39efaca8bbf8d1bf242e9f175cbe4c925116c3 \ + --hash=sha256:91bbf398ac8bb7d65a5a52127407c05f75a18d7015a270fdd94bbcb04e65d573 \ + --hash=sha256:936e8880cc00f839aa4173f94466a8406a96ddce814651075f95837316369899 \ + --hash=sha256:953dd5481bd6252bd480d6ec431f61d7d87fdcbbb71b0d2bdcfc6ae00bb6fb10 \ + --hash=sha256:95ae6c5a196e2f239150aa4a479967351df7f44800c93e5a975ec726fef005e2 \ + --hash=sha256:9a2b5915c333e4364367140443b59f09feae42184459b913f0f41b9fed55794a \ + --hash=sha256:9ae6c3363261021144121427b1552b29e7b59de9d6a75bf51e03bc072efb3c37 \ + --hash=sha256:9b556596c49fa1232b0fff4b0e69b9d4083a502e60e404b44341e2f8fb7187f5 \ + --hash=sha256:9c131447768ed7bc05a02553d939e7f0e807e533441901dd504e217b76307745 \ + --hash=sha256:9d9d5726474cbbef279fd709008f91a49c4f758bec9c062dfbba88eab00e3ff9 \ + --hash=sha256:a1bdcbebd4e13446a14de4dd1825f1e778e099f17f79718b4aeaf2403624b0f7 \ + --hash=sha256:a602ed9bd2c7d85bd58592c28e101bd9ff9c718fbde06545a70945ffd5d11868 \ + --hash=sha256:a8edae5253efa75c2fc79a90068fe540b197d1c7ab5803b800fccfe240eed33c \ + --hash=sha256:a905affe76f1802edcac554e3ccf68188bea16546071d7583fb1b693f9cf756b \ + --hash=sha256:a9e7c6d89c77bb2770c9491d988f26a4b161d05c8ca58f63fb1f1b6b9a74be45 \ + --hash=sha256:aa9b5abd07f71b081a33115d9758ef6077924082055005808f68feccb27616bd \ + --hash=sha256:aaa5c173a26960fe67daa69aa93d6d6a1cd714a6eb13802d4e4bd1d24a530644 \ + --hash=sha256:ac7674d1638df129d9cb4503d20ffc3922bd463c865ef3cb412f2c926108e9a4 \ + --hash=sha256:b1541e50b78e15fa06a2670157a1962ef06591d4c998b998047fff5e3236880e \ + --hash=sha256:b1980dbcaad634fe78e710c8587383e6e3f61dbe146bcbfd13a9c8ab2d7b1192 \ + --hash=sha256:bafa65e3acae612a7799ada439bd202403414ebe23f52e5b17f6ffc2eb98c2be \ + --hash=sha256:bb5bd6212eb0edfd1e8f254585290ea1dadc3687dd8fd5e2fd9a87c31915cdab \ + --hash=sha256:bbdd69e20fe2943b51e2841fc1e6a3c1de460d630f65bde12452d8c97209464d \ + --hash=sha256:bc354b1393dce46026ab13075f77b30e40b61b1a53e852e99d3cc5dd1af4bc85 \ + --hash=sha256:bcee502c649fa6351b44bb014b98c09cb00982a475a1912a9881ca28ab4f9cd9 \ + --hash=sha256:bdd9abccd0927673cffe601d2c6cdad1c9321bf3437a2f507d6b037ef91ea307 \ + --hash=sha256:c42ae7e010d7d6bc51875d768110c10e8a59494855c3d4c348b068f5fb81fdcd \ + --hash=sha256:c71b5b860c5215fdbaa56f715bc218e45a98477f816b46cfde4a84d25b13274e \ + --hash=sha256:c7721a3ef41591341388bb2265395ce522aba52f969d33dacd822da8f018aff8 \ + --hash=sha256:ca8e44b5ba3edb682ea4e6185b49661fc22b230cf811b9c13963c9f982d1d964 \ + --hash=sha256:cb53669442895763e61df5c995f0e8361b61662f26c1b04ee82899c2789c8f69 \ + --hash=sha256:cc02c06e9e320869d7d1bd323df6dd4281e78ac2e7f8526835d3d48c69060683 \ + --hash=sha256:d3caa09e613ece43ac292fbed513a4bce170681a447d25ffcbc1b647d45a39c5 \ + --hash=sha256:d82411dbf4d3127b6cde7da0f9373e37ad3a43e89ef374965465928f01c2b979 \ + --hash=sha256:dbcb2dc07308453db428a95a4d03259bd8caea97d7f0776842299f2d00c72fc8 \ + --hash=sha256:dd4fda67f5faaef4f9ee5383435048ee3e11ad996901225ad7615bc92245bc8e \ + --hash=sha256:ddd92e18b783aeb86ad2132d84a4b795fc5ec612e3545c1b687e7747e66e2b53 \ + --hash=sha256:de362ac8bc962408ad8fae28f3967ce1a262b5d63ab8cefb42662566737f1dc7 \ + --hash=sha256:e214025e23db238805a600f1f37bf9f9a15413c7bf5f9d6ae194f84980c78722 \ + --hash=sha256:e8f9f93a23634cfafbad6e46ad7d09e0f4a25a2400e4a64b1b7b7c0fbaa06d9d \ + --hash=sha256:e96a1788f24d03e8d61679f9881a883ecdf9c445a38f9ae3f3f193ab6c591c66 \ + --hash=sha256:ec53a09aee61d45e7dbe7e91252ff0491b6b5fee3d85b2d45b173d8ab453efc1 \ + --hash=sha256:f10250bb190fb0742e3e1958dd5c100524c2cc5096c67c8da51233f7448dc137 \ + --hash=sha256:f1faee2a831fe249e1bae9cbc68d3cd8a30f7e37851deee4d7962b17c410dd56 \ + --hash=sha256:f610d980e3fccf4394ab3806de6065682982f3d27c12d4ce3ee46a8183d64a6a \ + --hash=sha256:f6c35b2f87c004270fa2e703b872fcc984d714d430b305145c39d53074e1ffe0 \ + --hash=sha256:f836f39678cb47c9541f04d8ed4545719dc31ad850bf1832d6b4171e30d65d23 \ + --hash=sha256:f99768232f036b4776ce419d3244a04fe83784bce871b16d2c2e984c7fcea847 \ + --hash=sha256:fd814847901df6e8de13ce69b84c31fc9b3fb591224d6762d0b256d510cbf382 \ + --hash=sha256:fdb325b7fba1e2c40b9b1db407f85642e32404131c08480dd652110fc908561b + # via + # cyclonedx-python-lib + # elmclient +markdown-it-py==3.0.0 \ + --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ + --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb + # via rich +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba + # via markdown-it-py +mock==5.1.0 \ + --hash=sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744 \ + --hash=sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d + # via -r yaku-apps-python/3rdparty/requirements.txt +more-itertools==10.5.0 \ + --hash=sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef \ + --hash=sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6 + # via + # jaraco-classes + # jaraco-functools +msgpack==1.1.0 \ + --hash=sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b \ + --hash=sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf \ + --hash=sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca \ + --hash=sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330 \ + --hash=sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f \ + --hash=sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f \ + --hash=sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39 \ + --hash=sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247 \ + --hash=sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b \ + --hash=sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c \ + --hash=sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7 \ + --hash=sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044 \ + --hash=sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6 \ + --hash=sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b \ + --hash=sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0 \ + --hash=sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2 \ + --hash=sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468 \ + --hash=sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7 \ + --hash=sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734 \ + --hash=sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434 \ + --hash=sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325 \ + --hash=sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1 \ + --hash=sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846 \ + --hash=sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88 \ + --hash=sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420 \ + --hash=sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e \ + --hash=sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2 \ + --hash=sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59 \ + --hash=sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb \ + --hash=sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68 \ + --hash=sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915 \ + --hash=sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f \ + --hash=sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701 \ + --hash=sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b \ + --hash=sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d \ + --hash=sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa \ + --hash=sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d \ + --hash=sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd \ + --hash=sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc \ + --hash=sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48 \ + --hash=sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb \ + --hash=sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74 \ + --hash=sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b \ + --hash=sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346 \ + --hash=sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e \ + --hash=sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6 \ + --hash=sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5 \ + --hash=sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f \ + --hash=sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5 \ + --hash=sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b \ + --hash=sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c \ + --hash=sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f \ + --hash=sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec \ + --hash=sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8 \ + --hash=sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5 \ + --hash=sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d \ + --hash=sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e \ + --hash=sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e \ + --hash=sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870 \ + --hash=sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f \ + --hash=sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96 \ + --hash=sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c \ + --hash=sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd \ + --hash=sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788 + # via cachecontrol +msrest==0.7.1 \ + --hash=sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32 \ + --hash=sha256:6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9 + # via azure-devops +mypy==1.4.1 \ + --hash=sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042 \ + --hash=sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd \ + --hash=sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2 \ + --hash=sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01 \ + --hash=sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7 \ + --hash=sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3 \ + --hash=sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816 \ + --hash=sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3 \ + --hash=sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc \ + --hash=sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4 \ + --hash=sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b \ + --hash=sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8 \ + --hash=sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c \ + --hash=sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462 \ + --hash=sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7 \ + --hash=sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc \ + --hash=sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258 \ + --hash=sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b \ + --hash=sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9 \ + --hash=sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6 \ + --hash=sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f \ + --hash=sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1 \ + --hash=sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828 \ + --hash=sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878 \ + --hash=sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f \ + --hash=sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b + # via -r yaku-apps-python/3rdparty/requirements.txt +mypy-extensions==1.0.0 \ + --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \ + --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 + # via mypy +nh3==0.2.18 \ + --hash=sha256:0411beb0589eacb6734f28d5497ca2ed379eafab8ad8c84b31bb5c34072b7164 \ + --hash=sha256:14c5a72e9fe82aea5fe3072116ad4661af5cf8e8ff8fc5ad3450f123e4925e86 \ + --hash=sha256:19aaba96e0f795bd0a6c56291495ff59364f4300d4a39b29a0abc9cb3774a84b \ + --hash=sha256:34c03fa78e328c691f982b7c03d4423bdfd7da69cd707fe572f544cf74ac23ad \ + --hash=sha256:36c95d4b70530b320b365659bb5034341316e6a9b30f0b25fa9c9eff4c27a204 \ + --hash=sha256:3a157ab149e591bb638a55c8c6bcb8cdb559c8b12c13a8affaba6cedfe51713a \ + --hash=sha256:42c64511469005058cd17cc1537578eac40ae9f7200bedcfd1fc1a05f4f8c200 \ + --hash=sha256:5f36b271dae35c465ef5e9090e1fdaba4a60a56f0bb0ba03e0932a66f28b9189 \ + --hash=sha256:6955369e4d9f48f41e3f238a9e60f9410645db7e07435e62c6a9ea6135a4907f \ + --hash=sha256:7b7c2a3c9eb1a827d42539aa64091640bd275b81e097cd1d8d82ef91ffa2e811 \ + --hash=sha256:8ce0f819d2f1933953fca255db2471ad58184a60508f03e6285e5114b6254844 \ + --hash=sha256:94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4 \ + --hash=sha256:a7f1b5b2c15866f2db413a3649a8fe4fd7b428ae58be2c0f6bca5eefd53ca2be \ + --hash=sha256:c8b3a1cebcba9b3669ed1a84cc65bf005728d2f0bc1ed2a6594a992e817f3a50 \ + --hash=sha256:de3ceed6e661954871d6cd78b410213bdcb136f79aafe22aa7182e028b8c7307 \ + --hash=sha256:f0eca9ca8628dbb4e916ae2491d72957fdd35f7a5d326b7032a345f111ac07fe + # via readme-renderer +numpy==1.26.4 \ + --hash=sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b \ + --hash=sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818 \ + --hash=sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20 \ + --hash=sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0 \ + --hash=sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010 \ + --hash=sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a \ + --hash=sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea \ + --hash=sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c \ + --hash=sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71 \ + --hash=sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110 \ + --hash=sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be \ + --hash=sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a \ + --hash=sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a \ + --hash=sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5 \ + --hash=sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed \ + --hash=sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd \ + --hash=sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c \ + --hash=sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e \ + --hash=sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0 \ + --hash=sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c \ + --hash=sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a \ + --hash=sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b \ + --hash=sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0 \ + --hash=sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6 \ + --hash=sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2 \ + --hash=sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a \ + --hash=sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30 \ + --hash=sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218 \ + --hash=sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5 \ + --hash=sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07 \ + --hash=sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2 \ + --hash=sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4 \ + --hash=sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764 \ + --hash=sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef \ + --hash=sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3 \ + --hash=sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f + # via pandas +oauthlib==3.2.2 \ + --hash=sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca \ + --hash=sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918 + # via requests-oauthlib +ollama==0.3.1 \ + --hash=sha256:032572fb494a4fba200c65013fe937a65382c846b5f358d9e8918ecbc9ac44b5 \ + --hash=sha256:db50034c73d6350349bdfba19c3f0d54a3cea73eb97b35f9d7419b2fc7206454 + # via + # -r yaku-apps-python/3rdparty/requirements.txt + # langchain-ollama +openai==1.40.8 \ + --hash=sha256:3ed4ddad48e0dde059c9b4d3dc240e47781beca2811e52ba449ddc4a471a2fd4 \ + --hash=sha256:e225f830b946378e214c5b2cfa8df28ba2aeb7e9d44f738cb2a926fd971f5bc0 + # via + # -r yaku-apps-python/3rdparty/requirements.txt + # langchain-openai +openpyxl==3.0.10 \ + --hash=sha256:0ab6d25d01799f97a9464630abacbb34aafecdcaa0ef3cba6d6b3499867d0355 \ + --hash=sha256:e47805627aebcf860edb4edf7987b1309c1b3632f3750538ed962bbcc3bd7449 + # via + # -r yaku-apps-python/3rdparty/requirements.txt + # elmclient +oracledb==2.2.0 \ + --hash=sha256:0b4968f39871d501ab16a2fe05b5b4ae954e338e6b9dcefeb9bced998ddd4c4b \ + --hash=sha256:10d2cd354a15e2b7e191256a0179874068fc64fa6543b2e20c9c1c38f0dd0839 \ + --hash=sha256:1272bf562bcd6ff5e23b1e1fe8c3363d7a66fe8f48b1e00c4fb081d5436e1df5 \ + --hash=sha256:14a7f2572c358604186d857c80f384ad03226e372731770911856541a06bdd34 \ + --hash=sha256:19408844bd4af5b4d40f06c3e5b88c6bfce4a749f61ab766f41b22c4070c5c15 \ + --hash=sha256:253a85eef53d97815b4d838e5275d0a99e33ec340eb4b945cd2371e2bcede46b \ + --hash=sha256:35b6524b57979dbe8463af06648ad9972bce06e014a292ad96fec34c62665a8b \ + --hash=sha256:3fe57091a1463efac692b352e99f9daeab5ab375bab2060c5caba9a3a7743c15 \ + --hash=sha256:437d7c5a36f7e72ca36e1ac3f1a7c087bffa1cd0ba3a84471e54506c8572a5ad \ + --hash=sha256:4e461d1c7ef4d3f03d84595a13754390a62300976782d7c29efc07fcc915e1b3 \ + --hash=sha256:581b7067283910a53b1ac1a50c0046058a21bd5c073d529bf695113db6d25f62 \ + --hash=sha256:61bbf9cd64a2f3b65a12550329b2f0caed7d9aa5e892c0ce69d9ea7b3cb3cb8e \ + --hash=sha256:6c7da69d18cf02e469e15215af9c6f219256972a172c0e544a2ecc2a5cab9aa5 \ + --hash=sha256:97fdc27a15f6441434a7ef563f522c8ceac19c2933f2da1082125670a2e2fc6b \ + --hash=sha256:aa1fe78ed0cbf98593c1f3f620f751b725b189f8c845577e39a372f44b2bf384 \ + --hash=sha256:b5ad105aabc8ff32e3d3a343a92cf84976cf2454b6a6ff02065383fc3863e68d \ + --hash=sha256:b924ee3e7d41edb367e5bb4cbb30990ad447fedda9ef0fe29b691d36a8d338c2 \ + --hash=sha256:ba96a450275bceb5e0928e0dc01b5fb200e81ba04e99499d4930ccba681fd88a \ + --hash=sha256:bcef115bd147d6f267e3b09cbc3fc04189bff69e94d05c1e266c698668061e8d \ + --hash=sha256:c22a2052997a01e59a4c9c33c9c0593eebcb1d893addeda9cd57003c2e088a85 \ + --hash=sha256:c2e2e3f00d7eb7f4dabfa8996dc70db03bd7dbe474d2d1dc381daeff54cfdeff \ + --hash=sha256:c4b7e14b04dc2af4697ca561f9bcac110a67a7be2ccf868d789e92771017feca \ + --hash=sha256:c6a1365d3e05ca73b638ef939f9a609fed0ae5da75d13b2cfb75601ab8b85fce \ + --hash=sha256:d0245f677e27ee0990eb0213485031dacdc837a89569563f1594b82ccb362255 \ + --hash=sha256:de3f9fa10b5f5c5dbe80dc7bdea5e5746abd411217e812fae66cc61c68f3f8f6 \ + --hash=sha256:e0010aee0ed0a57964ce9f6cb0e2315a4ffce947121e0bb1c618e5091e64bab4 \ + --hash=sha256:e5ca9c050e18b2b1005b40d44a2098155445836071253ee5d547c7f285fc7729 \ + --hash=sha256:efed536635b0fec5c1484eda55fad4affa57672b87596ec6273123a3133ba5b6 \ + --hash=sha256:f52c7df38b13243b5ce583457b80748a34682b9bb8370da2497868b71976798b \ + --hash=sha256:fa5c2982076366f59dade28b554b43a257ad426e55359124bc37f191f51c2d46 \ + --hash=sha256:fbf07e0e88c9ff1555c9301d95c69e0d48263cf7df63172043fe0a042539e687 + # via -r yaku-apps-python/3rdparty/requirements.txt +orjson==3.10.7 \ + --hash=sha256:084e537806b458911137f76097e53ce7bf5806dda33ddf6aaa66a028f8d43a23 \ + --hash=sha256:09b2d92fd95ad2402188cf51573acde57eb269eddabaa60f69ea0d733e789fe9 \ + --hash=sha256:0fa5886854673222618638c6df7718ea7fe2f3f2384c452c9ccedc70b4a510a5 \ + --hash=sha256:11748c135f281203f4ee695b7f80bb1358a82a63905f9f0b794769483ea854ad \ + --hash=sha256:1193b2416cbad1a769f868b1749535d5da47626ac29445803dae7cc64b3f5c98 \ + --hash=sha256:144888c76f8520e39bfa121b31fd637e18d4cc2f115727865fdf9fa325b10412 \ + --hash=sha256:1d9c0e733e02ada3ed6098a10a8ee0052dd55774de3d9110d29868d24b17faa1 \ + --hash=sha256:23820a1563a1d386414fef15c249040042b8e5d07b40ab3fe3efbfbbcbcb8864 \ + --hash=sha256:33cfb96c24034a878d83d1a9415799a73dc77480e6c40417e5dda0710d559ee6 \ + --hash=sha256:348bdd16b32556cf8d7257b17cf2bdb7ab7976af4af41ebe79f9796c218f7e91 \ + --hash=sha256:34a566f22c28222b08875b18b0dfbf8a947e69df21a9ed5c51a6bf91cfb944ac \ + --hash=sha256:3dcfbede6737fdbef3ce9c37af3fb6142e8e1ebc10336daa05872bfb1d87839c \ + --hash=sha256:430ee4d85841e1483d487e7b81401785a5dfd69db5de01314538f31f8fbf7ee1 \ + --hash=sha256:44a96f2d4c3af51bfac6bc4ef7b182aa33f2f054fd7f34cc0ee9a320d051d41f \ + --hash=sha256:479fd0844ddc3ca77e0fd99644c7fe2de8e8be1efcd57705b5c92e5186e8a250 \ + --hash=sha256:480f455222cb7a1dea35c57a67578848537d2602b46c464472c995297117fa09 \ + --hash=sha256:4829cf2195838e3f93b70fd3b4292156fc5e097aac3739859ac0dcc722b27ac0 \ + --hash=sha256:4b6146e439af4c2472c56f8540d799a67a81226e11992008cb47e1267a9b3225 \ + --hash=sha256:4e6c3da13e5a57e4b3dca2de059f243ebec705857522f188f0180ae88badd354 \ + --hash=sha256:5b24a579123fa884f3a3caadaed7b75eb5715ee2b17ab5c66ac97d29b18fe57f \ + --hash=sha256:6b0dd04483499d1de9c8f6203f8975caf17a6000b9c0c54630cef02e44ee624e \ + --hash=sha256:6ea2b2258eff652c82652d5e0f02bd5e0463a6a52abb78e49ac288827aaa1469 \ + --hash=sha256:7122a99831f9e7fe977dc45784d3b2edc821c172d545e6420c375e5a935f5a1c \ + --hash=sha256:74f4544f5a6405b90da8ea724d15ac9c36da4d72a738c64685003337401f5c12 \ + --hash=sha256:75ef0640403f945f3a1f9f6400686560dbfb0fb5b16589ad62cd477043c4eee3 \ + --hash=sha256:76ac14cd57df0572453543f8f2575e2d01ae9e790c21f57627803f5e79b0d3c3 \ + --hash=sha256:77d325ed866876c0fa6492598ec01fe30e803272a6e8b10e992288b009cbe149 \ + --hash=sha256:7c4c17f8157bd520cdb7195f75ddbd31671997cbe10aee559c2d613592e7d7eb \ + --hash=sha256:7db8539039698ddfb9a524b4dd19508256107568cdad24f3682d5773e60504a2 \ + --hash=sha256:8272527d08450ab16eb405f47e0f4ef0e5ff5981c3d82afe0efd25dcbef2bcd2 \ + --hash=sha256:82763b46053727a7168d29c772ed5c870fdae2f61aa8a25994c7984a19b1021f \ + --hash=sha256:8a9c9b168b3a19e37fe2778c0003359f07822c90fdff8f98d9d2a91b3144d8e0 \ + --hash=sha256:8de062de550f63185e4c1c54151bdddfc5625e37daf0aa1e75d2a1293e3b7d9a \ + --hash=sha256:974683d4618c0c7dbf4f69c95a979734bf183d0658611760017f6e70a145af58 \ + --hash=sha256:9ea2c232deedcb605e853ae1db2cc94f7390ac776743b699b50b071b02bea6fe \ + --hash=sha256:a0c6a008e91d10a2564edbb6ee5069a9e66df3fbe11c9a005cb411f441fd2c09 \ + --hash=sha256:a763bc0e58504cc803739e7df040685816145a6f3c8a589787084b54ebc9f16e \ + --hash=sha256:a7e19150d215c7a13f39eb787d84db274298d3f83d85463e61d277bbd7f401d2 \ + --hash=sha256:ac7cf6222b29fbda9e3a472b41e6a5538b48f2c8f99261eecd60aafbdb60690c \ + --hash=sha256:b48b3db6bb6e0a08fa8c83b47bc169623f801e5cc4f24442ab2b6617da3b5313 \ + --hash=sha256:b58d3795dafa334fc8fd46f7c5dc013e6ad06fd5b9a4cc98cb1456e7d3558bd6 \ + --hash=sha256:bdbb61dcc365dd9be94e8f7df91975edc9364d6a78c8f7adb69c1cdff318ec93 \ + --hash=sha256:bf6ba8ebc8ef5792e2337fb0419f8009729335bb400ece005606336b7fd7bab7 \ + --hash=sha256:c31008598424dfbe52ce8c5b47e0752dca918a4fdc4a2a32004efd9fab41d866 \ + --hash=sha256:cb61938aec8b0ffb6eef484d480188a1777e67b05d58e41b435c74b9d84e0b9c \ + --hash=sha256:d2d9f990623f15c0ae7ac608103c33dfe1486d2ed974ac3f40b693bad1a22a7b \ + --hash=sha256:d352ee8ac1926d6193f602cbe36b1643bbd1bbcb25e3c1a657a4390f3000c9a5 \ + --hash=sha256:d374d36726746c81a49f3ff8daa2898dccab6596864ebe43d50733275c629175 \ + --hash=sha256:de817e2f5fc75a9e7dd350c4b0f54617b280e26d1631811a43e7e968fa71e3e9 \ + --hash=sha256:e724cebe1fadc2b23c6f7415bad5ee6239e00a69f30ee423f319c6af70e2a5c0 \ + --hash=sha256:e72591bcfe7512353bd609875ab38050efe3d55e18934e2f18950c108334b4ff \ + --hash=sha256:e76be12658a6fa376fcd331b1ea4e58f5a06fd0220653450f0d415b8fd0fbe20 \ + --hash=sha256:eb8d384a24778abf29afb8e41d68fdd9a156cf6e5390c04cc07bbc24b89e98b5 \ + --hash=sha256:ed350d6978d28b92939bfeb1a0570c523f6170efc3f0a0ef1f1df287cd4f4960 \ + --hash=sha256:eef44224729e9525d5261cc8d28d6b11cafc90e6bd0be2157bde69a52ec83024 \ + --hash=sha256:f4db56635b58cd1a200b0a23744ff44206ee6aa428185e2b6c4a65b3197abdcd \ + --hash=sha256:fdf5197a21dd660cf19dfd2a3ce79574588f8f5e2dbf21bda9ee2d2b46924d84 + # via langsmith +oscrypto==1.3.0 \ + --hash=sha256:2b2f1d2d42ec152ca90ccb5682f3e051fb55986e1b170ebde472b133713e7085 \ + --hash=sha256:6f5fef59cb5b3708321db7cca56aed8ad7e662853351e7991fcf60ec606d47a4 + # via pyhanko-certvalidator +packageurl-python==0.15.6 \ + --hash=sha256:a40210652c89022772a6c8340d6066f7d5dc67132141e5284a4db7a27d0a8ab0 \ + --hash=sha256:cbc89afd15d5f4d05db4f1b61297e5b97a43f61f28799f6d282aff467ed2ee96 + # via cyclonedx-python-lib +packaging==23.2 \ + --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ + --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 + # via + # -r yaku-apps-python/3rdparty/requirements.txt + # deprecation + # jira + # langchain-core + # pytest +pandas==2.1.3 \ + --hash=sha256:0296a66200dee556850d99b24c54c7dfa53a3264b1ca6f440e42bad424caea03 \ + --hash=sha256:04d4c58e1f112a74689da707be31cf689db086949c71828ef5da86727cfe3f82 \ + --hash=sha256:08637041279b8981a062899da0ef47828df52a1838204d2b3761fbd3e9fcb549 \ + --hash=sha256:11a771450f36cebf2a4c9dbd3a19dfa8c46c4b905a3ea09dc8e556626060fe71 \ + --hash=sha256:1329dbe93a880a3d7893149979caa82d6ba64a25e471682637f846d9dbc10dd2 \ + --hash=sha256:1f539e113739a3e0cc15176bf1231a553db0239bfa47a2c870283fd93ba4f683 \ + --hash=sha256:22929f84bca106921917eb73c1521317ddd0a4c71b395bcf767a106e3494209f \ + --hash=sha256:321ecdb117bf0f16c339cc6d5c9a06063854f12d4d9bc422a84bb2ed3207380a \ + --hash=sha256:35172bff95f598cc5866c047f43c7f4df2c893acd8e10e6653a4b792ed7f19bb \ + --hash=sha256:3cc4469ff0cf9aa3a005870cb49ab8969942b7156e0a46cc3f5abd6b11051dfb \ + --hash=sha256:4441ac94a2a2613e3982e502ccec3bdedefe871e8cea54b8775992485c5660ef \ + --hash=sha256:465571472267a2d6e00657900afadbe6097c8e1dc43746917db4dfc862e8863e \ + --hash=sha256:59dfe0e65a2f3988e940224e2a70932edc964df79f3356e5f2997c7d63e758b4 \ + --hash=sha256:72c84ec1b1d8e5efcbff5312abe92bfb9d5b558f11e0cf077f5496c4f4a3c99e \ + --hash=sha256:7cf4cf26042476e39394f1f86868d25b265ff787c9b2f0d367280f11afbdee6d \ + --hash=sha256:7fa2ad4ff196768ae63a33f8062e6838efed3a319cf938fdf8b95e956c813042 \ + --hash=sha256:a5d53c725832e5f1645e7674989f4c106e4b7249c1d57549023ed5462d73b140 \ + --hash=sha256:acf08a73b5022b479c1be155d4988b72f3020f308f7a87c527702c5f8966d34f \ + --hash=sha256:b99c4e51ef2ed98f69099c72c75ec904dd610eb41a32847c4fcbc1a975f2d2b8 \ + --hash=sha256:d5ded6ff28abbf0ea7689f251754d3789e1edb0c4d0d91028f0b980598418a58 \ + --hash=sha256:de21e12bf1511190fc1e9ebc067f14ca09fccfb189a813b38d63211d54832f5f \ + --hash=sha256:f7ea8ae8004de0381a2376662c0505bb0a4f679f4c61fbfd122aa3d1b0e5f09d \ + --hash=sha256:fc77309da3b55732059e484a1efc0897f6149183c522390772d3561f9bf96c00 \ + --hash=sha256:fca5680368a5139d4920ae3dc993eb5106d49f814ff24018b64d8850a52c6ed2 \ + --hash=sha256:fcd76d67ca2d48f56e2db45833cf9d58f548f97f61eecd3fdc74268417632b8a + # via -r yaku-apps-python/3rdparty/requirements.txt +pillow==11.0.0 \ + --hash=sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7 \ + --hash=sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5 \ + --hash=sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903 \ + --hash=sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2 \ + --hash=sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38 \ + --hash=sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2 \ + --hash=sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9 \ + --hash=sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f \ + --hash=sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc \ + --hash=sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8 \ + --hash=sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d \ + --hash=sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2 \ + --hash=sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316 \ + --hash=sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a \ + --hash=sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25 \ + --hash=sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd \ + --hash=sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba \ + --hash=sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc \ + --hash=sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273 \ + --hash=sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa \ + --hash=sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a \ + --hash=sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b \ + --hash=sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a \ + --hash=sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae \ + --hash=sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291 \ + --hash=sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97 \ + --hash=sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06 \ + --hash=sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904 \ + --hash=sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b \ + --hash=sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b \ + --hash=sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8 \ + --hash=sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527 \ + --hash=sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947 \ + --hash=sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb \ + --hash=sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003 \ + --hash=sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5 \ + --hash=sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f \ + --hash=sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739 \ + --hash=sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944 \ + --hash=sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830 \ + --hash=sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f \ + --hash=sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3 \ + --hash=sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4 \ + --hash=sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84 \ + --hash=sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7 \ + --hash=sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6 \ + --hash=sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6 \ + --hash=sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9 \ + --hash=sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de \ + --hash=sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4 \ + --hash=sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47 \ + --hash=sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd \ + --hash=sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50 \ + --hash=sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c \ + --hash=sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086 \ + --hash=sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba \ + --hash=sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306 \ + --hash=sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699 \ + --hash=sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e \ + --hash=sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488 \ + --hash=sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa \ + --hash=sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2 \ + --hash=sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3 \ + --hash=sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9 \ + --hash=sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923 \ + --hash=sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2 \ + --hash=sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790 \ + --hash=sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734 \ + --hash=sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916 \ + --hash=sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1 \ + --hash=sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f \ + --hash=sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798 \ + --hash=sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb \ + --hash=sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2 \ + --hash=sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9 + # via jira +pkginfo==1.10.0 \ + --hash=sha256:5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297 \ + --hash=sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097 + # via twine +pluggy==1.5.0 \ + --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \ + --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 + # via pytest +py==1.11.0 \ + --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ + --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 + # via pytest-forked +py-serializable==0.15.0 \ + --hash=sha256:8fc41457d8ee5f5c5a12f41fd87bf1a4f2ecf9da39fee92059b728e78f320771 \ + --hash=sha256:d3f1201b33420c481aa83f7860c7bf2c2f036ba3ea82b6e15a96696457c36cd2 + # via cyclonedx-python-lib +pycparser==2.22 \ + --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \ + --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc + # via cffi +pydantic==1.10.13 \ + --hash=sha256:1740068fd8e2ef6eb27a20e5651df000978edce6da6803c2bef0bc74540f9548 \ + --hash=sha256:210ce042e8f6f7c01168b2d84d4c9eb2b009fe7bf572c2266e235edf14bacd80 \ + --hash=sha256:32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340 \ + --hash=sha256:3ecea2b9d80e5333303eeb77e180b90e95eea8f765d08c3d278cd56b00345d01 \ + --hash=sha256:4b03e42ec20286f052490423682016fd80fda830d8e4119f8ab13ec7464c0132 \ + --hash=sha256:4c5370a7edaac06daee3af1c8b1192e305bc102abcbf2a92374b5bc793818599 \ + --hash=sha256:56e3ff861c3b9c6857579de282ce8baabf443f42ffba355bf070770ed63e11e1 \ + --hash=sha256:5a1f9f747851338933942db7af7b6ee8268568ef2ed86c4185c6ef4402e80ba8 \ + --hash=sha256:5e08865bc6464df8c7d61439ef4439829e3ab62ab1669cddea8dd00cd74b9ffe \ + --hash=sha256:61d9dce220447fb74f45e73d7ff3b530e25db30192ad8d425166d43c5deb6df0 \ + --hash=sha256:654db58ae399fe6434e55325a2c3e959836bd17a6f6a0b6ca8107ea0571d2e17 \ + --hash=sha256:678bcf5591b63cc917100dc50ab6caebe597ac67e8c9ccb75e698f66038ea953 \ + --hash=sha256:6cf25c1a65c27923a17b3da28a0bdb99f62ee04230c931d83e888012851f4e7f \ + --hash=sha256:75ac15385a3534d887a99c713aa3da88a30fbd6204a5cd0dc4dab3d770b9bd2f \ + --hash=sha256:75b297827b59bc229cac1a23a2f7a4ac0031068e5be0ce385be1462e7e17a35d \ + --hash=sha256:7d6f6e7305244bddb4414ba7094ce910560c907bdfa3501e9db1a7fd7eaea127 \ + --hash=sha256:84bafe2e60b5e78bc64a2941b4c071a4b7404c5c907f5f5a99b0139781e69ed8 \ + --hash=sha256:854223752ba81e3abf663d685f105c64150873cc6f5d0c01d3e3220bcff7d36f \ + --hash=sha256:8ae5dd6b721459bfa30805f4c25880e0dd78fc5b5879f9f7a692196ddcb5a580 \ + --hash=sha256:8ef467901d7a41fa0ca6db9ae3ec0021e3f657ce2c208e98cd511f3161c762c6 \ + --hash=sha256:968ac42970f57b8344ee08837b62f6ee6f53c33f603547a55571c954a4225691 \ + --hash=sha256:97cce3ae7341f7620a0ba5ef6cf043975cd9d2b81f3aa5f4ea37928269bc1b87 \ + --hash=sha256:9849f031cf8a2f0a928fe885e5a04b08006d6d41876b8bbd2fc68a18f9f2e3fd \ + --hash=sha256:9f00790179497767aae6bcdc36355792c79e7bbb20b145ff449700eb076c5f96 \ + --hash=sha256:b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687 \ + --hash=sha256:b97c1fac8c49be29486df85968682b0afa77e1b809aff74b83081cc115e52f33 \ + --hash=sha256:bc0898c12f8e9c97f6cd44c0ed70d55749eaf783716896960b4ecce2edfd2d69 \ + --hash=sha256:c553f6a156deb868ba38a23cf0df886c63492e9257f60a79c0fd8e7173537653 \ + --hash=sha256:c636925f38b8db208e09d344c7aa4f29a86bb9947495dd6b6d376ad10334fb78 \ + --hash=sha256:c958d053453a1c4b1c2062b05cd42d9d5c8eb67537b8d5a7e3c3032943ecd261 \ + --hash=sha256:d3a3c792a58e1622667a2837512099eac62490cdfd63bd407993aaf200a4cf1f \ + --hash=sha256:e31647d85a2013d926ce60b84f9dd5300d44535a9941fe825dc349ae1f760df9 \ + --hash=sha256:e70ca129d2053fb8b728ee7d1af8e553a928d7e301a311094b8a0501adc8763d \ + --hash=sha256:efff03cc7a4f29d9009d1c96ceb1e7a70a65cfe86e89d34e4a5f2ab1e5693737 \ + --hash=sha256:f59ef915cac80275245824e9d771ee939133be38215555e9dc90c6cb148aaeb5 \ + --hash=sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0 + # via + # -r yaku-apps-python/3rdparty/requirements.txt + # langchain-core + # langsmith + # openai +pygments==2.18.0 \ + --hash=sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199 \ + --hash=sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a + # via + # readme-renderer + # rich +pyhanko==0.25.1 \ + --hash=sha256:045a0999c5e3b22caad86e4fa11ef488c3fd7f5b5886c045ca11ffa24254c33c \ + --hash=sha256:8718d9046d442589eef6dd6973110fa5e385555cc4a6b2b1aeca3c2f3b6742e9 + # via -r yaku-apps-python/3rdparty/requirements.txt +pyhanko-certvalidator==0.26.3 \ + --hash=sha256:47fba8e9dbf846d766f2e0a453572dd4b25b2f1397847a31fe892c8eb00391f5 \ + --hash=sha256:e386c87e202ff1caacf5fd941da6c3509e79db54dbd7b43c6550ceebe5e67077 + # via + # -r yaku-apps-python/3rdparty/requirements.txt + # pyhanko +pyjwt==2.9.0 \ + --hash=sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850 \ + --hash=sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c + # via dohq-artifactory +pypdf==3.17.3 \ + --hash=sha256:1f77682d95dc6308517caa96e267628f747100634ec43476166763a226320817 \ + --hash=sha256:70c072218e3729218676bdf107e921fa49d1838c2f46056ce26d495c7e58f641 + # via -r yaku-apps-python/3rdparty/requirements.txt +pyspnego==0.11.1 \ + --hash=sha256:129a4294f2c4d681d5875240ef87accc6f1d921e8983737fb0b59642b397951e \ + --hash=sha256:e92ed8b0a62765b9d6abbb86a48cf871228ddb97678598dc01c9c39a626823f6 + # via requests-ntlm +pytest==7.4.4 \ + --hash=sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280 \ + --hash=sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8 + # via + # -r yaku-apps-python/3rdparty/requirements.txt + # pytest-cov + # pytest-custom-exit-code + # pytest-forked + # pytest-mock + # pytest-xdist +pytest-cov==5.0.0 \ + --hash=sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652 \ + --hash=sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857 + # via -r yaku-apps-python/3rdparty/requirements.txt +pytest-custom-exit-code==0.3.0 \ + --hash=sha256:51ffff0ee2c1ddcc1242e2ddb2a5fd02482717e33a2326ef330e3aa430244635 \ + --hash=sha256:6e0ce6e57ce3a583cb7e5023f7d1021e19dfec22be41d9ad345bae2fc61caf3b + # via -r yaku-apps-python/3rdparty/requirements.txt +pytest-forked==1.6.0 \ + --hash=sha256:4dafd46a9a600f65d822b8f605133ecf5b3e1941ebb3588e943b4e3eb71a5a3f \ + --hash=sha256:810958f66a91afb1a1e2ae83089d8dc1cd2437ac96b12963042fbb9fb4d16af0 + # via pytest-xdist +pytest-mock==3.14.0 \ + --hash=sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f \ + --hash=sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0 + # via -r yaku-apps-python/3rdparty/requirements.txt +pytest-xdist==2.5.0 \ + --hash=sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf \ + --hash=sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65 + # via -r yaku-apps-python/3rdparty/requirements.txt +python-dateutil==2.9.0.post0 \ + --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ + --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 + # via + # -r yaku-apps-python/3rdparty/requirements.txt + # dohq-artifactory + # elmclient + # freezegun + # pandas +pytz==2023.3 \ + --hash=sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588 \ + --hash=sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb + # via + # -r yaku-apps-python/3rdparty/requirements.txt + # datetime + # elmclient + # pandas +pyyaml==6.0 \ + --hash=sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf \ + --hash=sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293 \ + --hash=sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b \ + --hash=sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57 \ + --hash=sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b \ + --hash=sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4 \ + --hash=sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07 \ + --hash=sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba \ + --hash=sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9 \ + --hash=sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287 \ + --hash=sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513 \ + --hash=sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0 \ + --hash=sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782 \ + --hash=sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0 \ + --hash=sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92 \ + --hash=sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f \ + --hash=sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 \ + --hash=sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc \ + --hash=sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1 \ + --hash=sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c \ + --hash=sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86 \ + --hash=sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4 \ + --hash=sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c \ + --hash=sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34 \ + --hash=sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b \ + --hash=sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d \ + --hash=sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c \ + --hash=sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb \ + --hash=sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7 \ + --hash=sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737 \ + --hash=sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3 \ + --hash=sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d \ + --hash=sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358 \ + --hash=sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53 \ + --hash=sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78 \ + --hash=sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803 \ + --hash=sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a \ + --hash=sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f \ + --hash=sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174 \ + --hash=sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5 + # via + # -r yaku-apps-python/3rdparty/requirements.txt + # langchain-core + # pyhanko +qrcode==8.0 \ + --hash=sha256:025ce2b150f7fe4296d116ee9bad455a6643ab4f6e7dce541613a4758cbce347 \ + --hash=sha256:9fc05f03305ad27a709eb742cf3097fa19e6f6f93bb9e2f039c0979190f6f1b1 + # via pyhanko +readme-renderer==44.0 \ + --hash=sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151 \ + --hash=sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1 + # via twine +referencing==0.35.1 \ + --hash=sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c \ + --hash=sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de + # via + # jsonschema + # jsonschema-specifications + # types-jsonschema +regex==2024.9.11 \ + --hash=sha256:01c2acb51f8a7d6494c8c5eafe3d8e06d76563d8a8a4643b37e9b2dd8a2ff623 \ + --hash=sha256:02087ea0a03b4af1ed6ebab2c54d7118127fee8d71b26398e8e4b05b78963199 \ + --hash=sha256:040562757795eeea356394a7fb13076ad4f99d3c62ab0f8bdfb21f99a1f85664 \ + --hash=sha256:042c55879cfeb21a8adacc84ea347721d3d83a159da6acdf1116859e2427c43f \ + --hash=sha256:079400a8269544b955ffa9e31f186f01d96829110a3bf79dc338e9910f794fca \ + --hash=sha256:07f45f287469039ffc2c53caf6803cd506eb5f5f637f1d4acb37a738f71dd066 \ + --hash=sha256:09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca \ + --hash=sha256:0cbff728659ce4bbf4c30b2a1be040faafaa9eca6ecde40aaff86f7889f4ab39 \ + --hash=sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d \ + --hash=sha256:0ea51dcc0835eea2ea31d66456210a4e01a076d820e9039b04ae8d17ac11dee6 \ + --hash=sha256:0ffbcf9221e04502fc35e54d1ce9567541979c3fdfb93d2c554f0ca583a19b35 \ + --hash=sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408 \ + --hash=sha256:16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5 \ + --hash=sha256:18406efb2f5a0e57e3a5881cd9354c1512d3bb4f5c45d96d110a66114d84d23a \ + --hash=sha256:18e707ce6c92d7282dfce370cd205098384b8ee21544e7cb29b8aab955b66fa9 \ + --hash=sha256:220e92a30b426daf23bb67a7962900ed4613589bab80382be09b48896d211e92 \ + --hash=sha256:23b30c62d0f16827f2ae9f2bb87619bc4fba2044911e2e6c2eb1af0161cdb766 \ + --hash=sha256:23f9985c8784e544d53fc2930fc1ac1a7319f5d5332d228437acc9f418f2f168 \ + --hash=sha256:297f54910247508e6e5cae669f2bc308985c60540a4edd1c77203ef19bfa63ca \ + --hash=sha256:2b08fce89fbd45664d3df6ad93e554b6c16933ffa9d55cb7e01182baaf971508 \ + --hash=sha256:2cce2449e5927a0bf084d346da6cd5eb016b2beca10d0013ab50e3c226ffc0df \ + --hash=sha256:313ea15e5ff2a8cbbad96ccef6be638393041b0a7863183c2d31e0c6116688cf \ + --hash=sha256:323c1f04be6b2968944d730e5c2091c8c89767903ecaa135203eec4565ed2b2b \ + --hash=sha256:35f4a6f96aa6cb3f2f7247027b07b15a374f0d5b912c0001418d1d55024d5cb4 \ + --hash=sha256:3b37fa423beefa44919e009745ccbf353d8c981516e807995b2bd11c2c77d268 \ + --hash=sha256:3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6 \ + --hash=sha256:46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c \ + --hash=sha256:4838e24ee015101d9f901988001038f7f0d90dc0c3b115541a1365fb439add62 \ + --hash=sha256:49b0e06786ea663f933f3710a51e9385ce0cba0ea56b67107fd841a55d56a231 \ + --hash=sha256:4db21ece84dfeefc5d8a3863f101995de646c6cb0536952c321a2650aa202c36 \ + --hash=sha256:54c4a097b8bc5bb0dfc83ae498061d53ad7b5762e00f4adaa23bee22b012e6ba \ + --hash=sha256:54d9ff35d4515debf14bc27f1e3b38bfc453eff3220f5bce159642fa762fe5d4 \ + --hash=sha256:55b96e7ce3a69a8449a66984c268062fbaa0d8ae437b285428e12797baefce7e \ + --hash=sha256:57fdd2e0b2694ce6fc2e5ccf189789c3e2962916fb38779d3e3521ff8fe7a822 \ + --hash=sha256:587d4af3979376652010e400accc30404e6c16b7df574048ab1f581af82065e4 \ + --hash=sha256:5b513b6997a0b2f10e4fd3a1313568e373926e8c252bd76c960f96fd039cd28d \ + --hash=sha256:5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71 \ + --hash=sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50 \ + --hash=sha256:635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d \ + --hash=sha256:64ce2799bd75039b480cc0360907c4fb2f50022f030bf9e7a8705b636e408fad \ + --hash=sha256:69dee6a020693d12a3cf892aba4808fe168d2a4cef368eb9bf74f5398bfd4ee8 \ + --hash=sha256:6a2644a93da36c784e546de579ec1806bfd2763ef47babc1b03d765fe560c9f8 \ + --hash=sha256:6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8 \ + --hash=sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd \ + --hash=sha256:6edd623bae6a737f10ce853ea076f56f507fd7726bee96a41ee3d68d347e4d16 \ + --hash=sha256:73d6d2f64f4d894c96626a75578b0bf7d9e56dcda8c3d037a2118fdfe9b1c664 \ + --hash=sha256:7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a \ + --hash=sha256:7fb89ee5d106e4a7a51bce305ac4efb981536301895f7bdcf93ec92ae0d91c7f \ + --hash=sha256:846bc79ee753acf93aef4184c040d709940c9d001029ceb7b7a52747b80ed2dd \ + --hash=sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a \ + --hash=sha256:8dee5b4810a89447151999428fe096977346cf2f29f4d5e29609d2e19e0199c9 \ + --hash=sha256:8e5fb5f77c8745a60105403a774fe2c1759b71d3e7b4ca237a5e67ad066c7199 \ + --hash=sha256:98eeee2f2e63edae2181c886d7911ce502e1292794f4c5ee71e60e23e8d26b5d \ + --hash=sha256:9d4a76b96f398697fe01117093613166e6aa8195d63f1b4ec3f21ab637632963 \ + --hash=sha256:9e8719792ca63c6b8340380352c24dcb8cd7ec49dae36e963742a275dfae6009 \ + --hash=sha256:a0b2b80321c2ed3fcf0385ec9e51a12253c50f146fddb2abbb10f033fe3d049a \ + --hash=sha256:a4cc92bb6db56ab0c1cbd17294e14f5e9224f0cc6521167ef388332604e92679 \ + --hash=sha256:a738b937d512b30bf75995c0159c0ddf9eec0775c9d72ac0202076c72f24aa96 \ + --hash=sha256:a8f877c89719d759e52783f7fe6e1c67121076b87b40542966c02de5503ace42 \ + --hash=sha256:a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8 \ + --hash=sha256:ae2941333154baff9838e88aa71c1d84f4438189ecc6021a12c7573728b5838e \ + --hash=sha256:b0d0a6c64fcc4ef9c69bd5b3b3626cc3776520a1637d8abaa62b9edc147a58f7 \ + --hash=sha256:b5b029322e6e7b94fff16cd120ab35a253236a5f99a79fb04fda7ae71ca20ae8 \ + --hash=sha256:b7aaa315101c6567a9a45d2839322c51c8d6e81f67683d529512f5bcfb99c802 \ + --hash=sha256:be1c8ed48c4c4065ecb19d882a0ce1afe0745dfad8ce48c49586b90a55f02366 \ + --hash=sha256:c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137 \ + --hash=sha256:c157bb447303070f256e084668b702073db99bbb61d44f85d811025fcf38f784 \ + --hash=sha256:c57d08ad67aba97af57a7263c2d9006d5c404d721c5f7542f077f109ec2a4a29 \ + --hash=sha256:c69ada171c2d0e97a4b5aa78fbb835e0ffbb6b13fc5da968c09811346564f0d3 \ + --hash=sha256:c94bb0a9f1db10a1d16c00880bdebd5f9faf267273b8f5bd1878126e0fbde771 \ + --hash=sha256:cb130fccd1a37ed894824b8c046321540263013da72745d755f2d35114b81a60 \ + --hash=sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a \ + --hash=sha256:d05ac6fa06959c4172eccd99a222e1fbf17b5670c4d596cb1e5cde99600674c4 \ + --hash=sha256:d552c78411f60b1fdaafd117a1fca2f02e562e309223b9d44b7de8be451ec5e0 \ + --hash=sha256:dd4490a33eb909ef5078ab20f5f000087afa2a4daa27b4c072ccb3cb3050ad84 \ + --hash=sha256:df5cbb1fbc74a8305b6065d4ade43b993be03dbe0f8b30032cced0d7740994bd \ + --hash=sha256:e28f9faeb14b6f23ac55bfbbfd3643f5c7c18ede093977f1df249f73fd22c7b1 \ + --hash=sha256:e464b467f1588e2c42d26814231edecbcfe77f5ac414d92cbf4e7b55b2c2a776 \ + --hash=sha256:e4c22e1ac1f1ec1e09f72e6c44d8f2244173db7eb9629cc3a346a8d7ccc31142 \ + --hash=sha256:e53b5fbab5d675aec9f0c501274c467c0f9a5d23696cfc94247e1fb56501ed89 \ + --hash=sha256:e93f1c331ca8e86fe877a48ad64e77882c0c4da0097f2212873a69bbfea95d0c \ + --hash=sha256:e997fd30430c57138adc06bba4c7c2968fb13d101e57dd5bb9355bf8ce3fa7e8 \ + --hash=sha256:e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35 \ + --hash=sha256:eab4bb380f15e189d1313195b062a6aa908f5bd687a0ceccd47c8211e9cf0d4a \ + --hash=sha256:eb1ae19e64c14c7ec1995f40bd932448713d3c73509e82d8cd7744dc00e29e86 \ + --hash=sha256:ecea58b43a67b1b79805f1a0255730edaf5191ecef84dbc4cc85eb30bc8b63b9 \ + --hash=sha256:ee439691d8c23e76f9802c42a95cfeebf9d47cf4ffd06f18489122dbb0a7ad64 \ + --hash=sha256:eee9130eaad130649fd73e5cd92f60e55708952260ede70da64de420cdcad554 \ + --hash=sha256:f47cd43a5bfa48f86925fe26fbdd0a488ff15b62468abb5d2a1e092a4fb10e85 \ + --hash=sha256:f6fff13ef6b5f29221d6904aa816c34701462956aa72a77f1f151a8ec4f56aeb \ + --hash=sha256:f745ec09bc1b0bd15cfc73df6fa4f726dcc26bb16c23a03f9e3367d357eeedd0 \ + --hash=sha256:f8404bf61298bb6f8224bb9176c1424548ee1181130818fcd2cbffddc768bed8 \ + --hash=sha256:f9268774428ec173654985ce55fc6caf4c6d11ade0f6f914d48ef4719eb05ebb \ + --hash=sha256:faa3c142464efec496967359ca99696c896c591c56c53506bac1ad465f66e919 + # via tiktoken +requests==2.32.3 \ + --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ + --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 + # via + # -r yaku-apps-python/3rdparty/requirements.txt + # azure-core + # cachecontrol + # dohq-artifactory + # elmclient + # jira + # langsmith + # msrest + # pyhanko + # pyhanko-certvalidator + # requests-mock + # requests-ntlm + # requests-oauthlib + # requests-toolbelt + # tiktoken + # twine +requests-mock==1.12.1 \ + --hash=sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563 \ + --hash=sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401 + # via -r yaku-apps-python/3rdparty/requirements.txt +requests-ntlm==1.2.0 \ + --hash=sha256:33c285f5074e317cbdd338d199afa46a7c01132e5c111d36bd415534e9b916a8 \ + --hash=sha256:b7781090c647308a88b55fb530c7b3705cef45349e70a83b8d6731e7889272a6 + # via -r yaku-apps-python/3rdparty/requirements.txt +requests-oauthlib==2.0.0 \ + --hash=sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36 \ + --hash=sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9 + # via + # jira + # msrest +requests-toolbelt==1.0.0 \ + --hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \ + --hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06 + # via + # elmclient + # jira + # langsmith + # twine +rfc3986==2.0.0 \ + --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \ + --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c + # via twine +rich==13.9.2 \ + --hash=sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c \ + --hash=sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1 + # via twine +rpds-py==0.20.0 \ + --hash=sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c \ + --hash=sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585 \ + --hash=sha256:11ef6ce74616342888b69878d45e9f779b95d4bd48b382a229fe624a409b72c5 \ + --hash=sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6 \ + --hash=sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef \ + --hash=sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2 \ + --hash=sha256:1e5f3cd7397c8f86c8cc72d5a791071431c108edd79872cdd96e00abd8497d29 \ + --hash=sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318 \ + --hash=sha256:22e6c9976e38f4d8c4a63bd8a8edac5307dffd3ee7e6026d97f3cc3a2dc02a0b \ + --hash=sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399 \ + --hash=sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739 \ + --hash=sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee \ + --hash=sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174 \ + --hash=sha256:338ca4539aad4ce70a656e5187a3a31c5204f261aef9f6ab50e50bcdffaf050a \ + --hash=sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344 \ + --hash=sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2 \ + --hash=sha256:3d2b1ad682a3dfda2a4e8ad8572f3100f95fad98cb99faf37ff0ddfe9cbf9d03 \ + --hash=sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5 \ + --hash=sha256:3fde368e9140312b6e8b6c09fb9f8c8c2f00999d1823403ae90cc00480221b22 \ + --hash=sha256:40ce74fc86ee4645d0a225498d091d8bc61f39b709ebef8204cb8b5a464d3c0e \ + --hash=sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96 \ + --hash=sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91 \ + --hash=sha256:4b16aa0107ecb512b568244ef461f27697164d9a68d8b35090e9b0c1c8b27752 \ + --hash=sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075 \ + --hash=sha256:4fe84294c7019456e56d93e8ababdad5a329cd25975be749c3f5f558abb48253 \ + --hash=sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee \ + --hash=sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad \ + --hash=sha256:54b43a2b07db18314669092bb2de584524d1ef414588780261e31e85846c26a5 \ + --hash=sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce \ + --hash=sha256:569b3ea770c2717b730b61998b6c54996adee3cef69fc28d444f3e7920313cf7 \ + --hash=sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b \ + --hash=sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8 \ + --hash=sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57 \ + --hash=sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3 \ + --hash=sha256:5b4f105deeffa28bbcdff6c49b34e74903139afa690e35d2d9e3c2c2fba18cec \ + --hash=sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209 \ + --hash=sha256:614fdafe9f5f19c63ea02817fa4861c606a59a604a77c8cdef5aa01d28b97921 \ + --hash=sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045 \ + --hash=sha256:65794e4048ee837494aea3c21a28ad5fc080994dfba5b036cf84de37f7ad5074 \ + --hash=sha256:6632f2d04f15d1bd6fe0eedd3b86d9061b836ddca4c03d5cf5c7e9e6b7c14580 \ + --hash=sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7 \ + --hash=sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5 \ + --hash=sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3 \ + --hash=sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0 \ + --hash=sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24 \ + --hash=sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139 \ + --hash=sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db \ + --hash=sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc \ + --hash=sha256:9824fb430c9cf9af743cf7aaf6707bf14323fb51ee74425c380f4c846ea70789 \ + --hash=sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f \ + --hash=sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2 \ + --hash=sha256:9d35cef91e59ebbeaa45214861874bc6f19eb35de96db73e467a8358d701a96c \ + --hash=sha256:a1862d2d7ce1674cffa6d186d53ca95c6e17ed2b06b3f4c476173565c862d232 \ + --hash=sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6 \ + --hash=sha256:aa7f429242aae2947246587d2964fad750b79e8c233a2367f71b554e9447949c \ + --hash=sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29 \ + --hash=sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489 \ + --hash=sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94 \ + --hash=sha256:af0fc424a5842a11e28956e69395fbbeab2c97c42253169d87e90aac2886d751 \ + --hash=sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2 \ + --hash=sha256:b4c29cbbba378759ac5786730d1c3cb4ec6f8ababf5c42a9ce303dc4b3d08cda \ + --hash=sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9 \ + --hash=sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51 \ + --hash=sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c \ + --hash=sha256:b8c00a3b1e70c1d3891f0db1b05292747f0dbcfb49c43f9244d04c70fbc40eb8 \ + --hash=sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989 \ + --hash=sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511 \ + --hash=sha256:c3e130fd0ec56cb76eb49ef52faead8ff09d13f4527e9b0c400307ff72b408e1 \ + --hash=sha256:c52d3f2f82b763a24ef52f5d24358553e8403ce05f893b5347098014f2d9eff2 \ + --hash=sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150 \ + --hash=sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c \ + --hash=sha256:ce9845054c13696f7af7f2b353e6b4f676dab1b4b215d7fe5e05c6f8bb06f965 \ + --hash=sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f \ + --hash=sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58 \ + --hash=sha256:d0b67d87bb45ed1cd020e8fbf2307d449b68abc45402fe1a4ac9e46c3c8b192b \ + --hash=sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f \ + --hash=sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d \ + --hash=sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821 \ + --hash=sha256:d72278a30111e5b5525c1dd96120d9e958464316f55adb030433ea905866f4de \ + --hash=sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121 \ + --hash=sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855 \ + --hash=sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272 \ + --hash=sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60 \ + --hash=sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02 \ + --hash=sha256:def7400461c3a3f26e49078302e1c1b38f6752342c77e3cf72ce91ca69fb1bc1 \ + --hash=sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140 \ + --hash=sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879 \ + --hash=sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940 \ + --hash=sha256:e6900ecdd50ce0facf703f7a00df12374b74bbc8ad9fe0f6559947fb20f82364 \ + --hash=sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4 \ + --hash=sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e \ + --hash=sha256:ec31a99ca63bf3cd7f1a5ac9fe95c5e2d060d3c768a09bc1d16e235840861420 \ + --hash=sha256:f0475242f447cc6cb8a9dd486d68b2ef7fbee84427124c232bff5f63b1fe11e5 \ + --hash=sha256:f2fbf7db2012d4876fb0d66b5b9ba6591197b0f165db8d99371d976546472a24 \ + --hash=sha256:f60012a73aa396be721558caa3a6fd49b3dd0033d1675c6d59c4502e870fcf0c \ + --hash=sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf \ + --hash=sha256:f90a4cd061914a60bd51c68bcb4357086991bd0bb93d8aa66a6da7701370708f \ + --hash=sha256:f918a1a130a6dfe1d7fe0f105064141342e7dd1611f2e6a21cd2f5c8cb1cfb3e \ + --hash=sha256:fa518bcd7600c584bf42e6617ee8132869e877db2f76bcdc281ec6a4113a53ab \ + --hash=sha256:faefcc78f53a88f3076b7f8be0a8f8d35133a3ecf7f3770895c25f8813460f08 \ + --hash=sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92 \ + --hash=sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a \ + --hash=sha256:fdfc3a892927458d98f3d55428ae46b921d1f7543b89382fdb483f5640daaec8 + # via + # jsonschema + # referencing +six==1.16.0 \ + --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 + # via + # anytree + # azure-core + # python-dateutil +sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via + # anyio + # httpx + # openai +sortedcontainers==2.4.0 \ + --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ + --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 + # via cyclonedx-python-lib +soupsieve==2.6 \ + --hash=sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb \ + --hash=sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9 + # via beautifulsoup4 +splunk-sdk==2.0.2 \ + --hash=sha256:d5ccf6e1b96e493b1399e071ef10f8337e0a39ac2b2acc73fdd375b87b4104cb + # via -r yaku-apps-python/3rdparty/requirements.txt +tenacity==8.5.0 \ + --hash=sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78 \ + --hash=sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687 + # via langchain-core +tiktoken==0.8.0 \ + --hash=sha256:02be1666096aff7da6cbd7cdaa8e7917bfed3467cd64b38b1f112e96d3b06a24 \ + --hash=sha256:1473cfe584252dc3fa62adceb5b1c763c1874e04511b197da4e6de51d6ce5a02 \ + --hash=sha256:18228d624807d66c87acd8f25fc135665617cab220671eb65b50f5d70fa51f69 \ + --hash=sha256:25e13f37bc4ef2d012731e93e0fef21dc3b7aea5bb9009618de9a4026844e560 \ + --hash=sha256:294440d21a2a51e12d4238e68a5972095534fe9878be57d905c476017bff99fc \ + --hash=sha256:2efaf6199717b4485031b4d6edb94075e4d79177a172f38dd934d911b588d54a \ + --hash=sha256:326624128590def898775b722ccc327e90b073714227175ea8febbc920ac0a99 \ + --hash=sha256:4177faa809bd55f699e88c96d9bb4635d22e3f59d635ba6fd9ffedf7150b9953 \ + --hash=sha256:5376b6f8dc4753cd81ead935c5f518fa0fbe7e133d9e25f648d8c4dabdd4bad7 \ + --hash=sha256:5637e425ce1fc49cf716d88df3092048359a4b3bbb7da762840426e937ada06d \ + --hash=sha256:56edfefe896c8f10aba372ab5706b9e3558e78db39dd497c940b47bf228bc419 \ + --hash=sha256:6adc8323016d7758d6de7313527f755b0fc6c72985b7d9291be5d96d73ecd1e1 \ + --hash=sha256:6b231f5e8982c245ee3065cd84a4712d64692348bc609d84467c57b4b72dcbc5 \ + --hash=sha256:6b2ddbc79a22621ce8b1166afa9f9a888a664a579350dc7c09346a3b5de837d9 \ + --hash=sha256:7e17807445f0cf1f25771c9d86496bd8b5c376f7419912519699f3cc4dc5c12e \ + --hash=sha256:845287b9798e476b4d762c3ebda5102be87ca26e5d2c9854002825d60cdb815d \ + --hash=sha256:881839cfeae051b3628d9823b2e56b5cc93a9e2efb435f4cf15f17dc45f21586 \ + --hash=sha256:886f80bd339578bbdba6ed6d0567a0d5c6cfe198d9e587ba6c447654c65b8edc \ + --hash=sha256:9269348cb650726f44dd3bbb3f9110ac19a8dcc8f54949ad3ef652ca22a38e21 \ + --hash=sha256:9a58deb7075d5b69237a3ff4bb51a726670419db6ea62bdcd8bd80c78497d7ab \ + --hash=sha256:9ccbb2740f24542534369c5635cfd9b2b3c2490754a78ac8831d99f89f94eeb2 \ + --hash=sha256:9fb0e352d1dbe15aba082883058b3cce9e48d33101bdaac1eccf66424feb5b47 \ + --hash=sha256:b07e33283463089c81ef1467180e3e00ab00d46c2c4bbcef0acab5f771d6695e \ + --hash=sha256:b591fb2b30d6a72121a80be24ec7a0e9eb51c5500ddc7e4c2496516dd5e3816b \ + --hash=sha256:c94ff53c5c74b535b2cbf431d907fc13c678bbd009ee633a2aca269a04389f9a \ + --hash=sha256:d2908c0d043a7d03ebd80347266b0e58440bdef5564f84f4d29fb235b5df3b04 \ + --hash=sha256:d622d8011e6d6f239297efa42a2657043aaed06c4f68833550cac9e9bc723ef1 \ + --hash=sha256:d8c2d0e5ba6453a290b86cd65fc51fedf247e1ba170191715b049dac1f628005 \ + --hash=sha256:d8f3192733ac4d77977432947d563d7e1b310b96497acd3c196c9bddb36ed9db \ + --hash=sha256:f13d13c981511331eac0d01a59b5df7c0d4060a8be1e378672822213da51e0a2 \ + --hash=sha256:fe9399bdc3f29d428f16a2f86c3c8ec20be3eac5f53693ce4980371c3245729b + # via langchain-openai +tqdm==4.66.5 \ + --hash=sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd \ + --hash=sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad + # via + # elmclient + # openai +twine==5.1.1 \ + --hash=sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997 \ + --hash=sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db + # via elmclient +types-beautifulsoup4==4.12.0.20240907 \ + --hash=sha256:32f5ac48514b488f15241afdd7d2f73f0baf3c54e874e23b66708503dd288489 \ + --hash=sha256:8d023b86530922070417a1d4c4d91678ab0ff2439b3b2b2cffa3b628b49ebab1 + # via -r yaku-apps-python/3rdparty/requirements.txt +types-html5lib==1.1.11.20240806 \ + --hash=sha256:575c4fd84ba8eeeaa8520c7e4c7042b7791f5ec3e9c0a5d5c418124c42d9e7e4 \ + --hash=sha256:8060dc98baf63d6796a765bbbc809fff9f7a383f6e3a9add526f814c086545ef + # via types-beautifulsoup4 +types-jsonschema==4.23.0.20240813 \ + --hash=sha256:be283e23f0b87547316c2ee6b0fd36d95ea30e921db06478029e10b5b6aa6ac3 \ + --hash=sha256:c93f48206f209a5bc4608d295ac39f172fb98b9e24159ce577dbd25ddb79a1c0 + # via -r yaku-apps-python/3rdparty/requirements.txt +types-mock==5.1.0.20240425 \ + --hash=sha256:5281a645d72e827d70043e3cc144fe33b1c003db084f789dc203aa90e812a5a4 \ + --hash=sha256:d586a01d39ad919d3ddcd73de6cde73ca7f3c69707219f722d1b8d7733641ad7 + # via -r yaku-apps-python/3rdparty/requirements.txt +types-openpyxl==3.1.5.20240918 \ + --hash=sha256:22a71a1b601ed8194e356e33e2bebb93dddc47915b410db14ace5a6b7b856955 \ + --hash=sha256:e9bf3c6f7966d347a2514b48f889f272d58e9b22a762244f778a5d66aee2101e + # via -r yaku-apps-python/3rdparty/requirements.txt +types-python-dateutil==2.9.0.20241003 \ + --hash=sha256:250e1d8e80e7bbc3a6c99b907762711d1a1cdd00e978ad39cb5940f6f0a87f3d \ + --hash=sha256:58cb85449b2a56d6684e41aeefb4c4280631246a0da1a719bdbe6f3fb0317446 + # via -r yaku-apps-python/3rdparty/requirements.txt +types-pytz==2024.2.0.20241003 \ + --hash=sha256:3e22df1336c0c6ad1d29163c8fda82736909eb977281cb823c57f8bae07118b7 \ + --hash=sha256:575dc38f385a922a212bac00a7d6d2e16e141132a3c955078f4a4fd13ed6cb44 + # via -r yaku-apps-python/3rdparty/requirements.txt +types-pyyaml==6.0.12.20240917 \ + --hash=sha256:392b267f1c0fe6022952462bf5d6523f31e37f6cea49b14cee7ad634b6301570 \ + --hash=sha256:d1405a86f9576682234ef83bcb4e6fff7c9305c8b1fbad5e0bcd4f7dbdc9c587 + # via -r yaku-apps-python/3rdparty/requirements.txt +types-requests==2.32.0.20241016 \ + --hash=sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95 \ + --hash=sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747 + # via -r yaku-apps-python/3rdparty/requirements.txt +types-xmltodict==0.14.0.20241009 \ + --hash=sha256:9224c2422c5b6359cf826685b4ee50b14dc2cb9134561ab793ef6b03dd7108e1 \ + --hash=sha256:92812e17ffa9171416b35806cb5f4ed3f8f52b6724b2c555e4733e902ef4afd0 + # via -r yaku-apps-python/3rdparty/requirements.txt +typing-extensions==4.12.2 \ + --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ + --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 + # via + # azure-core + # jira + # langchain-core + # mypy + # openai + # pydantic +tzdata==2024.2 \ + --hash=sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc \ + --hash=sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd + # via pandas +tzlocal==5.2 \ + --hash=sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8 \ + --hash=sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e + # via pyhanko +uritools==4.0.3 \ + --hash=sha256:bae297d090e69a0451130ffba6f2f1c9477244aa0a5543d66aed2d9f77d0dd9c \ + --hash=sha256:ee06a182a9c849464ce9d5fa917539aacc8edd2a4924d1b7aabeeecabcae3bc2 + # via pyhanko-certvalidator +urllib3==2.2.2 \ + --hash=sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472 \ + --hash=sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168 + # via + # -r yaku-apps-python/3rdparty/requirements.txt + # elmclient + # requests + # twine + # types-requests +xlrd==2.0.1 \ + --hash=sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd \ + --hash=sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88 + # via -r yaku-apps-python/3rdparty/requirements.txt +xmltodict==0.13.0 \ + --hash=sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56 \ + --hash=sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852 + # via -r yaku-apps-python/3rdparty/requirements.txt +zipp==3.20.2 \ + --hash=sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350 \ + --hash=sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29 + # via importlib-metadata +zope-interface==7.1.0 \ + --hash=sha256:07add15de0cc7e69917f7d286b64d54125c950aeb43efed7a5ea7172f000fbc1 \ + --hash=sha256:0ac20581fc6cd7c754f6dff0ae06fedb060fa0e9ea6309d8be8b2701d9ea51c4 \ + --hash=sha256:124149e2d42067b9c6597f4dafdc7a0983d0163868f897b7bb5dc850b14f9a87 \ + --hash=sha256:27cfb5205d68b12682b6e55ab8424662d96e8ead19550aad0796b08dd2c9a45e \ + --hash=sha256:2a29ac607e970b5576547f0e3589ec156e04de17af42839eedcf478450687317 \ + --hash=sha256:2b6a4924f5bad9fe21d99f66a07da60d75696a136162427951ec3cb223a5570d \ + --hash=sha256:2bd9e9f366a5df08ebbdc159f8224904c1c5ce63893984abb76954e6fbe4381a \ + --hash=sha256:3bcff5c09d0215f42ba64b49205a278e44413d9bf9fa688fd9e42bfe472b5f4f \ + --hash=sha256:3f005869a1a05e368965adb2075f97f8ee9a26c61898a9e52a9764d93774f237 \ + --hash=sha256:4a00ead2e24c76436e1b457a5132d87f83858330f6c923640b7ef82d668525d1 \ + --hash=sha256:4af4a12b459a273b0b34679a5c3dc5e34c1847c3dd14a628aa0668e19e638ea2 \ + --hash=sha256:5501e772aff595e3c54266bc1bfc5858e8f38974ce413a8f1044aae0f32a83a3 \ + --hash=sha256:5e28ea0bc4b084fc93a483877653a033062435317082cdc6388dec3438309faf \ + --hash=sha256:5e956b1fd7f3448dd5e00f273072e73e50dfafcb35e4227e6d5af208075593c9 \ + --hash=sha256:5fcf379b875c610b5a41bc8a891841533f98de0520287d7f85e25386cd10d3e9 \ + --hash=sha256:6159e767d224d8f18deff634a1d3722e68d27488c357f62ebeb5f3e2f5288b1f \ + --hash=sha256:661d5df403cd3c5b8699ac480fa7f58047a3253b029db690efa0c3cf209993ef \ + --hash=sha256:711eebc77f2092c6a8b304bad0b81a6ce3cf5490b25574e7309fbc07d881e3af \ + --hash=sha256:80a3c00b35f6170be5454b45abe2719ea65919a2f09e8a6e7b1362312a872cd3 \ + --hash=sha256:848b6fa92d7c8143646e64124ed46818a0049a24ecc517958c520081fd147685 \ + --hash=sha256:91b6c30689cfd87c8f264acb2fc16ad6b3c72caba2aec1bf189314cf1a84ca33 \ + --hash=sha256:9733a9a0f94ef53d7aa64661811b20875b5bc6039034c6e42fb9732170130573 \ + --hash=sha256:9940d5bc441f887c5f375ec62bcf7e7e495a2d5b1da97de1184a88fb567f06af \ + --hash=sha256:9e3e48f3dea21c147e1b10c132016cb79af1159facca9736d231694ef5a740a8 \ + --hash=sha256:a14c9decf0eb61e0892631271d500c1e306c7b6901c998c7035e194d9150fdd1 \ + --hash=sha256:a735f82d2e3ed47ca01a20dfc4c779b966b16352650a8036ab3955aad151ed8a \ + --hash=sha256:a99240b1d02dc469f6afbe7da1bf617645e60290c272968f4e53feec18d7dce8 \ + --hash=sha256:b7b25db127db3e6b597c5f74af60309c4ad65acd826f89609662f0dc33a54728 \ + --hash=sha256:b936d61dbe29572fd2cfe13e30b925e5383bed1aba867692670f5a2a2eb7b4e9 \ + --hash=sha256:bec001798ab62c3fc5447162bf48496ae9fba02edc295a9e10a0b0c639a6452e \ + --hash=sha256:cc8a318162123eddbdf22fcc7b751288ce52e4ad096d3766ff1799244352449d \ + --hash=sha256:d0a45b5af9f72c805ee668d1479480ca85169312211bed6ed18c343e39307d5f \ + --hash=sha256:e53c291debef523b09e1fe3dffe5f35dde164f1c603d77f770b88a1da34b7ed6 \ + --hash=sha256:ec1ef1fdb6f014d5886b97e52b16d0f852364f447d2ab0f0c6027765777b6667 \ + --hash=sha256:ec59fe53db7d32abb96c6d4efeed84aab4a7c38c62d7a901a9b20c09dd936e7a \ + --hash=sha256:f245d039f72e6f802902375755846f5de1ee1e14c3e8736c078565599bcab621 \ + --hash=sha256:ff115ef91c0eeac69cd92daeba36a9d8e14daee445b504eeea2b1c0b55821984 + # via datetime + +# The following packages are considered to be unsafe in a requirements file: +setuptools==75.1.0 \ + --hash=sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2 \ + --hash=sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538 + # via zope-interface diff --git a/yaku-apps-python/BUILD.bazel b/yaku-apps-python/BUILD.bazel new file mode 100644 index 00000000..83b60645 --- /dev/null +++ b/yaku-apps-python/BUILD.bazel @@ -0,0 +1,10 @@ +# exclude because on MAC, if I update the requirements lock file, this test fails + +#load("@rules_python//python:pip.bzl", "compile_pip_requirements") +# +#compile_pip_requirements( +# # base name for generated targets, typically "requirements" +# name = "requirements", +# requirements_in = ":3rdparty/requirements.txt", +# requirements_txt = ":3rdparty/requirements_lock.txt", +#) diff --git a/yaku-apps-python/README.md b/yaku-apps-python/README.md index d31a9cb4..10bf4434 100644 --- a/yaku-apps-python/README.md +++ b/yaku-apps-python/README.md @@ -1 +1,93 @@ -# yaku-apps-python \ No newline at end of file +# yaku-apps-python + +## What works + +* Running apps: e.g., `bazel run //yaku-apps-python/apps/$APPNAME` +* Running tests: e.g., `bazel test //yaku-apps-python/apps/$APPNAME:test` +* Running tests for the binaries: e.g., `bazel test //yaku-apps-python/apps/$APPNAME:test-bin` +* Building binaries: e.g., `bazel build //yaku-apps-python/apps/$APPNAME` +* Building wheel for `autopilot-utils`, see below +* Running tests with coverage, see below. + +## What doesn't work + +* We don't have PEX binaries, e.g. for `papsr`, yet. However it is possible to call pex inside a gen_rule. +* The "binary" build for `papsr` contains too many libraries, not just the given dependencies. + Likely, bazel just links the whole Python dependency set. +* Splunk, SharePoint, and SharePoint-Fetcher integration tests are not configured yet. +* Formatting/linting the code: There are three options: + 1. Just run `uvx ruff format yaku-apps-python` (uses `uv` to install the tool) + 2. or use + 3. or use Aspect rules: +* How can we debug with VS Code or other remote debugging tools? + +## Development instructions + +### Requirements files + +To update the Python requirements files: + +```bash +bazel run //yaku-apps-python:requirements.update +``` + +### Tests and coverage reports + +To run tests: + +```bash +bazel test //yaku-apps-python/... +``` + +To run tests with coverage: + +```bash +bazel coverage //yaku-apps-python/... --combined_report=lcov +``` + +And then you can convert the collected lcov files to html: + +```bash +genhtml --ignore-errors mismatch --output htmlcov "$(bazel info output_path)/_coverage/_coverage_report.dat" +``` + +You will find the coverage report in <../htmlcov/index.html>. + +### Autopilot Utils Wheel + +```bash +bazel build //yaku-apps-python/packages/autopilot-utils:wheel +``` + +## IDE Integration (VS Code) + +You need to tell the IDE where all the different Python packages are. This can be done in the `pyproject.toml` in +the root workspace folder. + +Just add the paths received from `find yaku-apps-python -type d -name src` to the `extraPaths` list in the `pyproject.toml`. + +## Extra virtual environment for VS Code auto-completion and linting + +As VS Code doesn't know about the Python dependencies declared in Bazel for the different packages, +we can instead create a virtual environment with all the dependencies installed. + +Then, we can point VS Code to this virtual environment for auto-completion and linting. + +```bash +cd yaku-apps-python/3rdparty/ +python3 -m venv .venv +.venv/bin/pip install -r requirements_lock.txt +``` + +Then, in VS Code, you can select this virtual environment as the Python interpreter. + +If you have `uv` installed, you can do this of course with `uv`: + +```bash +cd yaku-apps-python/3rdparty/ +uv venv create +uv pip install -r requirements_lock.txt +``` + +Note: I got a build error in the pip install step, due to some build issue with pyyaml 6.0. +Perhaps other (older) versions of pyyaml work better. diff --git a/yaku-apps-python/apps/artifactory-fetcher/BUILD b/yaku-apps-python/apps/artifactory-fetcher/BUILD deleted file mode 100644 index 445498e6..00000000 --- a/yaku-apps-python/apps/artifactory-fetcher/BUILD +++ /dev/null @@ -1,4 +0,0 @@ -pex_binary( - name="artifactory-fetcher", - entry_point="yaku.artifactory_fetcher.cli", -) diff --git a/yaku-apps-python/apps/artifactory-fetcher/BUILD.bazel b/yaku-apps-python/apps/artifactory-fetcher/BUILD.bazel new file mode 100644 index 00000000..fbc8642c --- /dev/null +++ b/yaku-apps-python/apps/artifactory-fetcher/BUILD.bazel @@ -0,0 +1,31 @@ +load("@aspect_rules_py//py:defs.bzl", "py_binary") +load("@rules_python//python:packaging.bzl", "py_package", "py_wheel") +load("//tools/pytest-runner:defs.bzl", "py_pytest_test") + +py_binary( + name = "artifactory-fetcher", + srcs = glob(["src/**/*.py"]) + ["src/yaku/artifactory_fetcher/_version.txt"], + imports = ["src"], + main = "src/yaku/artifactory_fetcher/__main__.py", + deps = [ + "//yaku-apps-python/packages/autopilot-utils:lib", + ], +) + +py_pytest_test( + name = "test", + srcs = glob(["tests/*.py"]), + deps = [ + ":artifactory-fetcher", + "@pip//mock", + "@pip//pytest_mock", + ], +) + +py_pytest_test( + name = "test-bin", + srcs = glob(["tests-pex/*.py"]), + deps = [ + ":artifactory-fetcher", + ], +) diff --git a/yaku-apps-python/apps/artifactory-fetcher/src/yaku/artifactory_fetcher/BUILD b/yaku-apps-python/apps/artifactory-fetcher/src/yaku/artifactory_fetcher/BUILD deleted file mode 100644 index b2a13697..00000000 --- a/yaku-apps-python/apps/artifactory-fetcher/src/yaku/artifactory_fetcher/BUILD +++ /dev/null @@ -1,6 +0,0 @@ -python_sources(name="app", dependencies=[":version"]) - -resource( - name="version", - source="_version.txt", -) diff --git a/yaku-apps-python/apps/artifactory-fetcher/src/yaku/artifactory_fetcher/__main__.py b/yaku-apps-python/apps/artifactory-fetcher/src/yaku/artifactory_fetcher/__main__.py new file mode 100644 index 00000000..76e5db64 --- /dev/null +++ b/yaku-apps-python/apps/artifactory-fetcher/src/yaku/artifactory_fetcher/__main__.py @@ -0,0 +1,4 @@ +from yaku.artifactory_fetcher.cli import main + +if __name__ == "__main__": + main() diff --git a/yaku-apps-python/apps/artifactory-fetcher/src/yaku/artifactory_fetcher/cli.py b/yaku-apps-python/apps/artifactory-fetcher/src/yaku/artifactory_fetcher/cli.py index 059ccd25..7de52b0f 100644 --- a/yaku-apps-python/apps/artifactory-fetcher/src/yaku/artifactory_fetcher/cli.py +++ b/yaku-apps-python/apps/artifactory-fetcher/src/yaku/artifactory_fetcher/cli.py @@ -63,6 +63,3 @@ def click_command(username: str, password: str, url: str, path: str, repository: provider=CLI, version_callback=read_version_from_package(__package__), ) - -if __name__ == "__main__": - main() diff --git a/yaku-apps-python/apps/artifactory-fetcher/tests-pex/BUILD b/yaku-apps-python/apps/artifactory-fetcher/tests-pex/BUILD deleted file mode 100644 index 98b98287..00000000 --- a/yaku-apps-python/apps/artifactory-fetcher/tests-pex/BUILD +++ /dev/null @@ -1,13 +0,0 @@ -APP = "artifactory-fetcher" -A_PP = APP.replace("-", "_") - -python_tests( - dependencies=[ - f"apps/{APP}/src/yaku/{A_PP}:app", - ], - runtime_package_dependencies=[ - f"apps/{APP}:{APP}", - ], - skip_bandit=True, - skip_mypy=True, -) diff --git a/yaku-apps-python/apps/artifactory-fetcher/tests-pex/test_pex.py b/yaku-apps-python/apps/artifactory-fetcher/tests-pex/test_pex.py index 44494499..f4fad468 100644 --- a/yaku-apps-python/apps/artifactory-fetcher/tests-pex/test_pex.py +++ b/yaku-apps-python/apps/artifactory-fetcher/tests-pex/test_pex.py @@ -8,7 +8,6 @@ def test_pex_version(): file_version = importlib.resources.read_text(f"yaku.{APP__NAME}", "_version.txt") output = subprocess.check_output( - [f"apps.{APP_NAME}/{APP_NAME}.pex", "--version"], encoding="utf-8" + [f"yaku-apps-python/apps/{APP_NAME}/{APP_NAME}", "--version"], encoding="utf-8" ) - assert output.strip() == file_version.strip() diff --git a/yaku-apps-python/apps/artifactory-fetcher/tests/BUILD b/yaku-apps-python/apps/artifactory-fetcher/tests/BUILD deleted file mode 100644 index 91bfe038..00000000 --- a/yaku-apps-python/apps/artifactory-fetcher/tests/BUILD +++ /dev/null @@ -1,7 +0,0 @@ -python_tests( - dependencies=[ - "3rdparty:reqs#pytest-mock", - ], - skip_bandit=True, - skip_mypy=True, -) diff --git a/yaku-apps-python/apps/excel-tools/BUILD b/yaku-apps-python/apps/excel-tools/BUILD deleted file mode 100644 index 25db6e0e..00000000 --- a/yaku-apps-python/apps/excel-tools/BUILD +++ /dev/null @@ -1,4 +0,0 @@ -pex_binary( - name="excel-tools", - entry_point="yaku.excel_tools.cli", -) diff --git a/yaku-apps-python/apps/excel-tools/BUILD.bazel b/yaku-apps-python/apps/excel-tools/BUILD.bazel new file mode 100644 index 00000000..682c99fa --- /dev/null +++ b/yaku-apps-python/apps/excel-tools/BUILD.bazel @@ -0,0 +1,35 @@ +load("@aspect_rules_py//py:defs.bzl", "py_binary") +load("@rules_python//python:packaging.bzl", "py_package", "py_wheel") +load("//tools/pytest-runner:defs.bzl", "py_pytest_test") + +PKG = "excel_tools" + +py_binary( + name = "excel-tools", + srcs = glob(["src/**/*.py"]) + ["src/yaku/" + PKG + "/_version.txt"], + imports = ["src"], + main = "src/yaku/" + PKG + "/__main__.py", + deps = [ + "//yaku-apps-python/packages/autopilot-utils:lib", + "@pip//openpyxl", + "@pip//pandas", + ], +) + +py_pytest_test( + name = "test", + srcs = glob(["tests/*.py"]), + deps = [ + ":excel-tools", + "@pip//mock", + "@pip//pytest_mock", + ], +) + +py_pytest_test( + name = "test-bin", + srcs = glob(["tests-pex/*.py"]), + deps = [ + ":excel-tools", + ], +) diff --git a/yaku-apps-python/apps/excel-tools/src/yaku/excel_tools/BUILD b/yaku-apps-python/apps/excel-tools/src/yaku/excel_tools/BUILD deleted file mode 100644 index b2a13697..00000000 --- a/yaku-apps-python/apps/excel-tools/src/yaku/excel_tools/BUILD +++ /dev/null @@ -1,6 +0,0 @@ -python_sources(name="app", dependencies=[":version"]) - -resource( - name="version", - source="_version.txt", -) diff --git a/yaku-apps-python/apps/excel-tools/src/yaku/excel_tools/__main__.py b/yaku-apps-python/apps/excel-tools/src/yaku/excel_tools/__main__.py new file mode 100644 index 00000000..f9a909db --- /dev/null +++ b/yaku-apps-python/apps/excel-tools/src/yaku/excel_tools/__main__.py @@ -0,0 +1,4 @@ +from yaku.excel_tools.cli import main + +if __name__ == "__main__": + main() diff --git a/yaku-apps-python/apps/excel-tools/src/yaku/excel_tools/cli.py b/yaku-apps-python/apps/excel-tools/src/yaku/excel_tools/cli.py index f2dd4956..b1916ea2 100644 --- a/yaku-apps-python/apps/excel-tools/src/yaku/excel_tools/cli.py +++ b/yaku-apps-python/apps/excel-tools/src/yaku/excel_tools/cli.py @@ -283,7 +283,3 @@ def add_empty_column(xlsx_path: str, column_name: str): workbook = load_workbook(xlsx_path) add_column_to_sheets(workbook, column_name, []) workbook.save(xlsx_path) - - -if __name__ == "__main__": - main() diff --git a/yaku-apps-python/apps/excel-tools/src/yaku/excel_tools/commands/BUILD b/yaku-apps-python/apps/excel-tools/src/yaku/excel_tools/commands/BUILD deleted file mode 100644 index db46e8d6..00000000 --- a/yaku-apps-python/apps/excel-tools/src/yaku/excel_tools/commands/BUILD +++ /dev/null @@ -1 +0,0 @@ -python_sources() diff --git a/yaku-apps-python/apps/excel-tools/src/yaku/excel_tools/commands/evaluate/BUILD b/yaku-apps-python/apps/excel-tools/src/yaku/excel_tools/commands/evaluate/BUILD deleted file mode 100644 index db46e8d6..00000000 --- a/yaku-apps-python/apps/excel-tools/src/yaku/excel_tools/commands/evaluate/BUILD +++ /dev/null @@ -1 +0,0 @@ -python_sources() diff --git a/yaku-apps-python/apps/excel-tools/src/yaku/excel_tools/utils/BUILD b/yaku-apps-python/apps/excel-tools/src/yaku/excel_tools/utils/BUILD deleted file mode 100644 index db46e8d6..00000000 --- a/yaku-apps-python/apps/excel-tools/src/yaku/excel_tools/utils/BUILD +++ /dev/null @@ -1 +0,0 @@ -python_sources() diff --git a/yaku-apps-python/apps/excel-tools/src/yaku/excel_tools/utils/vendored/BUILD b/yaku-apps-python/apps/excel-tools/src/yaku/excel_tools/utils/vendored/BUILD deleted file mode 100644 index db46e8d6..00000000 --- a/yaku-apps-python/apps/excel-tools/src/yaku/excel_tools/utils/vendored/BUILD +++ /dev/null @@ -1 +0,0 @@ -python_sources() diff --git a/yaku-apps-python/apps/excel-tools/tests-pex/BUILD b/yaku-apps-python/apps/excel-tools/tests-pex/BUILD deleted file mode 100644 index 1433c1e4..00000000 --- a/yaku-apps-python/apps/excel-tools/tests-pex/BUILD +++ /dev/null @@ -1,13 +0,0 @@ -APP = "excel-tools" -A_PP = APP.replace("-", "_") - -python_tests( - dependencies=[ - f"apps/{APP}/src/yaku/{A_PP}:app", - ], - runtime_package_dependencies=[ - f"apps/{APP}:{APP}", - ], - skip_bandit=True, - skip_mypy=True, -) diff --git a/yaku-apps-python/apps/excel-tools/tests-pex/test_pex.py b/yaku-apps-python/apps/excel-tools/tests-pex/test_pex.py index 113d8853..4821fa2e 100644 --- a/yaku-apps-python/apps/excel-tools/tests-pex/test_pex.py +++ b/yaku-apps-python/apps/excel-tools/tests-pex/test_pex.py @@ -8,7 +8,7 @@ def test_pex_version(): file_version = importlib.resources.read_text(f"yaku.{APP__NAME}", "_version.txt") output = subprocess.check_output( - [f"apps.{APP_NAME}/{APP_NAME}.pex", "--version"], encoding="utf-8" + [f"yaku-apps-python/apps/{APP_NAME}/{APP_NAME}", "--version"], encoding="utf-8" ) assert output.strip() == file_version.strip() diff --git a/yaku-apps-python/apps/excel-tools/tests/BUILD b/yaku-apps-python/apps/excel-tools/tests/BUILD deleted file mode 100644 index dedce2f2..00000000 --- a/yaku-apps-python/apps/excel-tools/tests/BUILD +++ /dev/null @@ -1 +0,0 @@ -python_tests(skip_bandit=True, skip_mypy=True) diff --git a/yaku-apps-python/apps/excel-tools/tests/evaluate/BUILD b/yaku-apps-python/apps/excel-tools/tests/evaluate/BUILD deleted file mode 100644 index a20aa7e0..00000000 --- a/yaku-apps-python/apps/excel-tools/tests/evaluate/BUILD +++ /dev/null @@ -1,12 +0,0 @@ -python_tests( - dependencies=[ - ":data", - ], - skip_bandit=True, - skip_mypy=True, -) - -files( - name="data", - sources=["data/**"], -) diff --git a/yaku-apps-python/apps/excel-tools/tests/utils/BUILD b/yaku-apps-python/apps/excel-tools/tests/utils/BUILD deleted file mode 100644 index a20aa7e0..00000000 --- a/yaku-apps-python/apps/excel-tools/tests/utils/BUILD +++ /dev/null @@ -1,12 +0,0 @@ -python_tests( - dependencies=[ - ":data", - ], - skip_bandit=True, - skip_mypy=True, -) - -files( - name="data", - sources=["data/**"], -) diff --git a/yaku-apps-python/apps/filecheck/BUILD b/yaku-apps-python/apps/filecheck/BUILD deleted file mode 100644 index b8120368..00000000 --- a/yaku-apps-python/apps/filecheck/BUILD +++ /dev/null @@ -1,4 +0,0 @@ -pex_binary( - name="filecheck", - entry_point="yaku.filecheck.cli", -) diff --git a/yaku-apps-python/apps/filecheck/BUILD.bazel b/yaku-apps-python/apps/filecheck/BUILD.bazel new file mode 100644 index 00000000..a7012275 --- /dev/null +++ b/yaku-apps-python/apps/filecheck/BUILD.bazel @@ -0,0 +1,35 @@ +load("@aspect_rules_py//py:defs.bzl", "py_binary") +load("@rules_python//python:packaging.bzl", "py_package", "py_wheel") +load("//tools/pytest-runner:defs.bzl", "py_pytest_test") + +NAME = "filecheck" + +PKG = "filecheck" + +py_binary( + name = NAME, + srcs = glob(["src/**/*.py"]) + ["src/yaku/" + PKG + "/_version.txt"], + imports = ["src"], + main = "src/yaku/" + PKG + "/__main__.py", + deps = [ + "//yaku-apps-python/packages/autopilot-utils:lib", + ], +) + +py_pytest_test( + name = "test", + srcs = glob(["tests/*.py"]), + deps = [ + ":" + NAME, + "@pip//mock", + "@pip//pytest_mock", + ], +) + +py_pytest_test( + name = "test-bin", + srcs = glob(["tests-pex/*.py"]), + deps = [ + ":" + NAME, + ], +) diff --git a/yaku-apps-python/apps/filecheck/src/yaku/filecheck/BUILD b/yaku-apps-python/apps/filecheck/src/yaku/filecheck/BUILD deleted file mode 100644 index b2a13697..00000000 --- a/yaku-apps-python/apps/filecheck/src/yaku/filecheck/BUILD +++ /dev/null @@ -1,6 +0,0 @@ -python_sources(name="app", dependencies=[":version"]) - -resource( - name="version", - source="_version.txt", -) diff --git a/yaku-apps-python/apps/filecheck/src/yaku/filecheck/__main__.py b/yaku-apps-python/apps/filecheck/src/yaku/filecheck/__main__.py new file mode 100644 index 00000000..0750f7a8 --- /dev/null +++ b/yaku-apps-python/apps/filecheck/src/yaku/filecheck/__main__.py @@ -0,0 +1,4 @@ +from yaku.filecheck.cli import main + +if __name__ == "__main__": + main() diff --git a/yaku-apps-python/apps/filecheck/src/yaku/filecheck/cli.py b/yaku-apps-python/apps/filecheck/src/yaku/filecheck/cli.py index 46cc3b87..47007762 100644 --- a/yaku-apps-python/apps/filecheck/src/yaku/filecheck/cli.py +++ b/yaku-apps-python/apps/filecheck/src/yaku/filecheck/cli.py @@ -24,7 +24,3 @@ def click_evaluator_callback(results: ResultsCollector): provider=CLI, version_callback=read_version_from_package(__package__), ) - - -if __name__ == "__main__": - main() diff --git a/yaku-apps-python/apps/filecheck/src/yaku/filecheck/commands/BUILD b/yaku-apps-python/apps/filecheck/src/yaku/filecheck/commands/BUILD deleted file mode 100644 index db46e8d6..00000000 --- a/yaku-apps-python/apps/filecheck/src/yaku/filecheck/commands/BUILD +++ /dev/null @@ -1 +0,0 @@ -python_sources() diff --git a/yaku-apps-python/apps/filecheck/tests-pex/BUILD b/yaku-apps-python/apps/filecheck/tests-pex/BUILD deleted file mode 100644 index 059036a1..00000000 --- a/yaku-apps-python/apps/filecheck/tests-pex/BUILD +++ /dev/null @@ -1,13 +0,0 @@ -APP = "filecheck" -A_PP = APP.replace("-", "_") - -python_tests( - dependencies=[ - f"apps/{APP}/src/yaku/{A_PP}:app", - ], - runtime_package_dependencies=[ - f"apps/{APP}:{APP}", - ], - skip_bandit=True, - skip_mypy=True, -) diff --git a/yaku-apps-python/apps/filecheck/tests-pex/test_pex.py b/yaku-apps-python/apps/filecheck/tests-pex/test_pex.py index 1e60b956..83069f40 100644 --- a/yaku-apps-python/apps/filecheck/tests-pex/test_pex.py +++ b/yaku-apps-python/apps/filecheck/tests-pex/test_pex.py @@ -8,7 +8,7 @@ def test_pex_version_flag(): file_version = importlib.resources.read_text(f"yaku.{APP__NAME}", "_version.txt") output = subprocess.check_output( - [f"apps.{APP_NAME}/{APP_NAME}.pex", "--version"], encoding="utf-8" + [f"yaku-apps-python/apps/{APP_NAME}/{APP_NAME}", "--version"], encoding="utf-8" ) assert output.strip() == file_version.strip() @@ -16,7 +16,7 @@ def test_pex_version_flag(): def test_pex_help_flag(): output = subprocess.check_output( - [f"apps.{APP_NAME}/{APP_NAME}.pex", "--help"], encoding="utf-8" + [f"yaku-apps-python/apps/{APP_NAME}/{APP_NAME}", "--help"], encoding="utf-8" ) assert output.strip().startswith("Usage: ") diff --git a/yaku-apps-python/apps/filecheck/tests/BUILD b/yaku-apps-python/apps/filecheck/tests/BUILD deleted file mode 100644 index 91bfe038..00000000 --- a/yaku-apps-python/apps/filecheck/tests/BUILD +++ /dev/null @@ -1,7 +0,0 @@ -python_tests( - dependencies=[ - "3rdparty:reqs#pytest-mock", - ], - skip_bandit=True, - skip_mypy=True, -) diff --git a/yaku-apps-python/apps/papsr/BUILD b/yaku-apps-python/apps/papsr/BUILD deleted file mode 100644 index b74eda96..00000000 --- a/yaku-apps-python/apps/papsr/BUILD +++ /dev/null @@ -1,4 +0,0 @@ -pex_binary( - name="papsr", - entry_point="yaku.papsr.cli", -) diff --git a/yaku-apps-python/apps/papsr/BUILD.bazel b/yaku-apps-python/apps/papsr/BUILD.bazel new file mode 100644 index 00000000..7bdebd4f --- /dev/null +++ b/yaku-apps-python/apps/papsr/BUILD.bazel @@ -0,0 +1,35 @@ +load("@aspect_rules_py//py:defs.bzl", "py_binary") +load("@rules_python//python:packaging.bzl", "py_package", "py_wheel") +load("//tools/pytest-runner:defs.bzl", "py_pytest_test") + +py_binary( + name = "papsr", + srcs = glob(["src/**/*.py"]) + ["src/yaku/papsr/_version.txt"], + imports = ["src"], + main = "src/yaku/papsr/cli.py", + deps = [ + "//yaku-apps-python/packages/autopilot-utils:lib", + "@pip//beautifulsoup4", + "@pip//jira", + "@pip//loguru", + "@pip//openpyxl", + "@pip//pandas", + "@pip//pydantic", + "@pip//pyhanko", + "@pip//pypdf", + "@pip//pytest", + "@pip//pyyaml", + "@pip//requests", + "@pip//xlrd", + ], +) + +py_pytest_test( + name = "test", + srcs = glob(["tests/*.py"]), + deps = [ + ":papsr", + "@pip//mock", + "@pip//pytest_mock", + ], +) diff --git a/yaku-apps-python/apps/papsr/src/yaku/papsr/BUILD b/yaku-apps-python/apps/papsr/src/yaku/papsr/BUILD deleted file mode 100644 index 633e1eaf..00000000 --- a/yaku-apps-python/apps/papsr/src/yaku/papsr/BUILD +++ /dev/null @@ -1,25 +0,0 @@ -python_sources( - name="app", - dependencies=[ - # if you modify this list of extra dependencies, make sure to update - # the list of builtin libraries in the user-documentation repo under - # source/autopilots/papsr/reference/builtin-libraries.md - ":version", - "3rdparty:reqs#openpyxl", - "3rdparty:reqs#pandas", - "3rdparty:reqs#pyhanko", - "3rdparty:reqs#pypdf", - "3rdparty:reqs#pyyaml", - "3rdparty:reqs#requests", - "3rdparty:reqs#xlrd", - "3rdparty:reqs#jira", - "3rdparty:reqs#beautifulsoup4", - "3rdparty:reqs#pytest", - "packages/autopilot-utils/src/yaku/autopilot_utils:lib", - ], -) - -resource( - name="version", - source="_version.txt", -) diff --git a/yaku-apps-python/apps/papsr/tests-pex/BUILD b/yaku-apps-python/apps/papsr/tests-pex/BUILD deleted file mode 100644 index 59c9b520..00000000 --- a/yaku-apps-python/apps/papsr/tests-pex/BUILD +++ /dev/null @@ -1,13 +0,0 @@ -APP = "papsr" -A_PP = APP.replace("-", "_") - -python_tests( - dependencies=[ - f"apps/{APP}/src/yaku/{A_PP}:app", - ], - runtime_package_dependencies=[ - f"apps/{APP}:{APP}", - ], - skip_bandit=True, - skip_mypy=True, -) diff --git a/yaku-apps-python/apps/papsr/tests-pex/test_pex.py b/yaku-apps-python/apps/papsr/tests-pex/test_pex.py deleted file mode 100644 index 0deb2bf1..00000000 --- a/yaku-apps-python/apps/papsr/tests-pex/test_pex.py +++ /dev/null @@ -1,47 +0,0 @@ -import importlib.resources -import subprocess -from pathlib import Path - -from yaku.autopilot_utils.results import assert_result_status -from yaku.papsr.cli import SAMPLE_CODE - -APP_NAME = "papsr" -APP__NAME = APP_NAME.replace("-", "_") - - -def test_pex_version(): - file_version = importlib.resources.read_text(f"yaku.{APP__NAME}", "_version.txt") - output = subprocess.check_output( - [f"apps.{APP_NAME}/{APP_NAME}.pex", "--version"], encoding="utf-8" - ) - - assert output.strip() == file_version.strip() - - -def test_pex_shows_usage_info_when_run_without_arguments(): - result = subprocess.run( - [f"apps.{APP_NAME}/{APP_NAME}.pex"], encoding="utf-8", capture_output=True - ) - - assert result.returncode != 0 - assert result.stderr.strip().startswith("Usage:") - - -def test_pex_can_load_and_execute_cli_from_module(tmp_path: Path): - sample_file = tmp_path / "sample_cli.py" - sample_file.write_text(SAMPLE_CODE) - - result = subprocess.run( - [f"apps.{APP_NAME}/{APP_NAME}.pex", str(sample_file)], - encoding="utf-8", - capture_output=True, - ) - - assert_result_status(result.stdout, "GREEN", reason="Fail flag was set to: False") - - result = subprocess.run( - [f"apps.{APP_NAME}/{APP_NAME}.pex", str(sample_file), "--fail"], - encoding="utf-8", - capture_output=True, - ) - assert_result_status(result.stdout, "RED", reason=".*But: Fail flag was set to: True") diff --git a/yaku-apps-python/apps/papsr/tests/BUILD b/yaku-apps-python/apps/papsr/tests/BUILD deleted file mode 100644 index 91bfe038..00000000 --- a/yaku-apps-python/apps/papsr/tests/BUILD +++ /dev/null @@ -1,7 +0,0 @@ -python_tests( - dependencies=[ - "3rdparty:reqs#pytest-mock", - ], - skip_bandit=True, - skip_mypy=True, -) diff --git a/yaku-apps-python/apps/papsr/tests/test_cli.py b/yaku-apps-python/apps/papsr/tests/test_cli.py index d905849e..e3bcd4c4 100644 --- a/yaku-apps-python/apps/papsr/tests/test_cli.py +++ b/yaku-apps-python/apps/papsr/tests/test_cli.py @@ -56,7 +56,7 @@ def test_load_cli_shows_error_if_file_does_not_exist(): load_cli(Path("non_existing_file.abcd")) -@pytest.mark.parametrize("module_name", ["test", "sys"]) +@pytest.mark.parametrize("module_name", ["os", "sys"]) def test_load_cli_shows_proper_import_error_in_case_of_module_name_conflicts( tmp_path: Path, module_name: str ): diff --git a/yaku-apps-python/apps/pdf-signature-evaluator/BUILD b/yaku-apps-python/apps/pdf-signature-evaluator/BUILD deleted file mode 100644 index b0f7ab9d..00000000 --- a/yaku-apps-python/apps/pdf-signature-evaluator/BUILD +++ /dev/null @@ -1,8 +0,0 @@ -pex_binary( - name="pdf-signature-evaluator", - entry_point="yaku.pdf_signature_evaluator.cli", -) - -poetry_requirements( - name="poetry", -) diff --git a/yaku-apps-python/apps/pdf-signature-evaluator/BUILD.bazel b/yaku-apps-python/apps/pdf-signature-evaluator/BUILD.bazel new file mode 100644 index 00000000..bf2918a7 --- /dev/null +++ b/yaku-apps-python/apps/pdf-signature-evaluator/BUILD.bazel @@ -0,0 +1,29 @@ +load("@aspect_rules_py//py:defs.bzl", "py_binary") +load("@rules_python//python:packaging.bzl", "py_package", "py_wheel") +load("//tools/pytest-runner:defs.bzl", "py_pytest_test") + +PKG = "pdf_signature_evaluator" + +py_binary( + name = "pdf-signature-evaluator", + srcs = glob(["src/**/*.py"]) + ["src/yaku/" + PKG + "/_version.txt"], + imports = ["src"], + main = "src/yaku/" + PKG + "/__main__.py", + deps = [ + "//yaku-apps-python/packages/autopilot-utils:lib", + "@pip//asn1crypto", + "@pip//pyhanko", + "@pip//pytz", + "@pip//pyyaml", + ], +) + +py_pytest_test( + name = "test", + srcs = glob(["tests/*.py"]), + deps = [ + ":pdf-signature-evaluator", + "@pip//mock", + "@pip//pytest_mock", + ], +) diff --git a/yaku-apps-python/apps/pdf-signature-evaluator/src/yaku/pdf_signature_evaluator/BUILD b/yaku-apps-python/apps/pdf-signature-evaluator/src/yaku/pdf_signature_evaluator/BUILD deleted file mode 100644 index b2a13697..00000000 --- a/yaku-apps-python/apps/pdf-signature-evaluator/src/yaku/pdf_signature_evaluator/BUILD +++ /dev/null @@ -1,6 +0,0 @@ -python_sources(name="app", dependencies=[":version"]) - -resource( - name="version", - source="_version.txt", -) diff --git a/yaku-apps-python/apps/pdf-signature-evaluator/src/yaku/pdf_signature_evaluator/__main__.py b/yaku-apps-python/apps/pdf-signature-evaluator/src/yaku/pdf_signature_evaluator/__main__.py new file mode 100644 index 00000000..adc631d9 --- /dev/null +++ b/yaku-apps-python/apps/pdf-signature-evaluator/src/yaku/pdf_signature_evaluator/__main__.py @@ -0,0 +1,4 @@ +from yaku.pdf_signature_evaluator.cli import main + +if __name__ == "__main__": + main() diff --git a/yaku-apps-python/apps/pdf-signature-evaluator/src/yaku/pdf_signature_evaluator/cli.py b/yaku-apps-python/apps/pdf-signature-evaluator/src/yaku/pdf_signature_evaluator/cli.py index 598dc091..68ab369a 100644 --- a/yaku-apps-python/apps/pdf-signature-evaluator/src/yaku/pdf_signature_evaluator/cli.py +++ b/yaku-apps-python/apps/pdf-signature-evaluator/src/yaku/pdf_signature_evaluator/cli.py @@ -6,7 +6,9 @@ from yaku.autopilot_utils.cli_base import make_autopilot_app, read_version_from_package from yaku.autopilot_utils.results import ResultsCollector -from .digital_signature_verification import digital_signature_verification +from .digital_signature_verification import ( + digital_signature_verification, +) class CLI: @@ -82,7 +84,4 @@ def click_evaluator_callback(results: ResultsCollector) -> tuple[str, str]: main = make_autopilot_app( provider=CLI, version_callback=read_version_from_package(__package__), -) - -if __name__ == "__main__": - main() +) \ No newline at end of file diff --git a/yaku-apps-python/apps/pdf-signature-evaluator/tests/BUILD b/yaku-apps-python/apps/pdf-signature-evaluator/tests/BUILD deleted file mode 100644 index 8aa08d13..00000000 --- a/yaku-apps-python/apps/pdf-signature-evaluator/tests/BUILD +++ /dev/null @@ -1,6 +0,0 @@ -python_tests(skip_bandit=True, skip_mypy=True, dependencies=[":expected_signers_yamls"]) - -files( - name="expected_signers_yamls", - sources=["fixtures/**"], -) diff --git a/yaku-apps-python/apps/pex-tool/BUILD b/yaku-apps-python/apps/pex-tool/BUILD deleted file mode 100644 index 2c5b4f61..00000000 --- a/yaku-apps-python/apps/pex-tool/BUILD +++ /dev/null @@ -1,4 +0,0 @@ -pex_binary( - name="pex-tool", - entry_point="yaku.pex_tool.cli", -) diff --git a/yaku-apps-python/apps/pex-tool/BUILD.bazel b/yaku-apps-python/apps/pex-tool/BUILD.bazel new file mode 100644 index 00000000..708ad783 --- /dev/null +++ b/yaku-apps-python/apps/pex-tool/BUILD.bazel @@ -0,0 +1,36 @@ +load("@aspect_rules_py//py:defs.bzl", "py_binary") +load("@rules_python//python:packaging.bzl", "py_package", "py_wheel") +load("//tools/pytest-runner:defs.bzl", "py_pytest_test") + +NAME = "pex-tool" + +PKG = "pex_tool" + +py_binary( + name = NAME, + srcs = glob(["src/**/*.py"]) + ["src/yaku/" + PKG + "/_version.txt"], + imports = ["src"], + main = "src/yaku/" + PKG + "/__main__.py", + deps = [ + "//yaku-apps-python/packages/autopilot-utils:lib", + "@pip//packaging", + ], +) + +py_pytest_test( + name = "test", + srcs = glob(["tests/*.py"]), + deps = [ + ":" + NAME, + "@pip//mock", + "@pip//pytest_mock", + ], +) + +py_pytest_test( + name = "test-bin", + srcs = glob(["tests-pex/*.py"]), + deps = [ + ":" + NAME, + ], +) diff --git a/yaku-apps-python/apps/pex-tool/src/yaku/pex_tool/BUILD b/yaku-apps-python/apps/pex-tool/src/yaku/pex_tool/BUILD deleted file mode 100644 index be055545..00000000 --- a/yaku-apps-python/apps/pex-tool/src/yaku/pex_tool/BUILD +++ /dev/null @@ -1,6 +0,0 @@ -python_sources(name="app", dependencies=[":version", "3rdparty:reqs#packaging"]) - -resource( - name="version", - source="_version.txt", -) diff --git a/yaku-apps-python/apps/pex-tool/src/yaku/pex_tool/__main__.py b/yaku-apps-python/apps/pex-tool/src/yaku/pex_tool/__main__.py new file mode 100644 index 00000000..3c414fb9 --- /dev/null +++ b/yaku-apps-python/apps/pex-tool/src/yaku/pex_tool/__main__.py @@ -0,0 +1,4 @@ +from yaku.pex_tool.cli import cli + +if __name__ == "__main__": + cli() diff --git a/yaku-apps-python/apps/pex-tool/src/yaku/pex_tool/cli.py b/yaku-apps-python/apps/pex-tool/src/yaku/pex_tool/cli.py index a0189916..a4a6f85f 100644 --- a/yaku-apps-python/apps/pex-tool/src/yaku/pex_tool/cli.py +++ b/yaku-apps-python/apps/pex-tool/src/yaku/pex_tool/cli.py @@ -14,6 +14,3 @@ class CLI: provider=CLI, version_callback=read_version_from_package(__package__), ) - -if __name__ == "__main__": - cli() diff --git a/yaku-apps-python/apps/pex-tool/src/yaku/pex_tool/commands/BUILD b/yaku-apps-python/apps/pex-tool/src/yaku/pex_tool/commands/BUILD deleted file mode 100644 index db46e8d6..00000000 --- a/yaku-apps-python/apps/pex-tool/src/yaku/pex_tool/commands/BUILD +++ /dev/null @@ -1 +0,0 @@ -python_sources() diff --git a/yaku-apps-python/apps/pex-tool/src/yaku/pex_tool/utils/BUILD b/yaku-apps-python/apps/pex-tool/src/yaku/pex_tool/utils/BUILD deleted file mode 100644 index db46e8d6..00000000 --- a/yaku-apps-python/apps/pex-tool/src/yaku/pex_tool/utils/BUILD +++ /dev/null @@ -1 +0,0 @@ -python_sources() diff --git a/yaku-apps-python/apps/pex-tool/tests-pex/BUILD b/yaku-apps-python/apps/pex-tool/tests-pex/BUILD deleted file mode 100644 index 627922a2..00000000 --- a/yaku-apps-python/apps/pex-tool/tests-pex/BUILD +++ /dev/null @@ -1,10 +0,0 @@ -python_tests( - dependencies=[ - f"apps/pex-tool:pex-tool", - ], - runtime_package_dependencies=[ - f"apps/pex-tool:pex-tool", - ], - skip_bandit=True, - skip_mypy=True, -) diff --git a/yaku-apps-python/apps/pex-tool/tests-pex/test_common_flags.py b/yaku-apps-python/apps/pex-tool/tests-pex/test_common_flags.py index 5d8c6f89..9a667299 100644 --- a/yaku-apps-python/apps/pex-tool/tests-pex/test_common_flags.py +++ b/yaku-apps-python/apps/pex-tool/tests-pex/test_common_flags.py @@ -5,19 +5,10 @@ APP__NAME = APP_NAME.replace("-", "_") -def test_pex_version(): - file_version = importlib.resources.read_text(f"yaku.{APP__NAME}", "_version.txt") - output = subprocess.check_output( - [f"apps.{APP_NAME}/{APP_NAME}.pex", "--version"], encoding="utf-8" - ) - - assert output.strip() == file_version.strip() - - def test_pex_version_flag(): file_version = importlib.resources.read_text(f"yaku.{APP__NAME}", "_version.txt") output = subprocess.check_output( - [f"apps.{APP_NAME}/{APP_NAME}.pex", "--version"], encoding="utf-8" + [f"yaku-apps-python/apps/{APP_NAME}/{APP_NAME}", "--version"], encoding="utf-8" ) assert output.strip() == file_version.strip() @@ -25,7 +16,7 @@ def test_pex_version_flag(): def test_pex_help_flag(): output = subprocess.check_output( - [f"apps.{APP_NAME}/{APP_NAME}.pex", "--help"], encoding="utf-8" + [f"yaku-apps-python/apps/{APP_NAME}/{APP_NAME}", "--help"], encoding="utf-8" ) assert output.strip().startswith("Usage: ") diff --git a/yaku-apps-python/apps/pex-tool/tests/BUILD b/yaku-apps-python/apps/pex-tool/tests/BUILD deleted file mode 100644 index 0778ce1b..00000000 --- a/yaku-apps-python/apps/pex-tool/tests/BUILD +++ /dev/null @@ -1,4 +0,0 @@ -python_tests( - skip_bandit=True, - skip_mypy=True, -) diff --git a/yaku-apps-python/apps/security-scanner/BUILD b/yaku-apps-python/apps/security-scanner/BUILD deleted file mode 100644 index a810bb90..00000000 --- a/yaku-apps-python/apps/security-scanner/BUILD +++ /dev/null @@ -1,4 +0,0 @@ -pex_binary( - name="security-scanner", - entry_point="yaku.security_scanner.cli", -) diff --git a/yaku-apps-python/apps/security-scanner/BUILD.bazel b/yaku-apps-python/apps/security-scanner/BUILD.bazel new file mode 100644 index 00000000..11372608 --- /dev/null +++ b/yaku-apps-python/apps/security-scanner/BUILD.bazel @@ -0,0 +1,29 @@ +load("@aspect_rules_py//py:defs.bzl", "py_binary") +load("@rules_python//python:packaging.bzl", "py_package", "py_wheel") +load("//tools/pytest-runner:defs.bzl", "py_pytest_test") + +NAME = "security-scanner" + +PKG = "security_scanner" + +py_binary( + name = NAME, + srcs = glob(["src/**/*.py"]) + ["src/yaku/" + PKG + "/_version.txt"], + imports = ["src"], + main = "src/yaku/" + PKG + "/__main__.py", + deps = [ + "//yaku-apps-python/packages/autopilot-utils:lib", + "@pip//asn1crypto", + "@pip//pyhanko", + "@pip//pytz", + "@pip//pyyaml", + ], +) + +py_pytest_test( + name = "test-bin", + srcs = glob(["tests-pex/*.py"]), + deps = [ + ":" + NAME, + ], +) diff --git a/yaku-apps-python/apps/security-scanner/src/yaku/security_scanner/BUILD b/yaku-apps-python/apps/security-scanner/src/yaku/security_scanner/BUILD deleted file mode 100644 index d967a52c..00000000 --- a/yaku-apps-python/apps/security-scanner/src/yaku/security_scanner/BUILD +++ /dev/null @@ -1,9 +0,0 @@ -python_sources( - name="app", - dependencies=[":version"], -) - -resource( - name="version", - source="_version.txt", -) diff --git a/yaku-apps-python/apps/security-scanner/src/yaku/security_scanner/__main__.py b/yaku-apps-python/apps/security-scanner/src/yaku/security_scanner/__main__.py new file mode 100644 index 00000000..7c0c4d97 --- /dev/null +++ b/yaku-apps-python/apps/security-scanner/src/yaku/security_scanner/__main__.py @@ -0,0 +1,4 @@ +from yaku.security_scanner.cli import main + +if __name__ == "__main__": + main() diff --git a/yaku-apps-python/apps/security-scanner/src/yaku/security_scanner/cli.py b/yaku-apps-python/apps/security-scanner/src/yaku/security_scanner/cli.py index a3aa9e99..3d2d5653 100644 --- a/yaku-apps-python/apps/security-scanner/src/yaku/security_scanner/cli.py +++ b/yaku-apps-python/apps/security-scanner/src/yaku/security_scanner/cli.py @@ -2,8 +2,8 @@ from yaku.autopilot_utils.cli_base import make_autopilot_app, read_version_from_package from yaku.autopilot_utils.results import ResultsCollector -from yaku.security_scanner.config import load_configuration -from yaku.security_scanner.scanner import SecurityScanner +from .config import load_configuration +from .scanner import SecurityScanner class CLI: @@ -43,7 +43,3 @@ def click_evaluator_callback(cls, results: ResultsCollector) -> Tuple[str, str]: provider=CLI, version_callback=read_version_from_package(__package__), ) - - -if __name__ == "__main__": - main() diff --git a/yaku-apps-python/apps/security-scanner/tests-pex/BUILD b/yaku-apps-python/apps/security-scanner/tests-pex/BUILD deleted file mode 100644 index 9597774b..00000000 --- a/yaku-apps-python/apps/security-scanner/tests-pex/BUILD +++ /dev/null @@ -1,13 +0,0 @@ -APP = "security-scanner" -A_PP = APP.replace("-", "_") - -python_tests( - dependencies=[ - f"apps/{APP}/src/yaku/{A_PP}:app", - ], - runtime_package_dependencies=[ - f"apps/{APP}:{APP}", - ], - skip_bandit=True, - skip_mypy=True, -) diff --git a/yaku-apps-python/apps/security-scanner/tests-pex/test_pex.py b/yaku-apps-python/apps/security-scanner/tests-pex/test_pex.py index 8c1f7a15..79c4df95 100644 --- a/yaku-apps-python/apps/security-scanner/tests-pex/test_pex.py +++ b/yaku-apps-python/apps/security-scanner/tests-pex/test_pex.py @@ -8,7 +8,7 @@ def test_pex_version_flag(): file_version = importlib.resources.read_text(f"yaku.{APP__NAME}", "_version.txt") output = subprocess.check_output( - [f"apps.{APP_NAME}/{APP_NAME}.pex", "--version"], encoding="utf-8" + [f"yaku-apps-python/apps/{APP_NAME}/{APP_NAME}", "--version"], encoding="utf-8" ) assert output.strip() == file_version.strip() @@ -16,7 +16,7 @@ def test_pex_version_flag(): def test_pex_help_flag(): output = subprocess.check_output( - [f"apps.{APP_NAME}/{APP_NAME}.pex", "--help"], encoding="utf-8" + [f"yaku-apps-python/apps/{APP_NAME}/{APP_NAME}", "--help"], encoding="utf-8" ) assert output.strip().startswith("Usage: ") diff --git a/yaku-apps-python/apps/sharepoint-evaluator/BUILD b/yaku-apps-python/apps/sharepoint-evaluator/BUILD deleted file mode 100644 index 1f2668ff..00000000 --- a/yaku-apps-python/apps/sharepoint-evaluator/BUILD +++ /dev/null @@ -1,4 +0,0 @@ -pex_binary( - name="sharepoint-evaluator", - entry_point="yaku.sharepoint_evaluator.cli", -) diff --git a/yaku-apps-python/apps/sharepoint-evaluator/BUILD.bazel b/yaku-apps-python/apps/sharepoint-evaluator/BUILD.bazel new file mode 100644 index 00000000..06784616 --- /dev/null +++ b/yaku-apps-python/apps/sharepoint-evaluator/BUILD.bazel @@ -0,0 +1,40 @@ +load("@aspect_rules_py//py:defs.bzl", "py_binary") +load("@rules_python//python:packaging.bzl", "py_package", "py_wheel") +load("//tools/pytest-runner:defs.bzl", "py_pytest_test") + +NAME = "sharepoint-evaluator" + +PKG = "sharepoint_evaluator" + +py_binary( + name = NAME, + srcs = glob(["src/**/*.py"]) + ["src/yaku/" + PKG + "/_version.txt"], + imports = ["src"], + main = "src/yaku/" + PKG + "/__main__.py", + deps = [ + "//yaku-apps-python/packages/autopilot-utils:lib", + "@pip//click", + "@pip//pydantic", + "@pip//pytz", + "@pip//pyyaml", + ], +) + +py_pytest_test( + name = "test", + srcs = glob(["tests/*.py"]), + data = glob(["tests/data/**"]), + deps = [ + ":" + NAME, + "@pip//mock", + "@pip//pytest_mock", + ], +) + +py_pytest_test( + name = "test-bin", + srcs = glob(["tests-pex/*.py"]), + deps = [ + ":" + NAME, + ], +) diff --git a/yaku-apps-python/apps/sharepoint-evaluator/src/yaku/sharepoint_evaluator/BUILD b/yaku-apps-python/apps/sharepoint-evaluator/src/yaku/sharepoint_evaluator/BUILD deleted file mode 100644 index b2a13697..00000000 --- a/yaku-apps-python/apps/sharepoint-evaluator/src/yaku/sharepoint_evaluator/BUILD +++ /dev/null @@ -1,6 +0,0 @@ -python_sources(name="app", dependencies=[":version"]) - -resource( - name="version", - source="_version.txt", -) diff --git a/yaku-apps-python/apps/sharepoint-evaluator/src/yaku/sharepoint_evaluator/__main__.py b/yaku-apps-python/apps/sharepoint-evaluator/src/yaku/sharepoint_evaluator/__main__.py new file mode 100644 index 00000000..893aae44 --- /dev/null +++ b/yaku-apps-python/apps/sharepoint-evaluator/src/yaku/sharepoint_evaluator/__main__.py @@ -0,0 +1,4 @@ +from yaku.sharepoint_evaluator.cli import main + +if __name__ == "__main__": + main() diff --git a/yaku-apps-python/apps/sharepoint-evaluator/src/yaku/sharepoint_evaluator/cli.py b/yaku-apps-python/apps/sharepoint-evaluator/src/yaku/sharepoint_evaluator/cli.py index 5c3e74e8..107c7e71 100644 --- a/yaku-apps-python/apps/sharepoint-evaluator/src/yaku/sharepoint_evaluator/cli.py +++ b/yaku-apps-python/apps/sharepoint-evaluator/src/yaku/sharepoint_evaluator/cli.py @@ -184,6 +184,3 @@ def sharepoint_evaluator(settings: Settings, config_file: ConfigFile): provider=CLI, version_callback=read_version_from_package(__package__), ) - -if __name__ == "__main__": - main() diff --git a/yaku-apps-python/apps/sharepoint-evaluator/tests-pex/BUILD b/yaku-apps-python/apps/sharepoint-evaluator/tests-pex/BUILD deleted file mode 100644 index 495a52e4..00000000 --- a/yaku-apps-python/apps/sharepoint-evaluator/tests-pex/BUILD +++ /dev/null @@ -1,13 +0,0 @@ -APP = "sharepoint-evaluator" -A_PP = APP.replace("-", "_") - -python_tests( - dependencies=[ - f"apps/{APP}/src/yaku/{A_PP}:app", - ], - runtime_package_dependencies=[ - f"apps/{APP}:{APP}", - ], - skip_bandit=True, - skip_mypy=True, -) diff --git a/yaku-apps-python/apps/sharepoint-evaluator/tests-pex/test_pex.py b/yaku-apps-python/apps/sharepoint-evaluator/tests-pex/test_pex.py index 44173f6b..87880e6d 100644 --- a/yaku-apps-python/apps/sharepoint-evaluator/tests-pex/test_pex.py +++ b/yaku-apps-python/apps/sharepoint-evaluator/tests-pex/test_pex.py @@ -8,7 +8,7 @@ def test_pex_version(): file_version = importlib.resources.read_text(f"yaku.{APP__NAME}", "_version.txt") output = subprocess.check_output( - [f"apps.{APP_NAME}/{APP_NAME}.pex", "--version"], encoding="utf-8" + [f"yaku-apps-python/apps/{APP_NAME}/{APP_NAME}", "--version"], encoding="utf-8" ) assert output.strip() == file_version.strip() diff --git a/yaku-apps-python/apps/sharepoint-evaluator/tests/BUILD b/yaku-apps-python/apps/sharepoint-evaluator/tests/BUILD deleted file mode 100644 index ee3211fe..00000000 --- a/yaku-apps-python/apps/sharepoint-evaluator/tests/BUILD +++ /dev/null @@ -1,14 +0,0 @@ -python_tests( - dependencies=[ - "3rdparty:reqs#pytest-mock", - "3rdparty:reqs#pydantic", - ":data", - ], - skip_bandit=True, - skip_mypy=True, -) - -files( - name="data", - sources=["data/**"], -) diff --git a/yaku-apps-python/apps/sharepoint-fetcher/BUILD b/yaku-apps-python/apps/sharepoint-fetcher/BUILD deleted file mode 100644 index ae8c37d3..00000000 --- a/yaku-apps-python/apps/sharepoint-fetcher/BUILD +++ /dev/null @@ -1,4 +0,0 @@ -pex_binary( - name="sharepoint-fetcher", - entry_point="yaku.sharepoint_fetcher.cli", -) diff --git a/yaku-apps-python/apps/sharepoint-fetcher/BUILD.bazel b/yaku-apps-python/apps/sharepoint-fetcher/BUILD.bazel new file mode 100644 index 00000000..2d104c61 --- /dev/null +++ b/yaku-apps-python/apps/sharepoint-fetcher/BUILD.bazel @@ -0,0 +1,33 @@ +load("@aspect_rules_py//py:defs.bzl", "py_binary") +load("@rules_python//python:packaging.bzl", "py_package", "py_wheel") +load("//tools/pytest-runner:defs.bzl", "py_pytest_test") + +PKG = "sharepoint_fetcher" + +py_binary( + name = "sharepoint-fetcher", + srcs = glob(["src/**/*.py"]) + ["src/yaku/" + PKG + "/_version.txt"], + imports = ["src"], + main = "src/yaku/" + PKG + "/cli.py", + deps = [ + "//yaku-apps-python/packages/autopilot-utils:lib", + "@pip//click", + "@pip//pydantic", + "@pip//pytz", + "@pip//pyyaml", + "@pip//requests", + "@pip//requests_ntlm", + ], +) + +py_pytest_test( + name = "test", + srcs = glob(["tests/*.py"]), + data = glob(["tests/data/**"]), + deps = [ + ":sharepoint-fetcher", + "@pip//mock", + "@pip//pytest_mock", + "@pip//requests_mock", + ], +) diff --git a/yaku-apps-python/apps/sharepoint-fetcher/src/yaku/sharepoint_fetcher/BUILD b/yaku-apps-python/apps/sharepoint-fetcher/src/yaku/sharepoint_fetcher/BUILD deleted file mode 100644 index 770410ba..00000000 --- a/yaku-apps-python/apps/sharepoint-fetcher/src/yaku/sharepoint_fetcher/BUILD +++ /dev/null @@ -1,13 +0,0 @@ -python_sources( - name="app", - dependencies=[ - ":version", - "3rdparty:reqs#requests", - ], -) - - -resource( - name="version", - source="_version.txt", -) diff --git a/yaku-apps-python/apps/sharepoint-fetcher/src/yaku/sharepoint_fetcher/cloud/BUILD b/yaku-apps-python/apps/sharepoint-fetcher/src/yaku/sharepoint_fetcher/cloud/BUILD deleted file mode 100644 index db46e8d6..00000000 --- a/yaku-apps-python/apps/sharepoint-fetcher/src/yaku/sharepoint_fetcher/cloud/BUILD +++ /dev/null @@ -1 +0,0 @@ -python_sources() diff --git a/yaku-apps-python/apps/sharepoint-fetcher/src/yaku/sharepoint_fetcher/on_premise/BUILD b/yaku-apps-python/apps/sharepoint-fetcher/src/yaku/sharepoint_fetcher/on_premise/BUILD deleted file mode 100644 index db46e8d6..00000000 --- a/yaku-apps-python/apps/sharepoint-fetcher/src/yaku/sharepoint_fetcher/on_premise/BUILD +++ /dev/null @@ -1 +0,0 @@ -python_sources() diff --git a/yaku-apps-python/apps/sharepoint-fetcher/tests-integration/BUILD b/yaku-apps-python/apps/sharepoint-fetcher/tests-integration/BUILD deleted file mode 100644 index 327e0626..00000000 --- a/yaku-apps-python/apps/sharepoint-fetcher/tests-integration/BUILD +++ /dev/null @@ -1,14 +0,0 @@ -APP = "sharepoint-fetcher" -A_PP = APP.replace("-", "_") - -python_tests( - dependencies=[ - f"apps/{APP}/src/yaku/{A_PP}:app", - ], - runtime_package_dependencies=[ - f"apps/{APP}:{APP}", - ], - skip_bandit=True, - skip_mypy=True, - tags=["integration"], -) diff --git a/yaku-apps-python/apps/sharepoint-fetcher/tests-pex/BUILD b/yaku-apps-python/apps/sharepoint-fetcher/tests-pex/BUILD deleted file mode 100644 index 9165b0f8..00000000 --- a/yaku-apps-python/apps/sharepoint-fetcher/tests-pex/BUILD +++ /dev/null @@ -1,13 +0,0 @@ -APP = "sharepoint-fetcher" -A_PP = APP.replace("-", "_") - -python_tests( - dependencies=[ - f"apps/{APP}/src/yaku/{A_PP}:app", - ], - runtime_package_dependencies=[ - f"apps/{APP}:{APP}", - ], - skip_bandit=True, - skip_mypy=True, -) diff --git a/yaku-apps-python/apps/sharepoint-fetcher/tests-pex/test_pex.py b/yaku-apps-python/apps/sharepoint-fetcher/tests-pex/test_pex.py index e611e66f..7c82a73e 100644 --- a/yaku-apps-python/apps/sharepoint-fetcher/tests-pex/test_pex.py +++ b/yaku-apps-python/apps/sharepoint-fetcher/tests-pex/test_pex.py @@ -8,7 +8,7 @@ def test_pex_version(): file_version = importlib.resources.read_text(f"yaku.{APP__NAME}", "_version.txt") output = subprocess.check_output( - [f"apps.{APP_NAME}/{APP_NAME}.pex", "--version"], encoding="utf-8" + [f"yaku-apps-python/apps/{APP_NAME}/{APP_NAME}", "--version"], encoding="utf-8" ) assert output.strip() == file_version.strip() diff --git a/yaku-apps-python/apps/sharepoint-fetcher/tests/BUILD b/yaku-apps-python/apps/sharepoint-fetcher/tests/BUILD deleted file mode 100644 index 913b5652..00000000 --- a/yaku-apps-python/apps/sharepoint-fetcher/tests/BUILD +++ /dev/null @@ -1,14 +0,0 @@ -python_tests( - dependencies=[ - "3rdparty:reqs#pytest-mock", - "3rdparty:reqs#requests-mock", - ], - skip_bandit=True, - skip_mypy=True, -) - - -python_test_utils( - name="test_utils", - skip_mypy=True, -) diff --git a/yaku-apps-python/apps/sharepoint/BUILD b/yaku-apps-python/apps/sharepoint/BUILD deleted file mode 100644 index 791c6812..00000000 --- a/yaku-apps-python/apps/sharepoint/BUILD +++ /dev/null @@ -1,4 +0,0 @@ -pex_binary( - name="sharepoint", - entry_point="yaku.sharepoint.cli", -) diff --git a/yaku-apps-python/apps/sharepoint/BUILD.bazel b/yaku-apps-python/apps/sharepoint/BUILD.bazel new file mode 100644 index 00000000..ebcff664 --- /dev/null +++ b/yaku-apps-python/apps/sharepoint/BUILD.bazel @@ -0,0 +1,40 @@ +load("@aspect_rules_py//py:defs.bzl", "py_binary") +load("@rules_python//python:packaging.bzl", "py_package", "py_wheel") +load("//tools/pytest-runner:defs.bzl", "py_pytest_test") + +NAME = "sharepoint" + +PKG = "sharepoint" + +py_binary( + name = NAME, + srcs = glob(["src/**/*.py"]) + ["src/yaku/" + PKG + "/_version.txt"], + imports = ["src"], + main = "src/yaku/" + PKG + "/__main__.py", + deps = [ + "//yaku-apps-python/packages/autopilot-utils:lib", + "@pip//click", + "@pip//pydantic", + "@pip//requests", + "@pip//requests_ntlm", + ], +) + +py_pytest_test( + name = "test", + srcs = glob(["tests/*.py"]), + deps = [ + ":" + NAME, + "@pip//mock", + "@pip//pytest_mock", + "@pip//requests_mock", + ], +) + +py_pytest_test( + name = "test-bin", + srcs = glob(["tests-pex/*.py"]), + deps = [ + ":" + NAME, + ], +) diff --git a/yaku-apps-python/apps/sharepoint/src/yaku/sharepoint/BUILD b/yaku-apps-python/apps/sharepoint/src/yaku/sharepoint/BUILD deleted file mode 100644 index 792ac3e1..00000000 --- a/yaku-apps-python/apps/sharepoint/src/yaku/sharepoint/BUILD +++ /dev/null @@ -1,12 +0,0 @@ -python_sources( - name="app", - dependencies=[ - ":version", - "3rdparty:reqs#requests", - ], -) - -resource( - name="version", - source="_version.txt", -) diff --git a/yaku-apps-python/apps/sharepoint/src/yaku/sharepoint/__main__.py b/yaku-apps-python/apps/sharepoint/src/yaku/sharepoint/__main__.py new file mode 100644 index 00000000..aaa2279e --- /dev/null +++ b/yaku-apps-python/apps/sharepoint/src/yaku/sharepoint/__main__.py @@ -0,0 +1,4 @@ +from yaku.sharepoint.cli import main + +if __name__ == "__main__": + main() diff --git a/yaku-apps-python/apps/sharepoint/src/yaku/sharepoint/cli.py b/yaku-apps-python/apps/sharepoint/src/yaku/sharepoint/cli.py index bdfc28a3..e0a5bfd6 100644 --- a/yaku-apps-python/apps/sharepoint/src/yaku/sharepoint/cli.py +++ b/yaku-apps-python/apps/sharepoint/src/yaku/sharepoint/cli.py @@ -85,7 +85,7 @@ def upload_folder(folder: str, sharepoint_path: str, force: bool, colors: bool): upload_directory_command(folder, Settings(), sharepoint_path, force) -if __name__ == "__main__": +def main(): log_level = os.getenv("LOG_LEVEL", "INFO") if log_level == "DEBUG": logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) diff --git a/yaku-apps-python/apps/sharepoint/src/yaku/sharepoint/client/BUILD b/yaku-apps-python/apps/sharepoint/src/yaku/sharepoint/client/BUILD deleted file mode 100644 index db46e8d6..00000000 --- a/yaku-apps-python/apps/sharepoint/src/yaku/sharepoint/client/BUILD +++ /dev/null @@ -1 +0,0 @@ -python_sources() diff --git a/yaku-apps-python/apps/sharepoint/src/yaku/sharepoint/commands/BUILD b/yaku-apps-python/apps/sharepoint/src/yaku/sharepoint/commands/BUILD deleted file mode 100644 index db46e8d6..00000000 --- a/yaku-apps-python/apps/sharepoint/src/yaku/sharepoint/commands/BUILD +++ /dev/null @@ -1 +0,0 @@ -python_sources() diff --git a/yaku-apps-python/apps/sharepoint/tests-integration/BUILD b/yaku-apps-python/apps/sharepoint/tests-integration/BUILD deleted file mode 100644 index cb5fd635..00000000 --- a/yaku-apps-python/apps/sharepoint/tests-integration/BUILD +++ /dev/null @@ -1,14 +0,0 @@ -APP = "sharepoint" -A_PP = APP.replace("-", "_") - -python_tests( - dependencies=[ - f"apps/{APP}/src/yaku/{A_PP}:app", - ], - runtime_package_dependencies=[ - f"apps/{APP}:{APP}", - ], - skip_bandit=True, - skip_mypy=True, - tags=["integration"], -) diff --git a/yaku-apps-python/apps/sharepoint/tests-pex/BUILD b/yaku-apps-python/apps/sharepoint/tests-pex/BUILD deleted file mode 100644 index f9d8ed84..00000000 --- a/yaku-apps-python/apps/sharepoint/tests-pex/BUILD +++ /dev/null @@ -1,13 +0,0 @@ -APP = "sharepoint" -A_PP = APP.replace("-", "_") - -python_tests( - dependencies=[ - f"apps/{APP}/src/yaku/{A_PP}:app", - ], - runtime_package_dependencies=[ - f"apps/{APP}:{APP}", - ], - skip_bandit=True, - skip_mypy=True, -) diff --git a/yaku-apps-python/apps/sharepoint/tests-pex/test_pex.py b/yaku-apps-python/apps/sharepoint/tests-pex/test_pex.py index 4d1037d4..4450be16 100644 --- a/yaku-apps-python/apps/sharepoint/tests-pex/test_pex.py +++ b/yaku-apps-python/apps/sharepoint/tests-pex/test_pex.py @@ -8,7 +8,7 @@ def test_pex_version(): file_version = importlib.resources.read_text(f"yaku.{APP__NAME}", "_version.txt") output = subprocess.check_output( - [f"apps.{APP_NAME}/{APP_NAME}.pex", "--version"], encoding="utf-8" + [f"yaku-apps-python/apps/{APP_NAME}/{APP_NAME}", "--version"], encoding="utf-8" ) assert output.strip() == file_version.strip() diff --git a/yaku-apps-python/apps/sharepoint/tests/BUILD b/yaku-apps-python/apps/sharepoint/tests/BUILD deleted file mode 100644 index ae2f5350..00000000 --- a/yaku-apps-python/apps/sharepoint/tests/BUILD +++ /dev/null @@ -1,9 +0,0 @@ -python_tests( - dependencies=[ - "3rdparty:reqs#pytest-mock", - "3rdparty:reqs#requests", - "3rdparty:reqs#requests-mock", - ], - skip_bandit=True, - skip_mypy=True, -) diff --git a/yaku-apps-python/apps/splunk-fetcher/BUILD b/yaku-apps-python/apps/splunk-fetcher/BUILD deleted file mode 100644 index b5efdd9f..00000000 --- a/yaku-apps-python/apps/splunk-fetcher/BUILD +++ /dev/null @@ -1,11 +0,0 @@ -poetry_requirements( - name="reqs", - module_mapping={ - "splunk_sdk": ["splunklib"], - }, -) - -pex_binary( - name="splunk-fetcher", - entry_point="yaku.splunk_fetcher.cli", -) diff --git a/yaku-apps-python/apps/splunk-fetcher/BUILD.bazel b/yaku-apps-python/apps/splunk-fetcher/BUILD.bazel new file mode 100644 index 00000000..f761b7fc --- /dev/null +++ b/yaku-apps-python/apps/splunk-fetcher/BUILD.bazel @@ -0,0 +1,38 @@ +load("@aspect_rules_py//py:defs.bzl", "py_binary") +load("@rules_python//python:packaging.bzl", "py_package", "py_wheel") +load("//tools/pytest-runner:defs.bzl", "py_pytest_test") + +NAME = "splunk-fetcher" + +PKG = "splunk_fetcher" + +py_binary( + name = NAME, + srcs = glob(["src/**/*.py"]) + ["src/yaku/" + PKG + "/_version.txt"], + imports = ["src"], + main = "src/yaku/" + PKG + "/__main__.py", + deps = [ + "//yaku-apps-python/packages/autopilot-utils:lib", + "@pip//click", + "@pip//splunk_sdk", + ], +) + +py_pytest_test( + name = "test", + srcs = glob(["tests/*.py"]), + data = glob(["tests/data/**"]), + deps = [ + ":" + NAME, + "@pip//mock", + "@pip//pytest_mock", + ], +) + +py_pytest_test( + name = "test-bin", + srcs = glob(["tests-pex/*.py"]), + deps = [ + ":" + NAME, + ], +) diff --git a/yaku-apps-python/apps/splunk-fetcher/src/yaku/splunk_fetcher/BUILD b/yaku-apps-python/apps/splunk-fetcher/src/yaku/splunk_fetcher/BUILD deleted file mode 100644 index b2a13697..00000000 --- a/yaku-apps-python/apps/splunk-fetcher/src/yaku/splunk_fetcher/BUILD +++ /dev/null @@ -1,6 +0,0 @@ -python_sources(name="app", dependencies=[":version"]) - -resource( - name="version", - source="_version.txt", -) diff --git a/yaku-apps-python/apps/splunk-fetcher/src/yaku/splunk_fetcher/__main__.py b/yaku-apps-python/apps/splunk-fetcher/src/yaku/splunk_fetcher/__main__.py new file mode 100644 index 00000000..59cca672 --- /dev/null +++ b/yaku-apps-python/apps/splunk-fetcher/src/yaku/splunk_fetcher/__main__.py @@ -0,0 +1,4 @@ +from yaku.splunk_fetcher.cli import main + +if __name__ == "__main__": + main(auto_envvar_prefix="SPLUNK") diff --git a/yaku-apps-python/apps/splunk-fetcher/src/yaku/splunk_fetcher/cli.py b/yaku-apps-python/apps/splunk-fetcher/src/yaku/splunk_fetcher/cli.py index 0127ca43..e6ad55d6 100644 --- a/yaku-apps-python/apps/splunk-fetcher/src/yaku/splunk_fetcher/cli.py +++ b/yaku-apps-python/apps/splunk-fetcher/src/yaku/splunk_fetcher/cli.py @@ -226,6 +226,3 @@ def get_output_path(result_file: str) -> Path: provider=CLI, version_callback=read_version_from_package(__package__), ) - -if __name__ == "__main__": - main(auto_envvar_prefix="SPLUNK") diff --git a/yaku-apps-python/apps/splunk-fetcher/src/yaku/splunk_fetcher/splunk/BUILD b/yaku-apps-python/apps/splunk-fetcher/src/yaku/splunk_fetcher/splunk/BUILD deleted file mode 100644 index db46e8d6..00000000 --- a/yaku-apps-python/apps/splunk-fetcher/src/yaku/splunk_fetcher/splunk/BUILD +++ /dev/null @@ -1 +0,0 @@ -python_sources() diff --git a/yaku-apps-python/apps/splunk-fetcher/src/yaku/splunk_fetcher/splunk/result.py b/yaku-apps-python/apps/splunk-fetcher/src/yaku/splunk_fetcher/splunk/result.py index 72c6ef0e..15c454e1 100644 --- a/yaku-apps-python/apps/splunk-fetcher/src/yaku/splunk_fetcher/splunk/result.py +++ b/yaku-apps-python/apps/splunk-fetcher/src/yaku/splunk_fetcher/splunk/result.py @@ -7,12 +7,6 @@ class SplunkResult: - messages: list[dict] = [] - results: list[dict] = [] - fieldnames: list[str] = [] - override_csv: bytes | None = None - override_json: bytes | None = None - # Overrides have to be added, # as the results in the reader do not contain empty columns, which are expected so far def __init__( @@ -21,6 +15,12 @@ def __init__( override_csv: bytes | None = None, override_json: bytes | None = None, ): + self.messages: list[dict] = [] + self.results: list[dict] = [] + self.fieldnames: list[str] = [] + self.override_csv: bytes | None = None + self.override_json: bytes | None = None + logger.info("Processing SplunkResult ...") for item in reader: logger.debug(f"Processing item {item}") diff --git a/yaku-apps-python/apps/splunk-fetcher/tests-integration/BUILD b/yaku-apps-python/apps/splunk-fetcher/tests-integration/BUILD deleted file mode 100644 index 9f8fb1f4..00000000 --- a/yaku-apps-python/apps/splunk-fetcher/tests-integration/BUILD +++ /dev/null @@ -1,14 +0,0 @@ -APP = "splunk-fetcher" -A_PP = APP.replace("-", "_") - -python_tests( - dependencies=[ - f"apps/{APP}/src/yaku/{A_PP}:app", - ], - runtime_package_dependencies=[ - f"apps/{APP}:{APP}", - ], - skip_bandit=True, - skip_mypy=True, - tags=["integration"], -) diff --git a/yaku-apps-python/apps/splunk-fetcher/tests-pex/BUILD b/yaku-apps-python/apps/splunk-fetcher/tests-pex/BUILD deleted file mode 100644 index 5c8962c3..00000000 --- a/yaku-apps-python/apps/splunk-fetcher/tests-pex/BUILD +++ /dev/null @@ -1,13 +0,0 @@ -APP = "splunk-fetcher" -A_PP = APP.replace("-", "_") - -python_tests( - dependencies=[ - f"apps/{APP}/src/yaku/{A_PP}:app", - ], - runtime_package_dependencies=[ - f"apps/{APP}:{APP}", - ], - skip_bandit=True, - skip_mypy=True, -) diff --git a/yaku-apps-python/apps/splunk-fetcher/tests-pex/test_pex.py b/yaku-apps-python/apps/splunk-fetcher/tests-pex/test_pex.py index cb69dc74..b50ef64f 100644 --- a/yaku-apps-python/apps/splunk-fetcher/tests-pex/test_pex.py +++ b/yaku-apps-python/apps/splunk-fetcher/tests-pex/test_pex.py @@ -8,7 +8,7 @@ def test_pex_version(): file_version = importlib.resources.read_text(f"yaku.{APP__NAME}", "_version.txt") output = subprocess.check_output( - [f"apps.{APP_NAME}/{APP_NAME}.pex", "--version"], encoding="utf-8" + [f"yaku-apps-python/apps/{APP_NAME}/{APP_NAME}", "--version"], encoding="utf-8" ) assert output.strip() == file_version.strip() diff --git a/yaku-apps-python/apps/splunk-fetcher/tests/BUILD b/yaku-apps-python/apps/splunk-fetcher/tests/BUILD deleted file mode 100644 index dedce2f2..00000000 --- a/yaku-apps-python/apps/splunk-fetcher/tests/BUILD +++ /dev/null @@ -1 +0,0 @@ -python_tests(skip_bandit=True, skip_mypy=True) diff --git a/yaku-apps-python/packages/autopilot-utils/BUILD b/yaku-apps-python/packages/autopilot-utils/BUILD deleted file mode 100644 index b2b54c0f..00000000 --- a/yaku-apps-python/packages/autopilot-utils/BUILD +++ /dev/null @@ -1,11 +0,0 @@ -externally_versioned_python_distribution( - name="dist", - dependencies=[ - "packages/autopilot-utils/src/yaku/autopilot_utils:lib", - ], - provides=python_artifact( - name="autopilot_utils", - python_requires=">=3.9", - version_file="src/yaku/autopilot_utils/_version.txt", - ), -) diff --git a/yaku-apps-python/packages/autopilot-utils/BUILD.bazel b/yaku-apps-python/packages/autopilot-utils/BUILD.bazel new file mode 100644 index 00000000..2d4f69bd --- /dev/null +++ b/yaku-apps-python/packages/autopilot-utils/BUILD.bazel @@ -0,0 +1,36 @@ +load("@aspect_rules_py//py:defs.bzl", "py_library") +load("@rules_python//python:packaging.bzl", "py_wheel") +load("//tools/pytest-runner:defs.bzl", "py_pytest_test") + +py_library( + name = "lib", + srcs = glob(["src/**/*.py"]) + ["src/yaku/autopilot_utils/_version.txt"], + imports = ["src"], + visibility = ["//yaku-apps-python:__subpackages__"], + deps = [ + "@pip//click", + "@pip//dohq_artifactory", + "@pip//loguru", + "@pip//pydantic", + "@pip//requests", + ], +) + +py_wheel( + name = "wheel", + distribution = "yaku-autopilot-utils", + version = "0.0.1", + deps = [":lib"], +) + +py_pytest_test( + name = "test", + srcs = glob(["tests/**/test_*.py"]), + deps = [ + ":lib", + "@pip//freezegun", + "@pip//mock", + "@pip//pytest_mock", + "@pip//pytz", + ], +) diff --git a/yaku-apps-python/packages/autopilot-utils/src/yaku/autopilot_utils/BUILD b/yaku-apps-python/packages/autopilot-utils/src/yaku/autopilot_utils/BUILD deleted file mode 100644 index 1069f34a..00000000 --- a/yaku-apps-python/packages/autopilot-utils/src/yaku/autopilot_utils/BUILD +++ /dev/null @@ -1,6 +0,0 @@ -python_sources(name="lib", dependencies=[":version"]) - -resource( - name="version", - source="_version.txt", -) diff --git a/yaku-apps-python/packages/autopilot-utils/tests/BUILD b/yaku-apps-python/packages/autopilot-utils/tests/BUILD deleted file mode 100644 index 50eeeada..00000000 --- a/yaku-apps-python/packages/autopilot-utils/tests/BUILD +++ /dev/null @@ -1,4 +0,0 @@ -python_tests( - skip_bandit=True, - skip_mypy=False, -) diff --git a/yaku-apps-python/pants.ci.toml b/yaku-apps-python/pants.ci.toml deleted file mode 100644 index 14da85bd..00000000 --- a/yaku-apps-python/pants.ci.toml +++ /dev/null @@ -1,3 +0,0 @@ -[stats] -log = true - diff --git a/yaku-apps-python/pants.toml b/yaku-apps-python/pants.toml deleted file mode 100644 index 1e459afe..00000000 --- a/yaku-apps-python/pants.toml +++ /dev/null @@ -1,60 +0,0 @@ -[GLOBAL] -pants_version = "2.20.0" -colors = true -pythonpath = ["%(buildroot)s/pants-plugins"] -backend_packages = [ - "pants.backend.build_files.fmt.ruff", - "pants.backend.experimental.python.lint.ruff.check", - "pants.backend.experimental.python.lint.ruff.format", - "pants.backend.plugin_development", - "pants.backend.python", - "pants.backend.python.lint.bandit", - "pants.backend.python.lint.isort", - "pants.backend.python.typecheck.mypy", - "python-utils", -] -pants_ignore.add = ["!papsr_playground/**"] - -[anonymous-telemetry] -enabled = false - -[bandit] -args = ['--ini', '%(buildroot)s/.bandit'] - -[test] -timeout_default = 600 -output = "all" -report = true - -[coverage-py] -global_report = true -report = ["console", "xml", "json"] - -[pytest] -install_from_resolve = "python-default" - -[python] -interpreter_constraints = ["==3.10.*"] -enable_resolves = true - -[python-infer] -use_rust_parser = true - -[python.resolves] -python-default = "3rdparty/python-lockfile.txt" - -[mypy] -install_from_resolve = "python-default" - -[repl] -shell = "ipython" - -[debug-adapter] -host = "127.0.0.1" -port = 5678 - -[generate-lockfiles] -diff = true - -[subprocess-environment] -env_vars.add = ["http_proxy", "https_proxy", "no_proxy"] diff --git a/yaku-apps-typescript/.commitlintrc.yaml b/yaku-apps-typescript/.commitlintrc.yaml new file mode 100644 index 00000000..13163173 --- /dev/null +++ b/yaku-apps-typescript/.commitlintrc.yaml @@ -0,0 +1,53 @@ +--- # The rules below have been manually copied from @commitlint/config-conventional +# and match the v1.0.0 specification: +# https://www.conventionalcommits.org/en/v1.0.0/#specification +# +# You can remove them and uncomment the config below when the following issue is +# fixed: https://github.com/conventional-changelog/commitlint/issues/613 +# +# extends: +# - '@commitlint/config-conventional' +rules: + body-leading-blank: [1, always] + body-max-line-length: [2, always, 100] + footer-leading-blank: [1, always] + footer-max-line-length: [2, always, 100] + header-max-length: [2, always, 100] + scope-case: [2, always, kebab-case] + scope-enum: + - 2 + - always + - [ + ado-work-items-evaluator, + ado-work-items-fetcher, + defender-for-cloud, + docupedia-fetcher, + git-fetcher, + html-finalizer, + jira-evaluator, + jira-fetcher, + jira-finalizer, + json-evaluator, + manual-answer-evaluator, + mend-fetcher, + ocaas-app, + oneq-finalizer, + smb-fetcher, + sonarqube, + sonarqube-evaluator, + xc-bosch-requirements-evaluator, + xc-conformity-requirements-evaluator, + xc-open-defects-evaluator, + ] + subject-case: + - 2 + - always + - [sentence-case] + subject-empty: [2, never] + subject-full-stop: [2, never, '.'] + type-case: [2, always, lower-case] + type-empty: [2, never] + type-enum: + - 2 + - always + - [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] diff --git a/yaku-apps-typescript/.gitignore b/yaku-apps-typescript/.gitignore new file mode 100644 index 00000000..ae261023 --- /dev/null +++ b/yaku-apps-typescript/.gitignore @@ -0,0 +1,45 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules + +# testing +.coverage +coverage +.coverage +coverage.cobertura.xml +cobertura-coverage.xml +cobertura-coverage.xml.* +integration-test-results.xml +test-results.xml + +# misc +.DS_Store + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local +.envrc +.env + +# turbo +.turbo + +# build +dist + +# python +__pycache__/ +.pytest_cache/ +*.egg-info/ + +# IntelliJ +.idea/ diff --git a/yaku-apps-typescript/.husky/commit-msg b/yaku-apps-typescript/.husky/commit-msg new file mode 100755 index 00000000..80416c7b --- /dev/null +++ b/yaku-apps-typescript/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx --no-install commitlint --edit "$1" diff --git a/yaku-apps-typescript/.husky/pre-commit b/yaku-apps-typescript/.husky/pre-commit new file mode 100755 index 00000000..7e154687 --- /dev/null +++ b/yaku-apps-typescript/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npm run lint-staged diff --git a/yaku-apps-typescript/.husky/pre-push b/yaku-apps-typescript/.husky/pre-push new file mode 100755 index 00000000..75fac8e1 --- /dev/null +++ b/yaku-apps-typescript/.husky/pre-push @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npm run lint diff --git a/yaku-apps-typescript/.prettierignore b/yaku-apps-typescript/.prettierignore new file mode 100644 index 00000000..3d8818fd --- /dev/null +++ b/yaku-apps-typescript/.prettierignore @@ -0,0 +1,7 @@ +*.ejs.* +*.d.ts +.pytest_cache +node_modules +dist +*.js +apps/json-evaluator/test/samples/bad_JSON_data.json \ No newline at end of file diff --git a/yaku-apps-typescript/.prettierrc b/yaku-apps-typescript/.prettierrc new file mode 100644 index 00000000..48d73a20 --- /dev/null +++ b/yaku-apps-typescript/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": false, + "singleQuote": true + } diff --git a/yaku-apps-typescript/.vscode/extensions.json b/yaku-apps-typescript/.vscode/extensions.json new file mode 100644 index 00000000..762584a5 --- /dev/null +++ b/yaku-apps-typescript/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["vivaxy.vscode-conventional-commits"] +} diff --git a/yaku-apps-typescript/README.md b/yaku-apps-typescript/README.md index 69cffa57..d3eb9963 100644 --- a/yaku-apps-typescript/README.md +++ b/yaku-apps-typescript/README.md @@ -1 +1,77 @@ -# qg-apps-typescript \ No newline at end of file +![Code Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/git3wid/6bf2099ffe34b1daa3e2c00571cc28f7/raw/qg-apps-typescript-coverage-badge.json) + +# QG Apps in TypeScript + +## Overview + +This repository is a monorepo based on [turbo repo](https://turborepo.org/docs) that contains all standard apps of Yaku written in Typescript. The apps are used within autopilots defined in the configuration for a certain workflow run. Technically, each app is a command line executable that is configured using environment variables and that produces the output as expected from a workflow app. + +The repository has the following structure: + +| Folder | Description | +| :----------- | :-------------------------------------------------------------------------------------- | +| **apps** | Source code of standard QG apps developed. Each app is stored in its own folder. | +| **packages** | All additional packages used in the apps and other typescript based code of the tooling | + +## Installation + +Prerequisite: Since all apps in this repository are typescript based, you need node.js and npm installed on your machine. The apps are tested against the latest minor version of node 18 with the corresponding npm in version 9. + +After cloning the repository, you have to set up the repository. Since this is a monorepo, it is enough to run the following command in the repository root folder: + +```bash +npm install -ws --install-workspace-root +``` + +**Note**: Some of the apps in this repository are using internal dependencies that are stored in Github packages. To be able to install these dependencies, you need to log to the github packages repository with your npm client. + +This is done with the following command: + +```bash +npm login --registry https://npm.pkg.github.com --scope @B-S-F --auth-type legacy +``` + +Then, you need to enter your github username and a personal access token with the scope `read:packages` as password. + +To build and test the apps, run + +```bash +npm run build +npm run test +npm run test:integration:ci +``` + +The latter two commands work on root level of the repository as well as in any of the app or package folders. + +### Running an app + +Each app can be started by running: + +```bash +npm run start # in the corresponding app folder +npm run start -w # in the repository root folder +``` + +Please check necessary environment variables needed by the app to execute properly. A set of examples for many apps can be found in the [documentation repository](https://github.com/B-S-F/qg-api-service/tree/main/tests/e2e-tests/src/e2e-tests) + +## Developing the apps in this repository + +The workflow for enhancing the functionality of the apps in this repository is based on the standard pull request workflow of Github. If you want to do a change, work on your own development branch and push the branch to github. Afterwards start a pull request. By starting the pull request, a check action workflow is triggered that validates your changes against the expectations. Expectations are, that no regression test is failing and that additional constraints like an open source scan and code coverage requirements are met. + +In addition to succeed the pull request workflow, a review is needed for the pull request in order to enable it for merging. If both requirements are met, the pull request can be merged to the main branch of the repository. + +## Releasing an app + +Each app has its own life cycle. Therefore, each app can be released on its own. Each app follows the idea of semantic versioning, i.e., an app has a major, a minor and a patch version that is updated during the release. A release is executed with a release action workflow that can be triggered through the GitHub user interface. The workflow allows you to specify the commit-ish that contains the content to be released and the version type of the release (major, minor or patch). After the successful execution of the release workflow, a tag with the new version is created in a branch. A pull request is automatically opened, which must be manually merged in the main branch after checking the changes executed. The changes are mainly the adaptations of the relevant files to the new version. In addition, the app has been pushed to the [Github npm package registry](https://github.com/orgs/B-S-F/packages?repo_name=qg-apps-typescript). + +### Handling of packages + +All packages stored under the `packages` folder are handled like the apps. Each is a npm package on its own and can be developed and released like an app. The release workflow allows to select each package in the same manner as any of the apps. The only difference is, that packages are meant to be used in apps itself, so after a release, the dependency to the package needs to be updated manually, so that the new version is used from that point on. + +## Create a new app + +The creation of a new app is supported by a template repository, which contains the relevant structure for an app. Find here the [template repository](https://github.com/B-S-F/typescript-app-template). Copy the folder structure, e.g., here to the app folder and implement the required functionality + +## More Information + +For more information about the different apps, please check the [documentation](https://cuddly-adventure-1991k8p.pages.github.io/autopilots/index.html). diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/.env.sample b/yaku-apps-typescript/apps/ado-work-items-evaluator/.env.sample new file mode 100644 index 00000000..e2dbc085 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/.env.sample @@ -0,0 +1,2 @@ +export ADO_WORK_ITEMS_JSON_NAME="data.json" +export ADO_CONFIG_FILE_PATH="sample/task_config.yaml" \ No newline at end of file diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/.eslintrc.cjs b/yaku-apps-typescript/apps/ado-work-items-evaluator/.eslintrc.cjs new file mode 100644 index 00000000..502509d7 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/.eslintrc.cjs @@ -0,0 +1,5 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +module.exports = require("@B-S-F/eslint-config/eslint-preset"); \ No newline at end of file diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/.prettierrc b/yaku-apps-typescript/apps/ado-work-items-evaluator/.prettierrc new file mode 100644 index 00000000..b2095be8 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": false, + "singleQuote": true +} diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/README.md b/yaku-apps-typescript/apps/ado-work-items-evaluator/README.md new file mode 100644 index 00000000..e5289a30 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/README.md @@ -0,0 +1,3 @@ +# ado-work-items-evaluator + +An evaluator to check the response returned by the "ado-work-items-fetcher", according to the rules defined in a custom configuration yaml file diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/package.json b/yaku-apps-typescript/apps/ado-work-items-evaluator/package.json new file mode 100644 index 00000000..43040bfc --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/package.json @@ -0,0 +1,44 @@ +{ + "name": "@B-S-F/ado-work-items-evaluator", + "version": "0.8.0", + "author": "", + "bin": { + "ado-work-items-evaluator": "dist/index.js" + }, + "type": "module", + "dependencies": { + "@B-S-F/issue-validators": "^0.1.0", + "@B-S-F/autopilot-utils": "^0.11.0", + "js-yaml": "^4.1.0" + }, + "description": "", + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "eslint": "*", + "nodemon": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + }, + "files": [ + "dist" + ], + "main": "dist/index.js", + "scripts": { + "build": "tsup", + "dev": "nodemon --watch \"src/**\" --exec npm run start", + "start": "node ./dist/index.js", + "lint": "eslint '**/*.ts'", + "format": "prettier --write '**/*.{ts,md}'", + "test": "vitest run && npm run test:update-cobertura-file", + "test:update-cobertura-file": "sed -i'.bak' 's,package name=\",package name=\"'${npm_package_name}/',g' coverage/cobertura-coverage.xml", + "test:dev": "vitest -w", + "test:coverage": "vitest --coverage", + "test:ui": "vitest --ui", + "test:integration:local": "npx rimraf dist && npm run build && npm run test:integration:ci", + "test:integration:ci": "npx vitest run --config vitest-integration.config.ts" + } +} diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/sample/config.yaml b/yaku-apps-typescript/apps/ado-work-items-evaluator/sample/config.yaml new file mode 100644 index 00000000..e5914588 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/sample/config.yaml @@ -0,0 +1,26 @@ +workItems: + query: "SELECT [System.Id], [System.State] FROM WorkItems WHERE [System.TeamProject] = @project AND [System.WorkItemType] == 'Task'" + neededFields: + - "AssignedTo" + - "BugType" + - "Microsoft.VSTS.Scheduling.TargetDate" + toCheck: + dueDateFieldName: "Microsoft.VSTS.Scheduling.TargetDate" + properties: + state: + fieldName: "State" + conditions: + resolvedValues: + - "Closed" + assignees: + fieldName: "AssignedTo" + conditions: + expectedValues: + - "Name" + bugType: + fieldName: "BugType" + conditions: + illegalValues: + - "Critical" + resolvedValues: + - "Resolved" \ No newline at end of file diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/sample/data.json b/yaku-apps-typescript/apps/ado-work-items-evaluator/sample/data.json new file mode 100644 index 00000000..2dafb80a --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/sample/data.json @@ -0,0 +1,64 @@ +{ + "workItems": [ + { + "Id": 2, + "Url": "https://dev.azure.com/ddt99/00000000-0000-0000-0000-0000000000/_workitems/edit/2", + "State": "Done", + "Title": "Create fetcher", + "AssignedTo": { + "displayName": "Name", + "url": "", + "_links": { + "avatar": { + "href": "" + } + }, + "id": "", + "uniqueName": "username@example.com", + "imageUrl": "", + "descriptor": "" + }, + "children": [] + }, + { + "Id": 3, + "Url": "https://dev.azure.com/ddt99/00000000-0000-0000-0000-0000000000/_workitems/edit/3", + "State": "To Do", + "Title": "Create evaluator", + "AssignedTo": { + "displayName": "Name", + "url": "", + "_links": { + "avatar": { + "href": "" + } + }, + "id": "", + "uniqueName": "username@example.com", + "imageUrl": "", + "descriptor": "" + }, + "children": [] + }, + { + "Id": 4, + "Url": "https://dev.azure.com/ddt99/00000000-0000-0000-0000-0000000000/_workitems/edit/4", + "State": "To Do", + "Title": "Integrate into QG CLI", + "AssignedTo": { + "displayName": "Name", + "url": "", + "_links": { + "avatar": { + "href": "" + } + }, + "id": "", + "uniqueName": "username@example.com", + "imageUrl": "", + "descriptor": "" + }, + "children": [] + } + ] +} diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/sample/review_config.yaml b/yaku-apps-typescript/apps/ado-work-items-evaluator/sample/review_config.yaml new file mode 100644 index 00000000..81bd48af --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/sample/review_config.yaml @@ -0,0 +1,49 @@ +workItems: + query: "SELECT [System.Id], [System.State] FROM WorkItems WHERE ([System.TeamProject] = @project AND [System.WorkItemType] == 'Epic' AND Id = 229047)" + neededFields: + - "AssignedTo" + - "Reviewer" + - "DueDate" + evaluate: + settings: + dueDateFieldName: "DueDate" + closedStates: + - 'Closed' + - 'Performed' + children: + get: true + evaluate: + checks: + dataExists: true + cycleInDays: 100 + fields: + state: + fieldName: "State" + conditions: + resolved: + - 'Closed' + - 'Performed' + assignee: + fieldName: "AssignedTo" + closedAfterDate: '2022.01.01' + conditions: + expected: + - 'Name' + + reviewers: + fieldName: "Reviewer" + closedAfterDate: '2022.01.01' + conditions: + expected: + - 'Name' + children: + get: true + evaluate: + checks: + fields: + state: + fieldName: "State" + conditions: + resolved: + - 'Closed' + - 'Performed' diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/sample/task_config.yaml b/yaku-apps-typescript/apps/ado-work-items-evaluator/sample/task_config.yaml new file mode 100644 index 00000000..99ea8dcc --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/sample/task_config.yaml @@ -0,0 +1,26 @@ +workItems: + query: "SELECT [System.Id], [System.State] FROM WorkItems WHERE [System.TeamProject] = @project AND [System.WorkItemType] == 'Task'" + neededFields: + - "AssignedTo" + - "BugType" + - "Microsoft.VSTS.Scheduling.TargetDate" + evaluate: + settings: + dueDateFieldName: "DueDate" + checks: + fields: + state: + fieldName: "State" + conditions: + resolved: + - "Closed" + assignees: + fieldName: "AssignedTo" + conditions: + expected: + - "Name" + bugType: + fieldName: "BugType" + conditions: + illegal: + - "Critical" diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/src/evaluate.ts b/yaku-apps-typescript/apps/ado-work-items-evaluator/src/evaluate.ts new file mode 100644 index 00000000..f279d904 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/src/evaluate.ts @@ -0,0 +1,398 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { + CheckGeneralResult, + Conditions, + InvalidIssues, + InvalidResolvedValues, + Issue, + checkClosedIssuesAfterDate, + checkInCycle, + checkProperty, +} from '@B-S-F/issue-validators' + +import { AppOutput, Result } from '@B-S-F/autopilot-utils' +import * as thisModule from './evaluate.js' // needed for testing +import { Config, Dictionary, WorkItemsConfig } from './types.js' + +export function initializeInvalidWorkItemsField( + invalidWorkItems: InvalidIssues, + field: string +) { + if (!invalidWorkItems[field]) { + invalidWorkItems[field] = { + exists: [], + expected: [], + illegal: [], + resolved: { + [CheckGeneralResult.overdue]: [], + [CheckGeneralResult.undefinedDueDate]: [], + }, + } + } +} + +export function checkWorkItems( + workItems: Issue[], + fields: Dictionary, + stateFieldName: string, + closedStates: string[], + dueDateFieldName: string +): InvalidIssues { + const invalidWorkItems: InvalidIssues = {} + + for (const field in fields) { + const fieldValue = fields[field] + const fieldName = + fieldValue.fieldName.charAt(0).toLowerCase() + + fieldValue.fieldName.slice(1) + + let workItemsToCheck: Issue[] + + if (fieldValue.closedAfterDate) { + const closedAfterDate = new Date(fieldValue.closedAfterDate) + workItemsToCheck = checkClosedIssuesAfterDate( + workItems, + stateFieldName, + closedStates, + dueDateFieldName, + closedAfterDate + ) + } else { + workItemsToCheck = workItems + } + + const conditions: Map = new Map< + Conditions, + string[] + >() + conditions.set(Conditions.exists, []) + for (const [key, condition] of Object.entries(fieldValue.conditions)) { + conditions.set(key as Conditions, condition as string[]) + } + + // TODO: use list from settings closedStates tag (see CLI generate) + conditions.forEach((values, conditionType) => { + const result = checkProperty( + false, + workItemsToCheck, + fieldName, + conditionType, + values, + dueDateFieldName + ) + thisModule.initializeInvalidWorkItemsField(invalidWorkItems, field) + + if (conditionType === Conditions.resolved) + invalidWorkItems[field][Conditions.resolved] = + result as InvalidResolvedValues + else invalidWorkItems[field][conditionType] = result as Issue[] + }) + } + return invalidWorkItems +} + +export function checkIfRelationExists(workItems: Issue[]) { + return { + invalidWorkItems: workItems.filter((workItem) => { + if (!('relations' in workItem)) return true + if (workItem.relations) { + return workItem.relations.length === 0 + } + }), + } +} + +function expectedIssueToResult( + field: string, + expected: string, + issue: Issue +): Result { + return { + criterion: `The work item [(${issue.id}) ${issue.title}] must have '${expected}' value in field '${field}'`, + justification: `The work item [(${issue.id}) ${issue.title}] has a wrong value '${issue[field]}' in field '${field}'`, + fulfilled: false, + metadata: { + url: issue.url, + id: issue.id, + title: issue.title, + field: field, + }, + } +} + +function illegalIssueToResult( + field: string, + illegal: string, + issue: Issue +): Result { + return { + criterion: `The work item [(${issue.id}) ${issue.title}] must not have '${illegal}' value in field '${field}'`, + justification: `The work item [(${issue.id}) ${issue.title}] has a wrong value '${issue[field]}' in field '${field}'`, + fulfilled: false, + metadata: { + url: issue.url, + id: issue.id, + title: issue.title, + field: field, + }, + } +} + +function existIssueToResult( + field: string, + conditions: string[], + issue: Issue +): Result { + return { + criterion: `The work item [(${issue.id}) ${ + issue.title + }] must satisfy any of the conditions '${conditions.join(', ')}'`, + justification: `The work item [(${issue.id}) ${issue.title}] does not satisfy any provided condition`, + fulfilled: false, + metadata: { + url: issue.url, + id: issue.id, + title: issue.title, + field: field, + }, + } +} + +function overdueToResult(dueDateFieldName: string, issue: Issue): Result { + return { + criterion: `The work item [(${issue.id}) ${issue.title}] must be resolved`, + justification: `The work item [(${issue.id}) ${issue.title}] was not resolved before its due date ${issue[dueDateFieldName]}`, + fulfilled: false, + metadata: { + url: issue.url, + id: issue.id, + title: issue.title, + dueDateField: dueDateFieldName, + }, + } +} + +function undefinedDueDateToResult( + dueDateFieldName: string, + issue: Issue +): Result { + return { + criterion: `The work item [(${issue.id}) ${issue.title}] must be resolved`, + justification: `The work item [(${issue.id}) ${issue.title}] has no due date field ${dueDateFieldName}`, + fulfilled: false, + metadata: { + url: issue.url, + id: issue.id, + title: issue.title, + dueDateField: dueDateFieldName, + }, + } +} + +function invalidResolvedValuesToResults( + invalidResolvedValues: InvalidResolvedValues, + dueDateFieldName: string +): Result[] { + const results: Result[] = [] + for (const [key, value] of Object.entries(invalidResolvedValues)) { + if (key === CheckGeneralResult.overdue) { + ;(value as Issue[]).forEach((issue) => { + results.push(overdueToResult(dueDateFieldName, issue)) + }) + } else if (key === CheckGeneralResult.undefinedDueDate) { + ;(value as Issue[]).forEach((issue) => { + results.push(undefinedDueDateToResult(dueDateFieldName, issue)) + }) + } + } + return results +} + +function invalidWorkItemsToResults( + invalidWorkItems: InvalidIssues, + dueDateFieldName: string +): Result[] { + const results: Result[] = [] + for (const [field, value] of Object.entries(invalidWorkItems)) { + for (const [condition, issues] of Object.entries(value)) { + if (condition === Conditions.resolved) { + results.push( + ...invalidResolvedValuesToResults( + issues as InvalidResolvedValues, + dueDateFieldName + ) + ) + } else if (condition === Conditions.expected) { + ;(issues as Issue[]).forEach((issue) => { + // TODO: expected value is not known so far + results.push(expectedIssueToResult(field, '?', issue)) + }) + } else if (condition === Conditions.illegal) { + // TODO: illegal value is not known so far + ;(issues as Issue[]).forEach((issue) => { + results.push(illegalIssueToResult(field, '?', issue)) + }) + } else if (condition === Conditions.exists) { + // TODO: conditions are not known so far + ;(issues as Issue[]).forEach((issue) => { + results.push(existIssueToResult(field, [], issue)) + }) + } + } + } + return results +} + +export function checkWorkItemsRecursively( + workItems: Issue[], + levelConfig: WorkItemsConfig, + dueDateFieldName: string, + stateFieldName: string, + closedStates: string[], + level = 0 +): Result[] { + const results: Result[] = [] + + if (levelConfig.evaluate?.checks?.dataExists) { + if (workItems.length === 0) { + results.push({ + criterion: `work items in level ${level} must be available`, + justification: `There are no work items in level ${level}`, + fulfilled: false, + }) + } else { + results.push({ + criterion: `work items in level ${level} must be available`, + justification: `There are ${workItems.length} work items in level ${level}`, + fulfilled: true, + }) + } + } + + if (levelConfig?.evaluate?.checks?.cycleInDays) { + const resultCheckInCycle = checkInCycle( + workItems, + stateFieldName, + closedStates, + levelConfig?.evaluate.checks?.cycleInDays, + dueDateFieldName + ) + if (!resultCheckInCycle) { + results.push({ + criterion: `work items in level ${level} must be in cycle`, + justification: `There are work items in level ${level} that are not in cycle`, + fulfilled: false, + }) + } else { + results.push({ + criterion: `work items in level ${level} must be in cycle`, + justification: `All work items in level ${level} are in cycle`, + fulfilled: true, + }) + } + } + + if (levelConfig?.evaluate?.checks?.relationsExists) { + const invalidWorkItems = thisModule.checkIfRelationExists(workItems) + if (invalidWorkItems.invalidWorkItems.length !== 0) { + results.push({ + criterion: `work items in level ${level} must have relations`, + justification: `There are ${invalidWorkItems.invalidWorkItems.length} work items in level ${level} that have no relations`, + fulfilled: false, + }) + } else { + results.push({ + criterion: `work items in level ${level} must have relations`, + justification: `All work items in level ${level} have relations`, + fulfilled: true, + }) + } + } + + const fields = levelConfig?.evaluate?.checks?.fields + if (fields) { + const invalidWorkItems = thisModule.checkWorkItems( + workItems, + fields, + stateFieldName, + closedStates, + dueDateFieldName + ) + if (Object.keys(invalidWorkItems).length !== 0) { + results.push( + ...invalidWorkItemsToResults(invalidWorkItems, dueDateFieldName) + ) + } else { + results.push({ + criterion: `work items in level ${level} must have valid fields`, + justification: `All work items in level ${level} have valid fields`, + fulfilled: true, + }) + } + } else { + results.push({ + criterion: `work items in level ${level} must have valid fields`, + justification: `No fields to check in level ${level}`, + fulfilled: true, + }) + } + + const relations = workItems + .filter((workItem) => workItem.relations) + .flatMap((workItem) => workItem.relations) + + if (relations.length !== 0 && levelConfig.children) { + const recursiveResults = thisModule.checkWorkItemsRecursively( + relations, + levelConfig.children, + dueDateFieldName, + stateFieldName, + closedStates, + level + 1 + ) + results.push(...recursiveResults) + } + + return results +} + +function determineClosedState(config: Config) { + let closedStates = config?.workItems?.evaluate?.settings?.closedStates + if (!closedStates) { + closedStates = ['Closed'] + console.log("No closed states specified, using default: ['Closed']") + } + return closedStates +} + +export function evaluate( + adoFetcherResponse: Dictionary, + evaluatorFileData: Config +): AppOutput { + const appOutput = new AppOutput() + const workItems = adoFetcherResponse.workItems + const dueDateFieldName = + evaluatorFileData?.workItems?.evaluate?.settings?.dueDateFieldName || '' + const closedStates = determineClosedState(evaluatorFileData) + const stateFieldName = 'state' + const results = thisModule.checkWorkItemsRecursively( + workItems, + evaluatorFileData.workItems, + dueDateFieldName, + stateFieldName, + closedStates + ) + appOutput.setStatus('GREEN') + appOutput.setReason('All work items are valid') + for (const result of results) { + if (!result.fulfilled) { + appOutput.setStatus('RED') + appOutput.setReason('Some work items are invalid') + } + appOutput.addResult(result) + } + return appOutput +} diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/src/index.ts b/yaku-apps-typescript/apps/ado-work-items-evaluator/src/index.ts new file mode 100644 index 00000000..a4f280c4 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/src/index.ts @@ -0,0 +1,43 @@ +#! /usr/bin/env node +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { AppError, AppOutput } from '@B-S-F/autopilot-utils' +import { readFile } from 'fs/promises' +import { load } from 'js-yaml' +import { exit } from 'process' +import { evaluate } from './evaluate.js' +import { Config, Dictionary } from './types.js' +import { getPathFromEnvVariable } from './util.js' + +const CONFIG_FILE_ENV_VAR = 'ADO_CONFIG_FILE_PATH' + +const main = async () => { + try { + const filepath = getPathFromEnvVariable( + 'ADO_WORK_ITEMS_JSON_NAME', + 'data.json' + ) + const configFilePath = getPathFromEnvVariable(CONFIG_FILE_ENV_VAR) + const rawData = await readFile(filepath) + const configData = await readFile(configFilePath, 'utf8') + + const adoData: Dictionary = JSON.parse(rawData.toString()) + const config = load(configData) as Config + + const appOutput = evaluate(adoData, config) + appOutput.write() + } catch (e) { + if (e instanceof AppError) { + const appOutput = new AppOutput() + appOutput.setStatus('FAILED') + appOutput.setReason(e.Reason()) + appOutput.write() + exit(0) + } + throw e + } +} + +main() diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/src/types.ts b/yaku-apps-typescript/apps/ado-work-items-evaluator/src/types.ts new file mode 100644 index 00000000..86e95eaa --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/src/types.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { InvalidIssues, Issue } from '@B-S-F/issue-validators' + +type Status = 'GREEN' | 'RED' | 'YELLOW' | 'NA' + +export enum CheckReviewsResult { + noData = 'noData', + oldLastReview = 'oldLastReview', +} + +export interface IFetcherCheckResult { + outputs: string[] + status: Status +} + +export interface Dictionary { + [key: string]: any +} + +export interface MessageInput { + fields: Dictionary + invalidWorkItems: InvalidIssues + dataCheck?: { + noData: boolean + cycleContext: boolean + } + relationCheck?: { + invalidWorkItems: Issue[] + } +} + +export type CheckFieldConfig = { + fieldName: string + closedAfterDate?: string + conditions: { + expected?: string[] + resolved?: string[] + illegal?: string[] + } +} + +export type ChecksConfig = { + dataExists?: boolean + cycleInDays?: number + relationsExists?: boolean + fields?: { [tag: string]: CheckFieldConfig } +} + +export type EvaluateConfig = { + settings?: { + dueDateFieldName?: string + closedStates?: string[] + } + checks?: ChecksConfig +} + +export type WorkItemsConfig = { + evaluate?: EvaluateConfig + children?: { + evaluate: EvaluateConfig + } +} + +export type Config = { + workItems: WorkItemsConfig +} diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/src/util.ts b/yaku-apps-typescript/apps/ado-work-items-evaluator/src/util.ts new file mode 100644 index 00000000..27dd0c9b --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/src/util.ts @@ -0,0 +1,33 @@ +import { AppError } from '@B-S-F/autopilot-utils' +import fs from 'fs' +import path from 'path' +export function getPathFromEnvVariable( + envVariableName: string, + alt?: string +): string { + const filePath: string | undefined = process.env[envVariableName] ?? alt + if (filePath === undefined || filePath.trim() === '') { + throw new AppError( + `The environment variable "${envVariableName}" is not set!` + ) + } + const relativePath = path.relative(process.cwd(), filePath.trim()) + validateFilePath(relativePath) + return relativePath +} + +function validateFilePath(filePath: string): void { + if (!fs.existsSync(filePath)) { + throw new AppError( + `File ${filePath} does not exist, no data can be evaluated` + ) + } + try { + fs.accessSync(filePath, fs.constants.R_OK) + } catch (e) { + throw new AppError(`${filePath} is not readable!`) + } + if (!fs.statSync(filePath).isFile()) { + throw new AppError(`${filePath} does not point to a file!`) + } +} diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/test/evaluate.test.ts b/yaku-apps-typescript/apps/ado-work-items-evaluator/test/evaluate.test.ts new file mode 100644 index 00000000..ad324355 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/test/evaluate.test.ts @@ -0,0 +1,572 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import * as evaluator from '../src/evaluate' +import { WorkItemsConfig } from '../src/types.js' + +const workItems = [ + { + title: 'Work Item 1', + id: 1, + url: 'url1', + state: 'Open', + assignedTo: 'Name 1', + dueDate: '2022.01.02', + relations: [], + }, + { + title: 'Work Item 2', + id: 2, + url: 'url2', + state: 'Closed', + assignedTo: 'Name 2', + dueDate: '2022.02.02', + relations: [], + }, +] + +const fields = { + state: { + fieldName: 'state', + conditions: { + resolved: ['Closed'], + }, + }, + assignee: { + fieldName: 'assignedTo', + conditions: { + expected: ['name'], + }, + closedAfterDate: '2022.01.02', + }, +} + +const stateFieldName = 'state' +const closedStates = ['closed'] +const dueDateFieldName = 'dueDate' + +describe('initializeInvalidWorkItemsField()', () => { + it("should initialize the field's dictionary with all condition types", () => { + const invalidIssues = {} + const expectedObject = { + state: { + exists: [], + expected: [], + illegal: [], + resolved: { + overdue: [], + undefinedDueDate: [], + }, + }, + } + // doesn't exist yet + evaluator.initializeInvalidWorkItemsField(invalidIssues, 'state') + expect(invalidIssues).toEqual(expectedObject) + // already exists + evaluator.initializeInvalidWorkItemsField(invalidIssues, 'state') + expect(invalidIssues).toEqual(expectedObject) + }) +}) +describe('checkWorkItems()', () => { + afterEach(() => { + vi.clearAllMocks() + }) + it('should return invalid work items', () => { + const result = evaluator.checkWorkItems( + workItems, + fields, + stateFieldName, + closedStates, + dueDateFieldName + ) + const expectedResult = { + state: { + exists: [], + expected: [], + illegal: [], + resolved: { undefinedDueDate: [], overdue: [workItems[0]] }, + }, + assignee: { + exists: [], + expected: workItems, + illegal: [], + resolved: { overdue: [], undefinedDueDate: [] }, + }, + } + expect(result).toEqual(expectedResult) + }) +}) +describe('checkIfRelationExists()', () => { + let items: any + beforeEach(() => { + items = structuredClone(workItems) + items[0].relations.push('test') + }) + it('should return a list of workItems with no relations', () => { + const result = evaluator.checkIfRelationExists(items) + expect(result.invalidWorkItems.length).toEqual(1) + }) +}) + +describe('checkWorkItemsRecursively()', () => { + let items: any + beforeEach(() => { + items = structuredClone(workItems) + }) + + it('should return a result if data does not exist', () => { + items = [] + const workItemsConfig: WorkItemsConfig = { + evaluate: { + checks: { + dataExists: true, + }, + }, + } + const results = evaluator.checkWorkItemsRecursively( + items, + workItemsConfig, + 'dueDate', + 'state', + ['Closed'] + ) + expect(results.length).toEqual(2) + expect(results[0]).toEqual({ + criterion: 'work items in level 0 must be available', + fulfilled: false, + justification: 'There are no work items in level 0', + }) + }) + + it('should return a result if data exists', () => { + const workItemsConfig: WorkItemsConfig = { + evaluate: { + checks: { + dataExists: true, + }, + }, + } + const results = evaluator.checkWorkItemsRecursively( + items, + workItemsConfig, + 'dueDate', + 'state', + ['Closed'] + ) + expect(results.length).toEqual(2) + expect(results[0]).toEqual({ + criterion: 'work items in level 0 must be available', + fulfilled: true, + justification: 'There are 2 work items in level 0', + }) + }) + + it('should return a result if work items are not in cycle', () => { + const workItemsConfig: WorkItemsConfig = { + evaluate: { + checks: { + cycleInDays: 1, + }, + }, + } + const results = evaluator.checkWorkItemsRecursively( + items, + workItemsConfig, + 'dueDate', + 'state', + ['Closed'] + ) + expect(results.length).toEqual(2) + expect(results[0]).toEqual({ + criterion: 'work items in level 0 must be in cycle', + fulfilled: false, + justification: 'There are work items in level 0 that are not in cycle', + }) + }) + + it('should return a result if work items are in cycle', () => { + const workItemsConfig: WorkItemsConfig = { + evaluate: { + checks: { + cycleInDays: 10000, + }, + }, + } + const results = evaluator.checkWorkItemsRecursively( + items, + workItemsConfig, + 'dueDate', + 'state', + ['Closed'] + ) + expect(results.length).toEqual(2) + expect(results[0]).toEqual({ + criterion: 'work items in level 0 must be in cycle', + fulfilled: true, + justification: 'All work items in level 0 are in cycle', + }) + }) + + it('should return a result if relations do not exist', () => { + const workItemsConfig: WorkItemsConfig = { + evaluate: { + checks: { + relationsExists: true, + }, + }, + } + const results = evaluator.checkWorkItemsRecursively( + items, + workItemsConfig, + 'dueDate', + 'state', + ['Closed'] + ) + expect(results.length).toEqual(2) + expect(results[0]).toEqual({ + criterion: 'work items in level 0 must have relations', + fulfilled: false, + justification: 'There are 2 work items in level 0 that have no relations', + }) + }) + + it('should return a result if relations exist', () => { + const workItemsConfig: WorkItemsConfig = { + evaluate: { + checks: { + relationsExists: true, + }, + }, + } + items[0].relations.push('test') + items[1].relations.push('test2') + const results = evaluator.checkWorkItemsRecursively( + items, + workItemsConfig, + 'dueDate', + 'state', + ['Closed'] + ) + expect(results.length).toEqual(2) + expect(results[0]).toEqual({ + criterion: 'work items in level 0 must have relations', + fulfilled: true, + justification: 'All work items in level 0 have relations', + }) + }) + it('should return a result if a work items DOES NOT satisfy any of the conditions', () => { + const workItems = [ + { + title: 'Work Item 1', + id: 1, + url: 'https://dev.azure.com/test/test/_workitems/1', + State: 'Open', + assigned: 'test1', + }, + { + title: 'Work Item 2', + id: 2, + url: 'https://dev.azure.com/test/test/_workitems/2', + State: 'Closed', + AssignedTo: 'test2', + }, + ] + const workItemsConfig: WorkItemsConfig = { + evaluate: { + checks: { + fields: { + assigned: { + fieldName: 'assigned', + conditions: { + expected: ['test1'], + }, + }, + }, + }, + }, + } + const results = evaluator.checkWorkItemsRecursively( + workItems, + workItemsConfig, + 'dueDate', + 'state', + ['Closed'] + ) + expect(results.length).toEqual(1) + expect(results[0]).toEqual({ + criterion: + "The work item [(2) Work Item 2] must satisfy any of the conditions ''", + fulfilled: false, + justification: + 'The work item [(2) Work Item 2] does not satisfy any provided condition', + metadata: { + field: 'assigned', + id: 2, + title: 'Work Item 2', + url: 'https://dev.azure.com/test/test/_workitems/2', + }, + }) + }) + it('should return a result if any of the work items DOES NOT have one of the enumerated values in expected condition', () => { + const workItems = [ + { + title: 'Work Item 1', + id: 1, + url: 'https://dev.azure.com/test/test/_workitems/1', + State: 'Open', + assigned: 'test1', + }, + { + title: 'Work Item 2', + id: 2, + url: 'https://dev.azure.com/test/test/_workitems/2', + State: 'Closed', + assigned: 'test2', + }, + ] + const workItemsConfig: WorkItemsConfig = { + evaluate: { + checks: { + fields: { + assigned: { + fieldName: 'assigned', + conditions: { + expected: ['test1'], + }, + }, + }, + }, + }, + } + const results = evaluator.checkWorkItemsRecursively( + workItems, + workItemsConfig, + 'dueDate', + 'state', + ['Closed'] + ) + expect(results.length).toEqual(1) + expect(results[0]).toEqual({ + criterion: + "The work item [(2) Work Item 2] must have '?' value in field 'assigned'", + fulfilled: false, + justification: + "The work item [(2) Work Item 2] has a wrong value 'test2' in field 'assigned'", + metadata: { + field: 'assigned', + id: 2, + title: 'Work Item 2', + url: 'https://dev.azure.com/test/test/_workitems/2', + }, + }) + }) + it('should return no result if all of the work items have one of the enumerated values in expected condition', () => { + const workItems = [ + { + title: 'Work Item 1', + id: 1, + url: 'https://dev.azure.com/test/test/_workitems/1', + State: 'Open', + assigned: 'test1', + }, + { + title: 'Work Item 2', + id: 2, + url: 'https://dev.azure.com/test/test/_workitems/2', + State: 'Closed', + assigned: 'test2', + }, + ] + const workItemsConfig: WorkItemsConfig = { + evaluate: { + checks: { + fields: { + assigned: { + fieldName: 'assigned', + conditions: { + expected: ['test1', 'test2'], + }, + }, + }, + }, + }, + } + const results = evaluator.checkWorkItemsRecursively( + workItems, + workItemsConfig, + 'dueDate', + 'state', + ['Closed'] + ) + expect(results.length).toEqual(0) + }) + it('should return a result if any of the issues DOES have one of the given values in illegal condition', () => { + const workItems = [ + { + title: 'Work Item 1', + id: 1, + url: 'https://dev.azure.com/test/test/_workitems/1', + State: 'Open', + assigned: 'test1', + }, + { + title: 'Work Item 2', + id: 2, + url: 'https://dev.azure.com/test/test/_workitems/2', + State: 'Closed', + assigned: 'test2', + }, + ] + const workItemsConfig: WorkItemsConfig = { + evaluate: { + checks: { + fields: { + assigned: { + fieldName: 'assigned', + conditions: { + illegal: ['test1'], + }, + }, + }, + }, + }, + } + const results = evaluator.checkWorkItemsRecursively( + workItems, + workItemsConfig, + 'dueDate', + 'state', + ['Closed'] + ) + expect(results.length).toEqual(1) + expect(results[0]).toEqual({ + criterion: + "The work item [(1) Work Item 1] must not have '?' value in field 'assigned'", + fulfilled: false, + justification: + "The work item [(1) Work Item 1] has a wrong value 'test1' in field 'assigned'", + metadata: { + field: 'assigned', + id: 1, + title: 'Work Item 1', + url: 'https://dev.azure.com/test/test/_workitems/1', + }, + }) + }) + it('should return no result if all of the work items have none of the given values in illegal condition', () => { + const workItems = [ + { + title: 'Work Item 1', + id: 1, + url: 'https://dev.azure.com/test/test/_workitems/1', + State: 'Open', + assigned: 'test1', + }, + { + title: 'Work Item 2', + id: 2, + url: 'https://dev.azure.com/test/test/_workitems/2', + State: 'Closed', + assigned: 'test2', + }, + ] + const workItemsConfig: WorkItemsConfig = { + evaluate: { + checks: { + fields: { + assigned: { + fieldName: 'assigned', + conditions: { + illegal: ['test3'], + }, + }, + }, + }, + }, + } + const results = evaluator.checkWorkItemsRecursively( + workItems, + workItemsConfig, + 'dueDate', + 'state', + ['Closed'] + ) + expect(results.length).toEqual(0) + }) + it('should return a result if a work item is Open and its due date is in the past', () => { + const workItems = [ + { + title: 'Work Item 1', + id: 1, + url: 'https://dev.azure.com/test/test/_workitems/1', + state: 'Open', + dueDate: '2020-01-01', + }, + { + title: 'Work Item 2', + id: 2, + url: 'https://dev.azure.com/test/test/_workitems/2', + state: 'Closed', + dueDate: '2020-01-01', + }, + { + title: 'Work Item 3', + id: 3, + url: 'https://dev.azure.com/test/test/_workitems/2', + state: 'Open', + }, + ] + const workItemsConfig: WorkItemsConfig = { + evaluate: { + settings: { + dueDateFieldName: 'dueDate', + }, + checks: { + fields: { + state: { + fieldName: 'state', + conditions: { + resolved: ['Closed'], + }, + }, + }, + }, + }, + } + + const results = evaluator.checkWorkItemsRecursively( + workItems, + workItemsConfig, + 'dueDate', + 'state', + ['Closed'] + ) + expect(results.length).toEqual(2) + expect(results[1]).toEqual({ + criterion: 'The work item [(1) Work Item 1] must be resolved', + fulfilled: false, + justification: + 'The work item [(1) Work Item 1] was not resolved before its due date 2020-01-01', + metadata: { + id: 1, + title: 'Work Item 1', + url: 'https://dev.azure.com/test/test/_workitems/1', + dueDateField: 'dueDate', + }, + }) + expect(results[0]).toEqual({ + criterion: 'The work item [(3) Work Item 3] must be resolved', + fulfilled: false, + justification: + 'The work item [(3) Work Item 3] has no due date field dueDate', + metadata: { + id: 3, + title: 'Work Item 3', + url: 'https://dev.azure.com/test/test/_workitems/2', + dueDateField: 'dueDate', + }, + }) + }) +}) diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/test/integration/ado-evaluator.int-spec.ts b/yaku-apps-typescript/apps/ado-work-items-evaluator/test/integration/ado-evaluator.int-spec.ts new file mode 100644 index 00000000..6d2fd51f --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/test/integration/ado-evaluator.int-spec.ts @@ -0,0 +1,120 @@ +import * as fs from 'fs' +import * as path from 'path' +import { beforeAll, describe, expect, it } from 'vitest' +import { run, RunProcessResult } from '../../../../integration-tests/src/util' + +type TestCase = { + name: string + dataName: string + configName: string + expectExitCode: number + expectedStatus: string + expectedReason: string + expectedCriterionAmount: number +} + +const testCases: TestCase[] = [ + { + name: 'full-config', + dataName: 'full-config.json', + configName: 'full-config.yaml', + expectExitCode: 0, + expectedStatus: 'GREEN', + expectedReason: 'All work items are valid', + expectedCriterionAmount: 1, + }, + { + name: 'missing-config', + dataName: 'data_missing.json', + configName: 'full-config.yaml', + expectExitCode: 0, + expectedStatus: 'FAILED', + expectedReason: + 'File test/integration/fixtures/data_missing.json does not exist, no data can be evaluated', + expectedCriterionAmount: 0, + }, + { + name: 'evaluate-assignees-of-workitems-33-34', + dataName: 'config-evaluate-assignees-of-workitems-33-34.json', + configName: 'config-evaluate-assignees-of-workitems-33-34.yaml', + expectExitCode: 0, + expectedStatus: 'RED', + expectedReason: 'Some work items are invalid', + expectedCriterionAmount: 1, + }, + { + name: 'evaluate-assignees-of-workitems-34', + dataName: 'config-evaluate-assignees-of-workitems-34.json', + configName: 'config-evaluate-assignees-of-workitems-34.yaml', + expectExitCode: 0, + expectedStatus: 'GREEN', + expectedReason: 'All work items are valid', + expectedCriterionAmount: 0, + }, +] + +function retrieveStatus(outputLines: string[]): string { + const statusLine = outputLines.find((line) => line.includes('status')) + if (statusLine) { + const status = JSON.parse(statusLine).status + return status + } + return '' +} + +function retrieveReason(outputLines: string[]): string { + const reasonLine = outputLines.find((line) => line.includes('reason')) + if (reasonLine) { + const reason = JSON.parse(reasonLine).reason + return reason + } + return '' +} + +function retrieveCriterionAmount(outputLines: string[]): number { + const criterionLines = outputLines.filter((line) => + line.includes('criterion') + ) + return criterionLines.length +} + +describe('Ado Fetcher', () => { + const adoEvaluatorExecutable: string = path.join( + __dirname, + '..', + '..', + 'dist', + 'index.js' + ) + + beforeAll(() => { + expect(fs.existsSync(adoEvaluatorExecutable)).to.be.true + }) + + it.each(testCases)('%s', async (testCase: TestCase) => { + const adoEnvironment = { + ADO_CONFIG_FILE_PATH: path.join( + __dirname, + 'fixtures', + testCase.configName + ), + ADO_WORK_ITEMS_JSON_NAME: path.join( + __dirname, + 'fixtures', + testCase.dataName + ), + } + const result: RunProcessResult = await run(adoEvaluatorExecutable, [], { + env: adoEnvironment, + }) + + console.log(result.stdout[0]) + expect(result.exitCode).toEqual(testCase.expectExitCode) + expect(result.stdout.length).toBeGreaterThan(0) + expect(retrieveStatus(result.stdout)).toEqual(testCase.expectedStatus) + expect(retrieveReason(result.stdout)).toEqual(testCase.expectedReason) + expect(retrieveCriterionAmount(result.stdout)).toEqual( + testCase.expectedCriterionAmount + ) + }) +}) diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/test/integration/fixtures/config-evaluate-assignees-of-workitems-33-34.json b/yaku-apps-typescript/apps/ado-work-items-evaluator/test/integration/fixtures/config-evaluate-assignees-of-workitems-33-34.json new file mode 100644 index 00000000..00812df1 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/test/integration/fixtures/config-evaluate-assignees-of-workitems-33-34.json @@ -0,0 +1,39 @@ +{ + "workItems": [ + { + "id": 33, + "url": "https://dev.azure.com/tn/00000000-0000-0000-0000-0000000000/_workitems/edit/33", + "commentCount": 0, + "priority": 2, + "state": "To Do", + "title": "Test Child Issue 1 for e2e tests", + "relations": [] + }, + { + "id": 34, + "url": "https://dev.azure.com/tn/00000000-0000-0000-0000-0000000000/_workitems/edit/34", + "assignedTo": { + "displayName": "CI CD Automation Technical User", + "url": "https://user-url.com", + "_links": { + "avatar": { + "href": "https://user-url.com" + } + }, + "id": "bf8ddc1d-b626-6826-a2b3-df3a95e033f1", + "uniqueName": "abc1ab@example.com", + "imageUrl": "https://dev.azure.com/team-neutrinos/_apis/GraphProfile/MemberAvatars/00000000-0000-0000-0000-0000000000", + "descriptor": "00000000-0000-0000-0000-0000000000" + }, + "description": "
Some Description
", + "commentCount": 1, + "priority": 2, + "startDate": "2023-05-24T00:00:00Z", + "targetDate": "2035-04-24T00:00:00Z", + "tags": "tag-1; tag-2", + "state": "To Do", + "title": "Test Related Epic 1 for e2e tests", + "relations": [] + } + ] +} diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/test/integration/fixtures/config-evaluate-assignees-of-workitems-33-34.yaml b/yaku-apps-typescript/apps/ado-work-items-evaluator/test/integration/fixtures/config-evaluate-assignees-of-workitems-33-34.yaml new file mode 100644 index 00000000..89c97feb --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/test/integration/fixtures/config-evaluate-assignees-of-workitems-33-34.yaml @@ -0,0 +1,20 @@ +workItems: + query: 'SELECT [System.Id], [System.State] FROM WorkItems WHERE [System.TeamProject] = @project AND [System.Id] IN (33, 34)' + neededFields: + - AssignedTo + - Description + - CommentCount + - Priority + - StartDate + - TargetDate + - Tags + evaluate: + settings: + closedStates: + checks: + fields: + assignee: + fieldName: assignedTo + conditions: + illegal: + - '' diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/test/integration/fixtures/config-evaluate-assignees-of-workitems-34.json b/yaku-apps-typescript/apps/ado-work-items-evaluator/test/integration/fixtures/config-evaluate-assignees-of-workitems-34.json new file mode 100644 index 00000000..25745495 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/test/integration/fixtures/config-evaluate-assignees-of-workitems-34.json @@ -0,0 +1,30 @@ +{ + "workItems": [ + { + "id": 34, + "url": "https://dev.azure.com/tn/00000000-0000-0000-0000-0000000000/_workitems/edit/34", + "assignedTo": { + "displayName": "CI CD Automation Technical User", + "url": "https://user-url.com", + "_links": { + "avatar": { + "href": "https://user-url.com" + } + }, + "id": "00000000-0000-0000-0000-0000000000", + "uniqueName": "abc1ab@example.com", + "imageUrl": "https://dev.azure.com/tn/_apis/GraphProfile/MemberAvatars/aad.00000000-0000-0000-0000-0000000000", + "descriptor": "aad.00000000-0000-0000-0000-0000000000" + }, + "description": "
Some Description
", + "commentCount": 1, + "priority": 2, + "startDate": "2023-05-24T00:00:00Z", + "targetDate": "2035-04-24T00:00:00Z", + "tags": "tag-1; tag-2", + "state": "To Do", + "title": "Test Related Epic 1 for e2e tests", + "relations": [] + } + ] +} diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/test/integration/fixtures/config-evaluate-assignees-of-workitems-34.yaml b/yaku-apps-typescript/apps/ado-work-items-evaluator/test/integration/fixtures/config-evaluate-assignees-of-workitems-34.yaml new file mode 100644 index 00000000..ffc528b8 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/test/integration/fixtures/config-evaluate-assignees-of-workitems-34.yaml @@ -0,0 +1,20 @@ +workItems: + query: 'SELECT [System.Id], [System.State] FROM WorkItems WHERE [System.TeamProject] = @project AND [System.Id] == 34' + neededFields: + - AssignedTo + - Description + - CommentCount + - Priority + - StartDate + - TargetDate + - Tags + evaluate: + settings: + closedStates: + checks: + fields: + assignee: + fieldName: assignedTo + conditions: + illegal: + - '' diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/test/integration/fixtures/full-config.yaml b/yaku-apps-typescript/apps/ado-work-items-evaluator/test/integration/fixtures/full-config.yaml new file mode 100644 index 00000000..33bb9376 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/test/integration/fixtures/full-config.yaml @@ -0,0 +1,31 @@ +workItems: + query: 'SELECT [System.Id], [System.State] FROM WorkItems WHERE [System.TeamProject] = @project AND [System.Id] IN (1, 3, 4, 5)' + neededFields: + - 'AssignedTo' + evaluate: + settings: + closedStates: + - 'Closed' + - 'Done' + checks: + dataExists: true + relationsExist: false + fields: + state: + fieldName: 'State' + conditions: + expected: + - 'Closed' + - 'Done' + relations: + get: true + evaluate: + checks: + fields: + state: + fieldName: 'State' + type: 'Child' + conditions: + expected: + - 'Done' + - 'Closed' diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/tsconfig.json b/yaku-apps-typescript/apps/ado-work-items-evaluator/tsconfig.json new file mode 100644 index 00000000..e1f51365 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@B-S-F/typescript-config/base.json", + "ts-node": { + "compilerOptions": { + "module": "ESNext" + } + }, + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/tsup.config.ts b/yaku-apps-typescript/apps/ado-work-items-evaluator/tsup.config.ts new file mode 100644 index 00000000..9fba5cc4 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/tsup.config.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/**/*.ts'], + splitting: false, + sourcemap: true, + clean: true, + target: 'node18', + format: ['esm'], + bundle: false, +}) diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/vitest-integration.config.ts b/yaku-apps-typescript/apps/ado-work-items-evaluator/vitest-integration.config.ts new file mode 100644 index 00000000..8bec0725 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/vitest-integration.config.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['test/integration/**/*.int-spec.ts'], + pool: 'threads', + poolOptions: { + threads: { + maxThreads: 1, + minThreads: 1, + }, + }, + typecheck: { + tsconfig: 'tsconfig.json', + }, + reporters: ['junit', 'default'], + outputFile: 'reports/integration-test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/ado-work-items-evaluator/vitest.config.ts b/yaku-apps-typescript/apps/ado-work-items-evaluator/vitest.config.ts new file mode 100644 index 00000000..1cf5e21f --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-evaluator/vitest.config.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + typecheck: { + tsconfig: 'tsconfig.json', + }, + coverage: { + provider: 'v8', + all: true, + enabled: true, + reporter: ['cobertura', 'json-summary', 'text-summary'], + include: ['src'], + }, + reporters: ['junit', 'default'], + outputFile: 'reports/test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/.env.sample b/yaku-apps-typescript/apps/ado-work-items-fetcher/.env.sample new file mode 100644 index 00000000..6d30ceb0 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/.env.sample @@ -0,0 +1,6 @@ +export ADO_API_ORG="" +export ADO_API_PROJECT="" +export ADO_API_PERSONAL_ACCESS_TOKEN="" +export ADO_CONFIG_FILE_PATH="./sample/config.yaml" +export ADO_APPLY_PROXY_SETTINGS="" +export evidence_path="" \ No newline at end of file diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/.eslintrc.cjs b/yaku-apps-typescript/apps/ado-work-items-fetcher/.eslintrc.cjs new file mode 100644 index 00000000..502509d7 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/.eslintrc.cjs @@ -0,0 +1,5 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +module.exports = require("@B-S-F/eslint-config/eslint-preset"); \ No newline at end of file diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/.prettierrc b/yaku-apps-typescript/apps/ado-work-items-fetcher/.prettierrc new file mode 100644 index 00000000..b2095be8 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": false, + "singleQuote": true +} diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/README.md b/yaku-apps-typescript/apps/ado-work-items-fetcher/README.md new file mode 100644 index 00000000..798b612c --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/README.md @@ -0,0 +1,3 @@ +# ado-work-items-fetcher + +An application to fetch Azure Devops work items via the Work Item Tracking API. It can be configured by a [configuration file](#prepare-configuration-file) that contains a Wiql (WorkItems Query Language) query and other filtering criteria. diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/package.json b/yaku-apps-typescript/apps/ado-work-items-fetcher/package.json new file mode 100644 index 00000000..a53154d1 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/package.json @@ -0,0 +1,50 @@ +{ + "name": "@B-S-F/ado-work-items-fetcher", + "version": "0.7.2", + "author": "", + "bin": { + "ado-work-items-fetcher": "dist/index.js" + }, + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "@B-S-F/issue-validators": "^0.1.0", + "axios": "^1.6.0", + "is-valid-hostname": "^1.0.2", + "tunnel": "^0.0.6", + "yaml": "^1.10.2", + "zod": "^3.22.3", + "zod-error": "^1.5.0" + }, + "description": "", + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@types/tunnel": "^0.0.2", + "@vitest/ui": "*", + "c8": "*", + "eslint": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + }, + "files": [ + "dist" + ], + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsup", + "start": "node ./dist/index.js", + "lint": "eslint '**/*.ts'", + "format": "prettier --write '**/*.{ts,md}'", + "test:integration:local": "npx rimraf dist && npm run build && npm run test:integration:ci", + "test:integration:ci": "npx vitest run --config vitest-integration.config.ts", + "test": "vitest run && npm run test:update-cobertura-file", + "test:update-cobertura-file": "sed -i'.bak' 's,package name=\",package name=\"'${npm_package_name}/',g' coverage/cobertura-coverage.xml", + "test:dev": "vitest -w", + "test:coverage": "vitest --coverage", + "test:ui": "vitest --ui" + } +} diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/sample/config.yaml b/yaku-apps-typescript/apps/ado-work-items-fetcher/sample/config.yaml new file mode 100644 index 00000000..81bd48af --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/sample/config.yaml @@ -0,0 +1,49 @@ +workItems: + query: "SELECT [System.Id], [System.State] FROM WorkItems WHERE ([System.TeamProject] = @project AND [System.WorkItemType] == 'Epic' AND Id = 229047)" + neededFields: + - "AssignedTo" + - "Reviewer" + - "DueDate" + evaluate: + settings: + dueDateFieldName: "DueDate" + closedStates: + - 'Closed' + - 'Performed' + children: + get: true + evaluate: + checks: + dataExists: true + cycleInDays: 100 + fields: + state: + fieldName: "State" + conditions: + resolved: + - 'Closed' + - 'Performed' + assignee: + fieldName: "AssignedTo" + closedAfterDate: '2022.01.01' + conditions: + expected: + - 'Name' + + reviewers: + fieldName: "Reviewer" + closedAfterDate: '2022.01.01' + conditions: + expected: + - 'Name' + children: + get: true + evaluate: + checks: + fields: + state: + fieldName: "State" + conditions: + resolved: + - 'Closed' + - 'Performed' diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/src/index.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/src/index.ts new file mode 100644 index 00000000..c98726ff --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/src/index.ts @@ -0,0 +1,129 @@ +#! /usr/bin/env node +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { AxiosInstance } from 'axios' +import * as fs from 'fs' +import { exit } from 'process' +import { createHttpClient } from './utils/http-client.js' +import { ApiDetails, getApiDetails } from './utils/api-details.js' +import { getPath } from './utils/util.js' +import { + EnvironmentError, + WorkItemsNotFoundError, +} from './utils/custom-errors.js' +import { readFile, writeFile } from 'fs/promises' +import YAML from 'yaml' +import path from 'path' +import { AppOutput, InitLogger } from '@B-S-F/autopilot-utils' +import { WorkItemConfigData } from './work-item/work-item-config-data.js' +import { Headers, WorkItem, createHeaders } from './work-item/work-item.js' + +const EVIDENCE_PATH_ENV_VAR = 'evidence_path' +const CONFIG_FILE_ENV_VAR = 'ADO_CONFIG_FILE_PATH' + +const main = async () => { + const logger = InitLogger('ado-fetcher', 'info') + logger.debug('Starting ADO Fetcher') + try { + if (process.env.NODE_TLS_REJECT_UNAUTHORIZED == '0') { + throw new EnvironmentError( + 'Environment variable NODE_TLS_REJECT_UNAUTHORIZED must not be set to 0 for security reasons' + ) + } + const evidencePath: string = getEvidencePath() + const evaluatorConfigFilePath = getConfigPath() + const outputFileName: string = + process.env.ADO_WORK_ITEMS_JSON_NAME ?? 'data.json' + const outputFilePath = path.join(evidencePath, outputFileName) + logger.debug(`Output file path: ${outputFilePath}`) + if (fs.existsSync(outputFilePath)) { + throw new EnvironmentError( + `File ${outputFilePath} exists already, can't write evidence!` + ) + } + const enableProxy = process.env.ADO_APPLY_PROXY_SETTINGS === 'true' + const apiDetails: ApiDetails = getApiDetails() + logger.debug(`API Details: ${JSON.stringify(apiDetails)}`) + // setup config + const configFileData = await YAML.parse( + await readFile(evaluatorConfigFilePath, { encoding: 'utf8' }) + ) + const configData: WorkItemConfigData = new WorkItemConfigData( + configFileData + ) + const httpClient: AxiosInstance = createHttpClient({ + azureDevOpsUrl: apiDetails.url, + enableProxy, + }) + const headers: Headers = createHeaders(apiDetails.personalAccessToken) + + // get data + logger.debug(`Starting to fetch data...`) + const workItem = new WorkItem(headers, httpClient, configData, apiDetails) + logger.debug(`Querying work item references...`) + const workItemReferences = await workItem.queryReferences() + logger.debug(`Found ${workItemReferences.length} work item references`) + const workItems = await workItem.getDetails(workItemReferences) + logger.debug(`Found ${workItems.length} work items`) + const workItemsFiltered = workItem.filterData(workItems) + logger.debug(`Filtered down to ${workItemsFiltered.length} work items`) + const dataToExport = { + workItems: workItemsFiltered, + } + + await writeFile(outputFilePath, JSON.stringify(dataToExport)) + logger.debug(`Wrote data to ${outputFilePath}`) + } catch (error: any) { + console.error(error) + exit(1) + } +} + +function getEvidencePath() { + const evidencePath: string = getPath(EVIDENCE_PATH_ENV_VAR) + try { + fs.accessSync(evidencePath, fs.constants.W_OK) + } catch (e) { + throw new EnvironmentError(`${evidencePath} is not writable!`) + } + if (!fs.statSync(evidencePath).isDirectory()) { + throw new EnvironmentError( + `${EVIDENCE_PATH_ENV_VAR} does not point to a directory!` + ) + } + return evidencePath +} + +function getConfigPath() { + const configPath: string = getPath(CONFIG_FILE_ENV_VAR) + try { + fs.accessSync(configPath, fs.constants.R_OK) + } catch (e) { + throw new EnvironmentError(`${configPath} is not readable!`) + } + if (!fs.statSync(configPath).isFile()) { + throw new EnvironmentError( + `${CONFIG_FILE_ENV_VAR} does not point to a file!` + ) + } + return configPath +} + +try { + main() +} catch (error) { + if ( + error instanceof EnvironmentError || + error instanceof WorkItemsNotFoundError + ) { + const output = new AppOutput() + output.setStatus('FAILED') + output.setReason(error.Reason()) + output.write() + process.exit(0) + } else { + throw error // to show the stack trace + } +} diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/src/utils/api-details.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/src/utils/api-details.ts new file mode 100644 index 00000000..0ebdb67b --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/src/utils/api-details.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { getEnvVariable } from './util.js' + +export const ADO_API_VERSION = '6.0' + +export type ApiDetails = ApiBaseDetails & ApiRequestDetails + +export interface ApiBaseDetails { + url: string + wiql: string +} + +export interface ApiRequestDetails { + org: string + project: string + personalAccessToken: string +} + +export function getApiDetails(): ApiDetails { + const org = getEnvVariable('ADO_API_ORG') + const project = getEnvVariable('ADO_API_PROJECT') + const personalAccessToken = getEnvVariable('ADO_API_PERSONAL_ACCESS_TOKEN') + + return { + wiql: process.env.ADO_API_WIQL ?? '_apis/wit/wiql', + url: process.env.ADO_URL ?? 'https://dev.azure.com', + org, + project, + personalAccessToken, + } +} + +export function createApiUrl(apiDetails: ApiDetails) { + if (!apiDetails.url.match(/^https:\/\//)) { + throw new Error('ADO fetcher can only establish https-secured connections') + } + const urlStr = `${apiDetails.url}/${apiDetails.org}/${apiDetails.project}/${apiDetails.wiql}` + const url: URL = new URL(urlStr) + url.searchParams.append('api-version', ADO_API_VERSION) + return url +} diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/src/utils/custom-errors.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/src/utils/custom-errors.ts new file mode 100644 index 00000000..79bc7030 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/src/utils/custom-errors.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { AppError } from '@B-S-F/autopilot-utils' + +export class WorkItemsNotFoundError extends AppError { + constructor(reason: string) { + super(reason) + this.name = 'WorkItemsNotFoundError' + } + + Reason(): string { + return super.Reason() + } +} + +export class EnvironmentError extends AppError { + constructor(reason: string) { + super(reason) + this.name = 'EnvironmentError' + } + Reason(): string { + return super.Reason() + } +} diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/src/utils/http-client.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/src/utils/http-client.ts new file mode 100644 index 00000000..ae3aceea --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/src/utils/http-client.ts @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import axios, { AxiosInstance } from 'axios' +import { Agent } from 'http' +import isValidHostname from 'is-valid-hostname' +import { httpsOverHttp } from 'tunnel' + +interface AdoHttpClientArgs { + azureDevOpsUrl: string + enableProxy?: boolean +} + +export function createHttpClient( + adoHttpClientArgs: AdoHttpClientArgs +): AxiosInstance { + let proxyTunnel: Agent + let httpClient: AxiosInstance + if (adoHttpClientArgs.enableProxy) { + proxyTunnel = httpsOverHttp({ + proxy: { + host: getProxyHost(), + port: getProxyPort(), + }, + }) + httpClient = axios.create({ + baseURL: adoHttpClientArgs.azureDevOpsUrl, + httpsAgent: proxyTunnel, + proxy: false, + }) + } else { + httpClient = axios.create({ baseURL: adoHttpClientArgs.azureDevOpsUrl }) + } + return httpClient +} + +function getProxyHost(): string { + let proxyHost: string | undefined = process.env.PROXY_HOST + if (proxyHost === undefined || proxyHost.trim() === '') { + throw new ReferenceError( + 'The environment variable "PROXY_HOST" is not set!' + ) + } + proxyHost = proxyHost.trim() + if (!isValidHostname(proxyHost)) { + throw new Error(`invalid PROXY_HOST: ${proxyHost}`) + } + return proxyHost +} + +function getProxyPort(): number { + let proxyPortAsString: string | undefined = process.env.PROXY_PORT + if (proxyPortAsString === undefined || proxyPortAsString.trim() === '') { + throw new ReferenceError('The environment variable PROXY_PORT" is not set!') + } + proxyPortAsString = proxyPortAsString.trim() + if (!proxyPortAsString.match(/^[0-9]{1,5}$/)) { + throw new Error('environment variable PROXY_PORT must contain digits only') + } + const proxyPort: number = parseInt(proxyPortAsString, 10) + if (isNaN(proxyPort) || proxyPort <= 0 || proxyPort > 65535) { + throw new Error( + 'environment variable PROXY_PORT does not represent an integer value in the range 0 < PROXY_PORT < 65535' + ) + } + return proxyPort +} diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/src/utils/util.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/src/utils/util.ts new file mode 100644 index 00000000..3da6e427 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/src/utils/util.ts @@ -0,0 +1,20 @@ +import fs from 'fs' +import { EnvironmentError } from './custom-errors.js' + +export function getEnvVariable(envVariableName: string): string { + const envVariable: string | undefined = process.env[envVariableName] + if (envVariable === undefined || envVariable.trim() === '') { + throw new EnvironmentError( + `The environment variable "${envVariableName}" is not set!` + ) + } + return envVariable.trim() +} + +export function getPath(envVariableName: string): string { + const path: string = getEnvVariable(envVariableName) + if (!fs.existsSync(path)) { + throw new Error(`${envVariableName} points to non-existing path ${path}`) + } + return path +} diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/src/work-item/ado-fetcher-config.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/src/work-item/ado-fetcher-config.ts new file mode 100644 index 00000000..6e57898a --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/src/work-item/ado-fetcher-config.ts @@ -0,0 +1,16 @@ +import { RelationsSchema } from './relations.js' +import { z } from 'zod' + +export const WorkItemsConfigSchema = z.object({ + query: z.string().min(1).optional(), + neededFields: z.array(z.string().min(1)).optional(), + hierarchyDepth: z.number().int().positive().optional(), + relations: RelationsSchema.optional(), +}) + +export type WorkItemsConfig = z.infer + +export const AdoFetcherConfigSchema = z.object({ + workItems: WorkItemsConfigSchema, +}) +export type AdoFetcherConfig = z.infer diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/src/work-item/index.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/src/work-item/index.ts new file mode 100644 index 00000000..dd3e2855 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/src/work-item/index.ts @@ -0,0 +1,4 @@ +export * from './ado-fetcher-config.js' +export * from './relations.js' +export * from './work-item.js' +export * from './work-item-config-data.js' diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/src/work-item/relations.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/src/work-item/relations.ts new file mode 100644 index 00000000..9a849f78 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/src/work-item/relations.ts @@ -0,0 +1,23 @@ +import { z } from 'zod' +export const relationTypes = ['Related', 'Child', 'Parent'] as const +const RelationTypesLiterals = relationTypes.map((relationType) => + z.literal(relationType) +) +const [first, second, ...others] = RelationTypesLiterals +export const RelationTypeSchema = z.union([first, second, ...others]) +export type RelationType = z.infer + +const BaseRelationsSchema = z.object({ + get: z.boolean().optional(), + relationType: RelationTypeSchema.optional(), +}) + +export type Relations = z.infer & { + relations?: Relations +} + +export const RelationsSchema: z.ZodType = BaseRelationsSchema.extend( + { + relations: z.lazy(() => RelationsSchema.optional()), + } +) diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/src/work-item/work-item-config-data.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/src/work-item/work-item-config-data.ts new file mode 100644 index 00000000..817c7426 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/src/work-item/work-item-config-data.ts @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { generateErrorMessage } from 'zod-error' +import { SafeParseReturnType } from 'zod/lib/types' +import { + AdoFetcherConfig, + AdoFetcherConfigSchema, + RelationType, +} from './index.js' + +const DEFAULT_FIELDS = ['State', 'Title'] + +export interface YamlData { + [key: string]: any +} + +export class WorkItemConfigData { + private readonly hierarchyDepth: number + private readonly data: AdoFetcherConfig + constructor(private readonly fetcherConfig: unknown) { + const result: SafeParseReturnType = + AdoFetcherConfigSchema.safeParse(fetcherConfig) + if (result.success) { + this.data = result.data + } else { + throw new Error(generateErrorMessage(result.error.issues)) + } + + this.hierarchyDepth = this.data.workItems?.hierarchyDepth ?? NaN + } + getRequestedFields(): string[] { + const uncheckedNeededFields: unknown[] = + this.data?.workItems?.neededFields ?? [] + for (const neededField of uncheckedNeededFields) { + if (typeof neededField !== 'string') { + throw new Error('workItems.neededFields may only contain string values') + } + } + const neededFields: string[] = uncheckedNeededFields as string[] + let combinedNeededFields = [...neededFields, ...DEFAULT_FIELDS] + combinedNeededFields = combinedNeededFields.map((field: string) => { + const trimmed: string = field.trim() + return trimmed.charAt(0).toLowerCase() + field.slice(1) + }) + return combinedNeededFields.filter((field: string) => field !== '') + } + + // get the relation type from the yaml file recursively + getRelationType(depth: number): RelationType | 'any' { + const iterations = this.getHierarchyDepth() - depth + let workItems = this.data.workItems + for (let i = 0; i < iterations; i++) { + if (workItems.relations) workItems = workItems.relations + } + return workItems.relations?.relationType ?? 'any' + } + + private calculateHierarchyDepth(): number { + let workItems = this.data.workItems + let depth = 0 + while (workItems.relations && workItems.relations.get === true) { + workItems = workItems.relations + depth++ + } + return depth + } + + getQuery(): string | undefined { + return this.data.workItems.query + } + + getHierarchyDepth(): number { + if (this.hierarchyDepth) return this.hierarchyDepth + return this.calculateHierarchyDepth() + } +} diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/src/work-item/work-item.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/src/work-item/work-item.ts new file mode 100644 index 00000000..2705ff0b --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/src/work-item/work-item.ts @@ -0,0 +1,236 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { Issue } from '@B-S-F/issue-validators' +import { AxiosInstance, isAxiosError } from 'axios' +import { + ADO_API_VERSION, + ApiDetails, + createApiUrl, +} from '../utils/api-details.js' +import { WorkItemsNotFoundError } from '../utils/custom-errors.js' +import { WorkItemConfigData, YamlData } from './work-item-config-data.js' + +export interface WiqlRequestBody { + query?: string +} +export interface Headers { + [key: string]: string +} + +export interface WorkItemReference { + id: number + url: string +} + +export function createHeaders(personalAccessToken: string): Headers { + return { + Authorization: + 'Basic ' + Buffer.from(':' + personalAccessToken).toString('base64'), + Accept: 'application/json', + 'Content-Type': 'application/json', + } +} + +export function createWiqlRequestBody( + query: string | undefined +): WiqlRequestBody { + return { + query: query, + } +} + +export class WorkItem { + constructor( + private readonly headers: Headers, + private readonly httpClient: AxiosInstance, + private readonly configData: WorkItemConfigData, + private readonly apiDetails: ApiDetails + ) {} + + async queryReferences(): Promise { + const url: URL = createApiUrl(this.apiDetails) + const wiqlRequestBody = createWiqlRequestBody(this.configData.getQuery()) + let response + try { + response = await this.httpClient.post(url.href, wiqlRequestBody, { + headers: this.headers, + }) + } catch (error) { + if (isAxiosError(error) && error.response) { + if (error.response.status == 400) { + throw new Error( + `Request failed with status code ${error.response.status} ${error.response.statusText}. Please check your WIQL query for errors.` + ) + } + throw new Error( + `Request failed with status code ${error.response.status} ${error.response.statusText}` + ) + } + throw error + } + const contentType = response.headers['content-type'] + if ( + response.status == 203 && + contentType && + contentType?.toString().split(';', 1)[0] == 'text/html' + ) { + throw new Error( + `Server returned status 203 and some HTML code instead of JSON. It could be that your API token is wrong!` + ) + } + if (response.data.workItems) { + return response.data.workItems + } else { + return [] + } // + } + + private async getRecursively( + parentUrl: string, + depth: number + ): Promise { + const url: URL = new URL(parentUrl) + url.searchParams.append('api-version', ADO_API_VERSION) + url.searchParams.append('$expand', 'relations') + let response + try { + response = await this.httpClient.get(url.href, { + headers: this.headers, + }) + } catch (error: any) { + const errorMessage = error.message ? error.message : error + console.log( + `Couldn't fetch relations, following error occurred: "${errorMessage}"` + ) + return {} + } + + if (response.data.relations && response.data.relations.length > 0) { + const relationsData = [] + for (const relation of response.data.relations) { + if (depth > 0) { + const data = await this.getRecursively(relation.url, depth - 1) + if (typeof data === 'object' && Object.keys(data).length !== 0) { + data.relationType = relation.attributes.name + relationsData.push(data) + } + } + } + response.data.relations = relationsData + } + return response.data + } + + async getDetails(referenceList: WorkItemReference[]) { + const data: Issue[] = [] + for (const reference of referenceList) { + try { + const depth = this.configData.getHierarchyDepth() + const recursiveData = await this.getRecursively(reference.url, depth) + data.push(recursiveData) + } catch (error: any) { + const errorMessage = error.message ? error.message : error + throw Error(`Couldn't fetch details from work item reference with id "${ + reference.id + }" - "${ + reference.url + }" at depth "${this.configData.getHierarchyDepth()}" + , following error occurred: "${errorMessage}"`) + } + } + return data + } + + private filterFields(workItem: Issue, neededFieldNames: string[]): Issue { + const filteredWorkItem: Issue = { + id: workItem.id || null, + url: workItem._links ? workItem._links.html.href : '', + ...(workItem.relationType && { relationType: workItem.relationType }), + ...(workItem.error && { error: workItem.error }), + } + const allFieldNames = Object.keys(workItem.fields || {}) + + for (const neededFieldName of neededFieldNames) { + // field name at the end with case insensitivity + const regex = new RegExp(`${neededFieldName}$`, 'gi') + const matchingKeys = allFieldNames.filter((field) => regex.test(field)) + + if (matchingKeys.length) { + const firstFoundValue = matchingKeys[0] + filteredWorkItem[neededFieldName] = workItem.fields[firstFoundValue] + } else { + console.warn( + `The field '${neededFieldName}' is not available on work item with id ${workItem.id}` + ) + } + } + return filteredWorkItem + } + + private filterFieldsFromAllLevels( + workItems: Issue[], + neededFieldNames: string[] + ): Issue[] { + const filteredWorkItems: Issue[] = [] + workItems.forEach((workItem: Issue) => { + const filteredData = this.filterFields(workItem, neededFieldNames) + if (workItem.relations) { + filteredData.relations = this.filterFieldsFromAllLevels( + workItem.relations as Issue[], + neededFieldNames + ) + } + filteredWorkItems.push(filteredData) + }) + return filteredWorkItems + } + + private filterRelations( + workItems: Issue[], + depth: number, + configData: YamlData + ): Issue[] { + workItems.forEach((workItem: Issue) => { + if ('relations' in workItem && workItem.relations.length !== 0) { + const filteredRelations = [] + for (const relation of workItem.relations) { + const relationType = this.configData.getRelationType(depth) + if ( + relationType === 'any' || + relation.relationType === relationType + ) { + filteredRelations.push(relation) + } + } + workItem.relations = filteredRelations + if (workItem.relations.length !== 0 && depth > 0) { + workItem.relations = this.filterRelations( + workItem.relations, + depth - 1, + configData + ) + } + } + }) + return workItems + } + + filterData(workItems: Issue[]): Issue[] { + if (!workItems.length) { + throw new WorkItemsNotFoundError('No work items found!') + } + const neededFieldNames: string[] = this.configData.getRequestedFields() + const filteredWorkItems: Issue[] = this.filterFieldsFromAllLevels( + workItems, + neededFieldNames + ) + const finalWorkItems: Issue[] = this.filterRelations( + filteredWorkItems, + this.configData.getHierarchyDepth(), + this.configData + ) + return finalWorkItems + } +} diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/ado-fetcher.int-spec.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/ado-fetcher.int-spec.ts new file mode 100644 index 00000000..0abf65b5 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/ado-fetcher.int-spec.ts @@ -0,0 +1,258 @@ +import * as fs from 'fs' +import * as path from 'path' +import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { + MockServer, + ReceivedRequest, + run, + RunProcessResult, +} from '../../../../integration-tests/src/util' +import { + defaultAdoEnvironment, + adoFetcherExecutable, + evidencePath, + mockServerPort, + verifyNoOutputFileWasWritten, +} from './common' +import { getAdoFixtures } from './fixtures/ado-fixtures' + +describe('Ado Fetcher', () => { + let mockServer: MockServer + + beforeAll(() => { + expect(fs.existsSync(adoFetcherExecutable)).to.be.true + }) + + beforeEach(() => { + fs.mkdirSync(evidencePath) + mockServer = new MockServer(getAdoFixtures(mockServerPort)) + }) + + afterEach(async () => { + await mockServer?.stop() + fs.rmSync(evidencePath, { recursive: true }) + }) + + it.each([ + 'evidence_path', + 'ADO_CONFIG_FILE_PATH', + 'ADO_API_ORG', + 'ADO_API_PROJECT', + 'ADO_API_PERSONAL_ACCESS_TOKEN', + ])( + `should fail when env variable %s is not set`, + async (variableName: string) => { + const forbiddenValues = ['', ' ', '\t', '\n'] as const + + for (const forbiddenValue of forbiddenValues) { + const env = { + ...defaultAdoEnvironment, + ...{ [variableName]: forbiddenValue }, + } + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env, + }) + + expect(result.exitCode).toEqual(1) + expect(result.stderr.length).toBeGreaterThan(0) + const expectedErrorMessage = `AppError [EnvironmentError]: The environment variable "${variableName}" is not set!` + expect(result.stderr[0]).toEqual(expectedErrorMessage) + expect(mockServer.getNumberOfRequests()).toEqual(0) + verifyNoOutputFileWasWritten() + } + } + ) + + it('should fail when no work items were returned by ADO', async () => { + await mockServer.stop() + mockServer = new MockServer({ + port: mockServerPort, + https: true, + responses: { + '/adoApiOrg/adoApiProject/_apis/wit/wiql': { + post: { + responseStatus: 200, + responseBody: { + workItems: [], + }, + }, + }, + }, + }) + + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: defaultAdoEnvironment, + }) + + expect(result.exitCode).toEqual(1) + expect(result.stdout).length(0) + expect(result.stderr.length).toBeGreaterThan(0) + expect(result.stderr[0]).toEqual( + 'AppError [WorkItemsNotFoundError]: No work items found!' + ) + expect(mockServer.getNumberOfRequests()).toEqual(1) + verifyNoOutputFileWasWritten() + }) + + it('should fail when ADO WIQL endpoint returns 404', async () => { + await mockServer.stop() + mockServer = new MockServer({ + port: mockServerPort, + https: true, + responses: { + '/adoApiOrg/adoApiProject/_apis/wit/wiql': { + post: { + responseStatus: 404, + }, + }, + }, + }) + + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: defaultAdoEnvironment, + }) + + expect(result.exitCode).toEqual(1) + expect(result.stdout).length(0) + expect(result.stderr.length).toBeGreaterThan(0) + expect(result.stderr[0]).toContain('Request failed with status code 404') + expect(mockServer.getNumberOfRequests()).toEqual(1) + verifyNoOutputFileWasWritten() + }) + + it('should succeed when config file contains a WIQL query', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...defaultAdoEnvironment, + ADO_CONFIG_FILE_PATH: path.join( + __dirname, + 'fixtures', + 'config-wiql.yaml' + ), + }, + }) + + // verify process terminated successfully + expect(result.exitCode).toEqual(0) + expect(result.stdout).length(0) + expect(result.stderr).length(0) + + // only verify WIQL request + const requests: ReceivedRequest[] = mockServer.getRequests( + '/adoApiOrg/adoApiProject/_apis/wit/wiql', + 'post' + ) + expect(requests).length(1) + expect(requests[0].headers.authorization).toEqual('Basic OnBhdA==') + expect(requests[0].query['api-version']).toEqual('6.0') + expect(requests[0].body).toEqual({ + query: 'SELECT [System.Id] FROM workitems', + }) + }) + + it('should succeed when config file contains no WIQL query', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: defaultAdoEnvironment, + }) + + // verify process terminated successfully + expect(result.exitCode).toEqual(0) + expect(result.stdout).length(0) + expect(result.stderr).length(0) + + // verify requests were sent correctly + function verifyRequests(receivedRequests: ReceivedRequest[]): void { + expect(receivedRequests).length(1) + expect(receivedRequests[0].headers.authorization).toEqual( + 'Basic OnBhdA==' + ) + expect(receivedRequests[0].query['api-version']).toEqual('6.0') + } + let requests: ReceivedRequest[] = mockServer.getRequests( + '/adoApiOrg/adoApiProject/_apis/wit/wiql', + 'post' + ) + verifyRequests(requests) + expect(requests[0].body).toEqual({}) + + // work items 1, 2, 4 are requested once + const workItemIds: number[] = [1, 2, 4] + workItemIds.forEach((workItemId) => { + requests = mockServer.getRequests( + `/adoApiOrg/adoApiProject/_apis/wit/workitems/${workItemId}`, + 'get' + ) + verifyRequests(requests) + expect(requests[0].query['$expand']).toEqual('relations') + }) + + // work item 3 is requested twice + requests = mockServer.getRequests( + '/adoApiOrg/adoApiProject/_apis/wit/workitems/3', + 'get' + ) + expect(requests).length(2) + expect(requests[0]).toEqual(requests[1]) + expect(requests[0].headers.authorization).toEqual('Basic OnBhdA==') + expect(requests[0].query['api-version']).toEqual('6.0') + expect(requests[0].query['$expand']).toEqual('relations') + + // finally verify no more requests than those verified above have been sent + expect(mockServer.getNumberOfRequests()).toEqual(6) + + // verify output file was written correctly + const outputFile: string = path.join(evidencePath, 'data.json') + const outputFileContentAsString: string = fs.readFileSync(outputFile, { + encoding: 'utf-8', + }) + const outputFileContentAsJson: unknown = JSON.parse( + outputFileContentAsString + ) + + expect(outputFileContentAsJson).toEqual({ + workItems: [ + { + id: 1, + url: '', + foo: 'fooW1', + bar: 'barW1', + state: 'stateW1', + title: 'titleW1', + relations: [ + { + id: 2, + url: '', + relationType: 'Related', + foo: 'fooW2', + bar: 'barW2', + state: 'stateW2', + title: 'titleW2', + relations: [ + { + id: 3, + url: '', + relationType: 'Related', + foo: 'fooW3', + bar: 'barW3', + state: 'stateW3', + title: 'titleW3', + relations: [], + }, + ], + }, + { + id: 3, + url: '', + relationType: 'Related', + foo: 'fooW3', + bar: 'barW3', + state: 'stateW3', + title: 'titleW3', + relations: [], + }, + ], + }, + ], + }) + }) +}) diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/common.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/common.ts new file mode 100644 index 00000000..363d80b5 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/common.ts @@ -0,0 +1,31 @@ +import * as fs from 'fs' +import * as path from 'path' +import { expect } from 'vitest' +import { MOCK_SERVER_CERT_PATH } from '../../../../integration-tests/src/util' + +export const fixturesPath: string = path.join(__dirname, 'fixtures') +export const evidencePath: string = path.join(__dirname, 'evidence_tmp') +export const adoFetcherExecutable: string = path.join( + __dirname, + '..', + '..', + 'dist', + 'index.js' +) + +export const mockServerPort = 8080 + +export const defaultAdoEnvironment = { + ADO_URL: `https://localhost:${mockServerPort}`, + ADO_API_ORG: 'adoApiOrg', + ADO_API_PROJECT: 'adoApiProject', + ADO_API_PERSONAL_ACCESS_TOKEN: 'pat', // will be base64-encoded as OnBhdA== in the auth header + ADO_CONFIG_FILE_PATH: path.join(fixturesPath, 'config.yaml'), + evidence_path: evidencePath, + NODE_EXTRA_CA_CERTS: MOCK_SERVER_CERT_PATH, +} as const + +export function verifyNoOutputFileWasWritten() { + const outputFile: string = path.join(evidencePath, 'data.json') + expect(fs.existsSync(outputFile)).toEqual(false) +} diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/ado-fixtures.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/ado-fixtures.ts new file mode 100644 index 00000000..a4f5c089 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/ado-fixtures.ts @@ -0,0 +1,124 @@ +import { MockServerOptions } from '../../../../../integration-tests/src/util' + +export function getAdoFixtures(port: number): MockServerOptions { + const host = `https://localhost:${port}` + + const workItem1Url = '/adoApiOrg/adoApiProject/_apis/wit/workitems/1' + const workItem2Url = '/adoApiOrg/adoApiProject/_apis/wit/workitems/2' + const workItem3Url = '/adoApiOrg/adoApiProject/_apis/wit/workitems/3' + const workItem4Url = '/adoApiOrg/adoApiProject/_apis/wit/workitems/4' + + return { + port, + https: true, + responses: { + [`/adoApiOrg/adoApiProject/_apis/wit/wiql`]: { + post: { + responseStatus: 200, + responseBody: { + workItems: [ + { + id: 1, + url: `${host}${workItem1Url}`, + }, + ], + }, + }, + }, + [workItem1Url]: { + get: { + responseStatus: 200, + responseBody: { + id: 1, + url: workItem1Url, + relations: [ + { + url: `${host}${workItem2Url}`, + attributes: { + name: 'Related', + }, + }, + { + url: `${host}${workItem3Url}`, + attributes: { + name: 'Related', + }, + }, + { + url: `${host}${workItem4Url}`, + attributes: { + name: 'Child', + }, + }, + ], + fields: { + foo: 'fooW1', + bar: 'barW1', + state: 'stateW1', + title: 'titleW1', + some: 'value', + }, + }, + }, + }, + [workItem2Url]: { + get: { + responseStatus: 200, + responseBody: { + id: 2, + url: workItem2Url, + relations: [ + { + url: `${host}${workItem3Url}`, + attributes: { + name: 'Related', + }, + }, + ], + fields: { + Foo: 'fooW2', + Bar: 'barW2', + State: 'stateW2', + Title: 'titleW2', + some: 'value', + }, + }, + }, + }, + [workItem3Url]: { + get: { + responseStatus: 200, + responseBody: { + id: 3, + url: workItem3Url, + relations: [], + fields: { + abcFoo: 'fooW3', + abcBar: 'barW3', + abcState: 'stateW3', + abcTitle: 'titleW3', + some: 'value', + }, + }, + }, + }, + [workItem4Url]: { + get: { + responseStatus: 200, + responseBody: { + id: 4, + url: workItem4Url, + relations: [], + fields: { + foo: 'fooW4', + bar: 'barW4', + state: 'stateW4', + title: 'titleW4', + some: 'value', + }, + }, + }, + }, + }, + } +} diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-empty.yaml b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-empty.yaml new file mode 100644 index 00000000..e69de29b diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-hierarchydepth-is-floating-point-number.yaml b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-hierarchydepth-is-floating-point-number.yaml new file mode 100644 index 00000000..aa3204d0 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-hierarchydepth-is-floating-point-number.yaml @@ -0,0 +1,2 @@ +workItems: + hierarchyDepth: 1.1 diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-hierarchydepth-is-negative.yaml b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-hierarchydepth-is-negative.yaml new file mode 100644 index 00000000..40fdb159 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-hierarchydepth-is-negative.yaml @@ -0,0 +1,2 @@ +workItems: + hierarchyDepth: -2 diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-hierarchydepth-is-no-number.yaml b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-hierarchydepth-is-no-number.yaml new file mode 100644 index 00000000..9391b0b0 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-hierarchydepth-is-no-number.yaml @@ -0,0 +1,2 @@ +workItems: + hierarchyDepth: abc diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-neededfields-contains-object.yaml b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-neededfields-contains-object.yaml new file mode 100644 index 00000000..d36daaf3 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-neededfields-contains-object.yaml @@ -0,0 +1,3 @@ +workItems: + neededFields: + - foo: bar diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-neededfields-is-no-array.yaml b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-neededfields-is-no-array.yaml new file mode 100644 index 00000000..7f169070 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-neededfields-is-no-array.yaml @@ -0,0 +1,2 @@ +workItems: + neededFields: 123 diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-no-object.yaml b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-no-object.yaml new file mode 100644 index 00000000..1edcf3d7 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-no-object.yaml @@ -0,0 +1 @@ +this is a plaintext diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-query-is-no-string.yaml b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-query-is-no-string.yaml new file mode 100644 index 00000000..fe9e9bd5 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-query-is-no-string.yaml @@ -0,0 +1,2 @@ +workItems: + query: 123 diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-relations-get-is-no-boolean.yaml b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-relations-get-is-no-boolean.yaml new file mode 100644 index 00000000..4d4f26a2 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-relations-get-is-no-boolean.yaml @@ -0,0 +1,6 @@ +workItems: + relations: + get: true + relationType: 'Related' + relations: + get: abc diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-relations-is-no-object.yaml b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-relations-is-no-object.yaml new file mode 100644 index 00000000..95feed78 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-relations-is-no-object.yaml @@ -0,0 +1,2 @@ +workItems: + relations: abc diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-relations-type-is-unsupported.yaml b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-relations-type-is-unsupported.yaml new file mode 100644 index 00000000..66cb00c0 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-relations-type-is-unsupported.yaml @@ -0,0 +1,6 @@ +workItems: + relations: + get: true + relationType: 'Related' + relations: + relationType: 'UnsupportedType' diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-wiql.yaml b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-wiql.yaml new file mode 100644 index 00000000..55f9cb26 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-wiql.yaml @@ -0,0 +1,2 @@ +workItems: + query: 'SELECT [System.Id] FROM workitems' diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-workitems-is-empty.yaml b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-workitems-is-empty.yaml new file mode 100644 index 00000000..2fc61b42 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-workitems-is-empty.yaml @@ -0,0 +1 @@ +workItems: diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-workitems-no-object.yaml b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-workitems-no-object.yaml new file mode 100644 index 00000000..5a373051 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config-workitems-no-object.yaml @@ -0,0 +1 @@ +workItems: abc diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config.yaml b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config.yaml new file mode 100644 index 00000000..47d1f968 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/fixtures/config.yaml @@ -0,0 +1,8 @@ +workItems: + hierarchyDepth: 2 + neededFields: + - foo + - bar + relations: + get: true + relationType: 'Related' diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/io-config.int-spec.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/io-config.int-spec.ts new file mode 100644 index 00000000..efd47055 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/io-config.int-spec.ts @@ -0,0 +1,190 @@ +import * as fs from 'fs' +import * as path from 'path' +import { describe } from 'vitest' +import { afterEach, beforeAll, beforeEach, expect, it } from 'vitest' +import { + MockServer, + RunProcessResult, + run, +} from '../../../../integration-tests/src/util' +import { + adoFetcherExecutable, + defaultAdoEnvironment, + evidencePath, + mockServerPort, +} from './common' +import { getAdoFixtures } from './fixtures/ado-fixtures' +import { verifyError } from './test-utils' + +describe('I/O Configuration', () => { + let mockServer: MockServer + + beforeAll(() => { + expect(fs.existsSync(adoFetcherExecutable)).to.be.true + }) + + beforeEach(() => { + fs.mkdirSync(evidencePath) + mockServer = new MockServer(getAdoFixtures(mockServerPort)) + }) + + afterEach(async () => { + await mockServer?.stop() + fs.rmSync(evidencePath, { recursive: true }) + }) + + it('should fail if evidence_path does not exist', async () => { + const evidencePath: string = path.join( + defaultAdoEnvironment.evidence_path, + 'other' + ) + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...defaultAdoEnvironment, + evidence_path: evidencePath, + }, + }) + verifyError( + result, + `Error: evidence_path points to non-existing path ${evidencePath}`, + mockServer + ) + }) + + it('should fail if evidence_path is not writable', async () => { + fs.chmodSync(evidencePath, 0o444) // change access to read-only + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: defaultAdoEnvironment, + }) + verifyError( + result, + `AppError [EnvironmentError]: ${evidencePath} is not writable!`, + mockServer + ) + fs.chmodSync(evidencePath, 0o777) // grant all access rights for cleanup + }) + + it('should fail if evidence_path points to a file', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...defaultAdoEnvironment, + evidence_path: defaultAdoEnvironment.ADO_CONFIG_FILE_PATH, + }, + }) + verifyError( + result, + 'AppError [EnvironmentError]: evidence_path does not point to a directory!', + mockServer + ) + }) + + it('should fail if config file does not exist', async () => { + const configPath: string = path.join( + __dirname, + 'fixtures', + 'non-existent.yaml' + ) + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...defaultAdoEnvironment, + ADO_CONFIG_FILE_PATH: configPath, + }, + }) + verifyError( + result, + `Error: ADO_CONFIG_FILE_PATH points to non-existing path ${configPath}`, + mockServer + ) + }) + + it('should fail if config file is not readable', async () => { + const configPath: string = path.join( + __dirname, + 'fixtures', + 'other-config.yaml' + ) + fs.copyFileSync(defaultAdoEnvironment.ADO_CONFIG_FILE_PATH, configPath) + fs.chmodSync(configPath, 0o222) // change access to write-only + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...defaultAdoEnvironment, + ADO_CONFIG_FILE_PATH: configPath, + }, + }) + verifyError( + result, + `AppError [EnvironmentError]: ${configPath} is not readable!`, + mockServer + ) + fs.rmSync(configPath) // cleanup + }) + + it('should fail if ADO_CONFIG_FILE_PATH points to a directory', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...defaultAdoEnvironment, + ADO_CONFIG_FILE_PATH: defaultAdoEnvironment.evidence_path, + }, + }) + verifyError( + result, + 'AppError [EnvironmentError]: ADO_CONFIG_FILE_PATH does not point to a file!', + mockServer + ) + }) + + it('should fail if ADO_WORK_ITEMS_JSON_NAME exists already', async () => { + const outputFileName = 'output.json' + const outputFilePath: string = path.join( + defaultAdoEnvironment.evidence_path, + outputFileName + ) + fs.writeFileSync(outputFilePath, 'some data') + expect(fs.existsSync(outputFilePath)).toEqual(true) // check precondition: output file created + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...defaultAdoEnvironment, + ADO_WORK_ITEMS_JSON_NAME: outputFileName, + }, + }) + verifyError( + result, + `AppError [EnvironmentError]: File ${outputFilePath} exists already, can't write evidence!`, + mockServer + ) + fs.rmSync(outputFilePath) // cleanup + }) + + it('should fail if ADO_URL is not https secured', async () => { + await mockServer?.stop() + mockServer = new MockServer({ + ...getAdoFixtures(mockServerPort), + https: false, + }) + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...defaultAdoEnvironment, + ADO_URL: `http://localhost:${mockServerPort}`, + }, + }) + verifyError( + result, + 'Error: ADO fetcher can only establish https-secured connections', + mockServer + ) + }) + + it('should fail if NODE_TLS_REJECT_UNAUTHORIZED is set to 0', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...defaultAdoEnvironment, + NODE_TLS_REJECT_UNAUTHORIZED: '0', + }, + }) + verifyError( + result, + 'AppError [EnvironmentError]: Environment variable NODE_TLS_REJECT_UNAUTHORIZED must not be set to 0 for security reasons', + mockServer + ) + }) +}) diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/proxy-settings.int-spec.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/proxy-settings.int-spec.ts new file mode 100644 index 00000000..ad4fad92 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/proxy-settings.int-spec.ts @@ -0,0 +1,170 @@ +import * as fs from 'fs' +import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { + MockServer, + RunProcessResult, + run, +} from '../../../../integration-tests/src/util' +import { + adoFetcherExecutable, + defaultAdoEnvironment, + evidencePath, + mockServerPort, +} from './common' +import { getAdoFixtures } from './fixtures/ado-fixtures' +import { verifyError } from './test-utils' + +describe('Ado Fetcher Proxy Settings', () => { + const adoEnvironment = { + ...defaultAdoEnvironment, + ADO_APPLY_PROXY_SETTINGS: 'true', + } + + let mockServer: MockServer + + beforeAll(() => { + expect(fs.existsSync(adoFetcherExecutable)).to.be.true + }) + + beforeEach(() => { + fs.mkdirSync(evidencePath) + mockServer = new MockServer(getAdoFixtures(mockServerPort)) + }) + + afterEach(async () => { + await mockServer?.stop() + fs.rmSync(evidencePath, { recursive: true }) + }) + + it('should fail when PROXY_HOST is not set', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...adoEnvironment, + PROXY_PORT: '9000', + }, + }) + verifyError( + result, + 'ReferenceError: The environment variable "PROXY_HOST" is not set!', + mockServer + ) + }) + + it('should fail when PROXY_PORT is not set', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...adoEnvironment, + PROXY_HOST: 'my.proxy', + }, + }) + verifyError( + result, + 'ReferenceError: The environment variable PROXY_PORT" is not set!', + mockServer + ) + }) + + it('should fail when PROXY_PORT is lower than 1', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...adoEnvironment, + PROXY_HOST: 'my.proxy', + PROXY_PORT: '0', + }, + }) + verifyError( + result, + 'Error: environment variable PROXY_PORT does not represent an integer value in the range 0 < PROXY_PORT < 65535', + mockServer + ) + }) + + it('should fail when PROXY_PORT is greater than 65535', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...adoEnvironment, + PROXY_HOST: 'my.proxy', + PROXY_PORT: '65536', + }, + }) + verifyError( + result, + 'Error: environment variable PROXY_PORT does not represent an integer value in the range 0 < PROXY_PORT < 65535', + mockServer + ) + }) + + it('should fail when PROXY_PORT does not represent a numeric value', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...adoEnvironment, + PROXY_HOST: 'my.proxy', + PROXY_PORT: 'abc', + }, + }) + verifyError( + result, + 'Error: environment variable PROXY_PORT must contain digits only', + mockServer + ) + }) + + it('should fail when PROXY_PORT represents a floating point value', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...adoEnvironment, + PROXY_HOST: 'my.proxy', + PROXY_PORT: '900.1', + }, + }) + verifyError( + result, + 'Error: environment variable PROXY_PORT must contain digits only', + mockServer + ) + }) + + it.each(['my.proxy', 'localhost', '127.0.0.1'])( + 'should succeed with valid hostname %s', + async (hostname: string) => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...adoEnvironment, + PROXY_HOST: hostname, + PROXY_PORT: '9000', + }, + }) + + expect(result.stdout).length(0) + if (result.exitCode !== 0) { + /* + * The fetcher will likely fail, since it either can't establish a connection to the given host + * or it can't resolve the hostname. + * However, in these cases, proxy settings were correct and the fetcher tried to make the request + * through the proxy. + * If the fetcher failed for any other reason, the following expect will make the test fail. + */ + expect( + result.stderr[0].match(/(ECONNREFUSED|ENOTFOUND)/g).length + ).toBeGreaterThan(0) + } + } + ) + + it.each([ + 'exa_mple.com', + '-example.com', + 'my.proxy:3000', + 'http://my.proxy', + 'https://my.proxy', + ])('should fail with invalid hostname %s', async (hostname: string) => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...adoEnvironment, + PROXY_HOST: hostname, + PROXY_PORT: '9000', + }, + }) + verifyError(result, `Error: invalid PROXY_HOST: ${hostname}`, mockServer) + }) +}) diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/test-utils.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/test-utils.ts new file mode 100644 index 00000000..daec90a7 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/test-utils.ts @@ -0,0 +1,18 @@ +import { expect } from 'vitest' +import { + MockServer, + RunProcessResult, +} from '../../../../integration-tests/src/util' +import { verifyNoOutputFileWasWritten } from './common' + +export function verifyError( + result: RunProcessResult, + expectedErrorMessage: string, + mockServer: MockServer +): void { + expect(result.exitCode).toEqual(1) + expect(result.stdout).length(0) + expect(result.stderr[0]).toEqual(expectedErrorMessage) + expect(mockServer.getNumberOfRequests()).toEqual(0) + verifyNoOutputFileWasWritten() +} diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/work-item-config.int-spec.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/work-item-config.int-spec.ts new file mode 100644 index 00000000..327cbed6 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/integration/work-item-config.int-spec.ts @@ -0,0 +1,264 @@ +import * as fs from 'fs' +import * as path from 'path' +import { describe } from 'vitest' +import { afterEach, beforeAll, beforeEach, expect, it } from 'vitest' +import { + MockServer, + RunProcessResult, + run, +} from '../../../../integration-tests/src/util' +import { + adoFetcherExecutable, + defaultAdoEnvironment, + evidencePath, + fixturesPath, + mockServerPort, +} from './common' +import { getAdoFixtures } from './fixtures/ado-fixtures' +import { verifyError } from './test-utils' + +describe('Work Item Configuration', () => { + let mockServer: MockServer + + beforeAll(() => { + expect(fs.existsSync(adoFetcherExecutable)).to.be.true + }) + + beforeEach(() => { + fs.mkdirSync(evidencePath) + mockServer = new MockServer(getAdoFixtures(mockServerPort)) + }) + + afterEach(async () => { + await mockServer?.stop() + fs.rmSync(evidencePath, { recursive: true }) + }) + + it('should fail if workItems configuration object is present, but empty', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...defaultAdoEnvironment, + ADO_CONFIG_FILE_PATH: path.join( + fixturesPath, + 'config-workitems-is-empty.yaml' + ), + }, + }) + + verifyError( + result, + 'Error: Code: invalid_type ~ Path: workItems ~ Message: Expected object, received null', + mockServer + ) + }) + + it('should fail with empty configuration file', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...defaultAdoEnvironment, + ADO_CONFIG_FILE_PATH: path.join(fixturesPath, 'config-empty.yaml'), + }, + }) + + verifyError( + result, + 'Error: Code: invalid_type ~ Path: ~ Message: Expected object, received null', + mockServer + ) + }) + + it('should fail if configuration file does not contain an object', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...defaultAdoEnvironment, + ADO_CONFIG_FILE_PATH: path.join(fixturesPath, 'config-no-object.yaml'), + }, + }) + + verifyError( + result, + 'Error: Code: invalid_type ~ Path: ~ Message: Expected object, received string', + mockServer + ) + }) + + it('should fail if workItems is not an object', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...defaultAdoEnvironment, + ADO_CONFIG_FILE_PATH: path.join( + fixturesPath, + 'config-workitems-no-object.yaml' + ), + }, + }) + + verifyError( + result, + 'Error: Code: invalid_type ~ Path: workItems ~ Message: Expected object, received string', + mockServer + ) + }) + + it('should fail if query is not a string', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...defaultAdoEnvironment, + ADO_CONFIG_FILE_PATH: path.join( + fixturesPath, + 'config-query-is-no-string.yaml' + ), + }, + }) + + verifyError( + result, + 'Error: Code: invalid_type ~ Path: workItems.query ~ Message: Expected string, received number', + mockServer + ) + }) + + it('should fail if neededFields is not an array', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...defaultAdoEnvironment, + ADO_CONFIG_FILE_PATH: path.join( + fixturesPath, + 'config-neededfields-is-no-array.yaml' + ), + }, + }) + + verifyError( + result, + 'Error: Code: invalid_type ~ Path: workItems.neededFields ~ Message: Expected array, received number', + mockServer + ) + }) + + it('should fail if neededField is an array, but contains an object', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...defaultAdoEnvironment, + ADO_CONFIG_FILE_PATH: path.join( + fixturesPath, + 'config-neededfields-contains-object.yaml' + ), + }, + }) + + verifyError( + result, + 'Error: Code: invalid_type ~ Path: workItems.neededFields[0] ~ Message: Expected string, received object', + mockServer + ) + }) + + it('should fail if hierarchyDepth is not a number', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...defaultAdoEnvironment, + ADO_CONFIG_FILE_PATH: path.join( + fixturesPath, + 'config-hierarchydepth-is-no-number.yaml' + ), + }, + }) + + verifyError( + result, + 'Error: Code: invalid_type ~ Path: workItems.hierarchyDepth ~ Message: Expected number, received string', + mockServer + ) + }) + + it('should fail if hierarchyDepth is a floating point number', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...defaultAdoEnvironment, + ADO_CONFIG_FILE_PATH: path.join( + fixturesPath, + 'config-hierarchydepth-is-floating-point-number.yaml' + ), + }, + }) + + verifyError( + result, + 'Error: Code: invalid_type ~ Path: workItems.hierarchyDepth ~ Message: Expected integer, received float', + mockServer + ) + }) + + it('should fail if hierarchyDepth is negative', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...defaultAdoEnvironment, + ADO_CONFIG_FILE_PATH: path.join( + fixturesPath, + 'config-hierarchydepth-is-negative.yaml' + ), + }, + }) + + verifyError( + result, + 'Error: Code: too_small ~ Path: workItems.hierarchyDepth ~ Message: Number must be greater than 0', + mockServer + ) + }) + + it('should fail if relations is no object', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...defaultAdoEnvironment, + ADO_CONFIG_FILE_PATH: path.join( + fixturesPath, + 'config-relations-is-no-object.yaml' + ), + }, + }) + + verifyError( + result, + 'Error: Code: invalid_type ~ Path: workItems.relations ~ Message: Expected object, received string', + mockServer + ) + }) + + it('should fail if relations.get is not a boolean', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...defaultAdoEnvironment, + ADO_CONFIG_FILE_PATH: path.join( + fixturesPath, + 'config-relations-get-is-no-boolean.yaml' + ), + }, + }) + + verifyError( + result, + 'Error: Code: invalid_type ~ Path: workItems.relations.relations.get ~ Message: Expected boolean, received string', + mockServer + ) + }) + + it('should fail if relations.relationType contains an unsupported value', async () => { + const result: RunProcessResult = await run(adoFetcherExecutable, [], { + env: { + ...defaultAdoEnvironment, + ADO_CONFIG_FILE_PATH: path.join( + fixturesPath, + 'config-relations-type-is-unsupported.yaml' + ), + }, + }) + + verifyError( + result, + 'Error: Code: invalid_union ~ Path: workItems.relations.relations.relationType ~ Message: Invalid input', + mockServer + ) + }) +}) diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/utils/api-details.test.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/utils/api-details.test.ts new file mode 100644 index 00000000..48dc9c7c --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/utils/api-details.test.ts @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { describe, expect, it } from 'vitest' +import { createApiUrl, getApiDetails } from '../../src/utils/api-details' + +describe('ApiDetails', () => { + it('getApiDetails() should return needed details for a request', () => { + process.env.ADO_URL = 'URL' + process.env.ADO_API_ORG = 'ORG' + process.env.ADO_API_PROJECT = 'PROJECT' + process.env.ADO_API_PERSONAL_ACCESS_TOKEN = 'TOKEN' + + const expectedResult = { + wiql: '_apis/wit/wiql', + url: 'URL', + org: 'ORG', + project: 'PROJECT', + personalAccessToken: 'TOKEN', + } + const result = getApiDetails() + expect(result).toEqual(expectedResult) + + delete process.env.AZURE_DEVOPS_URL, + process.env.ADO_API_ORG, + process.env.ADO_API_PROJECT, + process.env.ADO_API_PERSONAL_ACCESS_TOKEN + }) + + it('createApiUrl() should return corresponding URL', () => { + const apiDetails = { + version: '6.0', + wiql: '_apis/wit/wiql', + url: 'https://dev.azure.com', + org: 'ORG', + project: 'PROJECT', + personalAccessToken: 'TOKEN', + } + const result = createApiUrl(apiDetails) + expect(result.href).toEqual( + 'https://dev.azure.com/ORG/PROJECT/_apis/wit/wiql?api-version=6.0' + ) + }) +}) diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/utils/http-client.test.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/utils/http-client.test.ts new file mode 100644 index 00000000..016e9ebf --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/utils/http-client.test.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { afterEach, describe, expect, it, vi } from 'vitest' +import { createHttpClient } from '../../src/utils/http-client' + +describe('createHttpClient', () => { + afterEach(() => { + vi.clearAllMocks() + }) + it('returns a http client', () => { + const adoHttpClientArgs = { + azureDevOpsUrl: 'https://dev.azure.com', + } + const httpClient = createHttpClient(adoHttpClientArgs) + expect(httpClient).toBeDefined() + }) + + it('returns a http client with a baseUrl', () => { + const adoHttpClientArgs = { + azureDevOpsUrl: 'https://dev.azure.com', + } + const httpClient = createHttpClient(adoHttpClientArgs) + expect(httpClient.defaults.baseURL).toEqual('https://dev.azure.com') + }) +}) diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/work-item/filterData.test.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/work-item/filterData.test.ts new file mode 100644 index 00000000..0de2412b --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/work-item/filterData.test.ts @@ -0,0 +1,156 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import axios from 'axios' +import { describe, expect, it, vi } from 'vitest' +import { createHeaders, WorkItem } from '../../src/work-item/work-item' + +const headers = createHeaders('Test') +const configData = { + getQuery: () => 'Test', + getRequestedFields: () => ['title'], + getHierarchyDepth: () => 1, + getRelationType: () => 'Child', +} +const workItemObject = new WorkItem(headers, axios, configData) +const links = { + html: { + href: 'url', + }, +} + +describe('WorkItem', () => { + it('filterFields() should return only requested work item fields', () => { + const workItemData = { + id: 1, + fields: { + 'Deployment.Id': 2, + 'Category.Custom.Title': 'Feature 1', + status: 'Done', + assignedTo: { + displayName: 'Name', + uniqueName: 'address@example.com', + }, + }, + _links: links, + } + const neededFieldNames = ['Custom.Title', 'assignedTo', 'storyPoints'] + const spy = vi.spyOn(console, 'warn') + const result = workItemObject['filterFields']( + workItemData, + neededFieldNames + ) + expect(result).toEqual({ + id: 1, + url: 'url', + 'Custom.Title': 'Feature 1', + assignedTo: { + displayName: 'Name', + uniqueName: 'address@example.com', + }, + }) + expect(spy).toHaveBeenCalledWith( + "The field 'storyPoints' is not available on work item with id 1" + ) + }) + + it('filterFieldsFromAllLevels() should return from all levels only requested fields', () => { + const workItems = [ + { + id: 1, + fields: { + title: 'Feature 1', + }, + relations: [ + { + id: 2, + fields: { + title: 'Task 1', + }, + _links: links, + }, + ], + _links: links, + }, + ] + const neededFieldNames = ['title'] + const result = workItemObject['filterFieldsFromAllLevels']( + workItems, + neededFieldNames + ) + + expect(result).toEqual([ + { + id: 1, + url: 'url', + title: 'Feature 1', + relations: [{ id: 2, url: 'url', title: 'Task 1' }], + }, + ]) + }) + + it('filterData() should return filtered fields according to config data', () => { + const workItems = [ + { + id: 1, + fields: { + title: 'Feature 1', + }, + relations: [], + _links: links, + }, + ] + + const result = workItemObject['filterData'](workItems) + expect(result).toEqual([ + { id: 1, url: 'url', title: 'Feature 1', relations: [] }, + ]) + }) + + it('filterRelations() should return filtered relations according to config data', () => { + const workItems = [ + { + id: 1, + title: 'Feature 1', + url: 'url', + relations: [ + { + id: 2, + url: 'url', + title: 'Task 1', + relationType: 'Child', + relations: [], + }, + { + id: 3, + url: 'url', + title: 'Task 2', + relationType: 'Related', + relations: [], + }, + ], + _links: links, + }, + ] + + const result = workItemObject['filterRelations'](workItems, 1, configData) + expect(result).toEqual([ + { + id: 1, + url: 'url', + title: 'Feature 1', + _links: links, + relations: [ + { + id: 2, + url: 'url', + title: 'Task 1', + relationType: 'Child', + relations: [], + }, + ], + }, + ]) + }) +}) diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/work-item/getDetails.test.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/work-item/getDetails.test.ts new file mode 100644 index 00000000..5e87156d --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/work-item/getDetails.test.ts @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import axios from 'axios' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { createHeaders, WorkItem } from '../../src/work-item/work-item' + +vi.mock('axios', () => ({ + default: { + get: vi.fn(), + post: vi.fn(), + create: vi.fn(), + }, +})) +const headers = createHeaders('Test') +const getRecursivelyResponse = { + id: 1, + url: 'https://child/url', + relations: [ + { + id: 2, + attributes: { + name: 'Child', + }, + url: 'https://child/url', + }, + { + id: 3, + attributes: { + name: 'Child', + }, + url: 'https://child/url', + }, + ], +} + +const referenceList = [ + { + id: 1, + url: 'https://child/url', + }, + { + id: 4, + url: 'https://child/url', + }, +] + +const expectedResult = [ + { + id: 1, + url: 'https://child/url', + relations: [ + { + id: 2, + attributes: { + name: 'Child', + }, + url: 'https://child/url', + }, + { + id: 3, + attributes: { + name: 'Child', + }, + url: 'https://child/url', + }, + ], + }, + { + id: 1, + url: 'https://child/url', + relations: [ + { + id: 2, + attributes: { + name: 'Child', + }, + url: 'https://child/url', + }, + { + id: 3, + attributes: { + name: 'Child', + }, + url: 'https://child/url', + }, + ], + }, +] + +describe('WorkItem', () => { + afterEach(() => { + vi.clearAllMocks() + }) + + it('getDetails() should return work item details', async () => { + const configData = { + getHierarchyDepth: () => 2, + } + const workItem = new WorkItem(headers, axios, configData) + const mockedGetRecursively = vi.spyOn(workItem as any, 'getRecursively') + mockedGetRecursively.mockResolvedValue(getRecursivelyResponse) + const result = await workItem['getDetails'](referenceList) + expect(result).toEqual(expectedResult) + }) + + it('getDetails() should throw a Error', async () => { + const configData = { + getHierarchyDepth: () => 2, + } + const workItem = new WorkItem(headers, axios, configData) + const mockedGetRecursively = vi.spyOn(workItem as any, 'getRecursively') + mockedGetRecursively.mockRejectedValue(new Error('Error')) + await expect(workItem['getDetails'](referenceList)).rejects.toThrow('Error') + }) +}) diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/work-item/getRecursively.test.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/work-item/getRecursively.test.ts new file mode 100644 index 00000000..2b078e38 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/work-item/getRecursively.test.ts @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import axios from 'axios' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { createHeaders, WorkItem } from '../../src/work-item/work-item' + +vi.mock('axios', () => ({ + default: { + get: vi.fn(), + post: vi.fn(), + create: vi.fn(), + }, +})) + +const headers = createHeaders('Test') +const configData = { + getQuery: () => 'Test', + getRequestedFields: () => ['Title'], +} +const workItemObject = new WorkItem(headers, axios, configData, { + version: '6.0', + wiql: '_apis/wit/wiql', + url: 'https://dev.azure.com', + org: 'ORG', + project: 'PROJECT', + personalAccessToken: 'TOKEN', +}) + +const mockValueRelations = [ + { + id: 2, + attributes: { + name: 'Child', + }, + url: 'https://child/url', + relations: [], + }, + { + id: 3, + attributes: { + name: 'Child', + }, + url: 'https://child/url', + relations: [], + }, +] + +const mockValueParent = { + data: { + id: 1, + url: 'https://child/url', + relations: [], + }, +} + +const mockValue = JSON.parse(JSON.stringify(mockValueParent)) +mockValue.data.relations = mockValueRelations + +const mockedAxiosGet = vi.mocked(axios.get) + +describe('WorkItem', () => { + afterEach(() => { + vi.clearAllMocks() + }) + + it('getRecursively() should call axios.get with the correct url and headers', async () => { + mockedAxiosGet.mockResolvedValueOnce(structuredClone(mockValue)) + const expectedUrl = 'https://parent/url?api-version=6.0&%24expand=relations' + const expectedHeaders = { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: 'Basic OlRlc3Q=', + } + + await workItemObject['getRecursively']('https://parent/url', 0) + expect(axios.get).toHaveBeenCalledWith(expectedUrl, { + headers: expectedHeaders, + }) + }) + + it('getRecursively() should return parent only', async () => { + mockedAxiosGet.mockResolvedValueOnce(structuredClone(mockValue)) + + const result = await workItemObject['getRecursively']( + 'https://parent/url', + 0 + ) + expect(result).toEqual(mockValueParent.data) + }) + + it('getRecursively() should return relations with relationType', async () => { + mockedAxiosGet.mockResolvedValueOnce(structuredClone(mockValue)) + mockedAxiosGet.mockResolvedValueOnce({ + data: structuredClone(mockValueRelations[0]), + }) + mockedAxiosGet.mockResolvedValueOnce({ + data: structuredClone(mockValueRelations[1]), + }) + const expected = structuredClone(mockValue) + expected.data.relations[0].relationType = 'Child' + expected.data.relations[1].relationType = 'Child' + + const result = await workItemObject['getRecursively']( + 'https://parent/url', + 1 + ) + + expect(result).toEqual(expected.data) + }) + + it('getRecursively() should return relations', async () => { + mockedAxiosGet.mockResolvedValueOnce(structuredClone(mockValue)) + mockedAxiosGet.mockResolvedValueOnce({ + data: structuredClone(mockValueRelations[0]), + }) + mockedAxiosGet.mockResolvedValueOnce({ + data: structuredClone(mockValueRelations[1]), + }) + const expected = structuredClone(mockValue) + expected.data.relations[0].relationType = 'Child' + expected.data.relations[1].relationType = 'Child' + + const result = await workItemObject['getRecursively']( + 'https://parent/url', + 1 + ) + expect(result).toEqual(expected.data) + }) + + it('getRecursively() should return empty object when fetching fails', async () => { + mockedAxiosGet.mockRejectedValueOnce(new Error('Test')) + const result = await workItemObject['getRecursively']( + 'https://parent/url', + 0 + ) + expect(result).toEqual({}) + }) +}) diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/work-item/queryReferences.test.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/work-item/queryReferences.test.ts new file mode 100644 index 00000000..3f71df90 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/work-item/queryReferences.test.ts @@ -0,0 +1,216 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import axios from 'axios' +import { isAxiosError } from 'axios' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { ApiDetails } from '../../src/utils/api-details' +import { createHeaders, WorkItem } from '../../src/work-item/work-item' + +vi.mock('axios', () => ({ + default: { + get: vi.fn(), + post: vi.fn(), + create: vi.fn(), + }, + isAxiosError: vi.fn(), +})) + +const headers = createHeaders('Test') +const configData = { + getQuery: () => 'Test', + getRequestedFields: () => ['Title'], +} + +describe('WorkItem', () => { + afterEach(() => { + vi.clearAllMocks() + }) + + it('queryReferences() should return workItemReferences', async () => { + const mockedAxiosPost = vi.mocked(axios.post) + mockedAxiosPost.mockResolvedValue({ + data: { + workItems: [ + { + id: 1, + url: 'https://dev.azure.com/ORG/PROJECT/_apis/wit/workitems/1', + }, + ], + }, + headers: { + 'content-type': 'application/json', + }, + }) + + const apiDetails: ApiDetails = { + version: '6.0', + wiql: '_apis/wit/wiql', + url: 'https://dev.azure.com', + org: 'ORG', + project: 'PROJECT', + personalAccessToken: 'TOKEN', + } + const workItemObject: WorkItem = new WorkItem( + headers, + axios, + configData, + apiDetails + ) + const result = await workItemObject.queryReferences() + expect(mockedAxiosPost).toHaveBeenCalledWith( + 'https://dev.azure.com/ORG/PROJECT/_apis/wit/wiql?api-version=6.0', + { + query: 'Test', + }, + { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: 'Basic OlRlc3Q=', + }, + } + ) + expect(result).toEqual([ + { id: 1, url: 'https://dev.azure.com/ORG/PROJECT/_apis/wit/workitems/1' }, + ]) + }) + + it('queryReferences() should return empty array if no workitems are found', async () => { + const mockedAxiosPost = vi.mocked(axios.post) + mockedAxiosPost.mockResolvedValue({ + data: {}, // no .workItems here! + headers: { + 'content-type': 'application/json', + }, + }) + + const apiDetails: ApiDetails = { + version: '6.0', + wiql: '_apis/wit/wiql', + url: 'https://dev.azure.com', + org: 'ORG', + project: 'PROJECT', + personalAccessToken: 'TOKEN', + } + const workItemObject: WorkItem = new WorkItem( + headers, + axios, + configData, + apiDetails + ) + const result = await workItemObject.queryReferences() + expect(result).toEqual([]) + }) + + it('queryReferences() should return report about received 203 HTML response', async () => { + const mockedAxiosPost = vi.mocked(axios.post) + mockedAxiosPost.mockResolvedValue({ + headers: { + 'content-type': 'text/html; some=parameter', + }, + status: 203, + }) + + const apiDetails: ApiDetails = { + version: '6.0', + wiql: '_apis/wit/wiql', + url: 'https://dev.azure.com', + org: 'ORG', + project: 'PROJECT', + personalAccessToken: 'TOKEN', + } + const workItemObject: WorkItem = new WorkItem( + headers, + axios, + configData, + apiDetails + ) + + await expect(workItemObject.queryReferences()).rejects.toThrowError( + 'Server returned status 203 and some HTML code instead of JSON. It could be that your API token is wrong!' + ) + }) + + it('queryReferences() should return proper error in case of bad request', async () => { + vi.mocked(axios.post).mockRejectedValue({ + response: { status: 400, statusText: 'Bad request' }, + }) + vi.mocked(isAxiosError).mockReturnValue(true) + + const apiDetails: ApiDetails = { + version: '6.0', + wiql: '_apis/wit/wiql', + url: 'https://dev.azure.com', + org: 'ORG', + project: 'PROJECT', + personalAccessToken: 'TOKEN', + } + + const workItemObject: WorkItem = new WorkItem( + headers, + axios, + configData, + apiDetails + ) + + await expect(workItemObject.queryReferences()).rejects.toThrowError( + 'Request failed with status code 400 Bad request. Please check your WIQL query for errors.' + ) + }) + + it('queryReferences() should return proper error in case of 404 response', async () => { + vi.mocked(axios.post).mockRejectedValue({ + response: { status: 404, statusText: 'Not found' }, + }) + vi.mocked(isAxiosError).mockReturnValue(true) + + const apiDetails: ApiDetails = { + version: '6.0', + wiql: '_apis/wit/wiql', + url: 'https://dev.azure.com', + org: 'ORG', + project: 'PROJECT', + personalAccessToken: 'TOKEN', + } + + const workItemObject: WorkItem = new WorkItem( + headers, + axios, + configData, + apiDetails + ) + + await expect(workItemObject.queryReferences()).rejects.toThrowError( + 'Request failed with status code 404 Not found' + ) + }) + + it('queryReferences() should throw unknown errors as they are', async () => { + const customError = new Error('Some custom error') + + vi.mocked(axios.post).mockRejectedValue(customError) + vi.mocked(isAxiosError).mockReturnValue(false) + + const apiDetails: ApiDetails = { + version: '6.0', + wiql: '_apis/wit/wiql', + url: 'https://dev.azure.com', + org: 'ORG', + project: 'PROJECT', + personalAccessToken: 'TOKEN', + } + + const workItemObject: WorkItem = new WorkItem( + headers, + axios, + configData, + apiDetails + ) + + await expect(workItemObject.queryReferences()).rejects.toThrowError( + customError + ) + }) +}) diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/work-item/work-item-config-data.test.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/work-item/work-item-config-data.test.ts new file mode 100644 index 00000000..8f2f6ff3 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/work-item/work-item-config-data.test.ts @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { describe, expect, afterEach, it, vi } from 'vitest' +import { WorkItemConfigData } from '../../src/work-item/work-item-config-data' + +describe('WorkItemConfigData', () => { + afterEach(() => { + vi.clearAllMocks() + }) + it('getQuery() should return query', () => { + const configData = new WorkItemConfigData({ + workItems: { + query: 'query', + }, + }) + expect(configData.getQuery()).eq('query') + }) + + it('getHierarchyDepth() should return hierarchyDepth', () => { + const configData = new WorkItemConfigData({ + workItems: { + hierarchyDepth: 1, + }, + }) + expect(configData.getHierarchyDepth()).eq(1) + }) + + it('getHierarchyDepth() should return child hierarchyDepth', () => { + const configData = new WorkItemConfigData({ + workItems: { + relations: { + get: true, + relations: { + get: true, + }, + }, + }, + }) + expect(configData.getHierarchyDepth()).eq(2) + }) + + it('getRequestedFields() should return default fields', () => { + const configData = new WorkItemConfigData({ + workItems: { + neededFields: [], + }, + }) + const result = configData.getRequestedFields() + expect(result).toEqual(['state', 'title']) + }) + + it('getRequestedFields() should return default fields and needed fields', () => { + const configData = new WorkItemConfigData({ + workItems: { + neededFields: ['field1', 'field2'], + }, + }) + const result = configData.getRequestedFields() + expect(result).toEqual(['field1', 'field2', 'state', 'title']) + }) + it('getRelationType() should return "any" if a relation has no relationType', () => { + const configData = new WorkItemConfigData({ + workItems: { + relations: { + get: true, + }, + }, + }) + const mockedGetHierarchyDepth = vi.spyOn(configData, 'getHierarchyDepth') + mockedGetHierarchyDepth.mockReturnValueOnce(1) + const result = configData.getRelationType(1) + expect(result).toEqual('any') + }) + it('getRelationType() should return "any" if there is no relation', () => { + const configData = new WorkItemConfigData({ + workItems: {}, + }) + const mockedGetHierarchyDepth = vi.spyOn(configData, 'getHierarchyDepth') + mockedGetHierarchyDepth.mockReturnValueOnce(0) + const result = configData.getRelationType(0) + expect(result).toEqual('any') + }) + it('getRelationType() should return relationType of nested relations', () => { + const configData = new WorkItemConfigData({ + workItems: { + relations: { + get: true, + relationType: 'Child', + relations: { + get: true, + relationType: 'Related', + }, + }, + }, + }) + const mockedGetHierarchyDepth = vi.spyOn(configData, 'getHierarchyDepth') + mockedGetHierarchyDepth.mockReturnValue(2) + const result1 = configData.getRelationType(1) + expect(result1).toEqual('Related') + const result2 = configData.getRelationType(2) + expect(result2).toEqual('Child') + }) +}) diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/test/work-item/work-item.test.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/work-item/work-item.test.ts new file mode 100644 index 00000000..38250357 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/test/work-item/work-item.test.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { afterEach, describe, expect, it, vi } from 'vitest' +import { + createHeaders, + createWiqlRequestBody, +} from '../../src/work-item/work-item' + +describe('WorkItem', () => { + afterEach(() => { + vi.clearAllMocks() + }) + it('createHeaders() should return headers', () => { + expect(createHeaders('Test')).toEqual({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: 'Basic OlRlc3Q=', + }) + }) + + it('createtWiqlRequestBody() should return wiqlRequestBody', () => { + expect(createWiqlRequestBody('Test')).toEqual({ + query: 'Test', + }) + }) +}) diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/tsconfig.json b/yaku-apps-typescript/apps/ado-work-items-fetcher/tsconfig.json new file mode 100644 index 00000000..e1f51365 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@B-S-F/typescript-config/base.json", + "ts-node": { + "compilerOptions": { + "module": "ESNext" + } + }, + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/tsup.config.json b/yaku-apps-typescript/apps/ado-work-items-fetcher/tsup.config.json new file mode 100644 index 00000000..f35b9ed3 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/tsup.config.json @@ -0,0 +1,6 @@ +{ + "entry": ["src/index.ts"], + "splitting": false, + "sourcemap": true, + "clean": true +} diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/tsup.config.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/tsup.config.ts new file mode 100644 index 00000000..9fba5cc4 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/tsup.config.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/**/*.ts'], + splitting: false, + sourcemap: true, + clean: true, + target: 'node18', + format: ['esm'], + bundle: false, +}) diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/vitest-integration.config.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/vitest-integration.config.ts new file mode 100644 index 00000000..8bec0725 --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/vitest-integration.config.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['test/integration/**/*.int-spec.ts'], + pool: 'threads', + poolOptions: { + threads: { + maxThreads: 1, + minThreads: 1, + }, + }, + typecheck: { + tsconfig: 'tsconfig.json', + }, + reporters: ['junit', 'default'], + outputFile: 'reports/integration-test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/ado-work-items-fetcher/vitest.config.ts b/yaku-apps-typescript/apps/ado-work-items-fetcher/vitest.config.ts new file mode 100644 index 00000000..1cf5e21f --- /dev/null +++ b/yaku-apps-typescript/apps/ado-work-items-fetcher/vitest.config.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + typecheck: { + tsconfig: 'tsconfig.json', + }, + coverage: { + provider: 'v8', + all: true, + enabled: true, + reporter: ['cobertura', 'json-summary', 'text-summary'], + include: ['src'], + }, + reporters: ['junit', 'default'], + outputFile: 'reports/test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/defender-for-cloud/.eslintrc.cjs b/yaku-apps-typescript/apps/defender-for-cloud/.eslintrc.cjs new file mode 100644 index 00000000..34ad9dec --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/.eslintrc.cjs @@ -0,0 +1,5 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +module.exports = require("@B-S-F/eslint-config/eslint-preset"); diff --git a/yaku-apps-typescript/apps/defender-for-cloud/README.md b/yaku-apps-typescript/apps/defender-for-cloud/README.md new file mode 100644 index 00000000..75b0477b --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/README.md @@ -0,0 +1 @@ +Please see https://docs.bswf.tech/autopilots/index.html, section "Defender for Cloud" for an in depth tutorial regarding how to set up and use this autopilot and for the official background information. diff --git a/yaku-apps-typescript/apps/defender-for-cloud/package.json b/yaku-apps-typescript/apps/defender-for-cloud/package.json new file mode 100644 index 00000000..72674e36 --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/package.json @@ -0,0 +1,48 @@ +{ + "name": "@B-S-F/defender-for-cloud", + "version": "0.3.1", + "description": "", + "main": "dist/index.js", + "type": "module", + "scripts": { + "prepare": "npm run build", + "build": "tsup", + "dev": "nodemon --watch \"src/**\" --exec npm run start", + "lint": "eslint '**/*.ts'", + "start": "npm run build && node ./dist/index.js", + "test:coverage": "vitest --coverage", + "test:dev": "vitest -w", + "test:ui": "vitest --ui", + "test:update-cobertura-file": "sed -i'.bak' 's,package name=\",package name=\"'${npm_package_name}/',g' coverage/cobertura-coverage.xml", + "test": "vitest run && npm run test:update-cobertura-file", + "test:integration:local": "npx rimraf dist && npm run build && npm run test:integration:ci", + "test:integration:ci": "npx vitest run --config vitest-integration.config.ts" + }, + "keywords": [], + "author": "", + "license": "BIOSLv4", + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "@vitest/coverage-v8": "*", + "@vitest/ui": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "nodemon": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + }, + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "axios": "^1.6.0", + "qs": "^6.11.0" + }, + "bin": { + "defender-for-cloud": "dist/index.js" + } +} diff --git a/yaku-apps-typescript/apps/defender-for-cloud/src/alertsRetriever.ts b/yaku-apps-typescript/apps/defender-for-cloud/src/alertsRetriever.ts new file mode 100644 index 00000000..2058b140 --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/src/alertsRetriever.ts @@ -0,0 +1,45 @@ +import axios from 'axios' + +export const getDefenderForCloudAlerts = async ( + token: string, + subscriptionId: string +) => { + const baseUrl = + process.env.IS_INTEGRATION_TEST === 'true' + ? 'http://localhost:8080' + : 'https://management.azure.com' + const urlQueryParameters = + process.env.IS_INTEGRATION_TEST === 'true' ? '' : '?api-version=2022-01-01' + let URL = + baseUrl + + `/subscriptions/${subscriptionId}/providers/Microsoft.Security/alerts` + + urlQueryParameters + + const config = { + headers: { Authorization: `Bearer ${token}` }, + } + + let fetchNextPage = false + let alerts: any[] = [] + try { + do { + const response = await axios.get(URL, config) + if (response.status === 200) { + alerts = alerts.concat(response.data.value) + if (response.data.nextLink) { + URL = response.data.nextLink + fetchNextPage = true + } else { + fetchNextPage = false + } + } + } while (fetchNextPage) + } catch (error: any) { + console.log('Error response: ') + console.log(error.response.data) + throw new Error( + `Request for Azure alerts does not have status code 200. Status code: ${error.response.status}` + ) + } + return alerts +} diff --git a/yaku-apps-typescript/apps/defender-for-cloud/src/auth.ts b/yaku-apps-typescript/apps/defender-for-cloud/src/auth.ts new file mode 100644 index 00000000..ac50b9b7 --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/src/auth.ts @@ -0,0 +1,44 @@ +import axios from 'axios' +import qs from 'qs' + +export const generateAzureAccessToken = async ( + tenantId: string, + clientId: string, + grantType: string, + clientSecret: string +) => { + const baseUrl = + process.env.IS_INTEGRATION_TEST === 'true' + ? 'http://localhost:8080' + : 'https://login.microsoftonline.com' + const URL = baseUrl + `/${tenantId}/oauth2/token` + const postData = { + client_id: clientId, + client_secret: clientSecret, + resource: 'https://management.core.windows.net/', + grant_type: grantType, + } + + try { + const response = await axios.post(URL, qs.stringify(postData), { + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + }) + + if (!response.data?.access_token) { + throw new Error( + `Field "access_token" does not exist on response returned by Azure authenticator` + ) + } + return response.data.access_token + } catch (error: any) { + if (error instanceof Error) { + throw error + } else { + console.log('Error response: ') + console.log(error.response.data) + throw new Error( + `Request for Azure access token does not have status code 200. Status code: ${error.response.status}` + ) + } + } +} diff --git a/yaku-apps-typescript/apps/defender-for-cloud/src/index.ts b/yaku-apps-typescript/apps/defender-for-cloud/src/index.ts new file mode 100644 index 00000000..cb7829be --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/src/index.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env -S node --openssl-legacy-provider +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { run } from './run.js' +run() diff --git a/yaku-apps-typescript/apps/defender-for-cloud/src/recommendationsRetriever.ts b/yaku-apps-typescript/apps/defender-for-cloud/src/recommendationsRetriever.ts new file mode 100644 index 00000000..982a4c49 --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/src/recommendationsRetriever.ts @@ -0,0 +1,92 @@ +import axios from 'axios' + +export const getDefenderForCloudRecommendations = async ( + token: string, + subscriptionId: string +) => { + const baseUrl = + process.env.IS_INTEGRATION_TEST === 'true' + ? 'http://localhost:8080' + : 'https://management.azure.com' + const urlQueryParameters = + process.env.IS_INTEGRATION_TEST === 'true' ? '' : '?api-version=2020-01-01' + let URL = + baseUrl + + `/subscriptions/${subscriptionId}/providers/Microsoft.Security/assessments` + + urlQueryParameters + + const config = { + headers: { Authorization: `Bearer ${token}` }, + } + + let fetchNextPage = false + let recommendations: any[] = [] + try { + do { + const response = await axios.get(URL, config) + if (response.status === 200) { + recommendations = recommendations.concat(response.data.value) + if (response.data.nextLink) { + URL = response.data.nextLink + fetchNextPage = true + } else { + fetchNextPage = false + } + } + } while (fetchNextPage) + } catch (error: any) { + console.log('Error response: ') + console.log(error.response.data) + throw new Error( + `Request for Azure recommendations does not have status code 200. Status code: ${error.response.status}` + ) + } + + return recommendations +} + +export const getDefenderForCloudRecommendationsMetadata = async ( + token: string +) => { + const baseUrl = + process.env.IS_INTEGRATION_TEST === 'true' + ? 'http://localhost:8080' + : 'https://management.azure.com' + const urlQueryParameters = + process.env.IS_INTEGRATION_TEST === 'true' ? '' : '?api-version=2020-01-01' + let URL = + baseUrl + + `/providers/Microsoft.Security/assessmentMetadata` + + urlQueryParameters + + const config = { + headers: { Authorization: `Bearer ${token}` }, + } + + let fetchNextPage = false + let recommendationsMetadata: any[] = [] + try { + do { + const response = await axios.get(URL, config) + if (response.status === 200) { + recommendationsMetadata = recommendationsMetadata.concat( + response.data.value + ) + if (response.data.nextLink) { + URL = response.data.nextLink + fetchNextPage = true + } else { + fetchNextPage = false + } + } + } while (fetchNextPage) + } catch (error: any) { + console.log('Error response: ') + console.log(error.response.data) + throw new Error( + `Request for Azure recommendations metadata does not have status code 200. Status code: ${error.response.status}` + ) + } + + return recommendationsMetadata +} diff --git a/yaku-apps-typescript/apps/defender-for-cloud/src/run.ts b/yaku-apps-typescript/apps/defender-for-cloud/src/run.ts new file mode 100644 index 00000000..aa1c4efb --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/src/run.ts @@ -0,0 +1,502 @@ +import { AppOutput } from '@B-S-F/autopilot-utils' + +import { getDefenderForCloudAlerts } from './alertsRetriever.js' +import { + getDefenderForCloudRecommendations, + getDefenderForCloudRecommendationsMetadata, +} from './recommendationsRetriever.js' +import { generateAzureAccessToken } from './auth.js' + +import { exportJson } from './utils.js' + +export enum Filter { + AlertType, + KeyWords, + ResourceName, + Severity, + Categories, + Threats, + UserImpact, + ImplementationEffort, +} + +export async function getSecurityAlertsOnASubscription() { + const token = await generateAzureAccessToken( + process.env.TENANT_ID!, + process.env.CLIENT_ID!, + 'client_credentials', + process.env.CLIENT_SECRET! + ) + const alerts = await getDefenderForCloudAlerts( + token, + process.env.SUBSCRIPTION_ID! + ) + return alerts +} + +export async function getRecommendationsOnASubscription() { + const token = await generateAzureAccessToken( + process.env.TENANT_ID!, + process.env.CLIENT_ID!, + 'client_credentials', + process.env.CLIENT_SECRET! + ) + const recommendations = await getDefenderForCloudRecommendations( + token, + process.env.SUBSCRIPTION_ID! + ) + return recommendations +} + +export async function getRecommendationsMetadataOnASubscription() { + const token = await generateAzureAccessToken( + process.env.TENANT_ID!, + process.env.CLIENT_ID!, + 'client_credentials', + process.env.CLIENT_SECRET! + ) + const recommendationsMetadata = + await getDefenderForCloudRecommendationsMetadata(token) + return recommendationsMetadata +} + +export const prefixMatchAlerts = ( + alert: any, + filterValues: string[], + filterType: Filter +) => { + if (filterType === Filter.AlertType) { + for (const filterValue of filterValues) { + if (alert.properties.alertType.startsWith(filterValue)) { + return true + } + } + } + + if (filterType === Filter.KeyWords) { + for (const filterValue of filterValues) { + if ( + alert.properties.alertDisplayName.search(filterValue) !== -1 || + alert.properties.description.search(filterValue) !== -1 + ) { + return true + } + } + } + + if (filterType === Filter.ResourceName) { + for (const filterValue of filterValues) { + if (alert.properties.compromisedEntity.search(filterValue) !== -1) { + return true + } + } + } + + return false +} + +export const parseFilterValues = (inputFilter: string | null | undefined) => { + if (inputFilter == null || inputFilter.length <= 0) { + return null + } + return inputFilter.split(', ').map((entry) => { + return entry.trim() + }) +} + +export const prefixMatchRecommendations = ( + recommendation: any, + filterValues: string[], + filterType: Filter +) => { + if (filterType === Filter.Severity) { + for (const filterValue of filterValues) { + if (recommendation.properties.severity == filterValue) { + return true + } + } + } + + if (filterType === Filter.Categories) { + for (const filterValue of filterValues) { + if (recommendation.properties.categories.includes(filterValue)) { + return true + } + } + } + + if (filterType === Filter.Threats) { + for (const filterValue of filterValues) { + if (recommendation.properties.threats.includes(filterValue)) { + return true + } + } + } + + if (filterType === Filter.KeyWords) { + for (const filterValue of filterValues) { + if ( + recommendation.properties.displayName.search(filterValue) !== -1 || + recommendation.properties.description.search(filterValue) !== -1 + ) { + return true + } + } + } + + if (filterType === Filter.UserImpact) { + for (const filterValue of filterValues) { + if (recommendation.properties.userImpact == filterValue) { + return true + } + } + } + + if (filterType === Filter.ImplementationEffort) { + for (const filterValue of filterValues) { + if (recommendation.properties.implementationEffort == filterValue) { + return true + } + } + } + + return false +} + +export const validateRequiredEnvVariables = () => { + const defenderForCloudReportType = process.env.DATA_TYPE || 'alerts' + if ( + defenderForCloudReportType !== 'alerts' && + defenderForCloudReportType !== 'recommendations' + ) { + throw new Error( + `Invalid value for DATA_TYPE environment variable! DATA_TYPE should be either 'alerts' or 'recommendations' and in this case is '${defenderForCloudReportType}'` + ) + } + if (process.env.TENANT_ID == undefined || process.env.TENANT_ID == '') { + throw new Error( + 'Please provide TENANT_ID in the environmental variables before running the autopilot' + ) + } + if (process.env.CLIENT_ID == undefined || process.env.CLIENT_ID == '') { + throw new Error( + 'Please provide CLIENT_ID in the environmental variables before running the autopilot' + ) + } + if ( + process.env.CLIENT_SECRET == undefined || + process.env.CLIENT_SECRET == '' + ) { + throw new Error( + 'Please provide CLIENT_SECRET in the environmental variables before running the autopilot' + ) + } + if ( + process.env.SUBSCRIPTION_ID == undefined || + process.env.SUBSCRIPTION_ID == '' + ) { + throw new Error( + 'Please provide SUBSCRIPTION_ID in the environmental variables before running the autopilot' + ) + } + return defenderForCloudReportType +} + +export function getUnhealthyRecommendations( + recommendations: any[] +) { + const unhealthyRecommendations: any[] = [] + for (const recommendation of recommendations) { + if (recommendation.properties.status?.code === 'Unhealthy') { + unhealthyRecommendations.push(recommendation) + } + } + return unhealthyRecommendations +} + +export function combineRecommendationAndMetadata( + recommendations: any[], + metadata: any[] +) { + const metadataMap = new Map() + for (const meta of metadata) { + metadataMap.set(meta.name, meta) + } + + const combinedRecommendations: any[] = [] + for (const recommendation of recommendations) { + const meta: any = metadataMap.get(recommendation.name) + if (meta) { + recommendation.properties.policyDefinitionId = + meta.properties?.policyDefinitionId + recommendation.properties.assessmentType = meta.properties?.assessmentType + recommendation.properties.description = meta.properties?.description + recommendation.properties.remediationDescription = + meta.properties?.remediationDescription + recommendation.properties.categories = meta.properties?.categories || [] + recommendation.properties.severity = meta.properties?.severity || '' + recommendation.properties.userImpact = meta.properties?.userImpact || '' + recommendation.properties.implementationEffort = + meta.properties?.implementationEffort || '' + recommendation.properties.threats = meta.properties?.threats || [] + } + combinedRecommendations.push(recommendation) + } + return combinedRecommendations +} + +export const run = async () => { + const output = new AppOutput() + + try { + const dataType = validateRequiredEnvVariables() + if (dataType === 'alerts') { + const alertTypeFilter = parseFilterValues(process.env.ALERT_TYPE_FILTER) + const keyWordsFilter = parseFilterValues(process.env.KEY_WORDS_FILTER) + const resourceNameFilter = parseFilterValues( + process.env.RESOURCE_NAME_FILTER + ) + const alerts = await getSecurityAlertsOnASubscription() + + let mandatoryNumberOfFilterMatches = 0 + if (alertTypeFilter) { + mandatoryNumberOfFilterMatches = mandatoryNumberOfFilterMatches + 1 + } + if (keyWordsFilter) { + mandatoryNumberOfFilterMatches = mandatoryNumberOfFilterMatches + 1 + } + if (resourceNameFilter) { + mandatoryNumberOfFilterMatches = mandatoryNumberOfFilterMatches + 1 + } + + const matchedAlerts = [] + for (const alert of alerts) { + let numberOfFilterMatchesForCurrentAlert = 0 + if ( + alertTypeFilter && + prefixMatchAlerts(alert, alertTypeFilter, Filter.AlertType) + ) { + numberOfFilterMatchesForCurrentAlert = + numberOfFilterMatchesForCurrentAlert + 1 + } + if ( + keyWordsFilter && + prefixMatchAlerts(alert, keyWordsFilter, Filter.KeyWords) + ) { + numberOfFilterMatchesForCurrentAlert = + numberOfFilterMatchesForCurrentAlert + 1 + } + if ( + resourceNameFilter && + prefixMatchAlerts(alert, resourceNameFilter, Filter.ResourceName) + ) { + numberOfFilterMatchesForCurrentAlert = + numberOfFilterMatchesForCurrentAlert + 1 + } + if ( + numberOfFilterMatchesForCurrentAlert === + mandatoryNumberOfFilterMatches + ) { + matchedAlerts.push(alert) + } + } + + if (matchedAlerts.length > 0) { + output.setStatus('RED') + output.setReason( + `Retrieved ${matchedAlerts.length} alerts based on given filters` + ) + + for (const alert of matchedAlerts) { + output.addResult({ + criterion: 'Open Security Alert Defender for Cloud', + justification: `Found security alert with id: ${alert.id} and display name: ${alert.properties.alertDisplayName}`, + fulfilled: false, + metadata: { + compromisedEntity: alert.properties?.compromisedEntity, + alertType: alert.properties?.alertType, + alertDisplayName: alert.properties?.alertDisplayName, + description: alert.properties?.description, + severity: alert.properties?.severity, + timeGeneratedUtc: alert.properties?.timeGeneratedUtc, + productComponentName: alert.properties?.productComponentName, + remediationSteps: alert.properties?.remediationSteps, + alertUri: alert.properties?.alertUri, + }, + }) + } + } else { + output.setStatus('GREEN') + output.setReason(`No alerts found based on given filters`) + output.addResult({ + criterion: `There are no alerts found based on given filters for Defender for Cloud`, + justification: `No alerts found based on given filters`, + fulfilled: true, + metadata: {}, + }) + } + } else if (dataType === 'recommendations') { + const severityFilter = parseFilterValues(process.env.SEVERITY_FILTER) + const keyWordsFilter = parseFilterValues(process.env.KEY_WORDS_FILTER) + const categoriesFilter = parseFilterValues(process.env.CATEGORIES_FILTER) + const threatsFilter = parseFilterValues(process.env.THREATS_FILTER) + const userImpactFilter = parseFilterValues(process.env.USER_IMPACT_FILTER) + const implementatioEffortFilter = parseFilterValues( + process.env.IMPLEMENTATION_EFFORT_FILTER + ) + + const recommendations = await getRecommendationsOnASubscription() + const unhealthyRecommendations = getUnhealthyRecommendations(recommendations) + const recommendationsMetadata = + await getRecommendationsMetadataOnASubscription() + const recommendationsAndMetadata = combineRecommendationAndMetadata( + unhealthyRecommendations, + recommendationsMetadata + ) + + let mandatoryNumberOfFilterMatches = 0 + + if (severityFilter) { + mandatoryNumberOfFilterMatches = mandatoryNumberOfFilterMatches + 1 + } + if (keyWordsFilter) { + mandatoryNumberOfFilterMatches = mandatoryNumberOfFilterMatches + 1 + } + if (categoriesFilter) { + mandatoryNumberOfFilterMatches = mandatoryNumberOfFilterMatches + 1 + } + if (threatsFilter) { + mandatoryNumberOfFilterMatches = mandatoryNumberOfFilterMatches + 1 + } + if (userImpactFilter) { + mandatoryNumberOfFilterMatches = mandatoryNumberOfFilterMatches + 1 + } + if (implementatioEffortFilter) { + mandatoryNumberOfFilterMatches = mandatoryNumberOfFilterMatches + 1 + } + const matchedRecommendations = [] + for (const recommendation of recommendationsAndMetadata) { + let numberOfFilterMatchesForCurrentRecommendation = 0 + if ( + severityFilter && + prefixMatchRecommendations( + recommendation, + severityFilter, + Filter.Severity + ) + ) { + numberOfFilterMatchesForCurrentRecommendation = + numberOfFilterMatchesForCurrentRecommendation + 1 + } + if ( + keyWordsFilter && + prefixMatchRecommendations( + recommendation, + keyWordsFilter, + Filter.KeyWords + ) + ) { + numberOfFilterMatchesForCurrentRecommendation = + numberOfFilterMatchesForCurrentRecommendation + 1 + } + if ( + categoriesFilter && + prefixMatchRecommendations( + recommendation, + categoriesFilter, + Filter.Categories + ) + ) { + numberOfFilterMatchesForCurrentRecommendation = + numberOfFilterMatchesForCurrentRecommendation + 1 + } + if ( + threatsFilter && + prefixMatchRecommendations( + recommendation, + threatsFilter, + Filter.Threats + ) + ) { + numberOfFilterMatchesForCurrentRecommendation = + numberOfFilterMatchesForCurrentRecommendation + 1 + } + if ( + userImpactFilter && + prefixMatchRecommendations( + recommendation, + userImpactFilter, + Filter.UserImpact + ) + ) { + numberOfFilterMatchesForCurrentRecommendation = + numberOfFilterMatchesForCurrentRecommendation + 1 + } + if ( + implementatioEffortFilter && + prefixMatchRecommendations( + recommendation, + implementatioEffortFilter, + Filter.ImplementationEffort + ) + ) { + numberOfFilterMatchesForCurrentRecommendation = + numberOfFilterMatchesForCurrentRecommendation + 1 + } + if ( + numberOfFilterMatchesForCurrentRecommendation === + mandatoryNumberOfFilterMatches + ) { + matchedRecommendations.push(recommendation) + } + } + + if (matchedRecommendations.length > 0) { + output.setStatus('RED') + output.setReason( + `Retrieved ${matchedRecommendations.length} security recommendations based on given filters` + ) + + for (const recommendation of matchedRecommendations) { + output.addResult({ + criterion: 'Open Security Recommendation Defender for Cloud', + justification: `Found security recommendation with id: ${recommendation.id} and display name: ${recommendation.properties.displayName}`, + fulfilled: false, + metadata: { + status: recommendation.properties?.status?.code, + additionalData: recommendation.properties?.additionalData, + resourceDetails: recommendation.properties?.resourceDetails, + policyDefinitionId: recommendation.properties?.policyDefinitionId, + assessmentType: recommendation.properties?.assessmentType, + description: recommendation.properties?.description, + remediationDescription: + recommendation.properties?.remediationDescription, + categories: recommendation.properties?.categories, + severity: recommendation.properties?.severity, + userImpact: recommendation.properties?.userImpact, + implementationEffort: recommendation.properties?.implementationEffort, + threats: recommendation.properties?.threats, + }, + }) + } + } else { + output.setStatus('GREEN') + output.setReason( + `No security recommendations found based on given filters` + ) + output.addResult({ + criterion: `There are no security recommendations found based on given filters for Defender for Cloud`, + justification: `No security recommendations found based on given filters`, + fulfilled: true, + metadata: {}, + }) + } + } + } catch (error: any) { + output.setStatus('FAILED') + output.setReason(error.message) + } finally { + exportJson(output.data.results, './results.json') + output.write() + } +} diff --git a/yaku-apps-typescript/apps/defender-for-cloud/src/utils.ts b/yaku-apps-typescript/apps/defender-for-cloud/src/utils.ts new file mode 100644 index 00000000..39f5181c --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/src/utils.ts @@ -0,0 +1,11 @@ +import fs from 'fs/promises' +import fs_sync from 'fs' +import path from 'path' + +export async function exportJson(jsonContent: any, outputPath: string) { + const dirName = path.dirname(outputPath) + if (!fs_sync.existsSync(dirName)) { + fs_sync.mkdirSync(dirName, { recursive: true }) + } + await fs.writeFile(outputPath, JSON.stringify(jsonContent)) +} diff --git a/yaku-apps-typescript/apps/defender-for-cloud/test/fixtures/alerts.ts b/yaku-apps-typescript/apps/defender-for-cloud/test/fixtures/alerts.ts new file mode 100644 index 00000000..2b875db4 --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/test/fixtures/alerts.ts @@ -0,0 +1,204 @@ +export const alertProperties1 = { + compromisedEntity: 'runner-aks-dev', + alertType: 'K8S_PrivilegedContainer', + alertDisplayName: 'Privileged container detected', + description: + "Kubernetes audit log analysis detected a new privileged container. A privileged container has access to the node's resources and breaks the isolation between containers. If compromised, an attacker can use the privileged container to gain access to the node.", + severity: 'Informational', + timeGeneratedUtc: '2023-11-30T03:16:14.5970315Z', + productComponentName: 'Containers', + remediationSteps: + '["Find the container in the alert details.","If the container doesn’t need to run in privileged mode, remove the privileges from the container.","If the container is not legitimate, escalate the alert to the information security team."]', + alertUri: + 'https://portal.azure.com/#blade/Microsoft_Azure_Security_AzureDefenderForData/AlertBlade/alertId/00000000-0000-0000-0000-0000000000/subscriptionId/00000000-0000-0000-0000-0000000000', +} + +export const alertProperties2 = { + compromisedEntity: 'dev1aks', + alertType: 'K8S.NODE_SuspectDownloadArtifacts', + alertDisplayName: 'Detected suspicious file download', + description: + 'Analysis of processes running within a container or directly on a Kubernetes node, has detected a suspicious download of a remote file.', + severity: 'Low', + timeGeneratedUtc: '2023-10-02T11:10:23.2999447Z', + productComponentName: 'Containers', + remediationSteps: + '["Review and confirm that the command identified in the alert was legitimate activity that you expect to see on this host or device. If not, escalate the alert to the information security team."]', + alertUri: + 'https://portal.azure.com/#blade/Microsoft_Azure_Security_AzureDefenderForData/AlertBlade/alertId/00000000-0000-0000-0000-0000000000/subscriptionId/00000000-0000-0000-0000-0000000000', +} + +export const mockedAlertsUnitTestsFirstSet = [ + { + id: 'mockedId1', + properties: alertProperties1, + }, + { + id: 'mockedId2', + properties: alertProperties2, + }, +] + +export const mockedAlertsUnitTestsSecondSet = [ + { + id: 'mockedId3', + properties: alertProperties1, + }, + { + id: 'mockedId4', + properties: alertProperties2, + }, +] + +export const mockedAlertsUnitTestsThirdSet = [ + { + id: 'mockedId5', + properties: alertProperties1, + }, + { + id: 'mockedId6', + properties: alertProperties2, + }, +] + +export const mockedAlertsIntegrationTests = [ + { + id: 'mockedId1', + properties: alertProperties1, + }, + { + id: 'mockedId2', + properties: alertProperties2, + }, + { + id: 'mockedId3', + properties: alertProperties1, + }, + { + id: 'mockedId4', + properties: alertProperties1, + }, +] + +export const integrationTestResultsAlertsFixture1 = [ + { + result: { + criterion: 'Open Security Alert Defender for Cloud', + justification: + 'Found security alert with id: mockedId1 and display name: Privileged container detected', + fulfilled: false, + metadata: alertProperties1, + }, + }, + { + result: { + criterion: 'Open Security Alert Defender for Cloud', + justification: + 'Found security alert with id: mockedId2 and display name: Detected suspicious file download', + fulfilled: false, + metadata: alertProperties2, + }, + }, + { + result: { + criterion: 'Open Security Alert Defender for Cloud', + justification: + 'Found security alert with id: mockedId3 and display name: Privileged container detected', + fulfilled: false, + metadata: alertProperties1, + }, + }, + { + result: { + criterion: 'Open Security Alert Defender for Cloud', + justification: + 'Found security alert with id: mockedId4 and display name: Privileged container detected', + fulfilled: false, + metadata: alertProperties1, + }, + }, + { + status: 'RED', + reason: 'Retrieved 4 alerts based on given filters', + }, +] + +export const integrationTestResultsAlertsFixture2 = [ + { + result: { + criterion: 'Open Security Alert Defender for Cloud', + justification: + 'Found security alert with id: mockedId1 and display name: Privileged container detected', + fulfilled: false, + metadata: alertProperties1, + }, + }, + { + result: { + criterion: 'Open Security Alert Defender for Cloud', + justification: + 'Found security alert with id: mockedId3 and display name: Privileged container detected', + fulfilled: false, + metadata: alertProperties1, + }, + }, + { + result: { + criterion: 'Open Security Alert Defender for Cloud', + justification: + 'Found security alert with id: mockedId4 and display name: Privileged container detected', + fulfilled: false, + metadata: alertProperties1, + }, + }, + { + status: 'RED', + reason: 'Retrieved 3 alerts based on given filters', + }, +] + +export const integrationTestResultsAlertsFixture3 = [ + { + result: { + criterion: 'Open Security Alert Defender for Cloud', + justification: + 'Found security alert with id: mockedId2 and display name: Detected suspicious file download', + fulfilled: false, + metadata: alertProperties2, + }, + }, + { + status: 'RED', + reason: 'Retrieved 1 alerts based on given filters', + }, +] + +export const integrationTestResultsFixtureFAILED = [ + { + status: 'FAILED', + reason: 'Request failed with status code 400', + }, +] + +export const integrationTestResultsFixtureForClientSecretFAILED = [ + { + status: 'FAILED', + reason: 'Request failed with status code 401', + }, +] + +export const integrationTestResultsAlertsFixtureGREEN = [ + { + result: { + criterion: + 'There are no alerts found based on given filters for Defender for Cloud', + justification: 'No alerts found based on given filters', + fulfilled: true, + metadata: {}, + }, + }, + { + status: 'GREEN', + reason: 'No alerts found based on given filters', + }, +] diff --git a/yaku-apps-typescript/apps/defender-for-cloud/test/fixtures/recommendations.ts b/yaku-apps-typescript/apps/defender-for-cloud/test/fixtures/recommendations.ts new file mode 100644 index 00000000..6b6b6126 --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/test/fixtures/recommendations.ts @@ -0,0 +1,552 @@ +export const recommendationsProperties1 = { + resourceDetails: { + Source: 'Azure', + Id: '/subscriptions/00000000-0000-0000-0000-0000000000/resourceGroups/badge-storage-accountproviders/Microsoft.Network/virtualNetworks/private-link-vnet', + }, + displayName: 'Azure DDoS Protection Standard should be enabled', + status: { + code: 'Unhealthy', + cause: 'VnetHasNoAppGateways', + description: + 'There are no Application Gateway resources attached to this Virtual Network', + }, +} + +export const recommendationsProperties2 = { + resourceDetails: { + Source: 'Azure', + Id: '/subscriptions/00000000-0000-0000-0000-0000000000/resourceGroups/providers/Microsoft.Network/virtualNetworks/private-link-test-vnet/subnets/subnetPrivateLink', + }, + displayName: 'Subnets should be associated with a network security group', + status: { + code: 'Unhealthy', + cause: 'OffByPolicy', + description: 'The recommendation is disabled in policy', + }, +} + +export const recommendationsProperties3 = { + resourceDetails: { + Source: 'Azure', + Id: '/subscriptions/00000000-0000-0000-0000-0000000000/resourceGroups/badge-storage-accountproviders/Microsoft.Network/virtualNetworks/private-link-vnet', + }, + displayName: 'GKE cluster auto upgrade feature should be enabled', + status: { + code: 'Unhealthy', + }, +} + +export const recommendationsProperties4 = { + resourceDetails: { + Source: 'Azure', + Id: '/subscriptions/00000000-0000-0000-0000-0000000000/resourceGroups/badge-storage-accountproviders/Microsoft.Network/virtualNetworks/private-link-vnet', + }, + displayName: 'GKE cluster auto upgrade feature should be enabled', + status: { + code: 'Healthy', + }, +} + +export const recommendationsMetadataProperties1 = { + displayName: 'Azure DDoS Protection Standard should be enabled', + assessmentType: 'BuiltIn', + policyDefinitionId: + '/providers/Microsoft.Authorization/policyDefinitions/a7aca53f-2ed4-4466-a25e-0b45ade68efd', + description: + 'Defender for Cloud has discovered virtual networks with Application Gateway resources unprotected by the DDoS protection service. These resources contain public IPs. Enable mitigation of network volumetric and protocol attacks.', + remediationDescription: + "
1. Select a virtual network to enable the DDoS protection service standard on.
2. Select the Standard option.
3. Click 'Save'.", + categories: ['Networking'], + severity: 'Medium', + userImpact: 'Moderate', + implementationEffort: 'Moderate', + threats: ['ThreatResistance', 'DenialOfService'], +} + +export const recommendationsMetadataProperties2 = { + displayName: 'Subnets should be associated with a network security group', + assessmentType: 'BuiltIn', + policyDefinitionId: + '/providers/Microsoft.Authorization/policyDefinitions/e71308d3-144b-4262-b144-efdc3cc90517', + description: + "Protect your subnet from potential threats by restricting access to it with a network security group (NSG). NSGs contain a list of Access Control List (ACL) rules that allow or deny network traffic to your subnet. When an NSG is associated with a subnet, the ACL rules apply to all the VM instances and integrated services in that subnet, but don't apply to internal traffic inside the subnet. To secure resources in the same subnet from one another, enable NSG directly on the resources as well.
Note that the following subnet types will be listed as not applicable: GatewaySubnet, AzureFirewallSubnet, AzureBastionSubnet.", + remediationDescription: + "To enable Network Security Groups on your subnets:
1. Select a subnet to enable NSG on.
2. Click the 'Network security group' section.
3. Follow the steps and select an existing network security group to attach to this specific subnet.", + categories: ['IoT'], + severity: 'High', + userImpact: 'High', + implementationEffort: 'Moderate', + threats: ['MaliciousInsider', 'DataSpillage', 'DataExfiltration'], +} + +export const recommendationsMetadataProperties3 = { + displayName: 'GKE cluster auto upgrade feature should be enabled', + assessmentType: 'BuiltIn', + policyDefinitionId: + '/providers/Microsoft.Authorization/policyDefinitions/e71308d3-144b-4262-b144-efdc3cc90517', + description: + "This recommendation evaluates the management network property of a node pool for the key-value pair, 'key': 'autoUpgrade'.", + remediationDescription: + 'A GKE cluster auto upgrade feature, which keeps clusters and node pools on the latest stable version of Kubernetes', + categories: ['Compute'], + severity: 'High', + userImpact: 'High', + implementationEffort: 'Low', + threats: ['MaliciousInsider', 'DataSpillage'], +} + +export const combinedRecommendationsProperties1 = { + resourceDetails: { + Source: 'Azure', + Id: '/subscriptions/00000000-0000-0000-0000-0000000000/resourceGroups/badge-storage-accountproviders/Microsoft.Network/virtualNetworks/private-link-vnet', + }, + displayName: 'Azure DDoS Protection Standard should be enabled', + status: { + code: 'Unhealthy', + cause: 'VnetHasNoAppGateways', + description: + 'There are no Application Gateway resources attached to this Virtual Network', + }, + assessmentType: 'BuiltIn', + policyDefinitionId: + '/providers/Microsoft.Authorization/policyDefinitions/a7aca53f-2ed4-4466-a25e-0b45ade68efd', + description: + 'Defender for Cloud has discovered virtual networks with Application Gateway resources unprotected by the DDoS protection service. These resources contain public IPs. Enable mitigation of network volumetric and protocol attacks.', + remediationDescription: + "
1. Select a virtual network to enable the DDoS protection service standard on.
2. Select the Standard option.
3. Click 'Save'.", + categories: ['Networking'], + severity: 'Medium', + userImpact: 'Moderate', + implementationEffort: 'Moderate', + threats: ['ThreatResistance', 'DenialOfService'], +} + +export const combinedRecommendationsProperties2 = { + resourceDetails: { + Source: 'Azure', + Id: '/subscriptions/00000000-0000-0000-0000-0000000000/resourceGroups/providers/Microsoft.Network/virtualNetworks/private-link-test-vnet/subnets/subnetPrivateLink', + }, + displayName: 'Subnets should be associated with a network security group', + status: { + code: 'Unhealthy', + cause: 'OffByPolicy', + description: 'The recommendation is disabled in policy', + }, + assessmentType: 'BuiltIn', + policyDefinitionId: + '/providers/Microsoft.Authorization/policyDefinitions/e71308d3-144b-4262-b144-efdc3cc90517', + description: + "Protect your subnet from potential threats by restricting access to it with a network security group (NSG). NSGs contain a list of Access Control List (ACL) rules that allow or deny network traffic to your subnet. When an NSG is associated with a subnet, the ACL rules apply to all the VM instances and integrated services in that subnet, but don't apply to internal traffic inside the subnet. To secure resources in the same subnet from one another, enable NSG directly on the resources as well.
Note that the following subnet types will be listed as not applicable: GatewaySubnet, AzureFirewallSubnet, AzureBastionSubnet.", + remediationDescription: + "To enable Network Security Groups on your subnets:
1. Select a subnet to enable NSG on.
2. Click the 'Network security group' section.
3. Follow the steps and select an existing network security group to attach to this specific subnet.", + categories: ['IoT'], + severity: 'High', + userImpact: 'High', + implementationEffort: 'Moderate', + threats: ['MaliciousInsider', 'DataSpillage', 'DataExfiltration'], +} + +export const mockedHealthyRecommendation1 = { + type: 'mockedType1', + id: 'mockedId1', + name: 'mockedName1', + properties: recommendationsProperties4, +} + +export const mockedHealthyRecommendation2 = { + type: 'mockedType2', + id: 'mockedId2', + name: 'mockedName2', + properties: recommendationsProperties4, +} + +export const mockedUnhealthyRecommendation1 = { + type: 'mockedType1', + id: 'mockedId1', + name: 'mockedName1', + properties: recommendationsProperties1, +} + +export const mockedUnhealthyRecommendation2 = { + type: 'mockedType2', + id: 'mockedId2', + name: 'mockedName2', + properties: recommendationsProperties2, +} + +export const mockedCombinedRecommendationsFirstSet = [ + { + type: 'mockedType1', + id: 'mockedId1', + name: 'mockedName1', + properties: combinedRecommendationsProperties1, + }, + { + type: 'mockedType2', + id: 'mockedId2', + name: 'mockedName2', + properties: combinedRecommendationsProperties2, + }, +] + +export const mockedRecommendationsUnitTestsFirstSet = [ + { + type: 'mockedType1', + id: 'mockedId1', + name: 'mockedName1', + properties: recommendationsProperties1, + }, + { + type: 'mockedType2', + id: 'mockedId2', + name: 'mockedName2', + properties: recommendationsProperties2, + }, +] + +export const mockedRecommendationsUnitTestsSecondSet = [ + { + type: 'mockedType3', + id: 'mockedId3', + name: 'mockedName3', + properties: recommendationsProperties1, + }, + { + type: 'mockedType4', + id: 'mockedId4', + name: 'mockedName4', + properties: recommendationsProperties2, + }, +] + +export const mockedRecommendationsUnitTestsThirdSet = [ + { + type: 'mockedType5', + id: 'mockedId5', + name: 'mockedName5', + properties: recommendationsProperties1, + }, + { + type: 'mockedType6', + id: 'mockedId6', + name: 'mockedName6', + properties: recommendationsProperties2, + }, +] + +export const mockedRecommendationsMetadataUnitTestsFirstSet = [ + { + type: 'mockedType1', + id: 'mockedId1', + name: 'mockedName1', + properties: recommendationsMetadataProperties1, + }, + { + type: 'mockedType2', + id: 'mockedId2', + name: 'mockedName2', + properties: recommendationsMetadataProperties2, + }, +] + +export const mockedRecommendationsMetadataUnitTestsSecondSet = [ + { + type: 'mockedType3', + id: 'mockedId3', + name: 'mockedName3', + properties: recommendationsMetadataProperties1, + }, + { + type: 'mockedType4', + id: 'mockedId4', + name: 'mockedName4', + properties: recommendationsMetadataProperties2, + }, +] + +export const mockedRecommendationsMetadataUnitTestsThirdSet = [ + { + type: 'mockedType5', + id: 'mockedId5', + name: 'mockedName5', + properties: recommendationsMetadataProperties1, + }, + { + type: 'mockedType6', + id: 'mockedId6', + name: 'mockedName6', + properties: recommendationsMetadataProperties2, + }, +] + +export const recommendationsMetadataPropertiesMissingFields = { + displayName: 'Azure DDoS Protection Standard should be enabled', + assessmentType: 'BuiltIn', + policyDefinitionId: + '/providers/Microsoft.Authorization/policyDefinitions/a7aca53f-2ed4-4466-a25e-0b45ade68efd', + description: + 'Defender for Cloud has discovered virtual networks with Application Gateway resources unprotected by the DDoS protection service. These resources contain public IPs. Enable mitigation of network volumetric and protocol attacks.', + remediationDescription: + "
1. Select a virtual network to enable the DDoS protection service standard on.
2. Select the Standard option.
3. Click 'Save'.", + categories: ['Networking'], + severity: 'Medium', +} + +export const recommendationsMetadataPropertiesEmptyFields = { + displayName: 'Subnets should be associated with a network security group', + assessmentType: 'BuiltIn', + policyDefinitionId: + '/providers/Microsoft.Authorization/policyDefinitions/e71308d3-144b-4262-b144-efdc3cc90517', + description: + "Protect your subnet from potential threats by restricting access to it with a network security group (NSG). NSGs contain a list of Access Control List (ACL) rules that allow or deny network traffic to your subnet. When an NSG is associated with a subnet, the ACL rules apply to all the VM instances and integrated services in that subnet, but don't apply to internal traffic inside the subnet. To secure resources in the same subnet from one another, enable NSG directly on the resources as well.
Note that the following subnet types will be listed as not applicable: GatewaySubnet, AzureFirewallSubnet, AzureBastionSubnet.", + remediationDescription: + "To enable Network Security Groups on your subnets:
1. Select a subnet to enable NSG on.
2. Click the 'Network security group' section.
3. Follow the steps and select an existing network security group to attach to this specific subnet.", + categories: ['IoT'], + severity: 'High', + userImpact: '', + implementationEffort: '', + threats: [], +} + +export const combinedRecommendationsEmptyFields1 = { + resourceDetails: { + Source: 'Azure', + Id: '/subscriptions/00000000-0000-0000-0000-0000000000/resourceGroups/badge-storage-accountproviders/Microsoft.Network/virtualNetworks/private-link-vnet', + }, + displayName: 'Azure DDoS Protection Standard should be enabled', + status: { + code: 'Unhealthy', + cause: 'VnetHasNoAppGateways', + description: + 'There are no Application Gateway resources attached to this Virtual Network', + }, + assessmentType: 'BuiltIn', + policyDefinitionId: + '/providers/Microsoft.Authorization/policyDefinitions/a7aca53f-2ed4-4466-a25e-0b45ade68efd', + description: + 'Defender for Cloud has discovered virtual networks with Application Gateway resources unprotected by the DDoS protection service. These resources contain public IPs. Enable mitigation of network volumetric and protocol attacks.', + remediationDescription: + "
1. Select a virtual network to enable the DDoS protection service standard on.
2. Select the Standard option.
3. Click 'Save'.", + categories: ['Networking'], + severity: 'Medium', + userImpact: '', + implementationEffort: '', + threats: [], +} + +export const combinedRecommendationsEmptyFields2 = { + resourceDetails: { + Source: 'Azure', + Id: '/subscriptions/00000000-0000-0000-0000-0000000000/resourceGroups/providers/Microsoft.Network/virtualNetworks/private-link-test-vnet/subnets/subnetPrivateLink', + }, + displayName: 'Subnets should be associated with a network security group', + status: { + code: 'Unhealthy', + cause: 'OffByPolicy', + description: 'The recommendation is disabled in policy', + }, + assessmentType: 'BuiltIn', + policyDefinitionId: + '/providers/Microsoft.Authorization/policyDefinitions/e71308d3-144b-4262-b144-efdc3cc90517', + description: + "Protect your subnet from potential threats by restricting access to it with a network security group (NSG). NSGs contain a list of Access Control List (ACL) rules that allow or deny network traffic to your subnet. When an NSG is associated with a subnet, the ACL rules apply to all the VM instances and integrated services in that subnet, but don't apply to internal traffic inside the subnet. To secure resources in the same subnet from one another, enable NSG directly on the resources as well.
Note that the following subnet types will be listed as not applicable: GatewaySubnet, AzureFirewallSubnet, AzureBastionSubnet.", + remediationDescription: + "To enable Network Security Groups on your subnets:
1. Select a subnet to enable NSG on.
2. Click the 'Network security group' section.
3. Follow the steps and select an existing network security group to attach to this specific subnet.", + categories: ['IoT'], + severity: 'High', + userImpact: '', + implementationEffort: '', + threats: [], +} + +export const mockedRecommendationsMetadataMissingFields = [ + { + type: 'mockedType1', + id: 'mockedId1', + name: 'mockedName1', + properties: recommendationsMetadataPropertiesMissingFields, + }, + { + type: 'mockedType2', + id: 'mockedId2', + name: 'mockedName2', + properties: recommendationsMetadataPropertiesEmptyFields, + }, +] + +export const mockedCombinedRecommendationsWithEmptyFields = [ + { + type: 'mockedType1', + id: 'mockedId1', + name: 'mockedName1', + properties: combinedRecommendationsEmptyFields1, + }, + { + type: 'mockedType2', + id: 'mockedId2', + name: 'mockedName2', + properties: combinedRecommendationsEmptyFields2, + }, +] + +export const mockedRecommendationsIntegrationTests = [ + { + id: 'mockedId1', + name: 'mockedName1', + properties: recommendationsProperties1, + }, + { + id: 'mockedId2', + name: 'mockedName2', + properties: recommendationsProperties2, + }, + { + id: 'mockedId3', + name: 'mockedName3', + properties: recommendationsProperties3, + }, +] + +export const mockedRecommendationsMetadataIntegrationTests = [ + { + id: 'mockedId1', + name: 'mockedName1', + properties: recommendationsMetadataProperties1, + }, + { + id: 'mockedId2', + name: 'mockedName2', + properties: recommendationsMetadataProperties2, + }, + { + id: 'mockedId3', + name: 'mockedName3', + properties: recommendationsMetadataProperties3, + }, +] + +function setMetadata(recommendation, metadata) { + metadata = { + status: recommendation.properties?.status?.code, + resourceDetails: recommendation.properties?.resourceDetails, + policyDefinitionId: metadata.properties?.policyDefinitionId, + assessmentType: metadata.properties?.assessmentType, + description: metadata.properties?.description, + remediationDescription: metadata.properties?.remediationDescription, + categories: metadata.properties?.categories, + severity: metadata.properties?.severity, + userImpact: metadata.properties?.userImpact, + implementationEffort: metadata.properties?.implementationEffort, + threats: metadata.properties?.threats, + } + return metadata +} + +export const integrationTestResultsRecommendationsFixture1 = [ + { + result: { + criterion: 'Open Security Recommendation Defender for Cloud', + justification: + 'Found security recommendation with id: mockedId1 and display name: Azure DDoS Protection Standard should be enabled', + fulfilled: false, + metadata: setMetadata( + mockedRecommendationsIntegrationTests[0], + mockedRecommendationsMetadataIntegrationTests[0] + ), + }, + }, + { + result: { + criterion: 'Open Security Recommendation Defender for Cloud', + justification: + 'Found security recommendation with id: mockedId2 and display name: Subnets should be associated with a network security group', + fulfilled: false, + metadata: setMetadata( + mockedRecommendationsIntegrationTests[1], + mockedRecommendationsMetadataIntegrationTests[1] + ), + }, + }, + { + result: { + criterion: 'Open Security Recommendation Defender for Cloud', + justification: + 'Found security recommendation with id: mockedId3 and display name: GKE cluster auto upgrade feature should be enabled', + fulfilled: false, + metadata: setMetadata( + mockedRecommendationsIntegrationTests[2], + mockedRecommendationsMetadataIntegrationTests[2] + ), + }, + }, + { + status: 'RED', + reason: 'Retrieved 3 security recommendations based on given filters', + }, +] + +export const integrationTestResultsRecommendationsFixture2 = [ + { + result: { + criterion: 'Open Security Recommendation Defender for Cloud', + justification: + 'Found security recommendation with id: mockedId2 and display name: Subnets should be associated with a network security group', + fulfilled: false, + metadata: setMetadata( + mockedRecommendationsIntegrationTests[1], + mockedRecommendationsMetadataIntegrationTests[1] + ), + }, + }, + { + result: { + criterion: 'Open Security Recommendation Defender for Cloud', + justification: + 'Found security recommendation with id: mockedId3 and display name: GKE cluster auto upgrade feature should be enabled', + fulfilled: false, + metadata: setMetadata( + mockedRecommendationsIntegrationTests[2], + mockedRecommendationsMetadataIntegrationTests[2] + ), + }, + }, + { + status: 'RED', + reason: 'Retrieved 2 security recommendations based on given filters', + }, +] + +export const integrationTestResultsRecommendationsFixture3 = [ + { + result: { + criterion: 'Open Security Recommendation Defender for Cloud', + justification: + 'Found security recommendation with id: mockedId3 and display name: GKE cluster auto upgrade feature should be enabled', + fulfilled: false, + metadata: setMetadata( + mockedRecommendationsIntegrationTests[2], + mockedRecommendationsMetadataIntegrationTests[2] + ), + }, + }, + { + status: 'RED', + reason: 'Retrieved 1 security recommendations based on given filters', + }, +] + +export const integrationTestResultsRecommendationsFixtureGREEN = [ + { + result: { + criterion: + 'There are no security recommendations found based on given filters for Defender for Cloud', + justification: 'No security recommendations found based on given filters', + fulfilled: true, + metadata: {}, + }, + }, + { + status: 'GREEN', + reason: 'No security recommendations found based on given filters', + }, +] diff --git a/yaku-apps-typescript/apps/defender-for-cloud/test/fixtures/serverHelper.ts b/yaku-apps-typescript/apps/defender-for-cloud/test/fixtures/serverHelper.ts new file mode 100644 index 00000000..057d290e --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/test/fixtures/serverHelper.ts @@ -0,0 +1,99 @@ +import { MockServerOptions } from '../../../../integration-tests/src/util' +import { mockedAlertsIntegrationTests } from './alerts' +import { mockedRecommendationsIntegrationTests, mockedRecommendationsMetadataIntegrationTests } from './recommendations' + +export const createMockServerOptions = async ( + port: number, + responseStatus: number +): Promise => { + return { + port: port, + https: false, + responses: { + ['/mockedTenantId/oauth2/token']: { + post: { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { access_token: 'mockedAccessToken' }, + }, + }, + ['/subscriptions/mockedSubscriptionId/providers/Microsoft.Security/alerts']: + { + get: { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + value: mockedAlertsIntegrationTests, + }, + }, + }, + ['/subscriptions/mockedSubscriptionId/providers/Microsoft.Security/assessments']: + { + get: { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + value: mockedRecommendationsIntegrationTests, + }, + }, + }, + ['/providers/Microsoft.Security/assessmentMetadata']: + { + get: { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + value: mockedRecommendationsMetadataIntegrationTests, + }, + }, + }, + }, + } +} + +export const createMockServerOptionsFAILED = async ( + port: number, + responseStatus: number +): Promise => { + return { + port: port, + https: false, + responses: { + ['/mockedTenantId/oauth2/token']: { + post: { + responseStatus: responseStatus, + }, + }, + ['/subscriptions/mockedSubscriptionId/providers/Microsoft.Security/alerts']: + { + get: { + responseStatus: responseStatus, + }, + }, + ['/subscriptions/mockedSubscriptionId/providers/Microsoft.Security/assessment']: + { + get: { + responseStatus: responseStatus, + }, + }, + ['/providers/Microsoft.Security/assessmentMetadata']: + { + get: { + responseStatus: responseStatus, + }, + }, + }, + } +} diff --git a/yaku-apps-typescript/apps/defender-for-cloud/test/integration/defender-failed-status.int-spec.ts b/yaku-apps-typescript/apps/defender-for-cloud/test/integration/defender-failed-status.int-spec.ts new file mode 100644 index 00000000..9ad97c7a --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/test/integration/defender-failed-status.int-spec.ts @@ -0,0 +1,202 @@ +import * as fs from 'fs' +import * as path from 'path' +import { afterEach, beforeAll, describe, expect, it } from 'vitest' +import { + MockServer, + MockServerOptions, + RunProcessResult, + run, +} from '../../../../integration-tests/src/util' +import { createMockServerOptionsFAILED } from '../fixtures/serverHelper' +import { + integrationTestResultsFixtureFAILED, + integrationTestResultsFixtureForClientSecretFAILED, +} from '../fixtures/alerts' + +describe('Defender Autopilot FAILED status cases', () => { + let mockServer: MockServer | undefined + + const communEnvVariables = { + TENANT_ID: 'mockedTenantId', + CLIENT_ID: 'mockedClientId', + CLIENT_SECRET: 'mockedClientSecret', + SUBSCRIPTION_ID: 'mockedSubscriptionId', + IS_INTEGRATION_TEST: 'true', + } + + const defenderAutopilotExecutable: string = path.join( + __dirname, + '..', + '..', + 'dist', + 'index.js' + ) + + beforeAll(() => { + expect(fs.existsSync(defenderAutopilotExecutable)).to.be.true + }) + + afterEach(async () => { + await mockServer?.stop() + mockServer = undefined + }) + + it('should return status FAILED when environment variables are not set', async () => { + const env = {} + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + ...env, + }, + } + ) + + expect(result.exitCode).to.be.equal(0) + expect(result.stderr).to.have.length(0) + expect(result.stdout).to.not.have.length(0) + expect(result.stdout).to.include( + JSON.stringify({ + status: 'FAILED', + reason: + 'Please provide TENANT_ID in the environmental variables before running the autopilot', + }) + ) + }) + + it.each([ + { name: 'TENANT_ID', value: undefined }, + { name: 'CLIENT_ID', value: undefined }, + { name: 'CLIENT_SECRET', value: undefined }, + { name: 'SUBSCRIPTION_ID', value: undefined }, + ])('should set status FAILED when $name is not set', async (envVariable) => { + const env = { ...communEnvVariables } + env[`${envVariable.name}`] = envVariable.value + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + ...env, + }, + } + ) + + expect(result.exitCode).to.be.equal(0) + expect(result.stderr).to.have.length(0) + expect(result.stdout).to.not.have.length(0) + expect(result.stdout).to.include( + JSON.stringify({ + status: 'FAILED', + reason: + 'Please provide ' + + `${envVariable.name} ` + + 'in the environmental variables before running the autopilot', + }) + ) + }) + + it.each([ + { name: 'TENANT_ID', value: '' }, + { name: 'CLIENT_ID', value: '' }, + { name: 'CLIENT_SECRET', value: '' }, + { name: 'SUBSCRIPTION_ID', value: '' }, + ])( + 'should set status FAILED when $name is an empty string', + async (envVariable) => { + const env = { ...communEnvVariables } + env[`${envVariable.name}`] = envVariable.value + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + ...env, + }, + } + ) + + expect(result.exitCode).to.be.equal(0) + expect(result.stderr).to.have.length(0) + expect(result.stdout).to.not.have.length(0) + expect(result.stdout).to.include( + JSON.stringify({ + status: 'FAILED', + reason: + 'Please provide ' + + `${envVariable.name} ` + + 'in the environmental variables before running the autopilot', + }) + ) + } + ) + + it.each([ + { name: 'TENANT_ID' }, + { name: 'CLIENT_ID' }, + { name: 'SUBSCRIPTION_ID' }, + ])('should set status FAILED when $name is not correct', async () => { + const options: MockServerOptions = await createMockServerOptionsFAILED( + 8080, + 400 + ) + mockServer = new MockServer(options) + + const env = { ...communEnvVariables } + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + ...env, + }, + } + ) + + expect(result.exitCode).to.be.equal(0) + expect(result.stderr).to.have.length(0) + expect(result.stdout).to.not.have.length(0) + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsFixtureFAILED.map((element) => { + return JSON.stringify(element) + }) + ) + ) + }) + + it('should set status FAILED when CLIENT_SECRET is not correct', async () => { + const options: MockServerOptions = await createMockServerOptionsFAILED( + 8080, + 401 + ) + mockServer = new MockServer(options) + + const env = { ...communEnvVariables } + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + ...env, + }, + } + ) + + expect(result.exitCode).to.be.equal(0) + expect(result.stderr).to.have.length(0) + expect(result.stdout).to.not.have.length(0) + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsFixtureForClientSecretFAILED.map((element) => { + return JSON.stringify(element) + }) + ) + ) + }) +}) diff --git a/yaku-apps-typescript/apps/defender-for-cloud/test/integration/defender-green-status.int-spec.ts b/yaku-apps-typescript/apps/defender-for-cloud/test/integration/defender-green-status.int-spec.ts new file mode 100644 index 00000000..a1bd96ec --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/test/integration/defender-green-status.int-spec.ts @@ -0,0 +1,322 @@ +import * as fs from 'fs' +import * as path from 'path' +import { afterEach, beforeAll, describe, expect, it } from 'vitest' +import { + MockServer, + MockServerOptions, + RunProcessResult, + run, +} from '../../../../integration-tests/src/util' +import { createMockServerOptions } from '../fixtures/serverHelper' +import { integrationTestResultsAlertsFixtureGREEN } from '../fixtures/alerts' +import { integrationTestResultsRecommendationsFixtureGREEN } from '../fixtures/recommendations' + +describe('Defender Autopilot GREEN status cases', () => { + let mockServer: MockServer | undefined + const communEnvVariables = { + TENANT_ID: 'mockedTenantId', + CLIENT_ID: 'mockedClientId', + CLIENT_SECRET: 'mockedClientSecret', + SUBSCRIPTION_ID: 'mockedSubscriptionId', + IS_INTEGRATION_TEST: 'true', + } + + const defenderAutopilotExecutable: string = path.join( + __dirname, + '..', + '..', + 'dist', + 'index.js' + ) + + beforeAll(() => { + expect(fs.existsSync(defenderAutopilotExecutable)).to.be.true + }) + + afterEach(async () => { + await mockServer?.stop() + mockServer = undefined + }) + + it('should return 0 alerts and GREEN status when no alerts with given ALERT_TYPE_FILTER are found', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + ...communEnvVariables, + DATA_TYPE: 'alerts', + ALERT_TYPE_FILTER: 'RandomAlertType', + }, + } + ) + + expect(result.exitCode).to.be.equal(0) + expect(result.stdout).to.not.have.length(0) + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsAlertsFixtureGREEN.map((element) => { + return JSON.stringify(element) + }) + ) + ) + expect(result.stderr).to.have.length(0) + }) + + it('should return 0 alerts and GREEN status when no alerts with given KEY_WORDS_FILTER are found', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + ...communEnvVariables, + DATA_TYPE: 'alerts', + KEY_WORDS_FILTER: 'RandomKeyword1', + }, + } + ) + + expect(result.exitCode).to.be.equal(0) + expect(result.stdout).to.not.have.length(0) + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsAlertsFixtureGREEN.map((element) => { + return JSON.stringify(element) + }) + ) + ) + expect(result.stderr).to.have.length(0) + }) + + it('should return 0 alerts and GREEN status when no alerts with given RESOURCE_NAME_FILTER are found', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + ...communEnvVariables, + DATA_TYPE: 'alerts', + RESOURCE_NAME_FILTER: 'RandomResourceName', + }, + } + ) + + expect(result.exitCode).to.be.equal(0) + expect(result.stdout).to.not.have.length(0) + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsAlertsFixtureGREEN.map((element) => { + return JSON.stringify(element) + }) + ) + ) + expect(result.stderr).to.have.length(0) + }) + + it('should return 0 alerts and GREEN status when no alerts with all filters given are found', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + ...communEnvVariables, + DATA_TYPE: 'alerts', + ALERT_TYPE_FILTER: 'RandomAlert', + KEY_WORDS_FILTER: 'RandomKeyword', + RESOURCE_NAME_FILTER: 'RandomName', + }, + } + ) + + expect(result.exitCode).to.be.equal(0) + expect(result.stdout).to.not.have.length(0) + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsAlertsFixtureGREEN.map((element) => { + return JSON.stringify(element) + }) + ) + ) + expect(result.stderr).to.have.length(0) + }) + + it('should return 0 recommendations and GREEN status when no recommendations with given SEVERITY_FILTER are found', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + ...communEnvVariables, + DATA_TYPE: 'recommendations', + SEVERITY_FILTER: 'RandomSeverity', + }, + } + ) + + expect(result.exitCode).to.be.equal(0) + expect(result.stdout).to.not.have.length(0) + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsRecommendationsFixtureGREEN.map((element) => { + return JSON.stringify(element) + }) + ) + ) + expect(result.stderr).to.have.length(0) + }) + + it('should return 0 recommendations and GREEN status when no recommendations with given KEY_WORDS_FILTER are found', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + ...communEnvVariables, + DATA_TYPE: 'recommendations', + KEY_WORDS_FILTER: 'RandomKeyword', + }, + } + ) + + expect(result.exitCode).to.be.equal(0) + expect(result.stdout).to.not.have.length(0) + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsRecommendationsFixtureGREEN.map((element) => { + return JSON.stringify(element) + }) + ) + ) + expect(result.stderr).to.have.length(0) + }) + + it('should return 0 recommendations and GREEN status when no recommendations with given CATEGORIES_FILTER are found', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + ...communEnvVariables, + DATA_TYPE: 'recommendations', + CATEGORIES_FILTER: 'RandomCategories', + }, + } + ) + + expect(result.exitCode).to.be.equal(0) + expect(result.stdout).to.not.have.length(0) + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsRecommendationsFixtureGREEN.map((element) => { + return JSON.stringify(element) + }) + ) + ) + expect(result.stderr).to.have.length(0) + }) + + it('should return 0 recommendations and GREEN status when no recommendations with given THREATS_FILTER are found', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + ...communEnvVariables, + DATA_TYPE: 'recommendations', + THREATS_FILTER: 'RandomThreat', + }, + } + ) + + expect(result.exitCode).to.be.equal(0) + expect(result.stdout).to.not.have.length(0) + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsRecommendationsFixtureGREEN.map((element) => { + return JSON.stringify(element) + }) + ) + ) + expect(result.stderr).to.have.length(0) + }) + + it('should return 0 recommendations and GREEN status when no recommendations with given USER_IMPACT_FILTER are found', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + ...communEnvVariables, + DATA_TYPE: 'recommendations', + USER_IMPACT_FILTER: 'RandomUserImpact', + }, + } + ) + + expect(result.exitCode).to.be.equal(0) + expect(result.stdout).to.not.have.length(0) + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsRecommendationsFixtureGREEN.map((element) => { + return JSON.stringify(element) + }) + ) + ) + expect(result.stderr).to.have.length(0) + }) + + it('should return 0 recommendations and GREEN status when no recommendations with given IMPLEMENTATION_EFFORT_FILTER are found', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + ...communEnvVariables, + DATA_TYPE: 'recommendations', + IMPLEMENTATION_EFFORT_FILTER: 'RandomImplementationEffort', + }, + } + ) + + expect(result.exitCode).to.be.equal(0) + expect(result.stdout).to.not.have.length(0) + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsRecommendationsFixtureGREEN.map((element) => { + return JSON.stringify(element) + }) + ) + ) + expect(result.stderr).to.have.length(0) + }) +}) diff --git a/yaku-apps-typescript/apps/defender-for-cloud/test/integration/defender-red-status.int-spec.ts b/yaku-apps-typescript/apps/defender-for-cloud/test/integration/defender-red-status.int-spec.ts new file mode 100644 index 00000000..b1061b25 --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/test/integration/defender-red-status.int-spec.ts @@ -0,0 +1,536 @@ +import * as fs from 'fs' +import * as path from 'path' +import { afterEach, beforeAll, describe, expect, it } from 'vitest' +import { + MockServer, + MockServerOptions, + RunProcessResult, + run, +} from '../../../../integration-tests/src/util' +import { createMockServerOptions } from '../fixtures/serverHelper' +import { + integrationTestResultsAlertsFixture1, + integrationTestResultsAlertsFixture2, + integrationTestResultsAlertsFixture3, +} from '../fixtures/alerts' +import { + integrationTestResultsRecommendationsFixture1, + integrationTestResultsRecommendationsFixture2, + integrationTestResultsRecommendationsFixture3, +} from '../fixtures/recommendations' + + +describe('Defender Autopilot RED status cases', () => { + let mockServer: MockServer | undefined + const communEnvVariables = { + TENANT_ID: 'mockedTenantId', + CLIENT_ID: 'mockedClientId', + CLIENT_SECRET: 'mockedClientSecret', + SUBSCRIPTION_ID: 'mockedSubscriptionId', + IS_INTEGRATION_TEST: 'true', + } + + const defenderAutopilotExecutable: string = path.join( + __dirname, + '..', + '..', + 'dist', + 'index.js' + ) + + beforeAll(() => { + expect(fs.existsSync(defenderAutopilotExecutable)).to.be.true + }) + + afterEach(async () => { + await mockServer?.stop() + mockServer = undefined + }) + + it('should return 4 alerts when no filters are applied', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + DATA_TYPE: 'alerts', + ...communEnvVariables, + }, + } + ) + + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsAlertsFixture1.map((element) => { + return JSON.stringify(element) + }) + ) + ) + }) + + it('should return 3 alerts when ALERT_TYPE_FILTER = "K8S_, RandomPrefix" is applied', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + ...communEnvVariables, + DATA_TYPE: 'alerts', + ALERT_TYPE_FILTER: 'K8S_, RandomPrefix', + }, + } + ) + + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsAlertsFixture2.map((element) => { + return JSON.stringify(element) + }) + ) + ) + }) + + it('should return 1 alert when KEY_WORDS_FILTER = "RandomKeyword1, suspicious download, RandomKeyword2" is applied', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + ...communEnvVariables, + DATA_TYPE: 'alerts', + KEY_WORDS_FILTER: + 'RandomKeyword1, suspicious download, RandomKeyword2', + }, + } + ) + + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsAlertsFixture3.map((element) => { + return JSON.stringify(element) + }) + ) + ) + }) + + it('should return 3 alerts when RESOURCE_NAME_FILTER = "xyz, unn, zyx" is applied', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + ...communEnvVariables, + DATA_TYPE: 'alerts', + RESOURCE_NAME_FILTER: 'xyz, unn, zyx', + }, + } + ) + + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsAlertsFixture2.map((element) => { + return JSON.stringify(element) + }) + ) + ) + }) + + it('should return 1 alert when ALERT_TYPE_FILTER = "K8S_, K8S.NODE_" and KEY_WORDS_FILTER = "RandomKeyword1, suspicious download, RandomKeyword2" are applied', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + ...communEnvVariables, + DATA_TYPE: 'alerts', + ALERT_TYPE_FILTER: 'K8S_, K8S.NODE_', + KEY_WORDS_FILTER: + 'RandomKeyword1, suspicious download, RandomKeyword2', + }, + } + ) + + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsAlertsFixture3.map((element) => { + return JSON.stringify(element) + }) + ) + ) + }) + + it('should return 1 alert when ALERT_TYPE_FILTER = "RandomPrefix, K8S.NODE_" and RESOURCE_NAME_FILTER = "xyz, unn, yzx, 1aks"', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + ...communEnvVariables, + DATA_TYPE: 'alerts', + ALERT_TYPE_FILTER: 'RandomPrefix, K8S.NODE_', + RESOURCE_NAME_FILTER: 'xyz, unn, yzx, 1aks', + }, + } + ) + + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsAlertsFixture3.map((element) => { + return JSON.stringify(element) + }) + ) + ) + }) + + it('should return 4 alerts when KEY_WORDS_FILTER = "container, RandomKeyword1, processes, RandomKeyword2" and RESOURCE_NAME_FILTER = "xyz, unn, yzx, 1aks"', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + ...communEnvVariables, + DATA_TYPE: 'alerts', + KEY_WORDS_FILTER: + 'container, RandomKeyword1, processes, RandomKeyword2', + RESOURCE_NAME_FILTER: 'xyz, unn, yzx, 1aks', + }, + } + ) + + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsAlertsFixture1.map((element) => { + return JSON.stringify(element) + }) + ) + ) + }) + + it('should return 3 alerts when ALERT_TYPE_FILTER = "K8S_, K8S.NODE_", KEY_WORDS_FILTER = "RandomKeyword1, RandomKeyword2, container, processes" and RESOURCE_NAME_FILTER = "xyz, yzx, unn"', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + ...communEnvVariables, + DATA_TYPE: 'alerts', + ALERT_TYPE_FILTER: 'K8S_, K8S.NODE_', + KEY_WORDS_FILTER: + 'RandomKeyword1, RandomKeyword2, container, processes', + RESOURCE_NAME_FILTER: 'xyz, yzx, unn', + }, + } + ) + + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsAlertsFixture2.map((element) => { + return JSON.stringify(element) + }) + ) + ) + }) + + it('should return 3 recommendations when no filters are applied', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + DATA_TYPE: 'recommendations', + ...communEnvVariables, + }, + } + ) + + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsRecommendationsFixture1.map((element) => { + return JSON.stringify(element) + }) + ) + ) + }) + + it('should return 2 recommendations when SEVERITY_FILTER = "High"', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + DATA_TYPE: 'recommendations', + SEVERITY_FILTER: 'High', + ...communEnvVariables, + }, + } + ) + + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsRecommendationsFixture2.map((element) => { + return JSON.stringify(element) + }) + ) + ) + }) + + it('should return 1 recommendation when KEY_WORDS_FILTER = "GKE cluster"', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + DATA_TYPE: 'recommendations', + KEY_WORDS_FILTER: 'GKE cluster', + ...communEnvVariables, + }, + } + ) + + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsRecommendationsFixture3.map((element) => { + return JSON.stringify(element) + }) + ) + ) + }) + + it('should return 1 recommendation when CATEGORIES_FILTER = "Compute"', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + DATA_TYPE: 'recommendations', + CATEGORIES_FILTER: 'Compute', + ...communEnvVariables, + }, + } + ) + + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsRecommendationsFixture3.map((element) => { + return JSON.stringify(element) + }) + ) + ) + }) + + it('should return 2 recommendations when THREATS_FILTER = "MaliciousInsider, DataSpillage"', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + DATA_TYPE: 'recommendations', + THREATS_FILTER: 'MaliciousInsider, DataSpillage', + ...communEnvVariables, + }, + } + ) + + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsRecommendationsFixture2.map((element) => { + return JSON.stringify(element) + }) + ) + ) + }) + + it('should return 2 recommendations when USER_IMPACT_FILTER = "High"', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + DATA_TYPE: 'recommendations', + USER_IMPACT_FILTER: 'High', + ...communEnvVariables, + }, + } + ) + + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsRecommendationsFixture2.map((element) => { + return JSON.stringify(element) + }) + ) + ) + }) + + it('should return 1 recommendation when IMPLEMENTATION_EFFORT_FILTER = "Low"', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + DATA_TYPE: 'recommendations', + IMPLEMENTATION_EFFORT_FILTER: 'Low', + ...communEnvVariables, + }, + } + ) + + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsRecommendationsFixture3.map((element) => { + return JSON.stringify(element) + }) + ) + ) + }) + + it('should return 1 recommendation when IMPLEMENTATION_EFFORT_FILTER = "Low" and THREATS_FILTER = "MaliciousInsider', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + DATA_TYPE: 'recommendations', + IMPLEMENTATION_EFFORT_FILTER: 'Low', + THREATS_FILTER: 'MaliciousInsider', + ...communEnvVariables, + }, + } + ) + + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsRecommendationsFixture3.map((element) => { + return JSON.stringify(element) + }) + ) + ) + }) + + it('should return 2 recommendations when USER_IMPACT_FILTER = "High" and THREATS_FILTER = "MaliciousInsider', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + DATA_TYPE: 'recommendations', + USER_IMPACT_FILTER: 'High', + THREATS_FILTER: 'MaliciousInsider', + ...communEnvVariables, + }, + } + ) + + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsRecommendationsFixture2.map((element) => { + return JSON.stringify(element) + }) + ) + ) + }) + + it('should return 2 recommendations when KEY_WORDS_FILTER = "network" and SEVERITY_FILTER = "High', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + DATA_TYPE: 'recommendations', + SEVERITY_FILTER: 'High', + KEY_WORDS_FILTER: 'network', + ...communEnvVariables, + }, + } + ) + + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsRecommendationsFixture2.map((element) => { + return JSON.stringify(element) + }) + ) + ) + }) + + it('should return 3 recommendations when KEY_WORDS_FILTER = "network" and SEVERITY_FILTER = "High, Medium', async () => { + const options: MockServerOptions = await createMockServerOptions(8080, 200) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + defenderAutopilotExecutable, + [], + { + env: { + DATA_TYPE: 'recommendations', + SEVERITY_FILTER: 'High, Medium', + KEY_WORDS_FILTER: 'network', + ...communEnvVariables, + }, + } + ) + + expect(JSON.stringify(result.stdout)).toEqual( + JSON.stringify( + integrationTestResultsRecommendationsFixture1.map((element) => { + return JSON.stringify(element) + }) + ) + ) + }) + +}) diff --git a/yaku-apps-typescript/apps/defender-for-cloud/test/unit/alertsRetriever.test.ts b/yaku-apps-typescript/apps/defender-for-cloud/test/unit/alertsRetriever.test.ts new file mode 100644 index 00000000..e4c38f07 --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/test/unit/alertsRetriever.test.ts @@ -0,0 +1,86 @@ +import axios from 'axios' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { + getDefenderForCloudAlerts, +} from '../../src/alertsRetriever' +import { + mockedAlertsUnitTestsFirstSet, + mockedAlertsUnitTestsSecondSet, + mockedAlertsUnitTestsThirdSet, +} from '../fixtures/alerts' + +vi.mock('axios', () => ({ + default: { + get: vi.fn() + }, +})) + +const mockedAxiosGet = vi.mocked(axios.get) + +describe('Test "getDefenderForCloudAlerts()" from "alertsRetriever.ts"', async () => { + afterEach(() => { + vi.clearAllMocks() + }) + it('Should return a list of alerts if response status is 200', async () => { + mockedAxiosGet.mockResolvedValueOnce({ + status: 200, + data: { + value: mockedAlertsUnitTestsFirstSet, + }, + }) + + const result = await getDefenderForCloudAlerts( + 'mockedToken', + 'mockTenantId' + ) + expect(result).toEqual(mockedAlertsUnitTestsFirstSet) + }) + + it('Should return a list of alerts if response status is 200 and pagination is required', async () => { + mockedAxiosGet.mockResolvedValueOnce({ + status: 200, + data: { + value: mockedAlertsUnitTestsFirstSet, + nextLink: 'mockedLink1', + }, + }) + + mockedAxiosGet.mockResolvedValueOnce({ + status: 200, + data: { + value: mockedAlertsUnitTestsSecondSet, + nextLink: 'mockedLink2', + }, + }) + + mockedAxiosGet.mockResolvedValueOnce({ + status: 200, + data: { + value: mockedAlertsUnitTestsThirdSet, + }, + }) + + const result = await getDefenderForCloudAlerts( + 'mockedToken', + 'mockTenantId' + ) + expect(result).toEqual( + mockedAlertsUnitTestsFirstSet + .concat(mockedAlertsUnitTestsSecondSet) + .concat(mockedAlertsUnitTestsThirdSet) + ) + }) + + it('Should throw a specific error if status is not 200', async () => { + mockedAxiosGet.mockRejectedValueOnce({ + response: { + status: 400 + } }) + + await expect( + getDefenderForCloudAlerts('mockedToken', 'mockTenantId') + ).rejects.toThrowError( + 'Request for Azure alerts does not have status code 200. Status code: 400' + ) + }) +}) diff --git a/yaku-apps-typescript/apps/defender-for-cloud/test/unit/auth.test.ts b/yaku-apps-typescript/apps/defender-for-cloud/test/unit/auth.test.ts new file mode 100644 index 00000000..00c63b01 --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/test/unit/auth.test.ts @@ -0,0 +1,74 @@ +import axios from 'axios' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { + generateAzureAccessToken, + } from '../../src/auth' + +vi.mock('axios', () => ({ + default: { + post: vi.fn(), + create: vi.fn(), + }, +})) + +const mockedAxiosPost = vi.mocked(axios.post) + +describe('Test "generateAzureAccessToken()" from "auth.ts"', async () => { + afterEach(() => { + vi.clearAllMocks() + }) + it('Should return an access token if response status is 200', async () => { + mockedAxiosPost.mockResolvedValueOnce({ + status: 200, + data: { + access_token: 'mockAccesstoken', + }, + }) + + const result = await generateAzureAccessToken( + 'mockTenantId', + 'mockClientId', + 'mockGrantType', + 'mockClientSecret' + ) + expect(result).toEqual('mockAccesstoken') + }) + + it('Should throw a specific error if status is not 200', async () => { + mockedAxiosPost.mockRejectedValueOnce({ + response: { + status: 400, + } }) + + await expect( + generateAzureAccessToken( + 'mockTenantId', + 'mockClientId', + 'mockGrantType', + 'mockClientSecret' + ) + ).rejects.toThrowError( + 'Request for Azure access token does not have status code 200. Status code: 400' + ) + }) + + it('Should throw a specific error if field "access_token" does not exist on the response returned by Azure authenticator', async () => { + mockedAxiosPost.mockResolvedValueOnce({ + status: 200, + data: { + random_field: 'mockAccesstoken', + }, + }) + + await expect( + generateAzureAccessToken( + 'mockTenantId', + 'mockClientId', + 'mockGrantType', + 'mockClientSecret' + ) + ).rejects.toThrowError( + 'Field "access_token" does not exist on response returned by Azure authenticator' + ) + }) + }) \ No newline at end of file diff --git a/yaku-apps-typescript/apps/defender-for-cloud/test/unit/recommendationsRetriever.test.ts b/yaku-apps-typescript/apps/defender-for-cloud/test/unit/recommendationsRetriever.test.ts new file mode 100644 index 00000000..3d52dcfd --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/test/unit/recommendationsRetriever.test.ts @@ -0,0 +1,156 @@ +import axios from 'axios' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { + getDefenderForCloudRecommendations, + getDefenderForCloudRecommendationsMetadata, +} from '../../src/recommendationsRetriever' +import { + mockedRecommendationsUnitTestsFirstSet, + mockedRecommendationsUnitTestsSecondSet, + mockedRecommendationsUnitTestsThirdSet, + mockedRecommendationsMetadataUnitTestsFirstSet, + mockedRecommendationsMetadataUnitTestsSecondSet, + mockedRecommendationsMetadataUnitTestsThirdSet, +} from '../fixtures/recommendations' + +vi.mock('axios', () => ({ + default: { + get: vi.fn() + }, +})) + +const mockedAxiosGet = vi.mocked(axios.get) + +describe('Test "getDefenderForCloudRecommendations" from "recommendationsRetriever.ts"', async () => { + afterEach(() => { + vi.clearAllMocks() + }) + it('Should return a list of recommendations if response status is 200', async () => { + mockedAxiosGet.mockResolvedValueOnce({ + status: 200, + data: { + value: mockedRecommendationsUnitTestsFirstSet, + }, + }) + + const result = await getDefenderForCloudRecommendations( + 'mockedToken', + 'mockTenantId' + ) + expect(result).toEqual(mockedRecommendationsUnitTestsFirstSet) + }) + + it('Should return a list of recommendations if response status is 200 and pagination is required', async () => { + mockedAxiosGet.mockResolvedValueOnce({ + status: 200, + data: { + value: mockedRecommendationsUnitTestsFirstSet, + nextLink: 'mockedLink1', + }, + }) + + mockedAxiosGet.mockResolvedValueOnce({ + status: 200, + data: { + value: mockedRecommendationsUnitTestsSecondSet, + nextLink: 'mockedLink2', + }, + }) + + mockedAxiosGet.mockResolvedValueOnce({ + status: 200, + data: { + value: mockedRecommendationsUnitTestsThirdSet, + }, + }) + + const result = await getDefenderForCloudRecommendations( + 'mockedToken', + 'mockTenantId' + ) + expect(result).toEqual( + mockedRecommendationsUnitTestsFirstSet + .concat(mockedRecommendationsUnitTestsSecondSet) + .concat(mockedRecommendationsUnitTestsThirdSet) + ) + }) + + it('Should throw a specific error if status is not 200', async () => { + mockedAxiosGet.mockRejectedValueOnce({ + response: { + status: 400 + } }) + + await expect( + getDefenderForCloudRecommendations('mockedToken', 'mockTenantId') + ).rejects.toThrowError( + 'Request for Azure recommendations does not have status code 200. Status code: 400' + ) + }) +}) + +describe('Test "getDefenderForCloudRecommendationsMetadata" from "recommendationsRetriever.ts"', async () => { + afterEach(() => { + vi.clearAllMocks() + }) + it('Should return a list of recommendations metadata if response status is 200', async () => { + mockedAxiosGet.mockResolvedValueOnce({ + status: 200, + data: { + value: mockedRecommendationsMetadataUnitTestsFirstSet, + }, + }) + + const result = await getDefenderForCloudRecommendationsMetadata( + 'mockedToken' + ) + expect(result).toEqual(mockedRecommendationsMetadataUnitTestsFirstSet) + }) + + it('Should return a list of recommendation metadata if response status is 200 and pagination is required', async () => { + mockedAxiosGet.mockResolvedValueOnce({ + status: 200, + data: { + value: mockedRecommendationsMetadataUnitTestsFirstSet, + nextLink: 'mockedLink1', + }, + }) + + mockedAxiosGet.mockResolvedValueOnce({ + status: 200, + data: { + value: mockedRecommendationsMetadataUnitTestsSecondSet, + nextLink: 'mockedLink2', + }, + }) + + mockedAxiosGet.mockResolvedValueOnce({ + status: 200, + data: { + value: mockedRecommendationsMetadataUnitTestsThirdSet, + }, + }) + + const result = await getDefenderForCloudRecommendationsMetadata( + 'mockedToken' + ) + expect(result).toEqual( + mockedRecommendationsMetadataUnitTestsFirstSet + .concat(mockedRecommendationsMetadataUnitTestsSecondSet) + .concat(mockedRecommendationsMetadataUnitTestsThirdSet) + ) + }) + + it('Should throw a specific error if status is not 200', async () => { + mockedAxiosGet.mockRejectedValueOnce({ + response: { + status: 400 + } }) + + await expect( + getDefenderForCloudRecommendationsMetadata('mockedToken') + ).rejects.toThrowError( + 'Request for Azure recommendations metadata does not have status code 200. Status code: 400' + ) + }) +}) \ No newline at end of file diff --git a/yaku-apps-typescript/apps/defender-for-cloud/test/unit/run.test.ts b/yaku-apps-typescript/apps/defender-for-cloud/test/unit/run.test.ts new file mode 100644 index 00000000..67f94dbc --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/test/unit/run.test.ts @@ -0,0 +1,1403 @@ +import { afterEach, describe, expect, it, vi, beforeEach } from 'vitest' +import { + parseFilterValues, + prefixMatchAlerts, + prefixMatchRecommendations, + validateRequiredEnvVariables, + Filter, + getSecurityAlertsOnASubscription, + getRecommendationsOnASubscription, + getRecommendationsMetadataOnASubscription, + run, + getUnhealthyRecommendations, + combineRecommendationAndMetadata, +} from '../../src/run' + +import { mockedAlertsUnitTestsFirstSet } from '../fixtures/alerts' +import { + mockedHealthyRecommendation1, + mockedHealthyRecommendation2, + mockedUnhealthyRecommendation1, + mockedUnhealthyRecommendation2, + mockedRecommendationsUnitTestsFirstSet, + mockedRecommendationsMetadataUnitTestsFirstSet, + mockedRecommendationsMetadataUnitTestsSecondSet, + mockedCombinedRecommendationsFirstSet, + mockedRecommendationsMetadataMissingFields, + mockedCombinedRecommendationsWithEmptyFields, +} from '../fixtures/recommendations' +import * as alertsRetriever from '../../src/alertsRetriever' +import * as recommendationsRetriever from '../../src/recommendationsRetriever' +import * as autopilotUtils from '@B-S-F/autopilot-utils' +import * as auth from '../../src/auth' + +describe('Test "parseFilterValues()" from "run.ts"', () => { + it('Should correctly split an "input filter string" and return the array of input values', () => { + const inputFilterString = 'Kubernetes, critical, latest' + const result = parseFilterValues(inputFilterString) + expect(result).toEqual(['Kubernetes', 'critical', 'latest']) + }) + + it('Should correctly split an "input filter string" and return the array of input values even when newlines and multiple spaces are present', () => { + const inputFilterString = `Kubernetes , + critical , + latest` + const result = parseFilterValues(inputFilterString) + expect(result).toEqual(['Kubernetes', 'critical', 'latest']) + }) + + it('Should return null when null is given as input', () => { + const inputFilterString = null + const result = parseFilterValues(inputFilterString) + expect(result).toEqual(null) + }) + + it('Should return null when undefined is given as input', () => { + const inputFilterString = undefined + const result = parseFilterValues(inputFilterString) + expect(result).toEqual(null) + }) + + it('Should return null when an empty string is given as input', () => { + const inputFilterString = '' + const result = parseFilterValues(inputFilterString) + expect(result).toEqual(null) + }) +}) + +describe('Test "prefixMatchAlerts()" from "run.ts"', () => { + it('Should return "true" if the field "properties.alertType" of the alert starts with the AlertType filter given as input', () => { + const alert = { properties: { alertType: 'K8S_PrivilegedContainer' } } + const filterValues = ['K8S_'] + const filterType = Filter.AlertType + + const result = prefixMatchAlerts(alert, filterValues, filterType) + + expect(result).toEqual(true) + }) + + it('Should return "true" if the field "properties.alertType" of the alert starts with one of the multiple AlertType filters given as input', () => { + const alert = { properties: { alertType: 'K8S_PrivilegedContainer' } } + const filterValues = ['VM_', 'SQL_', 'K8S_', 'DNS_'] + const filterType = Filter.AlertType + + const result = prefixMatchAlerts(alert, filterValues, filterType) + + expect(result).toEqual(true) + }) + + it('Should return "false" if the field "properties.alertType" of the alert does not start with one of the multiple AlertType filters given as input', () => { + const alert = { properties: { alertType: 'K8S_PrivilegedContainer' } } + const filterValues = ['VM_', 'SQL_', 'K8S.NODE_', 'DNS_'] + const filterType = Filter.AlertType + + const result = prefixMatchAlerts(alert, filterValues, filterType) + + expect(result).toEqual(false) + }) + + it('Should return "true" if the field "properties.alertDisplayName" of the alert matches one of the multiple KeyWords filters given as input', () => { + const alert = { + properties: { + alertDisplayName: 'Privileged container detected', + description: + "Kubernetes audit log analysis detected a new privileged container. A privileged container has access to the node's resources and breaks the isolation between containers. If compromised, an attacker can use the privileged container to gain access to the node.", + }, + } + const filterValues = ['xyz', 'ntaine', 'abc'] + const filterType = Filter.KeyWords + + const result = prefixMatchAlerts(alert, filterValues, filterType) + + expect(result).toEqual(true) + }) + + it('Should return "true" if the field "properties.description" of the alert matches one of the multiple KeyWords filters given as input', () => { + const alert = { + properties: { + alertDisplayName: 'Privileged container detected', + description: + "Kubernetes audit log analysis detected a new privileged container. A privileged container has access to the node's resources and breaks the isolation between containers. If compromised, an attacker can use the privileged container to gain access to the node.", + }, + } + const filterValues = ['xyz', 'ompromise', 'abc'] + const filterType = Filter.KeyWords + + const result = prefixMatchAlerts(alert, filterValues, filterType) + + expect(result).toEqual(true) + }) + + it('Should return "false" if neither "properties.alertDisplayName" nor "properties.description" matches any of the multiple KeyWords filters given as input', () => { + const alert = { + properties: { + alertDisplayName: 'Privileged container detected', + description: + "Kubernetes audit log analysis detected a new privileged container. A privileged container has access to the node's resources and breaks the isolation between containers. If compromised, an attacker can use the privileged container to gain access to the node.", + }, + } + const filterValues = [ + 'xyz', + 'test string that does not exist in neither fields', + 'abc', + ] + const filterType = Filter.KeyWords + + const result = prefixMatchAlerts(alert, filterValues, filterType) + + expect(result).toEqual(false) + }) + + it('Should return "true" if the field "properties.compromisedEntity" of the alert matches one of the multiple ResourceName filters given as input', () => { + const alert = { + properties: { + compromisedEntity: 'top99-runner-aks-dev', + }, + } + const filterValues = ['xyz', 'unn', 'abc'] + const filterType = Filter.ResourceName + + const result = prefixMatchAlerts(alert, filterValues, filterType) + + expect(result).toEqual(true) + }) + + it('Should return "false" if the field "properties.compromisedEntity" of the alert does not match any of the multiple ResourceName filters given as input', () => { + const alert = { + properties: { + compromisedEntity: 'top99-runner-aks-dev', + }, + } + const filterValues = [ + 'xyz', + 'test string that does not match "properties.compromisedEntity"', + 'abc', + ] + const filterType = Filter.ResourceName + + const result = prefixMatchAlerts(alert, filterValues, filterType) + + expect(result).toEqual(false) + }) +}) + +describe('Test "prefixMatchRecommendations()" from "run.ts"', () => { + it('Should return "true" if the field "properties.severity" of the recommendation matches one of the multiple Severity filters given as input', () => { + const recommendation = { + properties: { + severity: 'Medium', + }, + } + const filterValues = ['Low', 'Medium'] + const filterType = Filter.Severity + const result = prefixMatchRecommendations(recommendation, filterValues, filterType) + expect(result).toEqual(true) + }) + + it('Should return "false" if the field "properties.severity" of the recommendation does not match any of the multiple Severity filters given as input', () => { + const recommendation = { + properties: { + severity: 'Medium', + }, + } + const filterValues = ['Low', 'High'] + const filterType = Filter.Severity + const result = prefixMatchRecommendations(recommendation, filterValues, filterType) + expect(result).toEqual(false) + }) + + it('Should return "true" if at least one value from the field "properties.categories" of the recommendation matches one of the multiple Categories filters given as input', () => { + const recommendation = { + properties: { + categories: ['Data'], + }, + } + const filterValues = ['Data', 'Networking', 'Unknown'] + const filterType = Filter.Categories + const result = prefixMatchRecommendations(recommendation, filterValues, filterType) + expect(result).toEqual(true) + }) + + it('Should return "false" if none of the values from the field "properties.categories" of the recommendation match any of the multiple Categories filters given as input', () => { + const recommendation = { + properties: { + categories: ['Data'], + }, + } + const filterValues = ['Compute', 'Networking', 'Unknown'] + const filterType = Filter.Categories + const result = prefixMatchRecommendations(recommendation, filterValues, filterType) + expect(result).toEqual(false) + }) + + it('Should return "true" if at least one value from the field "properties.threats" of the recommendation matches one of the multiple Threats filters given as input', () => { + const recommendation = { + properties: { + threats: [ + "DataExfiltration", + "DataSpillage", + "AccountBreach", + "ElevationOfPrivilege" + ], + } + } + const filterValues = ['abc', 'DataSpillage', 'xyz'] + const filterType = Filter.Threats + const result = prefixMatchRecommendations(recommendation, filterValues, filterType) + expect(result).toEqual(true) + }) + + it('Should return "false" if none of the values from the field "properties.threats" of the recommendation matches any of the multiple Threats filters given as input', () => { + const recommendation = { + properties: { + threats: [ + "DataExfiltration", + "DataSpillage", + "AccountBreach", + "ElevationOfPrivilege" + ], + } + } + const filterValues = ['abc', '123', 'xyz'] + const filterType = Filter.Threats + const result = prefixMatchRecommendations(recommendation, filterValues, filterType) + expect(result).toEqual(false) + }) + + it('Should return "true" if the field "properties.displayName" of the recommendation matches one of the multiple KeyWords filters given as input', () => { + const recommendation = { + properties: { + displayName: 'Azure DDoS Protection Standard should be enabled', + description: + 'Defender for Cloud has discovered virtual networks with Application Gateway resources unprotected by the DDoS protection service. These resources contain public IPs. Enable mitigation of network volumetric and protocol attacks.', + }, + } + const filterValues = ['xyz', 'Standard', 'abc'] + const filterType = Filter.KeyWords + + const result = prefixMatchRecommendations(recommendation, filterValues, filterType) + + expect(result).toEqual(true) + }) + + it('Should return "true" if the field "properties.description" of the recommendation matches one of the multiple KeyWords filters given as input', () => { + const recommendation = { + properties: { + displayName: 'Azure DDoS Protection Standard should be enabled', + description: + 'Defender for Cloud has discovered virtual networks with Application Gateway resources unprotected by the DDoS protection service. These resources contain public IPs. Enable mitigation of network volumetric and protocol attacks.', + }, + } + const filterValues = ['xyz', 'network', 'abc'] + const filterType = Filter.KeyWords + + const result = prefixMatchRecommendations(recommendation, filterValues, filterType) + + expect(result).toEqual(true) + }) + + it('Should return "false" if neither "properties.displayName" nor "properties.description" of the recommendation matches any of the multiple KeyWords filters given as input', () => { + const recommendation = { + properties: { + displayName: 'Azure DDoS Protection Standard should be enabled', + description: + 'Defender for Cloud has discovered virtual networks with Application Gateway resources unprotected by the DDoS protection service. These resources contain public IPs. Enable mitigation of network volumetric and protocol attacks.', + }, + } + const filterValues = [ + 'xyz', + 'test string that does not exist in neither fields', + 'abc', + ] + const filterType = Filter.KeyWords + + const result = prefixMatchRecommendations(recommendation, filterValues, filterType) + + expect(result).toEqual(false) + }) + + it('Should return "true" if the field "properties.userImpact" of the recommendation matches one of the multiple UserImpact filters given as input', () => { + const recommendation = { + properties: { + userImpact: 'Moderate', + }, + } + const filterValues = ['Moderate', 'Low', 'abc'] + const filterType = Filter.UserImpact + const result = prefixMatchRecommendations(recommendation, filterValues, filterType) + expect(result).toEqual(true) + }) + + it('Should return "false" if the field "properties.userImpact" of the recommendation does not match any of the multiple UserImpact filters given as input', () => { + const recommendation = { + properties: { + userImpact: 'Moderate', + }, + } + const filterValues = ['Low', 'High'] + const filterType = Filter.UserImpact + const result = prefixMatchRecommendations(recommendation, filterValues, filterType) + expect(result).toEqual(false) + }) + + it('Should return "true" if the field "properties.implementationEffort" of the recommendation matches one of the multiple ImplementationEffort filters given as input', () => { + const recommendation = { + properties: { + implementationEffort: 'Low', + }, + } + const filterValues = ['Moderate', 'Low', 'abc'] + const filterType = Filter.ImplementationEffort + const result = prefixMatchRecommendations(recommendation, filterValues, filterType) + expect(result).toEqual(true) + }) + + it('Should return "false" if the field "properties.implementationEffort" of the recommendation does not match any of the multiple ImplementationEffort filters given as input', () => { + const recommendation = { + properties: { + implementationEffort: 'Low', + }, + } + const filterValues = ['xyz', 'High'] + const filterType = Filter.ImplementationEffort + const result = prefixMatchRecommendations(recommendation, filterValues, filterType) + expect(result).toEqual(false) + }) +}) + +describe('Test "validateRequiredEnvVariables()" from "run.ts"', () => { + process.env.TENANT_ID = 'mockedTenantId' + process.env.CLIENT_ID = 'mockedClientId' + process.env.CLIENT_SECRET = 'mockedClientSecret' + process.env.SUBSCRIPTION_ID = 'mockedSubscriptionId' + + it('Should throw an error if DATA_TYPE is not equal to "alerts" or "recommendations"', () => { + process.env.DATA_TYPE = 'recommendation' + expect(() => validateRequiredEnvVariables()).toThrowError( + `Invalid value for DATA_TYPE environment variable! DATA_TYPE should be either 'alerts' or 'recommendations' and in this case is 'recommendation'` + ) + }) + + it('Should return default DATA_TYPE "alerts" if DATA_TYPE is undefined', () => { + delete process.env.DATA_TYPE + const result = validateRequiredEnvVariables() + expect(result).toEqual('alerts') + }) + + it('Should return default DATA_TYPE "alerts" if DATA_TYPE is an empty string', () => { + process.env.DATA_TYPE = '' + const result = validateRequiredEnvVariables() + expect(result).toEqual('alerts') + }) + + it('Should throw an error if TENANT_ID is undefined', () => { + delete process.env.TENANT_ID + expect(() => validateRequiredEnvVariables()).toThrowError( + 'Please provide TENANT_ID in the environmental variables before running the autopilot' + ) + }) + + it('Should throw an error if CLIENT_ID is empty string', () => { + process.env.TENANT_ID = 'mockedTenantId' + process.env.CLIENT_ID = '' + expect(() => validateRequiredEnvVariables()).toThrowError( + 'Please provide CLIENT_ID in the environmental variables before running the autopilot' + ) + }) + + it('Should throw an error if CLIENT_SECRET is undefined', () => { + process.env.CLIENT_ID = 'mockedClientId' + delete process.env.CLIENT_SECRET + expect(() => validateRequiredEnvVariables()).toThrowError( + 'Please provide CLIENT_SECRET in the environmental variables before running the autopilot' + ) + }) + + it('Should throw an error if SUBSCRIPTION_ID is empty string', () => { + process.env.CLIENT_SECRET = 'mockedClientSecret' + process.env.SUBSCRIPTION_ID = '' + expect(() => validateRequiredEnvVariables()).toThrowError( + 'Please provide SUBSCRIPTION_ID in the environmental variables before running the autopilot' + ) + }) +}) + +describe('Test "getSecurityAlertsOnASubscription()" from "run.ts"', async () => { + afterEach(() => { + vi.clearAllMocks() + }) + it('Should return a list of alerts if response status is 200', async () => { + const mockedTokenSpy = vi.spyOn(auth, 'generateAzureAccessToken') + const mockedToken = 'mockedToken' + mockedTokenSpy.mockResolvedValueOnce(mockedToken) + + const mockedSecurityAlertsSpy = vi.spyOn( + alertsRetriever, + 'getDefenderForCloudAlerts' + ) + const mockedSecurityAlerts = mockedAlertsUnitTestsFirstSet + mockedSecurityAlertsSpy.mockResolvedValueOnce(mockedSecurityAlerts) + + const result = await getSecurityAlertsOnASubscription() + + expect(result).toEqual(mockedSecurityAlerts) + }) + + it('Should throw a specific error from "generateAzureAccessToken()" if status is not 200 ', async () => { + const mockedTokenSpy = vi.spyOn(auth, 'generateAzureAccessToken') + mockedTokenSpy.mockRejectedValueOnce( + new Error( + 'Request for Azure access token does not have status code 200. Status code: 400' + ) + ) + + expect(getSecurityAlertsOnASubscription()).rejects.toThrow( + 'Request for Azure access token does not have status code 200. Status code: 400' + ) + }) + + it('Should throw a specific error from "getDefenderForCloudAlerts()" if status is not 200 ', async () => { + const mockedTokenSpy = vi.spyOn(auth, 'generateAzureAccessToken') + const mockedToken = 'mockedToken' + mockedTokenSpy.mockResolvedValueOnce(mockedToken) + + const mockedSecurityAlertsSpy = vi.spyOn( + alertsRetriever, + 'getDefenderForCloudAlerts' + ) + mockedSecurityAlertsSpy.mockRejectedValueOnce( + new Error( + 'Request for Azure alerts does not have status code 200. Status code: 400' + ) + ) + + expect(getSecurityAlertsOnASubscription()).rejects.toThrow( + 'Request for Azure alerts does not have status code 200. Status code: 400' + ) + }) +}) + +describe('Test "getRecommendationsOnASubscription()" from "run.ts"', async () => { + afterEach(() => { + vi.clearAllMocks() + }) + it('Should return a list of recommendations if response status is 200', async () => { + const mockedTokenSpy = vi.spyOn(auth, 'generateAzureAccessToken') + const mockedToken = 'mockedToken' + mockedTokenSpy.mockResolvedValueOnce(mockedToken) + + const mockedRecommendationsSpy = vi.spyOn( + recommendationsRetriever, + 'getDefenderForCloudRecommendations' + ) + const mockedRecommendations = mockedRecommendationsUnitTestsFirstSet + mockedRecommendationsSpy.mockResolvedValueOnce(mockedRecommendations) + + const result = await getRecommendationsOnASubscription() + + expect(result).toEqual(mockedRecommendations) + }) + + it('Should throw a specific error from "generateAzureAccessToken()" if status is not 200 ', async () => { + const mockedTokenSpy = vi.spyOn(auth, 'generateAzureAccessToken') + mockedTokenSpy.mockRejectedValueOnce( + new Error( + 'Request for Azure access token does not have status code 200. Status code: 400' + ) + ) + + expect(getRecommendationsOnASubscription()).rejects.toThrow( + 'Request for Azure access token does not have status code 200. Status code: 400' + ) + }) + + it('Should throw a specific error from "getDefenderForCloudRecommendations()" if status is not 200 ', async () => { + const mockedTokenSpy = vi.spyOn(auth, 'generateAzureAccessToken') + const mockedToken = 'mockedToken' + mockedTokenSpy.mockResolvedValueOnce(mockedToken) + + const mockedRecommendationsSpy = vi.spyOn( + recommendationsRetriever, + 'getDefenderForCloudRecommendations' + ) + mockedRecommendationsSpy.mockRejectedValueOnce( + new Error( + 'Request for Azure recommendations does not have status code 200. Status code: 400' + ) + ) + + expect(getRecommendationsOnASubscription()).rejects.toThrow( + 'Request for Azure recommendations does not have status code 200. Status code: 400' + ) + }) +}) + +describe('Test "getRecommendationsMetadataOnASubscription()" from "run.ts"', async () => { + afterEach(() => { + vi.clearAllMocks() + }) + it('Should return a list of recommendations metadata if response status is 200', async () => { + const mockedTokenSpy = vi.spyOn(auth, 'generateAzureAccessToken') + const mockedToken = 'mockedToken' + mockedTokenSpy.mockResolvedValueOnce(mockedToken) + + const mockedRecommendationsMetadataSpy = vi.spyOn( + recommendationsRetriever, + 'getDefenderForCloudRecommendationsMetadata' + ) + const mockedMetadata = mockedRecommendationsMetadataUnitTestsFirstSet + mockedRecommendationsMetadataSpy.mockResolvedValueOnce(mockedMetadata) + + const result = await getRecommendationsMetadataOnASubscription() + + expect(result).toEqual(mockedMetadata) + }) + + it('Should throw a specific error from "generateAzureAccessToken()" if status is not 200 ', async () => { + const mockedTokenSpy = vi.spyOn(auth, 'generateAzureAccessToken') + mockedTokenSpy.mockRejectedValueOnce( + new Error( + 'Request for Azure access token does not have status code 200. Status code: 400' + ) + ) + + expect(getRecommendationsMetadataOnASubscription()).rejects.toThrow( + 'Request for Azure access token does not have status code 200. Status code: 400' + ) + }) + + it('Should throw a specific error from "getDefenderForCloudRecommendationsMetadata()" if status is not 200 ', async () => { + const mockedTokenSpy = vi.spyOn(auth, 'generateAzureAccessToken') + const mockedToken = 'mockedToken' + mockedTokenSpy.mockResolvedValueOnce(mockedToken) + + const mockedRecommendationsMetadataSpy = vi.spyOn( + recommendationsRetriever, + 'getDefenderForCloudRecommendationsMetadata' + ) + mockedRecommendationsMetadataSpy.mockRejectedValueOnce( + new Error( + 'Request for Azure recommendations metadata does not have status code 200. Status code: 400' + ) + ) + + expect(getRecommendationsMetadataOnASubscription()).rejects.toThrow( + 'Request for Azure recommendations metadata does not have status code 200. Status code: 400' + ) + }) +}) + +describe('Test "getUnhealthyRecommendations()" from "run.ts"', () => { + it('Should return an array of all unhealthy recommendations', () => { + const mockedMixedRecommendations = [ + mockedUnhealthyRecommendation1, + mockedUnhealthyRecommendation2, + mockedHealthyRecommendation1, + mockedHealthyRecommendation2 + ] + + const mockedUnhealthyRecommendations: any[] = [ + mockedUnhealthyRecommendation1, + mockedUnhealthyRecommendation2 + ] + + const result = getUnhealthyRecommendations(mockedMixedRecommendations) + expect(result).toEqual(mockedUnhealthyRecommendations) + }) + + it('Should return an empty map if there are no unhealthy recommendations', () => { + const mockedMixedRecommendations = [ + mockedHealthyRecommendation1, + mockedHealthyRecommendation2 + ] + + const result = getUnhealthyRecommendations(mockedMixedRecommendations) + expect(result).toEqual([]) + }) +}) + +describe('Test "combineRecommendationAndMetadata()" from "run.ts"', () => { + it('Should return a list of merged recommendations if the metadata input includes items matching the name of any input recommendations', () => { + const copyMockedRecommendationsUnitTestsFirstSet = JSON.parse(JSON.stringify(mockedRecommendationsUnitTestsFirstSet)) + const result = combineRecommendationAndMetadata(copyMockedRecommendationsUnitTestsFirstSet, mockedRecommendationsMetadataUnitTestsFirstSet) + expect(result).toEqual(mockedCombinedRecommendationsFirstSet) + }) + + it('Should return a list of unchanged recommendations if the metadata input does not include items matching the name of any input recommendations', () => { + const copyMockedRecommendationsUnitTestsFirstSet = JSON.parse(JSON.stringify(mockedRecommendationsUnitTestsFirstSet)) + const result = combineRecommendationAndMetadata(copyMockedRecommendationsUnitTestsFirstSet, mockedRecommendationsMetadataUnitTestsSecondSet) + expect(result).toEqual(mockedRecommendationsUnitTestsFirstSet) + }) + + it('Should return a list of unchanged recommendations if the metadata input is empty', () => { + const copyMockedRecommendationsUnitTestsFirstSet = JSON.parse(JSON.stringify(mockedRecommendationsUnitTestsFirstSet)) + const result = combineRecommendationAndMetadata(copyMockedRecommendationsUnitTestsFirstSet, []) + expect(result).toEqual(mockedRecommendationsUnitTestsFirstSet) + }) + + it('Should return an empty list if the input recommendations list is empty and the metadata input is non-empty', () => { + const result = combineRecommendationAndMetadata([], mockedRecommendationsMetadataUnitTestsFirstSet) + expect(result).toEqual([]) + }) + + it('Should return recommendations with an empty properties field if the property is absent from the metadata properties', () => { + const copyMockedRecommendationsUnitTestsFirstSet = JSON.parse(JSON.stringify(mockedRecommendationsUnitTestsFirstSet)) + const result = combineRecommendationAndMetadata(copyMockedRecommendationsUnitTestsFirstSet, mockedRecommendationsMetadataMissingFields) + expect(result).toEqual(mockedCombinedRecommendationsWithEmptyFields) + }) +}) + +describe('Test "run()" from "run.ts"', async () => { + process.exit = vi.fn() + + beforeEach(() => { + delete process.env.TENANT_ID + delete process.env.CLIENT_ID + delete process.env.CLIENT_SECRET + delete process.env.SUBSCRIPTION_ID + + delete process.env.ALERT_TYPE_FILTER + delete process.env.KEY_WORDS_FILTER + delete process.env.RESOURCE_NAME_FILTER + }) + + afterEach(() => { + vi.unstubAllEnvs() + vi.clearAllMocks() + vi.restoreAllMocks() + }) + + describe('Environment variables validation', () => { + describe('Undefined required environment variables', () => { + beforeEach(() => { + vi.stubEnv('TENANT_ID', 'mockedTenantId') + vi.stubEnv('CLIENT_ID', 'mockedClientId') + vi.stubEnv('CLIENT_SECRET', 'mockedClientSecret') + vi.stubEnv('SUBSCRIPTION_ID', 'mockedSubscriptionId') + }) + + it.each([ + { name: 'TENANT_ID' }, + { name: 'CLIENT_ID' }, + { name: 'CLIENT_SECRET' }, + { name: 'SUBSCRIPTION_ID' }, + ])( + 'Should set status FAILED when $name is not set', + async (envVariable) => { + delete process.env[`${envVariable.name}`] + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + + await run() + + expect(spyStatus).toHaveBeenCalledWith('FAILED') + expect(spyReason).toHaveBeenCalledWith( + 'Please provide ' + + `${envVariable.name} ` + + 'in the environmental variables before running the autopilot' + ) + } + ) + it('Should set status FAILED when no required environment variables are set', async () => { + delete process.env.TENANT_ID + delete process.env.CLIENT_ID + delete process.env.CLIENT_SECRET + delete process.env.SUBSCRIPTION_ID + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + + await run() + + expect(spyStatus).toHaveBeenCalledWith('FAILED') + expect(spyReason).toHaveBeenCalledWith( + 'Please provide TENANT_ID in the environmental variables before running the autopilot' + ) + }) + }) + + describe('Required environment variables are empty strings', () => { + beforeEach(() => { + vi.stubEnv('TENANT_ID', 'mockedTenantId') + vi.stubEnv('CLIENT_ID', 'mockedClientId') + vi.stubEnv('CLIENT_SECRET', 'mockedClientSecret') + vi.stubEnv('SUBSCRIPTION_ID', 'mockedSubscriptionId') + }) + + afterEach(() => { + vi.unstubAllEnvs() + vi.clearAllMocks() + }) + + it.each([ + { + name: 'TENANT_ID', + value: '', + }, + { + name: 'CLIENT_ID', + value: '', + }, + { + name: 'CLIENT_SECRET', + value: '', + }, + { + name: 'SUBSCRIPTION_ID', + value: '', + }, + ])( + 'Should set status FAILED when $name is set, but empty', + async (envVariable) => { + delete process.env[`${envVariable.name}`] + vi.stubEnv(`${envVariable.name}`, envVariable.value) + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + + await run() + + expect(spyStatus).toHaveBeenCalledWith('FAILED') + expect(spyReason).toHaveBeenCalledWith( + 'Please provide ' + + `${envVariable.name} ` + + 'in the environmental variables before running the autopilot' + ) + } + ) + }) + }) + + describe('Filters configuration for alerts', () => { + beforeEach(() => { + vi.stubEnv('TENANT_ID', 'mockedTenantId') + vi.stubEnv('CLIENT_ID', 'mockedClientId') + vi.stubEnv('CLIENT_SECRET', 'mockedClientSecret') + vi.stubEnv('SUBSCRIPTION_ID', 'mockedSubscriptionId') + vi.stubEnv('ALERT_TYPE_FILTER', 'K8S_PrivilegedContainer, Kubernetes') + vi.stubEnv('KEY_WORDS_FILTER', 'K8S, suspicious, container') + vi.stubEnv('RESOURCE_NAME_FILTER', 'K8S, dev1aks') + vi.stubEnv('DATA_TYPE', 'alerts') + }) + afterEach(() => { + vi.unstubAllEnvs() + vi.clearAllMocks() + vi.restoreAllMocks() + }) + + it('Should return status RED with 2 retrieved alerts when there are zero filters', async () => { + delete process.env.ALERT_TYPE_FILTER + delete process.env.KEY_WORDS_FILTER + delete process.env.RESOURCE_NAME_FILTER + + const generateAzureAccessTokenSpy = vi.spyOn( + auth, + 'generateAzureAccessToken' + ) + const mockedToken = 'mockedToken' + generateAzureAccessTokenSpy.mockResolvedValueOnce(mockedToken) + + const getDefenderForCloudAlertsSpy = vi.spyOn( + alertsRetriever, + 'getDefenderForCloudAlerts' + ) + const mockedSecurityAlerts = mockedAlertsUnitTestsFirstSet + getDefenderForCloudAlertsSpy.mockResolvedValueOnce(mockedSecurityAlerts) + + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + + await run() + + expect(spyStatus).toHaveBeenCalledWith('RED') + expect(spyReason).toHaveBeenCalledWith( + 'Retrieved 2 alerts based on given filters' + ) + }) + + it('Should return status RED with 1 retrieved alert when there is ALERT_TYPE_FILTER filter', async () => { + delete process.env.KEY_WORDS_FILTER + delete process.env.RESOURCE_NAME_FILTER + + const generateAzureAccessTokenSpy = vi.spyOn( + auth, + 'generateAzureAccessToken' + ) + const mockedToken = 'mockedToken' + generateAzureAccessTokenSpy.mockResolvedValueOnce(mockedToken) + + const getDefenderForCloudAlertsSpy = vi.spyOn( + alertsRetriever, + 'getDefenderForCloudAlerts' + ) + const mockedSecurityAlerts = mockedAlertsUnitTestsFirstSet + getDefenderForCloudAlertsSpy.mockResolvedValueOnce(mockedSecurityAlerts) + + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + + await run() + + expect(spyStatus).toHaveBeenCalledWith('RED') + expect(spyReason).toHaveBeenCalledWith( + 'Retrieved 1 alerts based on given filters' + ) + }) + + it('Should return status RED with 2 retrieved alerts when there is KEY_WORDS_FILTER filter', async () => { + delete process.env.ALERT_TYPE_FILTER + delete process.env.RESOURCE_NAME_FILTER + + const generateAzureAccessTokenSpy = vi.spyOn( + auth, + 'generateAzureAccessToken' + ) + const mockedToken = 'mockedToken' + generateAzureAccessTokenSpy.mockResolvedValueOnce(mockedToken) + + const getDefenderForCloudAlertsSpy = vi.spyOn( + alertsRetriever, + 'getDefenderForCloudAlerts' + ) + const mockedSecurityAlerts = mockedAlertsUnitTestsFirstSet + getDefenderForCloudAlertsSpy.mockResolvedValueOnce(mockedSecurityAlerts) + + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + + await run() + + expect(spyStatus).toHaveBeenCalledWith('RED') + expect(spyReason).toHaveBeenCalledWith( + 'Retrieved 2 alerts based on given filters' + ) + }) + + it('Should return status RED with 1 retrieved alert when there is RESOURCE_NAME_FILTER filter', async () => { + delete process.env.ALERT_TYPE_FILTER + delete process.env.KEY_WORDS_FILTER + + const generateAzureAccessTokenSpy = vi.spyOn( + auth, + 'generateAzureAccessToken' + ) + const mockedToken = 'mockedToken' + generateAzureAccessTokenSpy.mockResolvedValueOnce(mockedToken) + + const getDefenderForCloudAlertsSpy = vi.spyOn( + alertsRetriever, + 'getDefenderForCloudAlerts' + ) + const mockedSecurityAlerts = mockedAlertsUnitTestsFirstSet + getDefenderForCloudAlertsSpy.mockResolvedValueOnce(mockedSecurityAlerts) + + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + + await run() + + expect(spyStatus).toHaveBeenCalledWith('RED') + expect(spyReason).toHaveBeenCalledWith( + 'Retrieved 1 alerts based on given filters' + ) + }) + + it('Should return status RED with 1 retrieved alert when there are ALERT_TYPE_FILTER and KEY_WORDS_FILTER filters', async () => { + delete process.env.RESOURCE_NAME_FILTER + + const generateAzureAccessTokenSpy = vi.spyOn( + auth, + 'generateAzureAccessToken' + ) + const mockedToken = 'mockedToken' + generateAzureAccessTokenSpy.mockResolvedValueOnce(mockedToken) + + const getDefenderForCloudAlertsSpy = vi.spyOn( + alertsRetriever, + 'getDefenderForCloudAlerts' + ) + const mockedSecurityAlerts = mockedAlertsUnitTestsFirstSet + getDefenderForCloudAlertsSpy.mockResolvedValueOnce(mockedSecurityAlerts) + + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + + await run() + + expect(spyStatus).toHaveBeenCalledWith('RED') + expect(spyReason).toHaveBeenCalledWith( + 'Retrieved 1 alerts based on given filters' + ) + }) + + it('Should return status GREEN with 0 retrieved alerts when there are all the filters', async () => { + const generateAzureAccessTokenSpy = vi.spyOn( + auth, + 'generateAzureAccessToken' + ) + const mockedToken = 'mockedToken' + generateAzureAccessTokenSpy.mockResolvedValueOnce(mockedToken) + + const getDefenderForCloudAlertsSpy = vi.spyOn( + alertsRetriever, + 'getDefenderForCloudAlerts' + ) + const mockedSecurityAlerts = mockedAlertsUnitTestsFirstSet + getDefenderForCloudAlertsSpy.mockResolvedValueOnce(mockedSecurityAlerts) + + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + + await run() + + expect(spyStatus).toHaveBeenCalledWith('GREEN') + expect(spyReason).toHaveBeenCalledWith( + 'No alerts found based on given filters' + ) + }) + }) + + describe('Filters configuration for recommendations', () => { + beforeEach(() => { + vi.stubEnv('TENANT_ID', 'mockedTenantId') + vi.stubEnv('CLIENT_ID', 'mockedClientId') + vi.stubEnv('CLIENT_SECRET', 'mockedClientSecret') + vi.stubEnv('SUBSCRIPTION_ID', 'mockedSubscriptionId') + vi.stubEnv('SEVERITY_FILTER', 'High') + vi.stubEnv('KEY_WORDS_FILTER', 'network, virtual') + vi.stubEnv('CATEGORIES_FILTER', 'Networking') + vi.stubEnv('THREATS_FILTER', 'ThreatResistance, DenialOfService, DataSpillage') + vi.stubEnv('USER_IMPACT_FILTER', 'Moderate') + vi.stubEnv('IMPLEMENTATION_EFFORT_FILTER', 'Moderate') + vi.stubEnv('DATA_TYPE', 'recommendations') + }) + afterEach(() => { + vi.unstubAllEnvs() + vi.clearAllMocks() + vi.restoreAllMocks() + }) + + it('Should return status RED with 2 retrieved unhealthy recommendations when there are zero filters', async () => { + delete process.env.SEVERITY_FILTER + delete process.env.KEY_WORDS_FILTER + delete process.env.CATEGORIES_FILTER + delete process.env.THREATS_FILTER + delete process.env.USER_IMPACT_FILTER + delete process.env.IMPLEMENTATION_EFFORT_FILTER + + const generateAzureAccessTokenSpy = vi.spyOn( + auth, + 'generateAzureAccessToken' + ) + const mockedToken = 'mockedToken' + generateAzureAccessTokenSpy.mockResolvedValueOnce(mockedToken) + + const getDefenderForCloudRecommendationsSpy = vi.spyOn( + recommendationsRetriever, + 'getDefenderForCloudRecommendations' + ) + const mockedRecommendations = mockedRecommendationsUnitTestsFirstSet + getDefenderForCloudRecommendationsSpy.mockResolvedValueOnce(mockedRecommendations) + + const getDefenderForCloudRecommendationsMetadataSpy = vi.spyOn( + recommendationsRetriever, + 'getDefenderForCloudRecommendationsMetadata' + ) + + generateAzureAccessTokenSpy.mockResolvedValueOnce(mockedToken) + const mockedRecommendationsMetadata = mockedRecommendationsMetadataUnitTestsFirstSet + getDefenderForCloudRecommendationsMetadataSpy.mockResolvedValueOnce(mockedRecommendationsMetadata) + + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + + await run() + + expect(spyStatus).toHaveBeenCalledWith('RED') + expect(spyReason).toHaveBeenCalledWith( + 'Retrieved 2 security recommendations based on given filters' + ) + }) + + it('Should return status RED with 1 retrieved unhealthy recommendation when there is SEVERITY_FILTER filter', async () => { + delete process.env.KEY_WORDS_FILTER + delete process.env.CATEGORIES_FILTER + delete process.env.THREATS_FILTER + delete process.env.USER_IMPACT_FILTER + delete process.env.IMPLEMENTATION_EFFORT_FILTER + + const generateAzureAccessTokenSpy = vi.spyOn( + auth, + 'generateAzureAccessToken' + ) + const mockedToken = 'mockedToken' + generateAzureAccessTokenSpy.mockResolvedValueOnce(mockedToken) + + const getDefenderForCloudRecommendationsSpy = vi.spyOn( + recommendationsRetriever, + 'getDefenderForCloudRecommendations' + ) + const mockedRecommendations = mockedRecommendationsUnitTestsFirstSet + getDefenderForCloudRecommendationsSpy.mockResolvedValueOnce(mockedRecommendations) + + const getDefenderForCloudRecommendationsMetadataSpy = vi.spyOn( + recommendationsRetriever, + 'getDefenderForCloudRecommendationsMetadata' + ) + + generateAzureAccessTokenSpy.mockResolvedValueOnce(mockedToken) + const mockedRecommendationsMetadata = mockedRecommendationsMetadataUnitTestsFirstSet + getDefenderForCloudRecommendationsMetadataSpy.mockResolvedValueOnce(mockedRecommendationsMetadata) + + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + + await run() + + expect(spyStatus).toHaveBeenCalledWith('RED') + expect(spyReason).toHaveBeenCalledWith( + 'Retrieved 1 security recommendations based on given filters' + ) + }) + + it('Should return status RED with 1 retrieved unhealthy recommendation when there is KEY_WORDS_FILTER filter', async () => { + delete process.env.SEVERITY + delete process.env.CATEGORIES_FILTER + delete process.env.THREATS_FILTER + delete process.env.USER_IMPACT_FILTER + delete process.env.IMPLEMENTATION_EFFORT_FILTER + + const generateAzureAccessTokenSpy = vi.spyOn( + auth, + 'generateAzureAccessToken' + ) + const mockedToken = 'mockedToken' + generateAzureAccessTokenSpy.mockResolvedValueOnce(mockedToken) + + const getDefenderForCloudRecommendationsSpy = vi.spyOn( + recommendationsRetriever, + 'getDefenderForCloudRecommendations' + ) + const mockedRecommendations = mockedRecommendationsUnitTestsFirstSet + getDefenderForCloudRecommendationsSpy.mockResolvedValueOnce(mockedRecommendations) + + const getDefenderForCloudRecommendationsMetadataSpy = vi.spyOn( + recommendationsRetriever, + 'getDefenderForCloudRecommendationsMetadata' + ) + + generateAzureAccessTokenSpy.mockResolvedValueOnce(mockedToken) + const mockedRecommendationsMetadata = mockedRecommendationsMetadataUnitTestsFirstSet + getDefenderForCloudRecommendationsMetadataSpy.mockResolvedValueOnce(mockedRecommendationsMetadata) + + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + + await run() + + expect(spyStatus).toHaveBeenCalledWith('RED') + expect(spyReason).toHaveBeenCalledWith( + 'Retrieved 1 security recommendations based on given filters' + ) + }) + + it('Should return status RED with 1 retrieved unhealthy recommendation when there is CATEGORIES_FILTER filter', async () => { + delete process.env.KEY_WORDS_FILTER + delete process.env.SEVERITY_FILTER + delete process.env.THREATS_FILTER + delete process.env.USER_IMPACT_FILTER + delete process.env.IMPLEMENTATION_EFFORT_FILTER + + const generateAzureAccessTokenSpy = vi.spyOn( + auth, + 'generateAzureAccessToken' + ) + const mockedToken = 'mockedToken' + generateAzureAccessTokenSpy.mockResolvedValueOnce(mockedToken) + + const getDefenderForCloudRecommendationsSpy = vi.spyOn( + recommendationsRetriever, + 'getDefenderForCloudRecommendations' + ) + const mockedRecommendations = mockedRecommendationsUnitTestsFirstSet + getDefenderForCloudRecommendationsSpy.mockResolvedValueOnce(mockedRecommendations) + + const getDefenderForCloudRecommendationsMetadataSpy = vi.spyOn( + recommendationsRetriever, + 'getDefenderForCloudRecommendationsMetadata' + ) + + generateAzureAccessTokenSpy.mockResolvedValueOnce(mockedToken) + const mockedRecommendationsMetadata = mockedRecommendationsMetadataUnitTestsFirstSet + getDefenderForCloudRecommendationsMetadataSpy.mockResolvedValueOnce(mockedRecommendationsMetadata) + + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + + await run() + + expect(spyStatus).toHaveBeenCalledWith('RED') + expect(spyReason).toHaveBeenCalledWith( + 'Retrieved 1 security recommendations based on given filters' + ) + }) + + it('Should return status RED with 2 retrieved unhealthy recommendations when there is THREATS_FILTER filter', async () => { + delete process.env.KEY_WORDS_FILTER + delete process.env.SEVERITY_FILTER + delete process.env.CATEGORIES_FILTER + delete process.env.USER_IMPACT_FILTER + delete process.env.IMPLEMENTATION_EFFORT_FILTER + + const generateAzureAccessTokenSpy = vi.spyOn( + auth, + 'generateAzureAccessToken' + ) + const mockedToken = 'mockedToken' + generateAzureAccessTokenSpy.mockResolvedValueOnce(mockedToken) + + const getDefenderForCloudRecommendationsSpy = vi.spyOn( + recommendationsRetriever, + 'getDefenderForCloudRecommendations' + ) + const mockedRecommendations = mockedRecommendationsUnitTestsFirstSet + getDefenderForCloudRecommendationsSpy.mockResolvedValueOnce(mockedRecommendations) + + const getDefenderForCloudRecommendationsMetadataSpy = vi.spyOn( + recommendationsRetriever, + 'getDefenderForCloudRecommendationsMetadata' + ) + + generateAzureAccessTokenSpy.mockResolvedValueOnce(mockedToken) + const mockedRecommendationsMetadata = mockedRecommendationsMetadataUnitTestsFirstSet + getDefenderForCloudRecommendationsMetadataSpy.mockResolvedValueOnce(mockedRecommendationsMetadata) + + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + + await run() + + expect(spyStatus).toHaveBeenCalledWith('RED') + expect(spyReason).toHaveBeenCalledWith( + 'Retrieved 2 security recommendations based on given filters' + ) + }) + + it('Should return status RED with 1 retrieved unhealthy recommendation when there is USER_IMPACT_FILTER filter', async () => { + delete process.env.SEVERITY_FILTER + delete process.env.KEY_WORDS_FILTER + delete process.env.CATEGORIES_FILTER + delete process.env.THREATS_FILTER + delete process.env.IMPLEMENTATION_EFFORT_FILTER + + const generateAzureAccessTokenSpy = vi.spyOn( + auth, + 'generateAzureAccessToken' + ) + const mockedToken = 'mockedToken' + generateAzureAccessTokenSpy.mockResolvedValueOnce(mockedToken) + + const getDefenderForCloudRecommendationsSpy = vi.spyOn( + recommendationsRetriever, + 'getDefenderForCloudRecommendations' + ) + const mockedRecommendations = mockedRecommendationsUnitTestsFirstSet + getDefenderForCloudRecommendationsSpy.mockResolvedValueOnce(mockedRecommendations) + + const getDefenderForCloudRecommendationsMetadataSpy = vi.spyOn( + recommendationsRetriever, + 'getDefenderForCloudRecommendationsMetadata' + ) + + generateAzureAccessTokenSpy.mockResolvedValueOnce(mockedToken) + const mockedRecommendationsMetadata = mockedRecommendationsMetadataUnitTestsFirstSet + getDefenderForCloudRecommendationsMetadataSpy.mockResolvedValueOnce(mockedRecommendationsMetadata) + + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + + await run() + + expect(spyStatus).toHaveBeenCalledWith('RED') + expect(spyReason).toHaveBeenCalledWith( + 'Retrieved 1 security recommendations based on given filters' + ) + }) + + it('Should return status RED with 2 retrieved unhealthy recommendations when there is IMPLEMENTATION_EFFORT_FILTER filter', async () => { + delete process.env.SEVERITY_FILTER + delete process.env.KEY_WORDS_FILTER + delete process.env.CATEGORIES_FILTER + delete process.env.THREATS_FILTER + delete process.env.USER_IMPACT_FILTER + + const generateAzureAccessTokenSpy = vi.spyOn( + auth, + 'generateAzureAccessToken' + ) + const mockedToken = 'mockedToken' + generateAzureAccessTokenSpy.mockResolvedValueOnce(mockedToken) + + const getDefenderForCloudRecommendationsSpy = vi.spyOn( + recommendationsRetriever, + 'getDefenderForCloudRecommendations' + ) + const mockedRecommendations = mockedRecommendationsUnitTestsFirstSet + getDefenderForCloudRecommendationsSpy.mockResolvedValueOnce(mockedRecommendations) + + const getDefenderForCloudRecommendationsMetadataSpy = vi.spyOn( + recommendationsRetriever, + 'getDefenderForCloudRecommendationsMetadata' + ) + + generateAzureAccessTokenSpy.mockResolvedValueOnce(mockedToken) + const mockedRecommendationsMetadata = mockedRecommendationsMetadataUnitTestsFirstSet + getDefenderForCloudRecommendationsMetadataSpy.mockResolvedValueOnce(mockedRecommendationsMetadata) + + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + + await run() + + expect(spyStatus).toHaveBeenCalledWith('RED') + expect(spyReason).toHaveBeenCalledWith( + 'Retrieved 2 security recommendations based on given filters' + ) + }) + + it('Should return status GREEN with 0 retrieved unhealthy recommendations when all the filter are present', async () => { + const generateAzureAccessTokenSpy = vi.spyOn( + auth, + 'generateAzureAccessToken' + ) + const mockedToken = 'mockedToken' + generateAzureAccessTokenSpy.mockResolvedValueOnce(mockedToken) + + const getDefenderForCloudRecommendationsSpy = vi.spyOn( + recommendationsRetriever, + 'getDefenderForCloudRecommendations' + ) + const mockedRecommendations = mockedRecommendationsUnitTestsFirstSet + getDefenderForCloudRecommendationsSpy.mockResolvedValueOnce(mockedRecommendations) + + const getDefenderForCloudRecommendationsMetadataSpy = vi.spyOn( + recommendationsRetriever, + 'getDefenderForCloudRecommendationsMetadata' + ) + + generateAzureAccessTokenSpy.mockResolvedValueOnce(mockedToken) + const mockedRecommendationsMetadata = mockedRecommendationsMetadataUnitTestsFirstSet + getDefenderForCloudRecommendationsMetadataSpy.mockResolvedValueOnce(mockedRecommendationsMetadata) + + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + + await run() + + expect(spyStatus).toHaveBeenCalledWith('GREEN') + expect(spyReason).toHaveBeenCalledWith( + 'No security recommendations found based on given filters' + ) + }) + }) +}) diff --git a/yaku-apps-typescript/apps/defender-for-cloud/test/unit/utils.test.ts b/yaku-apps-typescript/apps/defender-for-cloud/test/unit/utils.test.ts new file mode 100644 index 00000000..2461163e --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/test/unit/utils.test.ts @@ -0,0 +1,45 @@ +import { afterEach, vi, describe, it, expect } from 'vitest' +import * as fs from 'fs/promises' +import * as fs_sync from 'fs' +import { exportJson } from '../../src/utils' + +describe('Test "exportJson()" from "utils.ts"', () => { + vi.mock('fs') + vi.mock('fs/promises') + + afterEach(() => { + vi.clearAllMocks() + vi.restoreAllMocks() + }) + + it('Should write JSON content in a file from an existing directory', async () => { + const existsSyncSpy = vi.spyOn(fs_sync, 'existsSync') + existsSyncSpy.mockImplementation(() => false) + + const fileContent = { fileContent: 'fileContent' } + + expect.assertions(2) + await exportJson(fileContent, '/tmp/results.json') + + expect(fs_sync.mkdirSync).toHaveBeenCalled() + expect(fs.writeFile).toHaveBeenCalledWith( + '/tmp/results.json', + JSON.stringify(fileContent) + ) + }) + + it('Should write JSON content in a file from an non-existing directory', async () => { + const existsSyncSpy = vi.spyOn(fs_sync, 'existsSync') + existsSyncSpy.mockImplementation(() => true) + + const fileContent = { fileContent: 'fileContent' } + + expect.assertions(1) + await exportJson(fileContent, '/tmp/results.json') + + expect(fs.writeFile).toHaveBeenCalledWith( + '/tmp/results.json', + JSON.stringify(fileContent) + ) + }) +}) diff --git a/yaku-apps-typescript/apps/defender-for-cloud/tsconfig.json b/yaku-apps-typescript/apps/defender-for-cloud/tsconfig.json new file mode 100644 index 00000000..e1f51365 --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@B-S-F/typescript-config/base.json", + "ts-node": { + "compilerOptions": { + "module": "ESNext" + } + }, + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/yaku-apps-typescript/apps/defender-for-cloud/tsup.config.ts b/yaku-apps-typescript/apps/defender-for-cloud/tsup.config.ts new file mode 100644 index 00000000..a1f42d9c --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/**/*.ts'], + splitting: false, + clean: true, + target: 'node18', + format: ['esm'], + bundle: false, + sourcemap: true, +}) diff --git a/yaku-apps-typescript/apps/defender-for-cloud/vitest-integration.config.ts b/yaku-apps-typescript/apps/defender-for-cloud/vitest-integration.config.ts new file mode 100644 index 00000000..8bec0725 --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/vitest-integration.config.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['test/integration/**/*.int-spec.ts'], + pool: 'threads', + poolOptions: { + threads: { + maxThreads: 1, + minThreads: 1, + }, + }, + typecheck: { + tsconfig: 'tsconfig.json', + }, + reporters: ['junit', 'default'], + outputFile: 'reports/integration-test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/defender-for-cloud/vitest.config.ts b/yaku-apps-typescript/apps/defender-for-cloud/vitest.config.ts new file mode 100644 index 00000000..1cf5e21f --- /dev/null +++ b/yaku-apps-typescript/apps/defender-for-cloud/vitest.config.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + typecheck: { + tsconfig: 'tsconfig.json', + }, + coverage: { + provider: 'v8', + all: true, + enabled: true, + reporter: ['cobertura', 'json-summary', 'text-summary'], + include: ['src'], + }, + reporters: ['junit', 'default'], + outputFile: 'reports/test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/gh-app/.eslintrc.cjs b/yaku-apps-typescript/apps/gh-app/.eslintrc.cjs new file mode 100644 index 00000000..87d861fe --- /dev/null +++ b/yaku-apps-typescript/apps/gh-app/.eslintrc.cjs @@ -0,0 +1,15 @@ +module.exports = { + "extends": [ + "@B-S-F/eslint-config/eslint-preset" + ], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + 'no-sparse-arrays': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { destructuredArrayIgnorePattern: '^([.]{3})?_' }, + ], + "no-control-regex": 0 + }, +} \ No newline at end of file diff --git a/yaku-apps-typescript/apps/gh-app/.prettierrc b/yaku-apps-typescript/apps/gh-app/.prettierrc new file mode 100644 index 00000000..b2095be8 --- /dev/null +++ b/yaku-apps-typescript/apps/gh-app/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": false, + "singleQuote": true +} diff --git a/yaku-apps-typescript/apps/gh-app/env.sample b/yaku-apps-typescript/apps/gh-app/env.sample new file mode 100644 index 00000000..ef0ead80 --- /dev/null +++ b/yaku-apps-typescript/apps/gh-app/env.sample @@ -0,0 +1,4 @@ +export GH_APP_ID= +export GH_APP_PRIVATE_KEY= +export GH_APP_ORG= +export GH_APP_REPO= diff --git a/yaku-apps-typescript/apps/gh-app/package.json b/yaku-apps-typescript/apps/gh-app/package.json new file mode 100644 index 00000000..b52ba186 --- /dev/null +++ b/yaku-apps-typescript/apps/gh-app/package.json @@ -0,0 +1,50 @@ +{ + "name": "@B-S-F/gh-app", + "version": "0.3.1", + "description": "", + "main": "dist/index.js", + "files": [ + "dist" + ], + "type": "module", + "scripts": { + "build": "tsup", + "dev": "nodemon --watch \"src/**\" --exec npm run start", + "start": "node ./dist/index.js", + "lint": "eslint '**/*.ts'", + "format": "prettier --write '**/*.{ts,md}'", + "test": "vitest run && npm run test:update-cobertura-file", + "test:update-cobertura-file": "sed -i'.bak' 's,package name=\",package name=\"'${npm_package_name}/',g' coverage/cobertura-coverage.xml", + "test:dev": "vitest -w", + "test:coverage": "vitest --coverage", + "test:ui": "vitest --ui" + }, + "keywords": [], + "author": "", + "license": "", + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/jsonpath": "^0.2.0", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "@vitest/coverage-v8": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + }, + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "https-proxy-agent": "^7.0.4", + "octokit": "^3.1.2", + "undici": "^6.19.2", + "universal-github-app-jwt": "^2.2.0" + }, + "bin": { + "gh-app": "dist/index.js" + } +} diff --git a/yaku-apps-typescript/apps/gh-app/scripts/save-gh-app-auth-as-env-var.sh b/yaku-apps-typescript/apps/gh-app/scripts/save-gh-app-auth-as-env-var.sh new file mode 100755 index 00000000..07b8ceac --- /dev/null +++ b/yaku-apps-typescript/apps/gh-app/scripts/save-gh-app-auth-as-env-var.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# This is an example script how to save the GitHub App authentication token as an environment variable +# before using the GH CLI +# Needed environment variables: +# - GH_APP_ID= +# - GH_APP_PRIVATE_KEY= +# - GH_APP_ORG= +# - GH_APP_REPO= + +set -e +output=$(gh-app auth) +token=$(echo $output | grep -oP 'GITHUB_TOKEN":"\K[^"]+') +export GITHUB_TOKEN=$token +# Now you can use the GH CLI diff --git a/yaku-apps-typescript/apps/gh-app/src/auth.ts b/yaku-apps-typescript/apps/gh-app/src/auth.ts new file mode 100644 index 00000000..22153a6b --- /dev/null +++ b/yaku-apps-typescript/apps/gh-app/src/auth.ts @@ -0,0 +1,85 @@ +import { Octokit } from 'octokit' +import { ProxyAgent, fetch as undiciFetch } from 'undici' +import githubAppJwt from 'universal-github-app-jwt' +import { + GH_APP_ID, + GH_APP_PRIVATE_KEY, + GH_APP_ORG, + GH_APP_REPO, + checkEnvVariables, +} from './config.js' +import { getAppInstallation } from './octokit/get-app-installation.js' +import { getAppAccess } from './octokit/get-app-access.js' +import { AppOutput, GetLogger } from '@B-S-F/autopilot-utils' + +export async function authCmd(options) { + checkEnvVariables() + const token = await ghAppAuth() + const appOutput = new AppOutput() + if (options.tokenOnly) { + console.log(token) + } else { + appOutput.addOutput({ GITHUB_TOKEN: token }) + appOutput.write() + } +} + +const customFetch = (url: URL, options: any) => { + const logger = GetLogger() + if (process.env.HTTPS_PROXY) { + logger.debug(`Using HTTPS proxy: ${process.env.HTTPS_PROXY}`) + options.agent = new ProxyAgent({ + uri: process.env.HTTPS_PROXY, + }) + } else if (process.env.HTTP_PROXY) { + logger.debug(`Using HTTP proxy: ${process.env.HTTP_PROXY}`) + options.agent = new ProxyAgent({ + uri: process.env.HTTP_PROXY, + }) + } + return undiciFetch(url, { + ...options, + dispatcher: options.agent, + }) +} + +export async function ghAppAuth() { + const logger = GetLogger() + const { token } = await githubAppJwt({ + id: GH_APP_ID!, + privateKey: GH_APP_PRIVATE_KEY!, + }) + const octokit = new Octokit({ + auth: token, + request: { + fetch: customFetch, + }, + }) + const target = GH_APP_REPO ? `${GH_APP_ORG}/${GH_APP_REPO}` : GH_APP_ORG + const installation = await getAppInstallation( + octokit, + GH_APP_ORG!, + GH_APP_REPO + ) + if (!installation || !installation!.data.id) { + logger.error(`No app installation found in ${target}!`) + process.exit(1) + } + const installationId = installation.data.id + const appSlug = installation.data.app_slug + logger.debug( + `Found installation with id ${installationId} in ${target} for app ${appSlug}` + ) + logger.debug(`Requesting access token for installation ${installationId}`) + const access = await getAppAccess(octokit, installationId) + if (!access || !access.data.token) { + logger.error( + `No access token received for app installation ${installationId} in ${target}!` + ) + process.exit(1) + } + logger.info( + `Logged in as '${appSlug}'. Please store the token in the environment variable 'GH_TOKEN'.` + ) + return access.data.token +} diff --git a/yaku-apps-typescript/apps/gh-app/src/config.ts b/yaku-apps-typescript/apps/gh-app/src/config.ts new file mode 100644 index 00000000..c1aa77df --- /dev/null +++ b/yaku-apps-typescript/apps/gh-app/src/config.ts @@ -0,0 +1,27 @@ +import { GetLogger } from '@B-S-F/autopilot-utils' + +export const { GH_APP_ID, GH_APP_PRIVATE_KEY, GH_APP_ORG, GH_APP_REPO } = + process.env + +export const checkEnvVariables = () => { + const logger = GetLogger() + const { GH_APP_ID, GH_APP_PRIVATE_KEY, GH_APP_ORG, GH_APP_REPO } = process.env + let exit = false + if (!GH_APP_ID) { + logger.error('GH_APP_ID is not set') + exit = true + } + if (!GH_APP_PRIVATE_KEY) { + logger.error('GH_APP_PRIVATE_KEY is not set') + exit = true + } + if (!(GH_APP_ORG || (GH_APP_ORG && GH_APP_REPO))) { + logger.error( + 'Either GH_APP_ORG or both GH_APP_ORG and GH_APP_REPO must be set' + ) + exit = true + } + if (exit) { + process.exit(1) + } +} diff --git a/yaku-apps-typescript/apps/gh-app/src/index.ts b/yaku-apps-typescript/apps/gh-app/src/index.ts new file mode 100644 index 00000000..ca2ccce3 --- /dev/null +++ b/yaku-apps-typescript/apps/gh-app/src/index.ts @@ -0,0 +1,33 @@ +#!/usr/bin/env node + +import { authCmd } from './auth.js' +import { + AutopilotApp, + AutopilotAppCommand, + InitLogger, +} from '@B-S-F/autopilot-utils' +import * as packageJson from '../package.json' + +const version = packageJson.version +export const APP_NAME = 'gh-app' + +const app = new AutopilotApp( + APP_NAME, + version, + 'CLI to interact with a GitHub App.', + [ + new AutopilotAppCommand('auth') + .description('Authenticate as a GH App installation.') + .option('--token-only', 'Print only the token.') + .action((options) => { + if (options.tokenOnly) { + InitLogger('gh-app', 'error') + } else { + InitLogger('gh-app', 'info') + } + authCmd(options) + }), + ] +) + +app.run() diff --git a/yaku-apps-typescript/apps/gh-app/src/octokit/get-app-access.ts b/yaku-apps-typescript/apps/gh-app/src/octokit/get-app-access.ts new file mode 100644 index 00000000..65ae7e50 --- /dev/null +++ b/yaku-apps-typescript/apps/gh-app/src/octokit/get-app-access.ts @@ -0,0 +1,22 @@ +import { GetLogger } from '@B-S-F/autopilot-utils' +import { Octokit } from 'octokit' + +const logger = GetLogger() + +export async function getAppAccess(octokit: Octokit, installationId: number) { + try { + return await octokit.request( + `POST /app/installations/${installationId}/access_tokens` + ) + } catch (e) { + if ((e as Error).message) { + logger.error( + `Error requesting access token for app installation ${installationId}: ${ + (e as Error).message + }` + ) + process.exit(1) + } + throw e + } +} diff --git a/yaku-apps-typescript/apps/gh-app/src/octokit/get-app-installation.ts b/yaku-apps-typescript/apps/gh-app/src/octokit/get-app-installation.ts new file mode 100644 index 00000000..e68402a7 --- /dev/null +++ b/yaku-apps-typescript/apps/gh-app/src/octokit/get-app-installation.ts @@ -0,0 +1,31 @@ +import { GetLogger } from '@B-S-F/autopilot-utils' +import { Octokit } from 'octokit' + +const logger = GetLogger() + +export async function getAppInstallation( + octokit: Octokit, + org: string, + repo: string | undefined +) { + try { + if (repo) { + logger.debug(`Looking for app installation in ${org}/${repo}`) + return await octokit.request(`GET /repos/${org}/${repo}/installation`) + } else { + logger.debug(`Looking for app installation in ${org}`) + return await octokit.request(`GET /orgs/${org}/installation`) + } + } catch (e) { + if ((e as Error).message) { + const target = repo ? `${org}/${repo}` : org + logger.error( + `Error looking for app installation in ${target}: ${ + (e as Error).message + }` + ) + process.exit(1) + } + throw e + } +} diff --git a/yaku-apps-typescript/apps/gh-app/test/unit/auth.test.ts b/yaku-apps-typescript/apps/gh-app/test/unit/auth.test.ts new file mode 100644 index 00000000..b152d214 --- /dev/null +++ b/yaku-apps-typescript/apps/gh-app/test/unit/auth.test.ts @@ -0,0 +1,133 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { authCmd, ghAppAuth } from '../../src/auth.js' +import { GetLogger } from '@B-S-F/autopilot-utils' + +const logger = GetLogger() + +const mocks = vi.hoisted(() => ({ + getAppInstallationMock: vi.fn(), + getAppAccessMock: vi.fn(), +})) + +vi.mock('octokit', () => ({ + Octokit: vi.fn().mockImplementation(() => ({ + request: vi.fn(), + })), +})) + +vi.mock('universal-github-app-jwt', () => ({ + default: vi.fn().mockReturnValue({ token: 'token' }), +})) + +vi.mock('../../src/octokit/get-app-installation.js', () => ({ + getAppInstallation: mocks.getAppInstallationMock, +})) + +vi.mock('../../src/octokit/get-app-access', () => ({ + getAppAccess: mocks.getAppAccessMock, +})) + +describe('authCmd', async () => { + afterEach(() => { + vi.clearAllMocks() + mocks.getAppInstallationMock.mockReset() + mocks.getAppAccessMock.mockReset() + }) + it('should print a gh app installation token as an output', async () => { + // Arrange + mocks.getAppInstallationMock.mockResolvedValueOnce({ + data: { id: 1, app_slug: 'app_slug' }, + }) + mocks.getAppAccessMock.mockResolvedValueOnce({ data: { token: 'token' } }) + vi.spyOn(console, 'log') + vi.stubEnv('GH_APP_ID', '123') + vi.stubEnv('GH_APP_PRIVATE_KEY', 'private-key') + vi.stubEnv('GH_APP_ORG', 'org') + + // Act + await authCmd({}) + + // Assert + expect(console.log).toHaveBeenCalledWith( + '{"output":{"GITHUB_TOKEN":"token"}}' + ) + }) + it('should only print a gh app installation token', async () => { + // Arrange + mocks.getAppInstallationMock.mockResolvedValueOnce({ + data: { id: 1, app_slug: 'app_slug' }, + }) + mocks.getAppAccessMock.mockResolvedValueOnce({ data: { token: 'token' } }) + vi.spyOn(console, 'log') + vi.stubEnv('GH_APP_ID', '123') + vi.stubEnv('GH_APP_PRIVATE_KEY', 'private-key') + vi.stubEnv('GH_APP_ORG', 'org') + + // Act + await authCmd({ tokenOnly: true }) + + // Assert + expect(console.log).toHaveBeenCalledTimes(1) + expect(console.log).toHaveBeenCalledWith('token') + }) +}) + +describe('ghAppAuth', async () => { + vi.spyOn(process, 'exit').mockImplementation(() => { + throw new Error('process exit') + }) + vi.spyOn(logger, 'error') + vi.spyOn(logger, 'info') + + afterEach(() => { + vi.clearAllMocks() + mocks.getAppInstallationMock.mockReset() + mocks.getAppAccessMock.mockReset() + }) + it('should exit process if no installation found', async () => { + // Arrange + mocks.getAppInstallationMock.mockResolvedValueOnce({ data: {} }) + + // Act + await expect(() => ghAppAuth()).rejects.toThrow('process exit') + + // Assert + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('No app installation found') + ) + expect(process.exit).toHaveBeenCalledWith(1) + }) + + it('should exit process if no access token received', async () => { + // Arrange + mocks.getAppInstallationMock.mockResolvedValueOnce({ + data: { id: 1, app_slug: 'app_slug' }, + }) + + // Act + await expect(() => ghAppAuth()).rejects.toThrow('process exit') + + // Assert + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('No access token received') + ) + expect(process.exit).toHaveBeenCalledWith(1) + }) + + it('should return access token if installation and access token received', async () => { + // Arrange + mocks.getAppInstallationMock.mockResolvedValueOnce({ + data: { id: 1, app_slug: 'app_slug' }, + }) + mocks.getAppAccessMock.mockResolvedValueOnce({ data: { token: 'token' } }) + + // Act + const result = await ghAppAuth() + + // Assert + expect(result).toEqual('token') + expect(logger.info).toHaveBeenCalledWith( + expect.stringContaining('Logged in as') + ) + }) +}) diff --git a/yaku-apps-typescript/apps/gh-app/test/unit/config.test.ts b/yaku-apps-typescript/apps/gh-app/test/unit/config.test.ts new file mode 100644 index 00000000..eaff73ca --- /dev/null +++ b/yaku-apps-typescript/apps/gh-app/test/unit/config.test.ts @@ -0,0 +1,73 @@ +import { afterEach, describe, expect, it, vi, beforeEach } from 'vitest' + +import { checkEnvVariables } from '../../src/config' +import { GetLogger } from '@B-S-F/autopilot-utils' + +const logger = GetLogger() + +describe('checkEnvVariables', () => { + let exitSpy + let logSpy + + beforeEach(() => { + exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => { + throw new Error('process exit') + }) + logSpy = vi.spyOn(logger, 'error') + }) + + afterEach(() => { + vi.unstubAllEnvs() + vi.resetAllMocks() + }) + + it('should run without throwing an error if all env variables are set', () => { + // Arrange + vi.stubEnv('GH_APP_ID', '123') + vi.stubEnv('GH_APP_PRIVATE_KEY', 'private_key') + vi.stubEnv('GH_APP_ORG', 'org') + vi.stubEnv('GH_APP_REPO', 'repo') + + // Act & Assert + expect(() => checkEnvVariables()).not.toThrow() + }) + + it.each(['GH_APP_ID', 'GH_APP_PRIVATE_KEY'])( + 'should call process.exit(1) if %s is not set', + (envVar) => { + // Arrange + vi.stubEnv(envVar, '') + + // Act & Assert + expect(() => checkEnvVariables()).toThrow('process exit') + expect(logSpy).toHaveBeenCalledWith(`${envVar} is not set`) + expect(exitSpy).toHaveBeenCalledWith(1) + } + ) + + it('should call process.exit(1) if GH_APP_ORG is not set and GH_APP_REPO is set', () => { + // Arrange + vi.stubEnv('GH_APP_ORG', '') + vi.stubEnv('GH_APP_REPO', 'repo') + + // Act & Assert + expect(() => checkEnvVariables()).toThrow('process exit') + expect(logSpy).toHaveBeenCalledWith( + 'Either GH_APP_ORG or both GH_APP_ORG and GH_APP_REPO must be set' + ) + expect(exitSpy).toHaveBeenCalledWith(1) + }) + + it('should call process.exit(1) if GH_APP_ORG and GH_APP_REPO are not set', () => { + // Arrange + vi.stubEnv('GH_APP_ORG', '') + vi.stubEnv('GH_APP_REPO', '') + + // Act & Assert + expect(() => checkEnvVariables()).toThrow('process exit') + expect(logSpy).toHaveBeenCalledWith( + 'Either GH_APP_ORG or both GH_APP_ORG and GH_APP_REPO must be set' + ) + expect(exitSpy).toHaveBeenCalledWith(1) + }) +}) diff --git a/yaku-apps-typescript/apps/gh-app/test/unit/octokit/get-app-access.test.ts b/yaku-apps-typescript/apps/gh-app/test/unit/octokit/get-app-access.test.ts new file mode 100644 index 00000000..15e12038 --- /dev/null +++ b/yaku-apps-typescript/apps/gh-app/test/unit/octokit/get-app-access.test.ts @@ -0,0 +1,54 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { Octokit } from 'octokit' +import { getAppAccess } from '../../../src/octokit/get-app-access' +import { GetLogger } from '@B-S-F/autopilot-utils' + +const logger = GetLogger() + +describe('getAppAccess', () => { + vi.mock('octokit', () => ({ + Octokit: vi.fn().mockImplementation(() => ({ + request: vi.fn(), + })), + })) + const octokitMock = vi.mocked(new Octokit()) + vi.spyOn(process, 'exit').mockImplementation(() => { + throw new Error('process exit') + }) + vi.spyOn(logger, 'error') + + afterEach(() => { + vi.clearAllMocks() + }) + + it('should POST to /app/installations/:installationId/access_tokens', async () => { + // Arrange + const installationId = 1234 + + // Act + await getAppAccess(octokitMock, installationId) + + // Assert + expect(octokitMock.request).toHaveBeenCalledWith( + `POST /app/installations/${installationId}/access_tokens` + ) + }) + + it('should exit and log an error if octokit.request throws an error', async () => { + // Arrange + const installationId = 1234 + const error = new Error('mock error') + vi.mocked(octokitMock.request).mockRejectedValue(error) + + // Act + await expect(() => + getAppAccess(octokitMock, installationId) + ).rejects.toThrow('process exit') + + // Assert + expect(logger.error).toHaveBeenCalledWith( + `Error requesting access token for app installation ${installationId}: ${error.message}` + ) + expect(process.exit).toHaveBeenCalledWith(1) + }) +}) diff --git a/yaku-apps-typescript/apps/gh-app/test/unit/octokit/get-app-installation.test.ts b/yaku-apps-typescript/apps/gh-app/test/unit/octokit/get-app-installation.test.ts new file mode 100644 index 00000000..90c8ce3a --- /dev/null +++ b/yaku-apps-typescript/apps/gh-app/test/unit/octokit/get-app-installation.test.ts @@ -0,0 +1,71 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { Octokit } from 'octokit' +import { getAppInstallation } from '../../../src/octokit/get-app-installation' +import { GetLogger } from '@B-S-F/autopilot-utils' + +const logger = GetLogger() + +describe('getAppInstallation', () => { + vi.mock('octokit', () => ({ + Octokit: vi.fn().mockImplementation(() => ({ + request: vi.fn(), + })), + })) + const octokitMock = vi.mocked(new Octokit()) + vi.spyOn(process, 'exit').mockImplementation(() => { + throw new Error('process exit') + }) + vi.spyOn(logger, 'error') + + afterEach(() => { + vi.clearAllMocks() + }) + + it('should call octokit.request with the correct arguments when repo (and org) is set', async () => { + // Arrange + const org = 'mockOrg' + const repo = 'mockRepo' + vi.mocked + + // Act + await getAppInstallation(octokitMock, org, repo) + + // Assert + expect(octokitMock.request).toHaveBeenCalledWith( + `GET /repos/${org}/${repo}/installation` + ) + expect(logger.info) + }) + + it('should call octokit.request with the correct arguments when only org is set', async () => { + // Arrange + const org = 'mockOrg' + + // Act + await getAppInstallation(octokitMock, org, undefined) + + // Assert + expect(octokitMock.request).toHaveBeenCalledWith( + `GET /orgs/${org}/installation` + ) + }) + + it('should exit and log an error if octokit.request throws an error', async () => { + // Arrange + const org = 'mockOrg' + const repo = 'mockRepo' + const error = new Error('mock error') + vi.mocked(octokitMock.request).mockRejectedValue(error) + + // Act + await expect(() => + getAppInstallation(octokitMock, org, repo) + ).rejects.toThrow('process exit') + + // Assert + expect(logger.error).toHaveBeenCalledWith( + `Error looking for app installation in ${org}/${repo}: ${error.message}` + ) + expect(process.exit).toHaveBeenCalledWith(1) + }) +}) diff --git a/yaku-apps-typescript/apps/gh-app/tsup.config.json b/yaku-apps-typescript/apps/gh-app/tsup.config.json new file mode 100644 index 00000000..fac6d717 --- /dev/null +++ b/yaku-apps-typescript/apps/gh-app/tsup.config.json @@ -0,0 +1,7 @@ +{ + "entry": ["src/**/*.ts"], + "splitting": false, + "sourcemap": true, + "clean": true, + "format": ["esm"] +} diff --git a/yaku-apps-typescript/apps/gh-app/vitest.config.ts b/yaku-apps-typescript/apps/gh-app/vitest.config.ts new file mode 100644 index 00000000..6fc40afa --- /dev/null +++ b/yaku-apps-typescript/apps/gh-app/vitest.config.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + typecheck: { + tsconfig: 'tsconfig.json', + }, + coverage: { + provider: 'v8', + all: true, + enabled: true, + reporter: ['cobertura', 'json-summary', 'text-summary'], + include: ['src'], + exclude: ['**/src/index.ts'], + }, + reporters: ['junit', 'default'], + outputFile: 'reports/test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/git-fetcher/.env.sample b/yaku-apps-typescript/apps/git-fetcher/.env.sample new file mode 100644 index 00000000..1715371c --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/.env.sample @@ -0,0 +1,5 @@ +export GIT_FETCHER_SERVER_API_URL= +export GIT_FETCHER_SERVER_AUTH_METHOD= +export GIT_FETCHER_SERVER_TYPE= +export GIT_FETCHER_API_TOKEN= +export GIT_FETCHER_OUTPUT_FILE_PATH= diff --git a/yaku-apps-typescript/apps/git-fetcher/.eslintrc.cjs b/yaku-apps-typescript/apps/git-fetcher/.eslintrc.cjs new file mode 100644 index 00000000..502509d7 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/.eslintrc.cjs @@ -0,0 +1,5 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +module.exports = require("@B-S-F/eslint-config/eslint-preset"); \ No newline at end of file diff --git a/yaku-apps-typescript/apps/git-fetcher/.prettierrc b/yaku-apps-typescript/apps/git-fetcher/.prettierrc new file mode 100644 index 00000000..b2095be8 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": false, + "singleQuote": true +} diff --git a/yaku-apps-typescript/apps/git-fetcher/README.md b/yaku-apps-typescript/apps/git-fetcher/README.md new file mode 100644 index 00000000..6cbe5cca --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/README.md @@ -0,0 +1,147 @@ +# git-fetcher + +A fetcher that fetches git resources data from a git server. + +- Supported servers + - github + - bitbucket + +## Resources + +### Pull Requests + +To fetch pull requests, add the following line to the gitfetcher's configuration file + +```yaml +resource: prs +``` + +It is not possible to fetch multiple resources at the same time. To do so, run the gitfetcher multiple times with different configurations. + +Fetching pull requests is supported on the following server types: + +:white_check_mark: bitbucket + +:white_check_mark: github + +### Branches + +To fetch branches, add the following line to the gitfetcher's configuration file + +```yaml +resource: branches +``` + +Fetching branches is supported on the following server types: + +:white_check_mark: bitbucket + +:x: github + +### Tags + +To fetch tags, add the following line to the gitfetcher's configuration file + +```yaml +resource: tags +``` + +Fetching tags is supported on the following server types: + +:white_check_mark: bitbucket + +:x: github + +### Commits Metadata and Diff for a specific file. + +We can use this functionality to see which commits have modified a specific file over a period of time, and to also see the actual content that changed inside the target file. The input config has the same structure for both Bitbucket and Github. An example config would be: + +```yaml +org: aquatest +repo: fetcher-test-repo +resource: metadata-and-diff +filter: + startHash: afeaebf412c6d0b865a36cfdec37fdb46c0fab63 + endHash: 8036cf75f4b7365efea76cbd716ef12d352d7d29 +filePath: apps/git-fetcher/src/fetchers/git-fetcher.ts +``` + +'filter.startHash' and 'filePath' are mandatory parameters. 'filter.endHash' is optional. If 'filter.endHash' is not provided, the default 'endHash' will be 'master'. +Both Bitbucket API and Github API, which are used by the fetcher, require 'startHash' to be exclusive, and 'endHash' to be inclusive. This means that if we have the chain of commits below: + +``` +c1 -> c2 -> c3 -> c4 +``` + +and we want to see if a file changed in c4, then startHash should be c3. If we want to see if a file changed in c4 or c3, then startHash should be c2. +To obtain the statHash and endHash of a commit, we can simply click on that commit and take its value from the url (for both Bitbucket and Github). +The structure of the output file for Bitbucket is: + +``` +{ + commitsMetadata: [{},{},...{}], + diff: [{},{},...,{}] +} +``` + +The structure of the output file for Github is: + +``` +{ + commitsMetadata: [{},{},...{}], + diff: { + linesAdded: [], + linesRemoved: [] + } +} +``` + +Fetching Commits Metadata and Diff for a specific file is supported on the following server types: + +:white_check_mark: bitbucket + +:white_check_mark: github + +## Filtering + +It is possible to filter pull requests by certain criteria. Add a `filter` key to the git fetcher config. + +### By State (only Bitbucket) - optional + +To filter pull requests by state add parameter `filter.state` to the git fetcher configuration file. Possible values are `DECLINED, MERGED, OPEN, ALL`. Omitting this parameter will lead to no filtering which is the same as the `ALL` state filter. + +### By Date (only Bitbucket) - optional + +To filter pull requests by date add the parameter `filter.startDate` to get pull requests which were updated starting from this date (inclusive). +Add the parameter `filter.endDate` to get pull requests which were updated not later than this date (inclusive). `filter.endDate` can only be used together with `filter.startDate`. + +If `filter.startDate` is used without `filter.endDate` then all pull requests between the start date and the current date are fetched. + +Both filter values have the format `dd-MM-yyyy`. + +The date filter can be combined with the state filter only. + +### By Commit Hash (only Bitbucket) - optional + +To filter pull requests by commit hashes add the parameter `filter.startHash` to get pull requests which were updated after or at the same time as this commit hash. +Add the parameter `filter.endHash` to get pull requests which where updated no later than this commit hash was created. `filter.endHash` can only be used together with `filter.startHash`. + +If `filter.startHash` is used without `filter.endHash` then all pull requests between the start hash and the current date are fetched. + +The hash filter can be combined with the state filter only. + +### By Tag (only Bitbucket - optional) + +It is possible to filter pull requests by providing a start tag and an end tag name. Before the actual filtering takes +place, each tag name is transformed into a date, so that the date can be compared to the pull request date. Tags are +transformed into dates by fetching the commit metadata of the tagged commit. In the subsequent process, the commit's +timestamp will be used for the filtering. + +To filter pull requests by tags, add the parameter `filter.startTag` to get pull requests which were updated +after or at the same time as this tag. Add the parameter `filter.endTag` to get pull requests which were +updated no later than this tag. + +`filter.endTag` can only be used together with `filter.startTag`. If `filter.startTag` is used without `filter.endTag` +then all pull requests between the start tag and the current date are fetched. + +The tag filter can be combined with the state filter only. diff --git a/yaku-apps-typescript/apps/git-fetcher/example-config/git-fetcher-config.yml b/yaku-apps-typescript/apps/git-fetcher/example-config/git-fetcher-config.yml new file mode 100644 index 00000000..6b4ea438 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/example-config/git-fetcher-config.yml @@ -0,0 +1,7 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + state: ALL # Optional. One of: DECLINED, MERGED, OPEN, ALL. Only works with Bitbucket. + startDate: 01-06-2020 # Optional. Format dd-MM-yyyy. Only works with Bitbucket. + endDate: 31-12-2022 # Optional. Format dd-MM-yyyy. Works only if startDate is provided. Only works with Bitbucket. diff --git a/yaku-apps-typescript/apps/git-fetcher/package.json b/yaku-apps-typescript/apps/git-fetcher/package.json new file mode 100644 index 00000000..03d7ea63 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/package.json @@ -0,0 +1,53 @@ +{ + "name": "@B-S-F/git-fetcher", + "version": "0.7.0", + "description": "", + "type": "module", + "main": "dist/index.js", + "scripts": { + "prebuild": "npx rimraf ./dist", + "build": "tsup", + "start": "node ./dist/index.js", + "dev": "nodemon --watch \"src/**\" --exec npm run start", + "lint": "eslint '**/*.ts'", + "format": "prettier --write '**/*.{ts,md}'", + "test": "vitest run && npm run test:update-cobertura-file", + "test:update-cobertura-file": "sed -i'.bak' 's,package name=\",package name=\"'${npm_package_name}/',g' coverage/cobertura-coverage.xml", + "test:dev": "vitest --coverage --watch", + "test:coverage": "vitest --coverage", + "test:ui": "vitest --ui", + "test:integration:local": "npx rimraf dist && npm run build && npm run test:integration:ci", + "test:integration:ci": "npx vitest run --config vitest-integration.config.ts" + }, + "keywords": [], + "author": "", + "files": [ + "dist" + ], + "license": "", + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "date-fns": "^2.30.0", + "fs-extra": "^10.1.0", + "node-fetch": "^3.2.10", + "process": "^0.11.10", + "yaml": "^2.2.1", + "zod": "^3.22.3", + "zod-validation-error": "^1.3.0" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "@typescript-eslint/parser": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*" + }, + "bin": { + "git-fetcher": "dist/index.js" + } +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/fetchers/generate-git-fetcher.ts b/yaku-apps-typescript/apps/git-fetcher/src/fetchers/generate-git-fetcher.ts new file mode 100644 index 00000000..92f48f6b --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/fetchers/generate-git-fetcher.ts @@ -0,0 +1,69 @@ +import { BitbucketBranch } from '../model/bitbucket-branch' +import { BitbucketPr } from '../model/bitbucket-pr.js' +import { BitbucketTag } from '../model/bitbucket-tag' +import { CommitsMetadataAndDiff } from '../model/commits-metadata-and-diff.js' +import { + ConfigFileData, + GitConfigResource, + gitFetcherPullRequests, +} from '../model/config-file-data.js' +import { + GitServerConfig, + SupportedGitServerType, +} from '../model/git-server-config.js' +import { GithubPr } from '../model/github-pr.js' +import { ConfigurationError } from '../run.js' +import { GitFetcher } from './git-fetcher' +import { GitFetcherBitbucketCommitsAndDiff } from './git-fetcher-bitbucket-commits-and-diff.js' +import { GitFetcherBitbucketPrs } from './git-fetcher-bitbucket-prs.js' +import { GitFetcherBitbucketTagsAndBranches } from './git-fetcher-bitbucket-tags-and-branches.js' +import { GitFetcherGithubCommitsAndDiff } from './git-fetcher-github-commits-and-diff.js' +import { GitFetcherGithubPrs } from './git-fetcher-github-prs.js' + +export type GitResource = + | GithubPr + | BitbucketPr + | BitbucketBranch + | BitbucketTag + | CommitsMetadataAndDiff + +export default function generateGitFetcher( + gitServerConfig: GitServerConfig, + configFileData: ConfigFileData +): GitFetcher { + const gitServerType: SupportedGitServerType = gitServerConfig.gitServerType + const gitConfigResource: GitConfigResource = configFileData.data.resource + const pullRequestAliases: string[] = + gitFetcherPullRequests as unknown as string[] + + if ( + gitServerType === 'github' && + pullRequestAliases.includes(gitConfigResource) + ) { + return new GitFetcherGithubPrs(gitServerConfig, configFileData) + } else if ( + gitServerType === 'github' && + gitConfigResource === 'metadata-and-diff' + ) { + return new GitFetcherGithubCommitsAndDiff(gitServerConfig, configFileData) + } else if (gitServerType === 'bitbucket') { + if (gitConfigResource === 'branches' || gitConfigResource === 'tags') { + return new GitFetcherBitbucketTagsAndBranches( + gitServerConfig, + configFileData, + gitConfigResource + ) + } else if (pullRequestAliases.includes(gitConfigResource)) { + return new GitFetcherBitbucketPrs(gitServerConfig, configFileData) + } else if (gitConfigResource === 'metadata-and-diff') { + return new GitFetcherBitbucketCommitsAndDiff( + gitServerConfig, + configFileData + ) + } + } + + throw new ConfigurationError( + `Unsupported git server / git resource combination: ${gitServerType}/${gitConfigResource}` + ) +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/fetchers/git-fetcher-bitbucket-commits-and-diff.ts b/yaku-apps-typescript/apps/git-fetcher/src/fetchers/git-fetcher-bitbucket-commits-and-diff.ts new file mode 100644 index 00000000..3f49a7d3 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/fetchers/git-fetcher-bitbucket-commits-and-diff.ts @@ -0,0 +1,83 @@ +import { CommitsMetadataAndDiff } from '../model/commits-metadata-and-diff' +import { ConfigFileData } from '../model/config-file-data' +import { GitServerConfig } from '../model/git-server-config' +import { ConfigurationError } from '../run.js' +import { tryParseResponse } from '../utils/error-handling-helper.js' +import { handleResponseStatus } from '../utils/handle-response-status.js' +import { GitFetcher } from './git-fetcher' +import { getRequestOptions } from './utils/get-request-options.js' + +export class GitFetcherBitbucketCommitsAndDiff + implements GitFetcher +{ + constructor( + private readonly gitServerConfig: GitServerConfig, + private readonly config: ConfigFileData + ) {} + + private stripUrl = (url: string) => { + return url.replace(/\/*$/, '') + } + + public async fetchResource(): Promise { + const requestOptions: RequestInit = await getRequestOptions( + this.gitServerConfig + ) + const result: CommitsMetadataAndDiff = { + commitsMetadata: [], + diff: [], + } + try { + const serverName = this.stripUrl(this.gitServerConfig.gitServerApiUrl) + const projectKey = this.config.data.org + const repositorySlug = this.config.data.repo + const filePath = this.config.data.filePath + const startHash = this.config.data.filter?.startHash + let endHash = this.config.data.filter?.endHash + if (!filePath) { + throw new ConfigurationError( + `Please define the 'filePath' parameter in the config and try again` + ) + } + if (!startHash) { + throw new ConfigurationError( + `Please define the 'filter.startHash' parameter in the config and try again` + ) + } + if (!endHash) { + endHash = 'master' + } + let startPage = 0 + let responseBody + do { + const commitsUrl = `${serverName}/projects/${projectKey}/repos/${repositorySlug}/commits?path=${filePath}&since=${startHash}&until=${endHash}&start=${startPage}` + const response: Response = await fetch(commitsUrl, requestOptions) + if (response.status != 200) { + handleResponseStatus(response.status) + } + responseBody = await tryParseResponse(response) + result.commitsMetadata = result.commitsMetadata.concat( + responseBody.values + ) + startPage = responseBody.nextPageStart ?? 0 + } while (!responseBody.isLastPage) + + console.log( + `Fetched medata about ${result.commitsMetadata.length} commits` + ) + + const diffUrl = `${serverName}/projects/${projectKey}/repos/${repositorySlug}/diff/${filePath}?since=${startHash}&until=${endHash}` + const response: Response = await fetch(diffUrl, requestOptions) + if (response.status != 200) { + handleResponseStatus(response.status) + } + responseBody = await tryParseResponse(response) + result.diff = responseBody.diffs + + console.log(`Fetched ${result.diff.length} diff`) + } catch (error: any) { + throw new Error(error.message) + } + return result + } +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/fetchers/git-fetcher-bitbucket-prs.ts b/yaku-apps-typescript/apps/git-fetcher/src/fetchers/git-fetcher-bitbucket-prs.ts new file mode 100644 index 00000000..35832cc3 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/fetchers/git-fetcher-bitbucket-prs.ts @@ -0,0 +1,231 @@ +import { BitbucketCommit } from '../model/bitbucket-commit' +import { BitbucketPr } from '../model/bitbucket-pr' +import { BitbucketResponse } from '../model/bitbucket-response' +import { BitbucketTag } from '../model/bitbucket-tag' +import { + AllowedFilterStateType, + ConfigFileData, + DateFilter, + HashFilter, + TagFilter, +} from '../model/config-file-data.js' +import { GitServerConfig } from '../model/git-server-config.js' +import { ConfigurationError } from '../run.js' +import { tryParseResponse } from '../utils/error-handling-helper.js' +import { handleResponseStatus } from '../utils/handle-response-status.js' +import { GitFetcher } from './git-fetcher' +import { getRequestOptions } from './utils/get-request-options.js' + +/** + * @description Creates a GitFetcher which is able to fetch pull requests from Bitbucket + */ +export class GitFetcherBitbucketPrs implements GitFetcher { + private readonly strippedApiUrl: string + + /** + * @constructor + * @param {GitServerConfig} gitServerConfig + * @param {ConfigFileData} config + */ + constructor( + private gitServerConfig: GitServerConfig, + private config: ConfigFileData + ) { + this.strippedApiUrl = this.gitServerConfig.gitServerApiUrl.replace( + /\/*$/, + '' + ) + } + + /** + * Builds the request url as well as the request options to fetch pull requests from Bitbucket + * and calls the fetch-method. The fetch method is called as long as the response body's attribute + * 'isLastPage' is false. During every iteration, the fetched pull requests are pushed to an array + * that is returned, after the last iteration. After each iteration, the 'startPage' value increments, in + * accordance to the 'nextPageStart' attribute.In case a date filter is provided, the response + * is first filtered and then the filtered pull requests get pushed to the array. + * @returns an a promise for array of BitBucket pull requests, which have been filtered according to the configuration. + * @throws {Error} when either fetch response is not successful or response can't be parsed. + */ + public async fetchResource(): Promise { + const requestOptions: RequestInit = await getRequestOptions( + this.gitServerConfig + ) + let pullRequests: BitbucketPr[] = [] + + let currentPage: number | null = 0 + let responseBody: BitbucketResponse + + do { + const response: Response = await fetch( + this.composePrUrl(this.config.data.filter?.state, currentPage ?? 0), + requestOptions + ) + + if (response.status != 200) { + handleResponseStatus(response.status) + } + + responseBody = (await tryParseResponse( + response + )) as BitbucketResponse + + pullRequests.push(...responseBody.values) + currentPage = responseBody.nextPageStart ?? null + } while (!responseBody.isLastPage) + + if (this.config.data.filter?.startTag) { + pullRequests = await this.filterPullRequestsByTag( + pullRequests, + this.config.data.filter + ) + } else if (this.config.data.filter?.startHash) { + pullRequests = await this.filterPullRequestsByHash( + pullRequests, + this.config.data.filter + ) + } else if (this.config.data.filter?.startDate) { + pullRequests = this.filterPullRequestsByDate( + pullRequests, + this.config.data.filter + ) + } + + console.log( + `Fetched ${pullRequests.length} pull request${ + pullRequests.length === 1 ? '' : 's' + }` + ) + return pullRequests + } + + private composePrUrl( + stateFilter: AllowedFilterStateType = 'ALL', + startPage?: number + ): string { + let baseUrl = `${this.strippedApiUrl}/projects/${this.config.data.org}/repos/${this.config.data.repo}/pull-requests?state=${stateFilter}` + + if (startPage != null) { + baseUrl += `&start=${startPage}` + } + + return baseUrl + } + + private composeCommitUrl(hashValue: string): string { + return `${this.strippedApiUrl}/projects/${this.config.data.org}/repos/${this.config.data.repo}/commits/${hashValue}` + } + + private composeTagUrl(tagName: string): string { + return `${this.strippedApiUrl}/projects/${this.config.data.org}/repos/${this.config.data.repo}/tags/${tagName}` + } + + private filterPullRequestsByDate( + prs: BitbucketPr[], + dateFilter: DateFilter + ): BitbucketPr[] { + const startDateInMs = dateFilter.startDate!.getTime() + const endDateInMs = dateFilter.endDate + ? dateFilter.endDate.getTime() + : Date.now() + + return prs.filter( + (pr) => pr.updatedDate >= startDateInMs && pr.updatedDate <= endDateInMs + ) + } + + private async filterPullRequestsByTag( + prs: BitbucketPr[], + tagFilter: TagFilter + ): Promise { + const dateFilter: DateFilter = await this.tagToDateFilter(tagFilter) + return this.filterPullRequestsByDate(prs, dateFilter) + } + + private async filterPullRequestsByHash( + prs: BitbucketPr[], + hashFilter: HashFilter + ): Promise { + const dateFilter: DateFilter = await this.hashToDateFilter(hashFilter) + return this.filterPullRequestsByDate(prs, dateFilter) + } + + private async hashToDateFilter(hashFilter: HashFilter): Promise { + const committerTimestamps: number[] = [] + + const hashesToFilter: string[] = [hashFilter.startHash!] + if (hashFilter.endHash != null) { + hashesToFilter.push(hashFilter.endHash) + } else { + // now is the default if no endHash is given + committerTimestamps.push(Date.now()) + } + + let bitbucketCommit: BitbucketCommit + for (const hashValue of hashesToFilter) { + bitbucketCommit = await this.fetchCommit(hashValue) + committerTimestamps.push(bitbucketCommit.committerTimestamp) + } + + // sort ascending + committerTimestamps.sort() + + return { + startDate: new Date(committerTimestamps[0]), + endDate: new Date(committerTimestamps[1]), + } + } + + private async tagToDateFilter(tagFilter: TagFilter): Promise { + if (tagFilter.startTag === undefined) { + return {} + } + + const startDate: Date = await this.tagNameToDate(tagFilter.startTag) + const endDate: Date | undefined = + tagFilter.endTag !== undefined + ? await this.tagNameToDate(tagFilter.endTag) + : undefined + + return { + startDate, + endDate, + } + } + + private async fetchCommit(hashValue: string): Promise { + const requestOptions: RequestInit = await getRequestOptions( + this.gitServerConfig + ) + const response: Response = await fetch( + this.composeCommitUrl(hashValue), + requestOptions + ) + if (response.status !== 200) { + throw new ConfigurationError( + `Could not retrieve the commit hash ${hashValue} (status ${response.status})` + ) + } + return (await tryParseResponse(response)) as BitbucketCommit + } + + private async fetchTag(tagName: string): Promise { + const requestOptions: RequestInit = await getRequestOptions( + this.gitServerConfig + ) + const response: Response = await fetch( + this.composeTagUrl(tagName), + requestOptions + ) + if (response.status !== 200) { + throw new ConfigurationError(`Could not retrieve the tag ${tagName}`) + } + return (await tryParseResponse(response)) as BitbucketTag + } + + private async tagNameToDate(tagName: string): Promise { + const tag: BitbucketTag = await this.fetchTag(tagName) + const commit: BitbucketCommit = await this.fetchCommit(tag.latestCommit) + return new Date(commit.committerTimestamp) + } +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/fetchers/git-fetcher-bitbucket-tags-and-branches.ts b/yaku-apps-typescript/apps/git-fetcher/src/fetchers/git-fetcher-bitbucket-tags-and-branches.ts new file mode 100644 index 00000000..ec3e3465 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/fetchers/git-fetcher-bitbucket-tags-and-branches.ts @@ -0,0 +1,67 @@ +import { BitbucketBranch } from '../model/bitbucket-branch' +import { BitbucketResponse } from '../model/bitbucket-response.js' +import { BitbucketTag } from '../model/bitbucket-tag' +import { ConfigFileData } from '../model/config-file-data.js' +import { GitServerConfig } from '../model/git-server-config.js' +import { tryParseResponse } from '../utils/error-handling-helper.js' +import { handleResponseStatus } from '../utils/handle-response-status.js' +import { GitFetcher } from './git-fetcher' +import { getRequestOptions } from './utils/get-request-options.js' + +export class GitFetcherBitbucketTagsAndBranches + implements GitFetcher +{ + private readonly url: string + + constructor( + private readonly gitServerConfig: GitServerConfig, + private readonly config: ConfigFileData, + private readonly resourceType: 'tags' | 'branches' + ) { + const strippedApiUrl: string = gitServerConfig.gitServerApiUrl.replace( + /\/*$/, + '' + ) + this.url = `${strippedApiUrl}/projects/${this.config.data.org}/repos/${this.config.data.repo}/${resourceType}` + } + + public async fetchResource(): Promise<(BitbucketTag | BitbucketBranch)[]> { + const requestOptions: RequestInit = await getRequestOptions( + this.gitServerConfig + ) + const fetchedResources: (BitbucketTag | BitbucketBranch)[] = [] + + let startPage = 0 + let response: Response + let responseBody: BitbucketResponse + + do { + response = await fetch(`${this.url}?start=${startPage}`, requestOptions) + if (response.status !== 200) { + handleResponseStatus(response.status) + } + responseBody = (await tryParseResponse(response)) as BitbucketResponse< + BitbucketTag | BitbucketBranch + > + fetchedResources.push(...responseBody.values) + startPage = responseBody.nextPageStart ?? 0 + } while (!responseBody.isLastPage) + + let resource = '' + let postfix = '' + if (this.resourceType === 'branches') { + resource = 'branch' + postfix = 'es' + } else if (this.resourceType === 'tags') { + resource = 'tag' + postfix = 's' + } + + console.log( + `Fetched ${fetchedResources.length} ${resource}${ + fetchedResources.length === 1 ? '' : postfix + }` + ) + return fetchedResources + } +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/fetchers/git-fetcher-github-commits-and-diff.ts b/yaku-apps-typescript/apps/git-fetcher/src/fetchers/git-fetcher-github-commits-and-diff.ts new file mode 100644 index 00000000..b67ce5fb --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/fetchers/git-fetcher-github-commits-and-diff.ts @@ -0,0 +1,148 @@ +import { CommitsMetadataAndDiff } from '../model/commits-metadata-and-diff' +import { ConfigFileData } from '../model/config-file-data' +import { GitServerConfig } from '../model/git-server-config' +import { ConfigurationError } from '../run.js' +import { tryParseResponse } from '../utils/error-handling-helper.js' +import { handleResponseStatus } from '../utils/handle-response-status.js' +import { GitFetcher } from './git-fetcher' +import { getRequestOptions } from './utils/get-request-options.js' + +export class GitFetcherGithubCommitsAndDiff + implements GitFetcher +{ + constructor( + private readonly gitServerConfig: GitServerConfig, + private readonly config: ConfigFileData + ) {} + + private stripUrl = (url: string) => { + return url.replace(/\/*$/, '') + } + + private incrementSecondsAndReturnIsoFormat = (startDate: string) => { + const newDate = new Date( + new Date(startDate).setUTCSeconds(new Date(startDate).getUTCSeconds() + 1) + ) + return ( + newDate.getUTCFullYear() + + '-' + + (newDate.getUTCMonth() + 1) + + '-' + + newDate.getUTCDate() + + 'T' + + newDate.getUTCHours() + + ':' + + newDate.getUTCMinutes() + + ':' + + newDate.getUTCSeconds() + + 'Z' + ) + } + + public async fetchResource(): Promise { + const requestOptions: RequestInit = await getRequestOptions( + this.gitServerConfig + ) + const result: CommitsMetadataAndDiff = { + commitsMetadata: [], + diff: { + linesAdded: [], + linesRemoved: [], + }, + } + try { + const serverName = this.stripUrl(this.gitServerConfig.gitServerApiUrl) + const projectKey = this.config.data.org + const repositorySlug = this.config.data.repo + const filePath = this.config.data.filePath + const startHash = this.config.data.filter?.startHash + let endHash = this.config.data.filter?.endHash + if (!filePath) { + throw new ConfigurationError( + `Please define the 'filePath' parameter in the config and try again` + ) + } + if (!startHash) { + throw new ConfigurationError( + `Please define the 'filter.startHash' parameter in the config and try again` + ) + } + let startDate + const startCommitUrl = `${serverName}/repos/${projectKey}/${repositorySlug}/commits/${startHash}` + let response: Response = await fetch(startCommitUrl, requestOptions) + if (response.status != 200) { + handleResponseStatus(response.status) + } + let responseBody = await tryParseResponse(response) + startDate = responseBody.commit.committer.date + + console.log(`Fetched metadata about starting commit at ${startDate}`) + + //function below is required in order to have consistency between the '/compare' and '/commits' API calls + startDate = this.incrementSecondsAndReturnIsoFormat(startDate) + + if (!endHash) { + endHash = 'master' + } + + const endCommitUrl = `${serverName}/repos/${projectKey}/${repositorySlug}/commits/${endHash}` + response = await fetch(endCommitUrl, requestOptions) + if (response.status != 200) { + handleResponseStatus(response.status) + } + responseBody = await tryParseResponse(response) + const endDate = responseBody.commit.committer.date + + console.log(`Fetched metadata about ending commit at ${endDate}`) + + //?page=1&per_page=1 is required in order to force the request to return only the data we are interested in, + //which can decrease the response returned by tens of thousands of lines (19284 lines reduced in tested example) + const diffUrl = `${serverName}/repos/${projectKey}/${repositorySlug}/compare/${startHash}...${endHash}?page=1&per_page=1` + response = await fetch(diffUrl, requestOptions) + if (response.status != 200) { + handleResponseStatus(response.status) + } + responseBody = await tryParseResponse(response) + + for (const file of responseBody.files) { + if (file.filename === filePath) { + const linesAdded = file.patch.match(/\n\+[\S\s]*?(?=\n)/g) + result.diff['linesAdded'] = linesAdded + + const linesRemoved = file.patch.match(/\n-[\S\s]*?(?=\n)/g) + result.diff['linesRemoved'] = linesRemoved + break + } + } + + console.log( + `Fetched ${result.diff['linesAdded'].length} lines added and ${result.diff['linesRemoved'].length} lines removed` + ) + + let currentPage: number | null = 1 + while (currentPage != null) { + const commitsUrl = `${serverName}/repos/${projectKey}/${repositorySlug}/commits?path=${filePath}&since=${startDate}&until=${endDate}&page=${currentPage}&per_page=100` + const response: Response = await fetch(commitsUrl, requestOptions) + if (response.status != 200) { + handleResponseStatus(response.status) + } + const responseBody = await tryParseResponse(response) + + if (responseBody.length > 0) { + for (const data of responseBody) { + result.commitsMetadata.push(data.commit) + } + currentPage = currentPage + 1 + } else { + currentPage = null + } + } + console.log( + `Fetched metadata about ${result.commitsMetadata.length} commits` + ) + } catch (error: any) { + throw new Error(error.message) + } + return result + } +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/fetchers/git-fetcher-github-prs.ts b/yaku-apps-typescript/apps/git-fetcher/src/fetchers/git-fetcher-github-prs.ts new file mode 100644 index 00000000..1ccbabc1 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/fetchers/git-fetcher-github-prs.ts @@ -0,0 +1,101 @@ +import { ConfigFileData } from '../model/config-file-data.js' +import { GitServerConfig } from '../model/git-server-config.js' +import { GithubPr } from '../model/github-pr' +import { compareLabels } from '../utils/compare-labels.js' +import { tryParseResponse } from '../utils/error-handling-helper.js' +import { handleResponseStatus } from '../utils/handle-response-status.js' +import { GitFetcher } from './git-fetcher' +import { getRequestOptions } from './utils/get-request-options.js' + +/** + * @description Creates a GitFetcher which is able to fetch pull requests from GitHub + */ +export class GitFetcherGithubPrs implements GitFetcher { + /** + * @constructor + * @param {GitServerConfig} gitServerConfig + * @param {ConfigFileData} config + */ + constructor( + private gitServerConfig: GitServerConfig, + private config: ConfigFileData + ) {} + + /** + * Builds the request url as well as the request options to fetch pull requests from GitHub + * and calls the fetch-method. As long as the response body includes pull requests objects, the + * currentPage variable will be incremented and pull requests are pushed to the array, that's eventually + * returned. If the response body is empty and no pull requests were received, the loop ends, + * and the collected pull requests are returned. + * @returns an a promise for an array of GitHub pull requests, which have been filtered according to the configuration. + * @throws {Error} when either fetch response is not successful or response can't be parsed. + */ + public async fetchResource(): Promise { + const requestOptions: RequestInit = await getRequestOptions( + this.gitServerConfig + ) + + let pullRequests: GithubPr[] = [] + + let currentPage: number | null = 1 + let responseBody: GithubPr[] + + while (currentPage != null) { + const response: Response = await fetch( + this.composeUrl(currentPage), + requestOptions + ) + + if (response.status != 200) { + handleResponseStatus(response.status) + } + + responseBody = (await tryParseResponse(response)) as GithubPr[] + + if (responseBody.length > 0) { + pullRequests.push(...responseBody) + currentPage++ + } else { + currentPage = null + } + } + + if ( + pullRequests.length > 0 && + this.config.data.labels && + this.config.data.labels.length > 0 + ) { + const filteredPrs: GithubPr[] = [] + + pullRequests.forEach((pr: GithubPr) => { + if (compareLabels(this.config.data.labels, pr.labels)) { + filteredPrs.push(pr) + } + }) + + pullRequests = filteredPrs + } + + console.log( + `Fetched ${pullRequests.length} pull request${ + pullRequests.length === 1 ? '' : 's' + }` + ) + return pullRequests + } + + private composeUrl(startPage?: number): string { + const strippedApiUrl: string = this.gitServerConfig.gitServerApiUrl.replace( + /\/*$/, + '' + ) + const stateFilter = 'all' //will allow to set other state filters, introduced in future tickets + + let baseUrl = `${strippedApiUrl}/repos/${this.config.data.org}/${this.config.data.repo}/pulls?state=${stateFilter}&per_page=100` + if (startPage != null) { + baseUrl += `&page=${startPage}` + } + + return baseUrl + } +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/fetchers/git-fetcher.ts b/yaku-apps-typescript/apps/git-fetcher/src/fetchers/git-fetcher.ts new file mode 100644 index 00000000..73491746 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/fetchers/git-fetcher.ts @@ -0,0 +1,3 @@ +export interface GitFetcher { + fetchResource(): Promise +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/fetchers/index.ts b/yaku-apps-typescript/apps/git-fetcher/src/fetchers/index.ts new file mode 100644 index 00000000..62e232b8 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/fetchers/index.ts @@ -0,0 +1,2 @@ +export * from './git-fetcher.js' +export * from './generate-git-fetcher.js' diff --git a/yaku-apps-typescript/apps/git-fetcher/src/fetchers/utils/get-request-options.ts b/yaku-apps-typescript/apps/git-fetcher/src/fetchers/utils/get-request-options.ts new file mode 100644 index 00000000..39d8569e --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/fetchers/utils/get-request-options.ts @@ -0,0 +1,26 @@ +import { GitServerConfig } from '../../model/git-server-config' + +export async function getRequestOptions( + gitServerConfig: GitServerConfig +): Promise { + const options: RequestInit = { + method: 'GET', + } + + if (gitServerConfig.gitServerAuthMethod === 'basic') { + const encodedCredentials: string = Buffer.from( + `${gitServerConfig.gitServerUsername}:${gitServerConfig.gitServerPassword}` + ).toString('base64') + + options.headers = { + accept: 'application/vnd.github+json', + authorization: `Basic ${encodedCredentials}`, + } + } else if (gitServerConfig.gitServerAuthMethod === 'token') { + options.headers = { + accept: 'application/json', + authorization: `Bearer ${gitServerConfig.gitServerApiToken}`, + } + } + return options +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/index.ts b/yaku-apps-typescript/apps/git-fetcher/src/index.ts new file mode 100644 index 00000000..4b97d240 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/index.ts @@ -0,0 +1,18 @@ +#!/usr/bin/env node + +import { AppError, AppOutput } from '@B-S-F/autopilot-utils' +import { run } from './run.js' +const output = new AppOutput() +try { + await run(output) + output.write() +} catch (error) { + if (error instanceof AppError) { + output.setStatus('FAILED') + output.setReason(error.Reason()) + output.write() + process.exit(0) + } else { + throw error // to show the stack trace + } +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/model/bitbucket-branch.ts b/yaku-apps-typescript/apps/git-fetcher/src/model/bitbucket-branch.ts new file mode 100644 index 00000000..35ffe7fe --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/model/bitbucket-branch.ts @@ -0,0 +1,8 @@ +export type BitbucketBranch = { + id: string + displayId: string + type: 'BRANCH' + latestCommit: string + latestChangeset: string + isDefault: boolean +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/model/bitbucket-commit.ts b/yaku-apps-typescript/apps/git-fetcher/src/model/bitbucket-commit.ts new file mode 100644 index 00000000..9f001193 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/model/bitbucket-commit.ts @@ -0,0 +1,7 @@ +export type BitbucketCommit = { + /** The commit hash */ + id: string + /** Milliseconds since unix epoch of this commit */ + committerTimestamp: number + [s: string]: unknown +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/model/bitbucket-diff-response.ts b/yaku-apps-typescript/apps/git-fetcher/src/model/bitbucket-diff-response.ts new file mode 100644 index 00000000..1243c49f --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/model/bitbucket-diff-response.ts @@ -0,0 +1,8 @@ +export type BitbucketDiffResponse = { + fromHash: string + toHash: string + contextLines: number + whitespace: string + diffs: any + truncated: boolean +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/model/bitbucket-pr.ts b/yaku-apps-typescript/apps/git-fetcher/src/model/bitbucket-pr.ts new file mode 100644 index 00000000..ca43a8ac --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/model/bitbucket-pr.ts @@ -0,0 +1,7 @@ +export type BitbucketPr = { + id: number + state: string + /** Milliseconds since unix epoch of the last update to this pull request */ + updatedDate: number + [s: string]: unknown +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/model/bitbucket-response.ts b/yaku-apps-typescript/apps/git-fetcher/src/model/bitbucket-response.ts new file mode 100644 index 00000000..d5051a8d --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/model/bitbucket-response.ts @@ -0,0 +1,9 @@ +export type BitbucketResponse = { + size: number + limit: number + isLastPage: boolean + start: number + values: T[] + [s: string]: unknown + nextPageStart?: number | null +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/model/bitbucket-tag.ts b/yaku-apps-typescript/apps/git-fetcher/src/model/bitbucket-tag.ts new file mode 100644 index 00000000..774f0dec --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/model/bitbucket-tag.ts @@ -0,0 +1,8 @@ +export type BitbucketTag = { + id: string + displayId: string + type: 'TAG' + latestCommit: string + latestChangeset: string + hash: string +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/model/commits-metadata-and-diff.ts b/yaku-apps-typescript/apps/git-fetcher/src/model/commits-metadata-and-diff.ts new file mode 100644 index 00000000..9f846993 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/model/commits-metadata-and-diff.ts @@ -0,0 +1,4 @@ +export type CommitsMetadataAndDiff = { + commitsMetadata: any + diff: any +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/model/config-file-data.ts b/yaku-apps-typescript/apps/git-fetcher/src/model/config-file-data.ts new file mode 100644 index 00000000..f0913d5f --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/model/config-file-data.ts @@ -0,0 +1,134 @@ +import { z } from 'zod' +import { parse } from 'date-fns' + +/* Follows the schema dd-mm-yyyy **/ +const dateRegex = /^[0-9]{2}-[0-9]{2}-[0-9]{4,}$/ +const dateFormat = 'dd-MM-yyyy' + +/* 'DECLINED', 'MERGED', 'OPEN', 'ALL' only valid for Bitbucket. GitHub needs lowercase. */ +export const allowedFilterState = ['DECLINED', 'MERGED', 'OPEN', 'ALL'] as const +export type AllowedFilterStateType = (typeof allowedFilterState)[number] + +export const gitFetcherPullRequests = [ + 'pull-request', + 'pull-requests', + 'pr', + 'prs', + 'pullrequest', + 'pullrequests', + 'pull', + 'pulls', +] as const + +const gitFetcherResources = [ + ...gitFetcherPullRequests, + 'branches', + 'tags', + 'metadata-and-diff', +] as const + +export const GitConfigResourceSchema = z.enum(gitFetcherResources) +export type GitConfigResource = z.infer + +const BaseFilterSchema = z.object({ + state: z.enum(allowedFilterState).optional(), +}) + +const DateFilterSchema = z.object({ + startDate: z + .string() + .regex(dateRegex, `date must match the format ${dateFormat.toLowerCase()}`) + .transform((val) => parse(val, dateFormat, Date.now())) + .transform((val: Date) => { + val.setHours(0) + val.setMinutes(0) + val.setSeconds(0) + val.setMilliseconds(0) + return val + }) + .optional(), + endDate: z + .string() + .regex(dateRegex, `date must match the format ${dateFormat.toLowerCase()}`) + .transform((val) => parse(val, dateFormat, Date.now())) + .transform((val: Date) => { + val.setHours(23) + val.setMinutes(59) + val.setSeconds(59) + val.setMilliseconds(999) + return val + }) + .optional(), +}) + +const CommitHashFilterSchema = z.object({ + startHash: z.string().min(1).optional(), + endHash: z.string().min(1).optional(), +}) + +const TagFilterSchema = z.object({ + startTag: z.string().min(1).optional(), + endTag: z.string().min(1).optional(), +}) + +const FilterSchema = BaseFilterSchema.merge(DateFilterSchema) + .merge(CommitHashFilterSchema) + .merge(TagFilterSchema) + .refine((arg) => { + function isDefined(value: any): boolean { + return value !== undefined + } + + // transform each boolean to 0 or 1, so that it can be summed up + const hasDateFilter: 0 | 1 = Number( + isDefined(arg.startDate) || isDefined(arg.endDate) + ) as 0 | 1 + const hasHashFilter: 0 | 1 = Number( + isDefined(arg.startHash) || isDefined(arg.endHash) + ) as 0 | 1 + const hasTagFilter: 0 | 1 = Number( + isDefined(arg.startTag) || isDefined(arg.endTag) + ) as 0 | 1 + + // the sum shows how many of the filters were defined in config file + const numberOfFilters: number = hasDateFilter + hasHashFilter + hasTagFilter + // there must be no more than one filter type defined + return numberOfFilters <= 1 + }, 'Combining the date, hash and/or tag filter is not possible') + .refine( + (arg) => !(arg.endHash && !arg.startHash), + 'Specify filter.startHash if filter.endHash is provided' + ) + .refine( + (arg) => !(arg.endTag && !arg.startTag), + 'Specify filter.startTag if filter.endTag is provided' + ) + .refine( + (arg) => !(arg.endDate && !arg.startDate), + 'Specify filter.startDate if filter.endDate is provided' + ) + .refine( + (arg) => !(arg.endDate && arg.startDate && arg.endDate < arg.startDate), + 'filter.endDate must be after or equal filter.startDate' + ) + +export const GitFetcherConfigSchema = z.object({ + org: z.string().min(1), + repo: z.string().min(1), + resource: GitConfigResourceSchema, + labels: z.array(z.string().min(1)).optional(), + filter: FilterSchema.optional(), + filePath: z.string().optional(), +}) + +export type DateFilter = z.infer + +export type HashFilter = z.infer + +export type TagFilter = z.infer + +export type GitFetcherConfig = z.infer + +export class ConfigFileData { + constructor(public data: GitFetcherConfig) {} +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/model/git-server-config.ts b/yaku-apps-typescript/apps/git-fetcher/src/model/git-server-config.ts new file mode 100644 index 00000000..bea593b1 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/model/git-server-config.ts @@ -0,0 +1,27 @@ +export const supportedGitServerTypes = ['github', 'bitbucket'] as const +export type SupportedGitServerType = (typeof supportedGitServerTypes)[number] + +export const supportedAuthMethods = ['token', 'basic'] as const +export type SupportedAuthMethod = (typeof supportedAuthMethods)[number] + +interface GitServerConfigBase { + gitServerType: SupportedGitServerType + gitServerApiUrl: string + gitFetcherConfigFilePath: string + gitFetcherOutputFilePath: string +} + +export interface GitServerConfigAuthBasic extends GitServerConfigBase { + gitServerAuthMethod: 'basic' + gitServerUsername: string + gitServerPassword: string +} + +export interface GitServerConfigAuthToken extends GitServerConfigBase { + gitServerAuthMethod: 'token' + gitServerApiToken: string +} + +export type GitServerConfig = + | GitServerConfigAuthBasic + | GitServerConfigAuthToken diff --git a/yaku-apps-typescript/apps/git-fetcher/src/model/github-diff-response.ts b/yaku-apps-typescript/apps/git-fetcher/src/model/github-diff-response.ts new file mode 100644 index 00000000..b1943696 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/model/github-diff-response.ts @@ -0,0 +1,15 @@ +export type GithubDiffResponse = { + url: string + html_url: string + permalink_url: string + diff_url: string + patch_url: string + base_commit: any + merge_base_commit: any + status: string + ahead_by: number + behind_by: number + total_commits: number + commits: Array + files: Array +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/model/github-label.ts b/yaku-apps-typescript/apps/git-fetcher/src/model/github-label.ts new file mode 100644 index 00000000..2cc45e8b --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/model/github-label.ts @@ -0,0 +1,5 @@ +export type GithubLabel = { + id: number + name: string + [s: string]: unknown +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/model/github-multiple-commits-response.ts b/yaku-apps-typescript/apps/git-fetcher/src/model/github-multiple-commits-response.ts new file mode 100644 index 00000000..2099130c --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/model/github-multiple-commits-response.ts @@ -0,0 +1,11 @@ +export type GithubMultipleCommitsResponse = { + sha: string + node_id: string + commit: any + url: string + html_url: string + comments_url: string + author: any + committer: any + parents: Array +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/model/github-pr.ts b/yaku-apps-typescript/apps/git-fetcher/src/model/github-pr.ts new file mode 100644 index 00000000..12ab5a27 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/model/github-pr.ts @@ -0,0 +1,9 @@ +import { GithubLabel } from './github-label' + +export type GithubPr = { + /** Uniquely identifies a pull request */ + number: number + state: string + labels: GithubLabel[] + [s: string]: unknown +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/model/github-single-commit-response.ts b/yaku-apps-typescript/apps/git-fetcher/src/model/github-single-commit-response.ts new file mode 100644 index 00000000..94dedc4f --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/model/github-single-commit-response.ts @@ -0,0 +1,13 @@ +export type GithubSingleCommitResponse = { + sha: string + node_id: string + commit: any + url: string + html_url: string + comments_url: string + author: any + committer: any + parents: Array + stats: any + files: Array +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/run.ts b/yaku-apps-typescript/apps/git-fetcher/src/run.ts new file mode 100644 index 00000000..dd3346a8 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/run.ts @@ -0,0 +1,187 @@ +import { AppError, AppOutput } from '@B-S-F/autopilot-utils' +import * as fs from 'fs' +import path from 'path' +import * as process from 'process' +import { GitFetcher, GitResource } from './fetchers/index.js' +import generateGitFetcher from './fetchers/generate-git-fetcher.js' +import { ConfigFileData, GitFetcherConfig } from './model/config-file-data.js' +import { + GitServerConfig, + supportedAuthMethods, + supportedGitServerTypes, +} from './model/git-server-config.js' +import { validateFetcherConfig } from './utils/validation.js' + +export class EnvironmentError extends AppError { + constructor(reason: string) { + super(reason) + this.name = 'EnvironmentError' + } + + Reason(): string { + return super.Reason() + } +} + +export class ConfigurationError extends AppError { + constructor(reason: string) { + super(reason) + this.name = 'ConfigurationError' + } + + Reason(): string { + return super.Reason() + } +} + +const exportJsonToEvidenceDirectory = (data: any, filename: string) => { + const filepath: string = path.join(process.env.evidence_path!, filename) + fs.writeFileSync(filepath, JSON.stringify(data)) +} + +function validateEnvironmentVariables() { + const appErrors: AppError[] = [] + if (process.env.NODE_TLS_REJECT_UNAUTHORIZED == '0') { + appErrors.push( + new EnvironmentError( + 'NODE_TLS_REJECT_UNAUTHORIZED environment variable is set to 0 which is not allowed' + ) + ) + } + + const gitFetcherServerType: string | undefined = + process.env.GIT_FETCHER_SERVER_TYPE + if (!gitFetcherServerType) { + appErrors.push( + new EnvironmentError( + 'GIT_FETCHER_SERVER_TYPE environment variable is not set' + ) + ) + } + + if (!supportedGitServerTypes.includes(gitFetcherServerType as any)) { + appErrors.push( + new ConfigurationError( + `The server type "${process.env.GIT_FETCHER_SERVER_TYPE}" is not supported` + ) + ) + } + + if (!process.env.GIT_FETCHER_SERVER_API_URL) { + appErrors.push( + new EnvironmentError( + 'GIT_FETCHER_SERVER_API_URL environment variable is not set.' + ) + ) + } + + const httpsRegex = new RegExp(/^https:\/\//) + if (!process.env.GIT_FETCHER_SERVER_API_URL?.match(httpsRegex)) { + appErrors.push( + new ConfigurationError( + 'GIT_FETCHER_SERVER_API_URL environment variable must use secured connections with https' + ) + ) + } + + const gitFetcherAuthMethod: string | undefined = + process.env.GIT_FETCHER_SERVER_AUTH_METHOD + if ( + (gitFetcherAuthMethod === undefined || gitFetcherAuthMethod === 'token') && + !process.env.GIT_FETCHER_API_TOKEN?.trim() + ) { + appErrors.push( + new EnvironmentError( + 'GIT_FETCHER_API_TOKEN environment variable is required for "token" authentication, but is not set or empty.' + ) + ) + } + + if ( + gitFetcherAuthMethod !== undefined && + !(supportedAuthMethods as unknown as string[]).includes( + gitFetcherAuthMethod + ) + ) { + appErrors.push( + new ConfigurationError( + `No valid authentication method provided. Valid authentication methods are: ${supportedAuthMethods}` + ) + ) + } + + if ( + process.env.GIT_FETCHER_SERVER_AUTH_METHOD == 'basic' && + !process.env.GIT_FETCHER_USERNAME?.trim() + ) { + appErrors.push( + new EnvironmentError( + 'GIT_FETCHER_USERNAME environment variable is required for "basic" authentication, but is not set or empty.' + ) + ) + } + + if ( + process.env.GIT_FETCHER_SERVER_AUTH_METHOD == 'basic' && + !process.env.GIT_FETCHER_PASSWORD?.trim() + ) { + appErrors.push( + new EnvironmentError( + 'GIT_FETCHER_PASSWORD environment variable is required for "basic" authentication, but is not set or empty.' + ) + ) + } + + if (appErrors.length > 0) { + const concatenatedReason = appErrors.map((e) => e.Reason()).join('\n') + throw new AppError(concatenatedReason) + } +} + +export const run = async (output: AppOutput) => { + validateEnvironmentVariables() + + const env: GitServerConfig = setupGitServerConfig() + + const fetcherConfigFileData: GitFetcherConfig = await validateFetcherConfig( + env.gitFetcherConfigFilePath + ) + const config: ConfigFileData = new ConfigFileData(fetcherConfigFileData) + const fetcher: GitFetcher = generateGitFetcher(env, config) + const fetchedResources = await fetcher.fetchResource() + if (fetchedResources != undefined) { + const outputFilePath = env.gitFetcherOutputFilePath || 'data.json' + exportJsonToEvidenceDirectory( + fetchedResources, + env.gitFetcherOutputFilePath || '' + ) + output.addOutput({ + 'git-fetcher-result': outputFilePath, + }) + console.log( + `Fetch from ${ + env.gitServerApiUrl + } was successful with config ${JSON.stringify(config.data)}` + ) + } +} + +function setupGitServerConfig(): GitServerConfig { + return { + gitServerType: + (process.env + .GIT_FETCHER_SERVER_TYPE as GitServerConfig['gitServerType']) ?? '', + gitServerApiUrl: process.env.GIT_FETCHER_SERVER_API_URL ?? '', + gitServerAuthMethod: + (process.env + .GIT_FETCHER_SERVER_AUTH_METHOD as GitServerConfig['gitServerAuthMethod']) ?? + 'token', + gitServerUsername: process.env.GIT_FETCHER_USERNAME ?? '', + gitServerPassword: process.env.GIT_FETCHER_PASSWORD ?? '', + gitServerApiToken: process.env.GIT_FETCHER_API_TOKEN ?? '', + gitFetcherConfigFilePath: + process.env.GIT_FETCHER_CONFIG_FILE_PATH ?? 'git-fetcher-config.yml', + gitFetcherOutputFilePath: + process.env.GIT_FETCHER_OUTPUT_FILE_PATH ?? 'git-fetcher-data.json', + } +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/utils/compare-labels.ts b/yaku-apps-typescript/apps/git-fetcher/src/utils/compare-labels.ts new file mode 100644 index 00000000..cc4279ab --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/utils/compare-labels.ts @@ -0,0 +1,23 @@ +import { GithubLabel } from '../model/github-label' + +export function compareLabels( + requiredLabels: string[] | undefined, + fetchedLabels: GithubLabel[] +): boolean { + let result = false + if (requiredLabels && requiredLabels.length != 0) { + const fetchedLabelsNames: string[] = [] + if (fetchedLabels.length != 0) { + fetchedLabels.forEach((value) => { + fetchedLabelsNames.push(value.name) + }) + } + const filteredLabels = requiredLabels.filter((item) => + fetchedLabelsNames.includes(item) + ) + if (filteredLabels.length != 0) { + result = true + } + } + return result +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/utils/error-handling-helper.ts b/yaku-apps-typescript/apps/git-fetcher/src/utils/error-handling-helper.ts new file mode 100644 index 00000000..72925f65 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/utils/error-handling-helper.ts @@ -0,0 +1,10 @@ +export const tryParseResponse = async (response: Response) => { + try { + const responseBody = await response.json() + return responseBody + } catch (error) { + throw new Error( + `Response status was 200, however, the response body failed to be parsed as json. This most likely means your token is invalid or the input data is not correct. Returned error is: ${error}` + ) + } +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/utils/handle-response-status.ts b/yaku-apps-typescript/apps/git-fetcher/src/utils/handle-response-status.ts new file mode 100644 index 00000000..6a84dbdc --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/utils/handle-response-status.ts @@ -0,0 +1,22 @@ +import { ConfigurationError } from '../run.js' + +export function handleResponseStatus(statusCode: number) { + if (statusCode == 404) { + throw new ConfigurationError( + `Repository not found. Status code: ${statusCode}` + ) + } else if (statusCode == 401 || statusCode == 403) { + if ((process.env.GIT_FETCHER_SERVER_TYPE as string) === 'github') { + throw new ConfigurationError( + `Could not access the required repository, SSO Token might not be authorized for the required organization. Status code: ${statusCode}` + ) + } + throw new ConfigurationError( + `Could not access the required repository. Status code: ${statusCode}` + ) + } else { + throw new Error( + `Could not fetch data from git repository. Status code: ${statusCode}` + ) + } +} diff --git a/yaku-apps-typescript/apps/git-fetcher/src/utils/validation.ts b/yaku-apps-typescript/apps/git-fetcher/src/utils/validation.ts new file mode 100644 index 00000000..4108bda1 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/src/utils/validation.ts @@ -0,0 +1,25 @@ +import * as fs from 'fs' +import YAML from 'yaml' +import { SafeParseReturnType } from 'zod' +import { fromZodError } from 'zod-validation-error' +import { + GitFetcherConfig, + GitFetcherConfigSchema, +} from '../model/config-file-data.js' +import { ConfigurationError } from '../run.js' + +export async function validateFetcherConfig( + filePath: string +): Promise { + const uncheckedGitFetcherConfig: unknown = await YAML.parse( + fs.readFileSync(filePath, { encoding: 'utf8' }) + ) + + const result: SafeParseReturnType = + GitFetcherConfigSchema.safeParse(uncheckedGitFetcherConfig) + if (result.success) { + return result.data + } else { + throw new ConfigurationError(fromZodError(result.error).message) + } +} diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-invalid-end-date.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-invalid-end-date.yml new file mode 100644 index 00000000..ba0e99de --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-invalid-end-date.yml @@ -0,0 +1,6 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + startDate: 01-02-2020 + endDate: 01-2-2020 diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-invalid-start-date-after-end-date.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-invalid-start-date-after-end-date.yml new file mode 100644 index 00000000..c6d2778e --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-invalid-start-date-after-end-date.yml @@ -0,0 +1,6 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + startDate: 01-03-2020 + endDate: 01-02-2020 diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-invalid-start-date-missing.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-invalid-start-date-missing.yml new file mode 100644 index 00000000..49539730 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-invalid-start-date-missing.yml @@ -0,0 +1,5 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + endDate: 01-02-2020 diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-invalid-start-date.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-invalid-start-date.yml new file mode 100644 index 00000000..5238b8e0 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-invalid-start-date.yml @@ -0,0 +1,5 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + startDate: 1-02-2020 diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-valid-start-01-01-2019-end-31-12-2019.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-valid-start-01-01-2019-end-31-12-2019.yml new file mode 100644 index 00000000..3701b417 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-valid-start-01-01-2019-end-31-12-2019.yml @@ -0,0 +1,6 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + startDate: 01-01-2019 + endDate: 31-12-2019 diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-valid-start-01-01-2024-end-31-12-2024.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-valid-start-01-01-2024-end-31-12-2024.yml new file mode 100644 index 00000000..d8156aec --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-valid-start-01-01-2024-end-31-12-2024.yml @@ -0,0 +1,6 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + startDate: 01-01-2024 + endDate: 31-12-2024 diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-valid-start-01-02-2020.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-valid-start-01-02-2020.yml new file mode 100644 index 00000000..e3bbfda1 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-valid-start-01-02-2020.yml @@ -0,0 +1,5 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + startDate: 01-02-2020 diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-valid-start-01-06-2020-end-31-12-2022.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-valid-start-01-06-2020-end-31-12-2022.yml new file mode 100644 index 00000000..18f7d5f3 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-valid-start-01-06-2020-end-31-12-2022.yml @@ -0,0 +1,6 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + startDate: 01-06-2020 + endDate: 31-12-2022 diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-valid-start-01-06-2020-end-31-12-2023.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-valid-start-01-06-2020-end-31-12-2023.yml new file mode 100644 index 00000000..70bf5fa9 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-valid-start-01-06-2020-end-31-12-2023.yml @@ -0,0 +1,6 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + startDate: 01-06-2020 + endDate: 31-12-2023 diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-valid-start-01-06-2020.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-valid-start-01-06-2020.yml new file mode 100644 index 00000000..e7505c2c --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-valid-start-01-06-2020.yml @@ -0,0 +1,5 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + startDate: 01-06-2020 diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-valid-state-and-date.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-valid-state-and-date.yml new file mode 100644 index 00000000..f7f0cddc --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/date-filter/git-fetcher-config-valid-state-and-date.yml @@ -0,0 +1,7 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + state: MERGED + startDate: 01-06-2020 + endDate: 30-04-2023 diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-bitbucket.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-bitbucket.yml new file mode 100644 index 00000000..fd667ecd --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-bitbucket.yml @@ -0,0 +1,3 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-fetch-branches-from-bitbucket.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-fetch-branches-from-bitbucket.yml new file mode 100644 index 00000000..c98166f7 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-fetch-branches-from-bitbucket.yml @@ -0,0 +1,3 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: branches diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-fetch-commits-metadata-and-diff-from-bitbucket.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-fetch-commits-metadata-and-diff-from-bitbucket.yml new file mode 100644 index 00000000..27a536c3 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-fetch-commits-metadata-and-diff-from-bitbucket.yml @@ -0,0 +1,6 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: metadata-and-diff +filter: + startHash: 35cc5eec543e69aed90503f21cf12666bcbfda4f +filePath: Somefolder/something.py diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-fetch-commits-metadata-and-diff-from-github.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-fetch-commits-metadata-and-diff-from-github.yml new file mode 100644 index 00000000..5a6a8fd8 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-fetch-commits-metadata-and-diff-from-github.yml @@ -0,0 +1,7 @@ +org: aquatest +repo: github-fetcher-test-repo +resource: metadata-and-diff +filter: + startHash: afeaebf412c6d0b865a36cfdec37fdb46c0fab63 + endHash: 8036cf75f4b7365efea76cbd716ef12d352d7d29 +filePath: apps/git-fetcher/src/fetchers/git-fetcher.ts diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-fetch-tags-from-bitbucket.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-fetch-tags-from-bitbucket.yml new file mode 100644 index 00000000..ecbc19cd --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-fetch-tags-from-bitbucket.yml @@ -0,0 +1,3 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: tags diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-github-wrong-label.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-github-wrong-label.yml new file mode 100644 index 00000000..41bacd93 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-github-wrong-label.yml @@ -0,0 +1,5 @@ +org: aquatest +repo: github-fetcher-test-repo +resource: prs +labels: + - fooBar diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-github.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-github.yml new file mode 100644 index 00000000..0e4f3a4a --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-github.yml @@ -0,0 +1,5 @@ +org: aquatest +repo: github-fetcher-test-repo +resource: prs +labels: + - ignore diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-invalid-structure.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-invalid-structure.yml new file mode 100644 index 00000000..32736f12 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-invalid-structure.yml @@ -0,0 +1,3 @@ +org: aquatest +repo: github-fetcher-test-repo +resuorce: prs diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-invalid-values.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-invalid-values.yml new file mode 100644 index 00000000..a7dc30c7 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/git-fetcher-config-invalid-values.yml @@ -0,0 +1,5 @@ +org: aquatest +repo: '' +resource: ' ' +labels: + - ignore diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/hash-filter/git-fetcher-config-valid-start-and-end-hash.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/hash-filter/git-fetcher-config-valid-start-and-end-hash.yml new file mode 100644 index 00000000..055ad1ce --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/hash-filter/git-fetcher-config-valid-start-and-end-hash.yml @@ -0,0 +1,6 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + startHash: c11631a0ddccb9579feae43b949b53c369528f43 + endHash: a71631a0dcccb957afeae43b949b53c369528f4f diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/hash-filter/git-fetcher-config-valid-start-hash.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/hash-filter/git-fetcher-config-valid-start-hash.yml new file mode 100644 index 00000000..a4253a3d --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/hash-filter/git-fetcher-config-valid-start-hash.yml @@ -0,0 +1,5 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + startHash: c11631a0ddccb9579feae43b949b53c369528f43 diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/hash-filter/git-fetcher-config-valid-state-and-hash.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/hash-filter/git-fetcher-config-valid-state-and-hash.yml new file mode 100644 index 00000000..27300177 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/hash-filter/git-fetcher-config-valid-state-and-hash.yml @@ -0,0 +1,7 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + state: MERGED + startHash: c11631a0ddccb9579feae43b949b53c369528f43 + endHash: a71631a0dcccb957afeae43b949b53c369528f4f diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/state-filter/git-fetcher-config-bitbucket-ALL.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/state-filter/git-fetcher-config-bitbucket-ALL.yml new file mode 100644 index 00000000..7ab40eda --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/state-filter/git-fetcher-config-bitbucket-ALL.yml @@ -0,0 +1,5 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + state: ALL diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/state-filter/git-fetcher-config-bitbucket-DECLINED.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/state-filter/git-fetcher-config-bitbucket-DECLINED.yml new file mode 100644 index 00000000..6692a62c --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/state-filter/git-fetcher-config-bitbucket-DECLINED.yml @@ -0,0 +1,5 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + state: DECLINED diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/state-filter/git-fetcher-config-bitbucket-INVALID.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/state-filter/git-fetcher-config-bitbucket-INVALID.yml new file mode 100644 index 00000000..98702414 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/state-filter/git-fetcher-config-bitbucket-INVALID.yml @@ -0,0 +1,5 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + state: INVALID_STATE diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/state-filter/git-fetcher-config-bitbucket-MERGED.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/state-filter/git-fetcher-config-bitbucket-MERGED.yml new file mode 100644 index 00000000..1baef384 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/state-filter/git-fetcher-config-bitbucket-MERGED.yml @@ -0,0 +1,5 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + state: MERGED diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/state-filter/git-fetcher-config-bitbucket-OPEN.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/state-filter/git-fetcher-config-bitbucket-OPEN.yml new file mode 100644 index 00000000..65c143bb --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/state-filter/git-fetcher-config-bitbucket-OPEN.yml @@ -0,0 +1,5 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + state: OPEN diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/tag-filter/git-fetcher-config-valid-start-and-end-tag.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/tag-filter/git-fetcher-config-valid-start-and-end-tag.yml new file mode 100644 index 00000000..d00dcc7b --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/tag-filter/git-fetcher-config-valid-start-and-end-tag.yml @@ -0,0 +1,6 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + startTag: tag1 + endTag: tag2 diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/tag-filter/git-fetcher-config-valid-start-tag.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/tag-filter/git-fetcher-config-valid-start-tag.yml new file mode 100644 index 00000000..a6d8331b --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/tag-filter/git-fetcher-config-valid-start-tag.yml @@ -0,0 +1,5 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + startTag: tag1 diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/tag-filter/git-fetcher-config-valid-state-and-tag.yml b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/tag-filter/git-fetcher-config-valid-state-and-tag.yml new file mode 100644 index 00000000..4bfbeabe --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/configs/tag-filter/git-fetcher-config-valid-state-and-tag.yml @@ -0,0 +1,7 @@ +org: aquatest +repo: bitbucket-fetcher-test-repo +resource: prs +filter: + startTag: tag1 + endTag: tag2 + state: OPEN diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/fixtures/getBitbucketCommitsMetadataAndDiffMockServerResponse.ts b/yaku-apps-typescript/apps/git-fetcher/test/integration/fixtures/getBitbucketCommitsMetadataAndDiffMockServerResponse.ts new file mode 100644 index 00000000..776676e4 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/fixtures/getBitbucketCommitsMetadataAndDiffMockServerResponse.ts @@ -0,0 +1,265 @@ +import { MockServerOptions } from '../../../../../integration-tests/src/util' + +export const bitbucketCommitsMetadataEndpoint = + '/projects/aquatest/repos/bitbucket-fetcher-test-repo/commits' + +export const bitbucketDiffEndpoint = + '/projects/aquatest/repos/bitbucket-fetcher-test-repo/diff/Somefolder/something.py' + +export function getGitCommitsMetadataAndDiffMockServerResponse( + port: number +): MockServerOptions { + return { + port, + https: true, + responses: { + [bitbucketCommitsMetadataEndpoint]: { + get: [ + { + responseStatus: 200, + responseBody: { + values: [ + { + id: 'f5d0053ff3879c01edfd268e4d88e46747e99370', + displayId: 'f5d0053ff38', + author: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + authorTimestamp: 1690382590000, + committer: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + committerTimestamp: 1690382590000, + message: 'commit for integration tests 1', + parents: [ + { + id: 'ba414b6a0eede7338bd0a971b0c0b6076342e7a4', + displayId: 'ba414b6a0ee', + }, + ], + }, + { + id: 'ba414b6a0eede7338bd0a971b0c0b6076342e7a4', + displayId: 'ba414b6a0ee', + author: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + authorTimestamp: 1690367674000, + committer: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + committerTimestamp: 1690367674000, + message: 'add new commit for integration tests', + parents: [ + { + id: 'b94be7709aecbb65d5cd69f760c61a8fe740eda4', + displayId: 'b94be7709ae', + }, + ], + }, + ], + size: 2, + isLastPage: false, + start: 0, + limit: 2, + nextPageStart: 2, + }, + }, + { + responseStatus: 200, + responseBody: { + values: [ + { + id: 'b94be7709aecbb65d5cd69f760c61a8fe740eda4', + displayId: 'b94be7709ae', + author: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + authorTimestamp: 1690367578000, + committer: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + committerTimestamp: 1690367578000, + message: 'modify file for integration tests', + parents: [ + { + id: '19e27ca09fb986d1810b531dfca18dbfc927f906', + displayId: '19e27ca09fb', + }, + ], + }, + { + id: '19e27ca09fb986d1810b531dfca18dbfc927f906', + displayId: '19e27ca09fb', + author: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + authorTimestamp: 1688124368000, + committer: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + committerTimestamp: 1688124368000, + message: 'modify file to test api functionality', + parents: [ + { + id: '2844c40eea52eb6868679415e017e16a1c4d5a31', + displayId: '2844c40eea5', + }, + ], + }, + ], + size: 2, + isLastPage: false, + start: 2, + limit: 2, + nextPageStart: 4, + }, + }, + { + responseStatus: 200, + responseBody: { + values: [ + { + id: '2844c40eea52eb6868679415e017e16a1c4d5a31', + displayId: '2844c40eea5', + author: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + authorTimestamp: 1688046420000, + committer: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + committerTimestamp: 1688046420000, + message: 'add changes to test the api', + parents: [ + { + id: '35cc5eec543e69aed90503f21cf12666bcbfda4f', + displayId: '35cc5eec543', + }, + ], + }, + ], + size: 1, + isLastPage: true, + start: 4, + limit: 2, + nextPageStart: null, + }, + }, + ], + }, + [bitbucketDiffEndpoint]: { + get: [ + { + responseStatus: 200, + responseBody: { + fromHash: '35cc5eec543e69aed90503f21cf12666bcbfda4f', + toHash: 'master', + contextLines: 10, + whitespace: 'SHOW', + diffs: [ + { + source: { + components: ['Some folder', 'something.py'], + parent: 'Some folder', + name: 'something.py', + extension: 'py', + toString: 'Some folder/something.py', + }, + destination: { + components: ['Some folder', 'something.py'], + parent: 'Some folder', + name: 'something.py', + extension: 'py', + toString: 'Some folder/something.py', + }, + hunks: [ + { + sourceLine: 1, + sourceSpan: 1, + destinationLine: 1, + destinationSpan: 4, + segments: [ + { + type: 'REMOVED', + lines: [ + { + source: 1, + destination: 1, + line: 'print("Hello world!")', + truncated: false, + }, + ], + truncated: false, + }, + { + type: 'ADDED', + lines: [ + { + source: 2, + destination: 1, + line: 'print("Hello world! + some changes")', + truncated: false, + }, + { + source: 2, + destination: 2, + line: 'print("another line to test")', + truncated: false, + }, + { + source: 2, + destination: 3, + line: 'print("delete previous line and addd this one for integration tests")', + truncated: false, + }, + { + source: 2, + destination: 4, + line: 'print("another line")', + truncated: false, + }, + ], + truncated: false, + }, + ], + truncated: false, + }, + ], + truncated: false, + }, + ], + truncated: false, + }, + }, + ], + }, + }, + } +} + +export function getGitCommitsMetadataAndDiffErrorMockServerResponse( + port: number +): MockServerOptions { + return { + port, + https: true, + responses: { + [bitbucketCommitsMetadataEndpoint]: { + get: { + responseStatus: 404, + }, + }, + }, + } +} diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/fixtures/getBitbucketResponseOptions.ts b/yaku-apps-typescript/apps/git-fetcher/test/integration/fixtures/getBitbucketResponseOptions.ts new file mode 100644 index 00000000..4a93e390 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/fixtures/getBitbucketResponseOptions.ts @@ -0,0 +1,138 @@ +import { MockServerOptions } from '../../../../../integration-tests/src/util' +import { BitbucketCommit } from '../../../src/model/bitbucket-commit' +import { BitbucketPr } from '../../../src/model/bitbucket-pr' +import { BitbucketTag } from '../../../src/model/bitbucket-tag' + +export const PULL_REQUESTS_ENDPOINT = + '/projects/aquatest/repos/bitbucket-fetcher-test-repo/pull-requests' + +export function requestUrlCommit(commitHash: string): string { + return `/projects/aquatest/repos/bitbucket-fetcher-test-repo/commits/${commitHash}` +} + +export function requestUrlTag(tagName: string): string { + return `/projects/aquatest/repos/bitbucket-fetcher-test-repo/tags/${tagName}` +} + +export function createBitbucketCommits( + startDate: Date, + endDate: Date +): [BitbucketCommit, BitbucketCommit] { + return [ + { + id: 'c11631a0ddccb9579feae43b949b53c369528f43', + committerTimestamp: startDate.getTime(), + }, + { + id: 'a71631a0dcccb957afeae43b949b53c369528f4f', + committerTimestamp: endDate.getTime(), + }, + ] +} + +export function createBitbucketTags( + commits: [BitbucketCommit, BitbucketCommit] +): [BitbucketTag, BitbucketTag] { + return [ + { + id: 'refs/tags/tag1', + displayId: 'tag1', + type: 'TAG', + latestCommit: commits[0].id, + latestChangeset: commits[0].id, + hash: 'a13dbf0d42971adfc592103941cc7898652f3cbb', + }, + { + id: 'refs/tags/tag2', + displayId: 'tag2', + type: 'TAG', + latestCommit: commits[1].id, + latestChangeset: commits[1].id, + hash: 'd1994e10bd134e7b1cf25213d3efcb0004226e6f', + }, + ] +} + +export const bitBucketPrs: readonly BitbucketPr[] = [ + { + id: 1, + state: 'OPEN', + updatedDate: 1580515200000, // 01-02-2020 + }, + { + id: 2, + state: 'MERGED', + updatedDate: 1609459200000, // 01-01-2021 + }, + { + id: 3, + state: 'OPEN', + updatedDate: 1651269600000, // 30-04-2022 + }, + { + id: 4, + state: 'DECLINED', + updatedDate: 1678838400000, // 15-03-2023 + }, +] as const + +export type BitbucketMockConfig = { + port: number + pullRequestResponses: BitbucketPr[] | readonly BitbucketPr[] + commitResponses?: BitbucketCommit[] + tagResponses?: BitbucketTag[] +} + +export function getBitbucketResponseOptions( + config: BitbucketMockConfig +): MockServerOptions { + if (config.pullRequestResponses.length > 25) { + throw new Error('The number of pull requests must not be greater than 25.') + } + let response = { + [PULL_REQUESTS_ENDPOINT]: { + get: { + responseStatus: 200, + responseBody: { + isLastPage: true, + limit: 25, + size: config.pullRequestResponses.length, + start: 0, + values: config.pullRequestResponses, + }, + }, + }, + } + + const commits: BitbucketCommit[] = config.commitResponses ?? [] + commits.forEach((commit) => { + response = { + ...response, + [`/projects/aquatest/repos/bitbucket-fetcher-test-repo/commits/${commit.id}`]: + { + get: { + responseStatus: 200, + responseBody: commit, + }, + }, + } + }) + + const tags: BitbucketTag[] = config.tagResponses ?? [] + tags.forEach((tag) => { + response = { + ...response, + [requestUrlTag(tag.displayId)]: { + get: { + responseStatus: 200, + responseBody: tag, + }, + }, + } + }) + return { + port: config.port, + https: true, + responses: response, + } +} diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/fixtures/getGitBranchesMockServerResponse.ts b/yaku-apps-typescript/apps/git-fetcher/test/integration/fixtures/getGitBranchesMockServerResponse.ts new file mode 100644 index 00000000..78f8e7f3 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/fixtures/getGitBranchesMockServerResponse.ts @@ -0,0 +1,110 @@ +import { MockServerOptions } from '../../../../../integration-tests/src/util' + +export const bitbucketBranchesEndpoint = + '/projects/aquatest/repos/bitbucket-fetcher-test-repo/branches' + +export function getGitBranchesMockServerResponse( + port: number +): MockServerOptions { + return { + port, + https: true, + responses: { + [bitbucketBranchesEndpoint]: { + get: [ + { + responseStatus: 200, + responseBody: { + size: 2, + limit: 2, + start: 0, + isLastPage: false, + nextPageStart: 2, + values: [ + { + id: 'refs/heads/E2E_Test-150', + displayId: 'E2E_Test-150', + type: 'BRANCH', + latestCommit: '5dda2f9cc6abcb79d8675c20d8ebdab64099362c', + latestChangeset: '5dda2f9cc6abcb79d8675c20d8ebdab64099362c', + isDefault: false, + }, + { + id: 'refs/heads/E2E_Test-149', + displayId: 'E2E_Test-149', + type: 'BRANCH', + latestCommit: 'c11611a0dcccb9579aeae43b949b53cd69528f43', + latestChangeset: 'c11611a0dcccb9579aeae43b949b53cd69528f43', + isDefault: false, + }, + ], + }, + }, + { + responseStatus: 200, + responseBody: { + size: 2, + limit: 2, + start: 2, + isLastPage: false, + nextPageStart: 4, + values: [ + { + id: 'refs/heads/E2E_Test-148', + displayId: 'E2E_Test-148', + type: 'BRANCH', + latestCommit: '9506c496cf61143c89ce0a3c0ba86bb1596699bb', + latestChangeset: '9506c496cf61143c89ce0a3c0ba86bb1596699bb', + isDefault: false, + }, + { + id: 'refs/heads/main', + displayId: 'main', + type: 'BRANCH', + latestCommit: '7a9b95c4868e3c6c5633c7959073e95fbe7262cf', + latestChangeset: '7a9b95c4868e3c6c5633c7959073e95fbe7262cf', + isDefault: true, + }, + ], + }, + }, + { + responseStatus: 200, + responseBody: { + size: 1, + limit: 2, + start: 4, + isLastPage: true, + values: [ + { + id: 'refs/heads/E2E_Test-147', + displayId: 'E2E_Test-147', + type: 'BRANCH', + latestCommit: '3499c02a0fc8a4133b84610a1d85f93e7c848bcf', + latestChangeset: '3499c02a0fc8a4133b84610a1d85f93e7c848bcf', + isDefault: false, + }, + ], + }, + }, + ], + }, + }, + } +} + +export function getGitBranchesErrorMockServerResponse( + port: number +): MockServerOptions { + return { + port, + https: true, + responses: { + [bitbucketBranchesEndpoint]: { + get: { + responseStatus: 404, + }, + }, + }, + } +} diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/fixtures/getGitPullRequestsMockServerResponse.ts b/yaku-apps-typescript/apps/git-fetcher/test/integration/fixtures/getGitPullRequestsMockServerResponse.ts new file mode 100644 index 00000000..cbe6adea --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/fixtures/getGitPullRequestsMockServerResponse.ts @@ -0,0 +1,60 @@ +import { MockServerOptions } from '../../../../../integration-tests/src/util' + +export function getGitPullRequestsMockOptions( + port: number, + responseStatus: number +): MockServerOptions { + return { + port: port, + https: true, + responses: { + [`/projects/aquatest/repos/bitbucket-fetcher-test-repo/pull-requests`]: { + get: { + responseStatus: responseStatus, + responseBody: { + isLastPage: true, + limit: 25, + size: 2, + start: 0, + values: [ + { + id: 1, + title: 'foo 1', + }, + { + id: 2, + title: 'foo 2', + }, + ], + }, + }, + }, + [`/repos/aquatest/github-fetcher-test-repo/pulls`]: { + get: [ + { + responseStatus: responseStatus, + responseBody: [ + { + id: 1, + title: 'Dummy PR', + state: 'open', + labels: [ + { + id: 1, + url: 'www.foo.bar', + name: 'ignore', + default: false, + }, + ], + }, + ], + }, + { + responseStatus: responseStatus, + responseBody: [], + }, + ], + }, + }, + } +} diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/fixtures/getGitTagsSuccessMockServerResponse.ts b/yaku-apps-typescript/apps/git-fetcher/test/integration/fixtures/getGitTagsSuccessMockServerResponse.ts new file mode 100644 index 00000000..42d2e36d --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/fixtures/getGitTagsSuccessMockServerResponse.ts @@ -0,0 +1,110 @@ +import { MockServerOptions } from '../../../../../integration-tests/src/util' + +export const bitbucketTagsEndpoint = + '/projects/aquatest/repos/bitbucket-fetcher-test-repo/tags' + +export function getGitTagsSuccessMockServerResponse( + port: number +): MockServerOptions { + return { + port, + https: true, + responses: { + [bitbucketTagsEndpoint]: { + get: [ + { + responseStatus: 200, + responseBody: { + size: 2, + limit: 2, + start: 0, + isLastPage: false, + nextPageStart: 2, + values: [ + { + id: 'refs/tags/E2E_Test-150', + displayId: 'E2E_Test-150', + type: 'TAG', + latestCommit: '5dda2f9cc6abcb79d8675c20d8ebdab64099362c', + latestChangeset: '5dda2f9cc6abcb79d8675c20d8ebdab64099362c', + hash: 'a13dbf0d42971adfc592103941cc7898652f3cbb', + }, + { + id: 'refs/tags/E2E_Test-149', + displayId: 'E2E_Test-149', + type: 'TAG', + latestCommit: '68521d211c5e38c27381718149014c5ec20b1f8e', + latestChangeset: '68521d211c5e38c27381718149014c5ec20b1f8e', + hash: 'd1994e10bd134e7b1cf25213d3efcb0004226e6f', + }, + ], + }, + }, + { + responseStatus: 200, + responseBody: { + size: 2, + limit: 2, + start: 2, + isLastPage: false, + nextPageStart: 4, + values: [ + { + id: 'refs/tags/E2E_Test-148', + displayId: 'E2E_Test-148', + type: 'TAG', + latestCommit: '9506c496cf61143c89ce0a3c0ba86bb1596699bb', + latestChangeset: '9506c496cf61143c89ce0a3c0ba86bb1596699bb', + hash: '0efcb08a881c07bf2abaebd3858e01f8f0133628', + }, + { + id: 'refs/tags/E2E_Test-147', + displayId: 'E2E_Test-147', + type: 'TAG', + latestCommit: '3499c02a0fc8a4133b84610a1d85f93e7c848bcf', + latestChangeset: '3499c02a0fc8a4133b84610a1d85f93e7c848bcf', + hash: '36aa763314d8e8bdaf35a63a676dd5742f9e93bd', + }, + ], + }, + }, + { + responseStatus: 200, + responseBody: { + size: 1, + limit: 2, + start: 4, + isLastPage: true, + values: [ + { + id: 'refs/tags/other', + displayId: 'other', + type: 'TAG', + latestCommit: '2ff330ca717c86365505fa3b21cd81559c288274', + latestChangeset: '2ff330ca717c86365505fa3b21cd81559c288274', + hash: 'da27dfc61b069c6f0d91e107e166d0e09d7e1a03', + }, + ], + }, + }, + ], + }, + }, + } +} + +export function getGitTagsErrorMockServerResponse( + port: number +): MockServerOptions { + return { + port, + https: true, + responses: { + [bitbucketTagsEndpoint]: { + get: { + responseStatus: 404, + }, + }, + }, + } +} diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/fixtures/getGithubCommitsMetadataAndDiffMockServerResponse.ts b/yaku-apps-typescript/apps/git-fetcher/test/integration/fixtures/getGithubCommitsMetadataAndDiffMockServerResponse.ts new file mode 100644 index 00000000..eaa926fd --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/fixtures/getGithubCommitsMetadataAndDiffMockServerResponse.ts @@ -0,0 +1,1239 @@ +import { MockServerOptions } from '../../../../../integration-tests/src/util' + +export const githubStartCommitEndpoint = + '/repos/aquatest/github-fetcher-test-repo/commits/afeaebf412c6d0b865a36cfdec37fdb46c0fab63' + +export const githubEndCommitEndpoint = + '/repos/aquatest/github-fetcher-test-repo/commits/8036cf75f4b7365efea76cbd716ef12d352d7d29' + +export const githubDiffEndpoint = + '/repos/aquatest/github-fetcher-test-repo/compare/afeaebf412c6d0b865a36cfdec37fdb46c0fab63...8036cf75f4b7365efea76cbd716ef12d352d7d29' + +export const githubCommitsMetadataEndpoint = + '/repos/aquatest/github-fetcher-test-repo/commits' + +export function getGitCommitsMetadataAndDiffMockServerResponse( + port: number +): MockServerOptions { + return { + port, + https: true, + responses: { + [githubStartCommitEndpoint]: { + get: [ + { + responseStatus: 200, + responseBody: { + sha: 'afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + node_id: 'C_gnasfASGJKOGwakgawpARJWGJagja', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-03-06T14:11:29Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-03-06T14:11:29Z', + }, + message: + 'Add git fetcher app\n\nSigned-off-by: Tech User ', + tree: { + sha: '347a2c4dc61cb708a1067675fd6dbc1f0ea74608', + url: 'https://example.com/qg-apps-typescript/git/trees/347a2c4dc61cb708a1067675fd6dbc1f0ea74608', + }, + url: 'https://example.com/qg-apps-typescript/git/commits/afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://example.com/qg-apps-typescript/commits/afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + html_url: + 'https:/example.com/qg-apps-typescript/commit/afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + comments_url: + 'https://example.com/qg-apps-typescript/commits/afeaebf412c6d0b865a36cfdec37fdb46c0fab63/comments', + author: { + login: 'users/TxxxUxxGxxx', + id: 12345678, + node_id: 'sgsGASGJKASLJKGALWSJGag', + avatar_url: + 'https://avatars.githubusercontent.com/u/x12345678?v=4', + gravatar_id: '', + url: 'https://example.com/users/TxxxUxxGxxx', + html_url: 'https://github.com/users/TxxxUxxGxxx', + followers_url: + 'https://example.com/users/TxxxUxxGxxx/followers', + following_url: + 'https://example.com/users/TxxxUxxGxxx/following{/other_user}', + gists_url: + 'https://example.com/users/TxxxUxxGxxx/gists{/gist_id}', + starred_url: + 'https://example.com/users/TxxxUxxGxxx/starred{/owner}{/repo}', + subscriptions_url: + 'https://example.com/users/TxxxUxxGxxx/subscriptions', + organizations_url: 'https://example.com/users/TxxxUxxGxxx/orgs', + repos_url: 'https://example.com/users/TxxxUxxGxxx/repos', + events_url: + 'https://example.com/users/TxxxUxxGxxx/events{/privacy}', + received_events_url: + 'https://example.com/users/TxxxUxxGxxx/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'users/TxxxUxxGxxx', + id: 12345678, + node_id: 'sgsGASGJKASLJKGALWSJGag', + avatar_url: + 'https://avatars.githubusercontent.com/u/12345678?v=4', + gravatar_id: '', + url: 'https://example.com/users/TxxxUxxGxxx', + html_url: 'https://github.com/users/TxxxUxxGxxx', + followers_url: + 'https://example.com/users/TxxxUxxGxxx/followers', + following_url: + 'https://example.com/users/TxxxUxxGxxx/following{/other_user}', + gists_url: + 'https://example.com/users/TxxxUxxGxxx/gists{/gist_id}', + starred_url: + 'https://example.com/users/TxxxUxxGxxx/starred{/owner}{/repo}', + subscriptions_url: + 'https://example.com/users/TxxxUxxGxxx/subscriptions', + organizations_url: 'https://example.com/users/TxxxUxxGxxx/orgs', + repos_url: 'https://example.com/users/TxxxUxxGxxx/repos', + events_url: + 'https://example.com/users/TxxxUxxGxxx/events{/privacy}', + received_events_url: + 'https://example.com/users/TxxxUxxGxxx/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: '097079eba1e7749d6d86d324fa530f8e89e55595', + url: 'https://example.com/qg-apps-typescript/commits/097079eba1e7749d6d86d324fa530f8e89e55595', + html_url: + 'https:/example.com/qg-apps-typescript/commit/097079eba1e7749d6d86d324fa530f8e89e55595', + }, + ], + stats: { + total: 472, + additions: 472, + deletions: 0, + }, + files: [ + { + sha: '1715371c6198185606321fdc46678ea3c9c8e0fe', + filename: 'apps/git-fetcher/.env.sample', + status: 'added', + additions: 5, + deletions: 0, + changes: 5, + blob_url: + 'https:/example.com/qg-apps-typescript/blob/afeaebf412c6d0b865a36cfdec37fdb46c0fab63/apps%2Fgit-fetcher%2F.env.sample', + raw_url: + 'https:/example.com/qg-apps-typescript/raw/afeaebf412c6d0b865a36cfdec37fdb46c0fab63/apps%2Fgit-fetcher%2F.env.sample', + contents_url: + 'https://example.com/qg-apps-typescript/contents/apps%2Fgit-fetcher%2F.env.sample?ref=afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + patch: + '@@ -0,0 +1,5 @@\n+export GIT_FETCHER_SERVER_API_URL=\n+export GIT_FETCHER_SERVER_AUTH_METHOD=\n+export GIT_FETCHER_SERVER_TYPE=\n+export GIT_FETCHER_API_TOKEN=\n+export GIT_FETCHER_OUTPUT_FILE_PATH=', + }, + { + sha: '0b64f27e7472ca024519e443cafc398cd51436a9', + filename: 'apps/git-fetcher/.eslintrc.cjs', + status: 'added', + additions: 5, + deletions: 0, + changes: 5, + blob_url: + 'https:/example.com/qg-apps-typescript/blob/afeaebf412c6d0b865a36cfdec37fdb46c0fab63/apps%2Fgit-fetcher%2F.eslintrc.cjs', + raw_url: + 'https:/example.com/qg-apps-typescript/raw/afeaebf412c6d0b865a36cfdec37fdb46c0fab63/apps%2Fgit-fetcher%2F.eslintrc.cjs', + contents_url: + 'https://example.com/qg-apps-typescript/contents/apps%2Fgit-fetcher%2F.eslintrc.cjs?ref=afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + patch: + '@@ -0,0 +1,5 @@\n+/**\n+ * Copyright (c) 2022, 2023 by grow platform GmbH \n+ */\n+\n+module.exports = require("@B-S-F/eslint-config/eslint-preset");\n\\ No newline at end of file', + }, + ], + }, + }, + ], + }, + [githubEndCommitEndpoint]: { + get: [ + { + responseStatus: 200, + responseBody: { + sha: '8036cf75f4b7365efea76cbd716ef12d352d7d29', + node_id: + 'C_kwDOJEBj4toAKDgwMzZjZjc1ZjRiNzM2NWVmZWE3NmNiZDcxNmVmMTJkMzUyZDdkMjk', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-07-12T10:46:50Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-07-12T10:46:50Z', + }, + message: + 'add commits and diff retrieval of target file for both bitbucket and github', + tree: { + sha: '9450ecc9597185ad82f9c9b61df5337f5ad4a286', + url: 'https://example.com/qg-apps-typescript/git/trees/9450ecc9597185ad82f9c9b61df5337f5ad4a286', + }, + url: 'https://example.com/qg-apps-typescript/git/commits/8036cf75f4b7365efea76cbd716ef12d352d7d29', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://example.com/qg-apps-typescript/commits/8036cf75f4b7365efea76cbd716ef12d352d7d29', + html_url: + 'https:/example.com/qg-apps-typescript/commit/8036cf75f4b7365efea76cbd716ef12d352d7d29', + comments_url: + 'https://example.com/qg-apps-typescript/commits/8036cf75f4b7365efea76cbd716ef12d352d7d29/comments', + author: { + login: 'users/TxxxUxxGxxx', + id: 123456789132, + node_id: 'ASBNSABOsg6a54f', + avatar_url: + 'https://avatars.githubusercontent.com/u/123456789132?v=4', + gravatar_id: '', + url: 'https://example.com/users/TxxxUxxGxxx', + html_url: 'https://github.com/users/TxxxUxxGxxx', + followers_url: + 'https://example.com/users/TxxxUxxGxxx/followers', + following_url: + 'https://example.com/users/TxxxUxxGxxx/following{/other_user}', + gists_url: + 'https://example.com/users/TxxxUxxGxxx/gists{/gist_id}', + starred_url: + 'https://example.com/users/TxxxUxxGxxx/starred{/owner}{/repo}', + subscriptions_url: + 'https://example.com/users/TxxxUxxGxxx/subscriptions', + organizations_url: 'https://example.com/users/TxxxUxxGxxx/orgs', + repos_url: 'https://example.com/users/TxxxUxxGxxx/repos', + events_url: + 'https://example.com/users/TxxxUxxGxxx/events{/privacy}', + received_events_url: + 'https://example.com/users/TxxxUxxGxxx/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'users/TxxxUxxGxxx', + id: 987654321, + node_id: 'XASHSDGASFGasfafs', + avatar_url: + 'https://avatars.githubusercontent.com/u/1236549878945?v=4', + gravatar_id: '', + url: 'https://example.com/users/TxxxUxxGxxx', + html_url: 'https://example.com/users/TxxxUxxGxxx', + followers_url: + 'https://example.com/users/TxxxUxxGxxx/followers', + following_url: + 'https://example.com/users/TxxxUxxGxxx/following{/other_user}', + gists_url: + 'https://example.com/users/TxxxUxxGxxx/gists{/gist_id}', + starred_url: + 'https://example.com/users/TxxxUxxGxxx/starred{/owner}{/repo}', + subscriptions_url: + 'https://example.com/users/TxxxUxxGxxx/subscriptions', + organizations_url: 'https://example.com/users/TxxxUxxGxxx/orgs', + repos_url: 'https://example.com/users/TxxxUxxGxxx/repos', + events_url: + 'https://example.com/users/TxxxUxxGxxx/events{/privacy}', + received_events_url: + 'https://example.com/users/TxxxUxxGxxx/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: 'dc7e626d6da5b0b4911a1b11f0d5dcf6009f827f', + url: 'https://example.com/qg-apps-typescript/commits/dc7e626d6da5b0b4911a1b11f0d5dcf6009f827f', + html_url: + 'https:/example.com/qg-apps-typescript/commit/dc7e626d6da5b0b4911a1b11f0d5dcf6009f827f', + }, + ], + stats: { + total: 210, + additions: 208, + deletions: 2, + }, + files: [ + { + sha: '3de0f6888d7832a14dcf25eb6f080f955825a8de', + filename: + 'apps/git-fetcher/src/fetchers/generate-git-fetcher.ts', + status: 'modified', + additions: 14, + deletions: 0, + changes: 14, + blob_url: + 'https:/example.com/qg-apps-typescript/blob/8036cf75f4b7365efea76cbd716ef12d352d7d29/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgenerate-git-fetcher.ts', + raw_url: + 'https:/example.com/qg-apps-typescript/raw/8036cf75f4b7365efea76cbd716ef12d352d7d29/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgenerate-git-fetcher.ts', + contents_url: + 'https://example.com/qg-apps-typescript/contents/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgenerate-git-fetcher.ts?ref=8036cf75f4b7365efea76cbd716ef12d352d7d29', + patch: + "@@ -1,6 +1,7 @@\n import { BitbucketPr } from '../model/bitbucket-pr.js'\n import { BitbucketBranch } from '../model/bitbucket-branch'\n import { BitbucketTag } from '../model/bitbucket-tag'\n+import { CommitsMetadataAndDiff } from '../model/bitbucket-commits-metadata-and-diff.js'\n import {\n ConfigFileData,\n GitConfigResource,\n@@ -15,12 +16,15 @@ import { GitFetcher } from './git-fetcher'\n import { GitFetcherBitbucketPrs } from './git-fetcher-bitbucket-prs.js'\n import { GitFetcherBitbucketTagsAndBranches } from './git-fetcher-bitbucket-tags-and-branches.js'\n import { GitFetcherGithubPrs } from './git-fetcher-github-prs.js'\n+import { GitFetcherBitbucketCommitsAndDiff } from './git-fetcher-bitbucket-commits-and-diff.js'\n+import { GitFetcherGithubCommitsAndDiff } from './git-fetcher-github-commits-and-diff.js'\n \n export type GitResource =\n | GithubPr\n | BitbucketPr\n | BitbucketBranch\n | BitbucketTag\n+ | CommitsMetadataAndDiff\n \n export function generateGitFetcher(\n gitServerConfig: GitServerConfig,\n@@ -36,6 +40,11 @@ export function generateGitFetcher(\n pullRequestAliases.includes(gitConfigResource)\n ) {\n return new GitFetcherGithubPrs(gitServerConfig, configFileData)\n+ } else if (\n+ gitServerType === 'github' &&\n+ gitConfigResource === 'metadata-and-diff'\n+ ) {\n+ return new GitFetcherGithubCommitsAndDiff(gitServerConfig, configFileData)\n } else if (gitServerType === 'bitbucket') {\n if (gitConfigResource === 'branches' || gitConfigResource === 'tags') {\n return new GitFetcherBitbucketTagsAndBranches(\n@@ -45,6 +54,11 @@ export function generateGitFetcher(\n )\n } else if (pullRequestAliases.includes(gitConfigResource)) {\n return new GitFetcherBitbucketPrs(gitServerConfig, configFileData)\n+ } else if (gitConfigResource === 'metadata-and-diff') {\n+ return new GitFetcherBitbucketCommitsAndDiff(\n+ gitServerConfig,\n+ configFileData\n+ )\n }\n }\n ", + }, + { + sha: '6de16689e463732b398bd9c291ca797450216b73', + filename: + 'apps/git-fetcher/src/fetchers/git-fetcher-bitbucket-commits-and-diff.ts', + status: 'added', + additions: 72, + deletions: 0, + changes: 72, + blob_url: + 'https:/example.com/qg-apps-typescript/blob/8036cf75f4b7365efea76cbd716ef12d352d7d29/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher-bitbucket-commits-and-diff.ts', + raw_url: + 'https:/example.com/qg-apps-typescript/raw/8036cf75f4b7365efea76cbd716ef12d352d7d29/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher-bitbucket-commits-and-diff.ts', + contents_url: + 'https://example.com/qg-apps-typescript/contents/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher-bitbucket-commits-and-diff.ts?ref=8036cf75f4b7365efea76cbd716ef12d352d7d29', + patch: + "@@ -0,0 +1,72 @@\n+import { CommitsMetadataAndDiff } from '../model/bitbucket-commits-metadata-and-diff'\n+import { ConfigFileData } from '../model/config-file-data'\n+import { GitServerConfig } from '../model/git-server-config'\n+import { GitFetcher } from './git-fetcher'\n+import { getRequestOptions } from './utils/get-request-options.js'\n+\n+export class GitFetcherBitbucketCommitsAndDiff\n+ implements GitFetcher\n+{\n+ constructor(\n+ private readonly gitServerConfig: GitServerConfig,\n+ private readonly config: ConfigFileData\n+ ) {}\n+\n+ public async fetchResource(): Promise {\n+ const requestOptions: RequestInit = await getRequestOptions(\n+ this.gitServerConfig\n+ )\n+ let result: CommitsMetadataAndDiff = {\n+ commitsMetadata: [],\n+ diff: [],\n+ }\n+ try {\n+ const serverName = this.gitServerConfig.gitServerApiUrl\n+ const projectKey = this.config.data.org\n+ const repositorySlug = this.config.data.repo\n+ const filePath = this.config.data.filePath\n+ const startHash = this.config.data.filter?.startHash\n+ let endHash = this.config.data.filter?.endHash\n+ if (!filePath) {\n+ throw new Error(\n+ `Please define the 'filePath' parameter in the config and try again`\n+ )\n+ }\n+ if (!startHash) {\n+ throw new Error(\n+ `Please define the 'filter.startHash' parameter in the config and try again`\n+ )\n+ }\n+ if (!endHash) {\n+ endHash = 'master'\n+ }\n+ let startPage = 0\n+ let responseBody\n+ do {\n+ let commitsUrl = `${serverName}/projects/${projectKey}/repos/${repositorySlug}/commits?path=${filePath}&until=${endHash}&start=${startPage}`\n+ if (startHash) {\n+ commitsUrl = commitsUrl + `&since=${startHash}`\n+ }\n+ let response: Response = await fetch(commitsUrl, requestOptions)\n+ responseBody = await response.json()\n+ result.commitsMetadata = result.commitsMetadata.concat(\n+ responseBody.values\n+ )\n+ startPage = responseBody.nextPageStart ?? 0\n+ } while (!responseBody.isLastPage)\n+\n+ let diffUrl = `${serverName}/projects/${projectKey}/repos/${repositorySlug}/diff/${filePath}?&until=${endHash}`\n+\n+ if (startHash) {\n+ diffUrl = diffUrl + `&since=${startHash}`\n+ }\n+\n+ let response: Response = await fetch(diffUrl, requestOptions)\n+ responseBody = await response.json()\n+ result.diff = responseBody.diffs\n+ } catch (error: any) {\n+ throw new Error(`bitbucket commits and diffs: ${error.message}`)\n+ }\n+ return result\n+ }\n+}", + }, + ], + }, + }, + ], + }, + [githubDiffEndpoint]: { + get: [ + { + responseStatus: 200, + responseBody: { + url: 'https://example.com/qg-apps-typescript/compare/afeaebf412c6d0b865a36cfdec37fdb46c0fab63...8036cf75f4b7365efea76cbd716ef12d352d7d29', + html_url: + 'https:/example.com/qg-apps-typescript/compare/afeaebf412c6d0b865a36cfdec37fdb46c0fab63...8036cf75f4b7365efea76cbd716ef12d352d7d29', + permalink_url: + 'https:/example.com/qg-apps-typescript/compare/B-S-F:afeaebf...B-S-F:8036cf7', + diff_url: + 'https:/example.com/qg-apps-typescript/compare/afeaebf412c6d0b865a36cfdec37fdb46c0fab63...8036cf75f4b7365efea76cbd716ef12d352d7d29.diff', + patch_url: + 'https:/example.com/qg-apps-typescript/compare/afeaebf412c6d0b865a36cfdec37fdb46c0fab63...8036cf75f4b7365efea76cbd716ef12d352d7d29.patch', + base_commit: { + sha: 'afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + node_id: 'C_gnasfASGJKOGwakgawpARJWGJagja', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-03-06T14:11:29Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-03-06T14:11:29Z', + }, + message: + 'Add git fetcher app\n\nSigned-off-by: Tech User ', + tree: { + sha: '347a2c4dc61cb708a1067675fd6dbc1f0ea74608', + url: 'https://example.com/qg-apps-typescript/git/trees/347a2c4dc61cb708a1067675fd6dbc1f0ea74608', + }, + url: 'https://example.com/qg-apps-typescript/git/commits/afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://example.com/qg-apps-typescript/commits/afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + html_url: + 'https:/example.com/qg-apps-typescript/commit/afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + comments_url: + 'https://example.com/qg-apps-typescript/commits/afeaebf412c6d0b865a36cfdec37fdb46c0fab63/comments', + author: { + login: 'users/TxxxUxxGxxx', + id: 12345678, + node_id: 'sgsGASGJKASLJKGALWSJGag', + avatar_url: + 'https://avatars.githubusercontent.com/u/12345678?v=4', + gravatar_id: '', + url: 'https://example.com/users/TxxxUxxGxxx', + html_url: 'https://github.com/users/TxxxUxxGxxx', + followers_url: + 'https://example.com/users/TxxxUxxGxxx/followers', + following_url: + 'https://example.com/users/TxxxUxxGxxx/following{/other_user}', + gists_url: + 'https://example.com/users/TxxxUxxGxxx/gists{/gist_id}', + starred_url: + 'https://example.com/users/TxxxUxxGxxx/starred{/owner}{/repo}', + subscriptions_url: + 'https://example.com/users/TxxxUxxGxxx/subscriptions', + organizations_url: + 'https://example.com/users/TxxxUxxGxxx/orgs', + repos_url: 'https://example.com/users/TxxxUxxGxxx/repos', + events_url: + 'https://example.com/users/TxxxUxxGxxx/events{/privacy}', + received_events_url: + 'https://example.com/users/TxxxUxxGxxx/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'users/TxxxUxxGxxx', + id: 12345678, + node_id: 'sgsGASGJKASLJKGALWSJGag', + avatar_url: + 'https://avatars.githubusercontent.com/u/12345678?v=4', + gravatar_id: '', + url: 'https://example.com/users/TxxxUxxGxxx', + html_url: 'https://github.com/users/TxxxUxxGxxx', + followers_url: + 'https://example.com/users/TxxxUxxGxxx/followers', + following_url: + 'https://example.com/users/TxxxUxxGxxx/following{/other_user}', + gists_url: + 'https://example.com/users/TxxxUxxGxxx/gists{/gist_id}', + starred_url: + 'https://example.com/users/TxxxUxxGxxx/starred{/owner}{/repo}', + subscriptions_url: + 'https://example.com/users/TxxxUxxGxxx/subscriptions', + organizations_url: + 'https://example.com/users/TxxxUxxGxxx/orgs', + repos_url: 'https://example.com/users/TxxxUxxGxxx/repos', + events_url: + 'https://example.com/users/TxxxUxxGxxx/events{/privacy}', + received_events_url: + 'https://example.com/users/TxxxUxxGxxx/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: '097079eba1e7749d6d86d324fa530f8e89e55595', + url: 'https://example.com/qg-apps-typescript/commits/097079eba1e7749d6d86d324fa530f8e89e55595', + html_url: + 'https:/example.com/qg-apps-typescript/commit/097079eba1e7749d6d86d324fa530f8e89e55595', + }, + ], + }, + merge_base_commit: { + sha: 'afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + node_id: 'C_gnasfASGJKOGwakgawpARJWGJagja', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-03-06T14:11:29Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-03-06T14:11:29Z', + }, + message: + 'Add git fetcher app\n\nSigned-off-by: Tech User ', + tree: { + sha: '347a2c4dc61cb708a1067675fd6dbc1f0ea74608', + url: 'https://example.com/qg-apps-typescript/git/trees/347a2c4dc61cb708a1067675fd6dbc1f0ea74608', + }, + url: 'https://example.com/qg-apps-typescript/git/commits/afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://example.com/qg-apps-typescript/commits/afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + html_url: + 'https:/example.com/qg-apps-typescript/commit/afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + comments_url: + 'https://example.com/qg-apps-typescript/commits/afeaebf412c6d0b865a36cfdec37fdb46c0fab63/comments', + author: { + login: 'users/TxxxUxxGxxx', + id: 12345678, + node_id: 'sgsGASGJKASLJKGALWSJGag', + avatar_url: + 'https://avatars.githubusercontent.com/u/12345678?v=4', + gravatar_id: '', + url: 'https://example.com/users/TxxxUxxGxxx', + html_url: 'https://github.com/users/TxxxUxxGxxx', + followers_url: + 'https://example.com/users/TxxxUxxGxxx/followers', + following_url: + 'https://example.com/users/TxxxUxxGxxx/following{/other_user}', + gists_url: + 'https://example.com/users/TxxxUxxGxxx/gists{/gist_id}', + starred_url: + 'https://example.com/users/TxxxUxxGxxx/starred{/owner}{/repo}', + subscriptions_url: + 'https://example.com/users/TxxxUxxGxxx/subscriptions', + organizations_url: + 'https://example.com/users/TxxxUxxGxxx/orgs', + repos_url: 'https://example.com/users/TxxxUxxGxxx/repos', + events_url: + 'https://example.com/users/TxxxUxxGxxx/events{/privacy}', + received_events_url: + 'https://example.com/users/TxxxUxxGxxx/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'users/TxxxUxxGxxx', + id: 12345678, + node_id: 'sgsGASGJKASLJKGALWSJGag', + avatar_url: + 'https://avatars.githubusercontent.com/u/12345678?v=4', + gravatar_id: '', + url: 'https://example.com/users/TxxxUxxGxxx', + html_url: 'https://github.com/users/TxxxUxxGxxx', + followers_url: + 'https://example.com/users/TxxxUxxGxxx/followers', + following_url: + 'https://example.com/users/TxxxUxxGxxx/following{/other_user}', + gists_url: + 'https://example.com/users/TxxxUxxGxxx/gists{/gist_id}', + starred_url: + 'https://example.com/users/TxxxUxxGxxx/starred{/owner}{/repo}', + subscriptions_url: + 'https://example.com/users/TxxxUxxGxxx/subscriptions', + organizations_url: + 'https://example.com/users/TxxxUxxGxxx/orgs', + repos_url: 'https://example.com/users/TxxxUxxGxxx/repos', + events_url: + 'https://example.com/users/TxxxUxxGxxx/events{/privacy}', + received_events_url: + 'https://example.com/users/TxxxUxxGxxx/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: '097079eba1e7749d6d86d324fa530f8e89e55595', + url: 'https://example.com/qg-apps-typescript/commits/097079eba1e7749d6d86d324fa530f8e89e55595', + html_url: + 'https:/example.com/qg-apps-typescript/commit/097079eba1e7749d6d86d324fa530f8e89e55595', + }, + ], + }, + status: 'ahead', + ahead_by: 324, + behind_by: 0, + total_commits: 324, + commits: [ + { + sha: 'fbf45173f2c1fbf4f6f2439abba88946a2cc8360', + node_id: 'C_abcdefghIJKLMNOPQrstuvqABCDEFGHIJKLmnopqrstu123', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-03-06T14:15:43Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-03-06T14:15:43Z', + }, + message: + 'Add oneq-finalizer and git-fetcher to release workflow\n\nSigned-off-by: Tech User ', + tree: { + sha: '84152e149262f4883b1c724d9c9bb8e4e5d23fad', + url: 'https://example.com/qg-apps-typescript/git/trees/84152e149262f4883b1c724d9c9bb8e4e5d23fad', + }, + url: 'https://example.com/qg-apps-typescript/git/commits/fbf45173f2c1fbf4f6f2439abba88946a2cc8360', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://example.com/qg-apps-typescript/commits/fbf45173f2c1fbf4f6f2439abba88946a2cc8360', + html_url: + 'https:/example.com/qg-apps-typescript/commit/fbf45173f2c1fbf4f6f2439abba88946a2cc8360', + comments_url: + 'https://example.com/qg-apps-typescript/commits/fbf45173f2c1fbf4f6f2439abba88946a2cc8360/comments', + author: { + login: 'users/TxxxUxxGxxx', + id: 12345678, + node_id: 'sgsGASGJKASLJKGALWSJGag', + avatar_url: + 'https://avatars.githubusercontent.com/u/12345678?v=4', + gravatar_id: '', + url: 'https://example.com/users/TxxxUxxGxxx', + html_url: 'https://github.com/users/TxxxUxxGxxx', + followers_url: + 'https://example.com/users/TxxxUxxGxxx/followers', + following_url: + 'https://example.com/users/TxxxUxxGxxx/following{/other_user}', + gists_url: + 'https://example.com/users/TxxxUxxGxxx/gists{/gist_id}', + starred_url: + 'https://example.com/users/TxxxUxxGxxx/starred{/owner}{/repo}', + subscriptions_url: + 'https://example.com/users/TxxxUxxGxxx/subscriptions', + organizations_url: + 'https://example.com/users/TxxxUxxGxxx/orgs', + repos_url: 'https://example.com/users/TxxxUxxGxxx/repos', + events_url: + 'https://example.com/users/TxxxUxxGxxx/events{/privacy}', + received_events_url: + 'https://example.com/users/TxxxUxxGxxx/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'users/TxxxUxxGxxx', + id: 12345678, + node_id: 'sgsGASGJKASLJKGALWSJGag', + avatar_url: + 'https://avatars.githubusercontent.com/u/12345678?v=4', + gravatar_id: '', + url: 'https://example.com/users/TxxxUxxGxxx', + html_url: 'https://github.com/users/TxxxUxxGxxx', + followers_url: + 'https://example.com/users/TxxxUxxGxxx/followers', + following_url: + 'https://example.com/users/TxxxUxxGxxx/following{/other_user}', + gists_url: + 'https://example.com/users/TxxxUxxGxxx/gists{/gist_id}', + starred_url: + 'https://example.com/users/TxxxUxxGxxx/starred{/owner}{/repo}', + subscriptions_url: + 'https://example.com/users/TxxxUxxGxxx/subscriptions', + organizations_url: + 'https://example.com/users/TxxxUxxGxxx/orgs', + repos_url: 'https://example.com/users/TxxxUxxGxxx/repos', + events_url: + 'https://example.com/users/TxxxUxxGxxx/events{/privacy}', + received_events_url: + 'https://example.com/users/TxxxUxxGxxx/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: 'afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + url: 'https://example.com/qg-apps-typescript/commits/afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + html_url: + 'https:/example.com/qg-apps-typescript/commit/afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + }, + ], + }, + ], + files: [ + { + sha: 'ab49e18dfe46bfe03d9fa77837cff8ccd175de53', + filename: + 'apps/git-fetcher/src/fetchers/git-fetcher-github-prs.ts', + status: 'added', + additions: 106, + deletions: 0, + changes: 106, + blob_url: + 'https:/example.com/qg-apps-typescript/blob/8036cf75f4b7365efea76cbd716ef12d352d7d29/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher-github-prs.ts', + raw_url: + 'https:/example.com/qg-apps-typescript/raw/8036cf75f4b7365efea76cbd716ef12d352d7d29/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher-github-prs.ts', + contents_url: + 'https://example.com/qg-apps-typescript/contents/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher-github-prs.ts?ref=8036cf75f4b7365efea76cbd716ef12d352d7d29', + patch: + "@@ -0,0 +1,106 @@\n+import { GitFetcher } from './git-fetcher'\n+import { handleResponseStatus } from '../utils/handle-response-status.js'\n+import { getRequestOptions } from './utils/get-request-options.js'\n+import { GitServerConfig } from '../model/git-server-config.js'\n+import { ConfigFileData } from '../model/config-file-data.js'\n+import { compareLabels } from '../utils/compare-labels.js'\n+import { GithubPr } from '../model/github-pr'\n+\n+/**\n+ * @description Creates a GitFetcher which is able to fetch pull requests from GitHub\n+ */\n+export class GitFetcherGithubPrs implements GitFetcher {\n+ /**\n+ * @constructor\n+ * @param {GitServerConfig} gitServerConfig\n+ * @param {ConfigFileData} config\n+ */\n+ constructor(\n+ private gitServerConfig: GitServerConfig,\n+ private config: ConfigFileData\n+ ) {}\n+\n+ /**\n+ * Builds the request url as well as the request options to fetch pull requests from GitHub\n+ * and calls the fetch-method. As long as the response body includes pull requests objects, the\n+ * currentPage variable will be incremented and pull requests are pushed to the array, that's eventually\n+ * returned. If the response body is empty and no pull requests were received, the loop ends,\n+ * and the collected pull requests are returned.\n+ * @returns an a promise for an array of GitHub pull requests, which have been filtered according to the configuration.\n+ * @throws {Error} when either fetch response is not successful or response can't be parsed.\n+ */\n+ public async fetchResource(): Promise {\n+ const requestOptions: RequestInit = await getRequestOptions(\n+ this.gitServerConfig\n+ )\n+\n+ let pullRequests: GithubPr[] = []\n+\n+ let currentPage: number | null = 1\n+ let responseBody: GithubPr[]\n+\n+ while (currentPage != null) {\n+ try {\n+ const response: Response = await fetch(\n+ this.composeUrl(currentPage),\n+ requestOptions\n+ )\n+\n+ if (response.status != 200) {\n+ handleResponseStatus(response.status)\n+ }\n+\n+ responseBody = (await response.json()) as GithubPr[]\n+ } catch (error: any) {\n+ throw new Error(\n+ `Got the following error when running git fetcher: ${error.message}`\n+ )\n+ }\n+\n+ if (responseBody.length > 0) {\n+ pullRequests.push(...responseBody)\n+ currentPage++\n+ } else {\n+ currentPage = null\n+ }\n+ }\n+\n+ if (\n+ pullRequests.length > 0 &&\n+ this.config.data.labels &&\n+ this.config.data.labels.length > 0\n+ ) {\n+ const filteredPrs: GithubPr[] = []\n+\n+ pullRequests.forEach((pr: GithubPr) => {\n+ if (compareLabels(this.config.data.labels, pr.labels)) {\n+ filteredPrs.push(pr)\n+ }\n+ })\n+\n+ pullRequests = filteredPrs\n+ }\n+\n+ console.log(\n+ `Fetched ${pullRequests.length} pull request${\n+ pullRequests.length === 1 ? '' : 's'\n+ }`\n+ )\n+ return pullRequests\n+ }\n+\n+ private composeUrl(startPage?: number): string {\n+ const strippedApiUrl: string = this.gitServerConfig.gitServerApiUrl.replace(\n+ //*$/,\n+ ''\n+ )\n+ const stateFilter = 'all' //will allow to set other state filters, introduced in future tickets\n+\n+ let baseUrl = `${strippedApiUrl}/repos/${this.config.data.org}/${this.config.data.repo}/pulls?state=${stateFilter}&per_page=100`\n+ if (startPage != null) {\n+ baseUrl += `&page=${startPage}`\n+ }\n+\n+ return baseUrl\n+ }\n+}", + }, + { + sha: '73491746fc3d4c675726060b73df7dd8ccf72309', + filename: 'apps/git-fetcher/src/fetchers/git-fetcher.ts', + status: 'modified', + additions: 2, + deletions: 98, + changes: 100, + blob_url: + 'https:/example.com/qg-apps-typescript/blob/8036cf75f4b7365efea76cbd716ef12d352d7d29/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher.ts', + raw_url: + 'https:/example.com/qg-apps-typescript/raw/8036cf75f4b7365efea76cbd716ef12d352d7d29/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher.ts', + contents_url: + 'https://example.com/qg-apps-typescript/contents/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher.ts?ref=8036cf75f4b7365efea76cbd716ef12d352d7d29', + patch: + "@@ -1,99 +1,3 @@\n-import { GitServerConfig } from '../model/git-server-config'\n-import { ConfigFileData } from '../model/config-file-data'\n-import { handleResponseStatus } from '../utils/handle-response-status.js'\n-\n-export class GitFetcher {\n- constructor(public env: GitServerConfig, public config: ConfigFileData) {}\n-\n- public pullRequestValidInputs = [\n- 'pull-request',\n- 'pull-requests',\n- 'pr',\n- 'prs',\n- 'pullrequest',\n- 'pullrequests',\n- 'pull',\n- 'pulls',\n- ]\n-\n- public validateResourceName(resource: string) {\n- if (this.env.gitServerType == 'bitbucket') {\n- if (this.pullRequestValidInputs.includes(resource.toLocaleLowerCase())) {\n- return 'pull-requests'\n- } else {\n- throw new Error(`${resource} resource name not valid`)\n- }\n- } else if (this.env.gitServerType == 'github') {\n- if (this.pullRequestValidInputs.includes(resource.toLocaleLowerCase())) {\n- return 'pulls'\n- } else {\n- throw new Error(`${resource} resource name not valid`)\n- }\n- } else {\n- throw new Error(`${this.env.gitServerType} server type not supported`)\n- }\n- }\n-\n- public async getOptions() {\n- if (this.env.gitServerAuthMethod.toLocaleLowerCase() == 'basic') {\n- const options = {\n- method: 'GET',\n- auth: {\n- username: this.env.gitServerUsername,\n- password: this.env.gitServerPassword,\n- },\n- headers: {\n- Accept: 'application/vnd.github+json',\n- },\n- }\n- return options\n- } else if (this.env.gitServerAuthMethod.toLocaleLowerCase() == 'token') {\n- const options = {\n- method: 'GET',\n- headers: {\n- Accept: 'application/json',\n- Authorization: `Bearer ${this.env.gitServerApiToken}`,\n- },\n- }\n- return options\n- } else {\n- throw new Error('No valid auth method provided')\n- }\n- }\n-\n- public async buildUrl(resource: string) {\n- const resourceName = this.validateResourceName(resource)\n- if (this.env.gitServerType == 'bitbucket') {\n- const endpoint = `${this.env.gitServerApiUrl.replace(\n- //*$/,\n- ''\n- )}/projects/${this.config.data.org}/repos/${\n- this.config.data.repo\n- }/${resourceName}?state=ALL`\n- return endpoint\n- } else if (this.env.gitServerType == 'github') {\n- const endpoint = `${this.env.gitServerApiUrl.replace(//*$/, '')}/repos/${\n- this.config.data.org\n- }/${this.config.data.repo}/${resourceName}?state=all&per_page=100`\n- return endpoint\n- } else {\n- throw new Error(`${this.env.gitServerType} server type not supported`)\n- }\n- }\n-\n- public async runQuery() {\n- const url = await this.buildUrl(this.config.data.resource)\n- const options = await this.getOptions()\n- try {\n- const response = await fetch(url, options)\n- if (response.status != 200) {\n- handleResponseStatus(response.status)\n- }\n- return response.json()\n- } catch (error: any) {\n- throw new Error(\n- `Got the following error when running Git fetcher: ${error.message}`\n- )\n- }\n- }\n+export interface GitFetcher {\n+ fetchResource(): Promise\n }", + }, + { + sha: '62e232b894cc96e6bfb3be758e94e1ac57df1b99', + filename: 'apps/git-fetcher/src/fetchers/index.ts', + status: 'added', + additions: 2, + deletions: 0, + changes: 2, + blob_url: + 'https:/example.com/qg-apps-typescript/blob/8036cf75f4b7365efea76cbd716ef12d352d7d29/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Findex.ts', + raw_url: + 'https:/example.com/qg-apps-typescript/raw/8036cf75f4b7365efea76cbd716ef12d352d7d29/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Findex.ts', + contents_url: + 'https://example.com/qg-apps-typescript/contents/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Findex.ts?ref=8036cf75f4b7365efea76cbd716ef12d352d7d29', + patch: + "@@ -0,0 +1,2 @@\n+export * from './git-fetcher.js'\n+export * from './generate-git-fetcher.js'", + }, + ], + }, + }, + ], + }, + [githubCommitsMetadataEndpoint]: { + get: [ + { + responseStatus: 200, + responseBody: [ + { + sha: '8036cf75f4b7365efea76cbd716ef12d352d7d29', + node_id: + 'C_kgsakASFGKOGJASOGJKwaogjowakxxxkls12345awkrKgasjkgsakgp', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-07-12T10:46:50Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-07-12T10:46:50Z', + }, + message: + 'add commits and diff retrieval of target file for both bitbucket and github', + tree: { + sha: '9450ecc9597185ad82f9c9b61df5337f5ad4a286', + url: 'https://example.com/qg-apps-typescript/git/trees/9450ecc9597185ad82f9c9b61df5337f5ad4a286', + }, + url: 'https://example.com/qg-apps-typescript/git/commits/8036cf75f4b7365efea76cbd716ef12d352d7d29', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://example.com/qg-apps-typescript/commits/8036cf75f4b7365efea76cbd716ef12d352d7d29', + html_url: + 'https:/example.com/qg-apps-typescript/commit/8036cf75f4b7365efea76cbd716ef12d352d7d29', + comments_url: + 'https://example.com/qg-apps-typescript/commits/8036cf75f4b7365efea76cbd716ef12d352d7d29/comments', + author: { + login: 'users/TxxxUxxGxxx', + id: 123456789, + node_id: 'XjlwaKGAKSFGAgasg', + avatar_url: + 'https://avatars.githubusercontent.com/u/123456789132?v=4', + gravatar_id: '', + url: 'https://example.com/users/TxxxUxxGxxx', + html_url: 'https://github.com/users/TxxxUxxGxxx', + followers_url: + 'https://example.com/users/TxxxUxxGxxx/followers', + following_url: + 'https://example.com/users/TxxxUxxGxxx/following{/other_user}', + gists_url: + 'https://example.com/users/TxxxUxxGxxx/gists{/gist_id}', + starred_url: + 'https://example.com/users/TxxxUxxGxxx/starred{/owner}{/repo}', + subscriptions_url: + 'https://example.com/users/TxxxUxxGxxx/subscriptions', + organizations_url: + 'https://example.com/users/TxxxUxxGxxx/orgs', + repos_url: 'https://example.com/users/TxxxUxxGxxx/repos', + events_url: + 'https://example.com/users/TxxxUxxGxxx/events{/privacy}', + received_events_url: + 'https://example.com/users/TxxxUxxGxxx/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'users/TxxxUxxGxxx', + id: 123456789132, + node_id: 'ASBNSABOsg6a5', + avatar_url: + 'https://avatars.githubusercontent.com/u/123456789132?v=4', + gravatar_id: '', + url: 'https://example.com/users/TxxxUxxGxxx', + html_url: 'https://github.com/users/TxxxUxxGxxx', + followers_url: + 'https://example.com/users/TxxxUxxGxxx/followers', + following_url: + 'https://example.com/users/TxxxUxxGxxx/following{/other_user}', + gists_url: + 'https://example.com/users/TxxxUxxGxxx/gists{/gist_id}', + starred_url: + 'https://example.com/users/TxxxUxxGxxx/starred{/owner}{/repo}', + subscriptions_url: + 'https://example.com/users/TxxxUxxGxxx/subscriptions', + organizations_url: + 'https://example.com/users/TxxxUxxGxxx/orgs', + repos_url: 'https://example.com/users/TxxxUxxGxxx/repos', + events_url: + 'https://example.com/users/TxxxUxxGxxx/events{/privacy}', + received_events_url: + 'https://example.com/users/TxxxUxxGxxx/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: 'dc7e626d6da5b0b4911a1b11f0d5dcf6009f827f', + url: 'https://example.com/qg-apps-typescript/commits/dc7e626d6da5b0b4911a1b11f0d5dcf6009f827f', + html_url: + 'https:/example.com/qg-apps-typescript/commit/dc7e626d6da5b0b4911a1b11f0d5dcf6009f827f', + }, + ], + }, + { + sha: '8cf0dafa2bfcc3104aa0d69ae7cf9ff55ceab8cd', + node_id: 'C_asgaswqrojopwajgjagishjaijoarkwa', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-05-31T07:20:01Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-06-14T12:05:11Z', + }, + message: + 'Add functionality for fetching branches and tags from bitbucket', + tree: { + sha: '1a2539623b501741ccec196ad673071570a35dbe', + url: 'https://example.com/qg-apps-typescript/git/trees/1a2539623b501741ccec196ad673071570a35dbe', + }, + url: 'https://example.com/qg-apps-typescript/git/commits/8cf0dafa2bfcc3104aa0d69ae7cf9ff55ceab8cd', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://example.com/qg-apps-typescript/commits/8cf0dafa2bfcc3104aa0d69ae7cf9ff55ceab8cd', + html_url: + 'https:/example.com/qg-apps-typescript/commit/8cf0dafa2bfcc3104aa0d69ae7cf9ff55ceab8cd', + comments_url: + 'https://example.com/qg-apps-typescript/commits/8cf0dafa2bfcc3104aa0d69ae7cf9ff55ceab8cd/comments', + author: { + login: 'users/TxxxUxxGxxx', + id: 123456789123, + node_id: 'U_ab5dasEGh48', + avatar_url: + 'https://avatars.githubusercontent.com/u/123456789123?v=4', + gravatar_id: '', + url: 'https://example.com/users/TxxxUxxGxxx', + html_url: 'https://github.com/users/TxxxUxxGxxx', + followers_url: + 'https://example.com/users/TxxxUxxGxxx/followers', + following_url: + 'https://example.com/users/TxxxUxxGxxx/following{/other_user}', + gists_url: + 'https://example.com/users/TxxxUxxGxxx/gists{/gist_id}', + starred_url: + 'https://example.com/users/TxxxUxxGxxx/starred{/owner}{/repo}', + subscriptions_url: + 'https://example.com/users/TxxxUxxGxxx/subscriptions', + organizations_url: + 'https://example.com/users/TxxxUxxGxxx/orgs', + repos_url: 'https://example.com/users/TxxxUxxGxxx/repos', + events_url: + 'https://example.com/users/TxxxUxxGxxx/events{/privacy}', + received_events_url: + 'https://example.com/users/TxxxUxxGxxx/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'users/TxxxUxxGxxx', + id: 123456789123, + node_id: 'U_ab5dasEGh48', + avatar_url: + 'https://avatars.githubusercontent.com/u/123456789123?v=4', + gravatar_id: '', + url: 'https://example.com/users/TxxxUxxGxxx', + html_url: 'https://github.com/users/TxxxUxxGxxx', + followers_url: + 'https://example.com/users/TxxxUxxGxxx/followers', + following_url: + 'https://example.com/users/TxxxUxxGxxx/following{/other_user}', + gists_url: + 'https://example.com/users/TxxxUxxGxxx/gists{/gist_id}', + starred_url: + 'https://example.com/users/TxxxUxxGxxx/starred{/owner}{/repo}', + subscriptions_url: + 'https://example.com/users/TxxxUxxGxxx/subscriptions', + organizations_url: + 'https://example.com/users/TxxxUxxGxxx/orgs', + repos_url: 'https://example.com/users/TxxxUxxGxxx/repos', + events_url: + 'https://example.com/users/TxxxUxxGxxx/events{/privacy}', + received_events_url: + 'https://example.com/users/TxxxUxxGxxx/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: '2f6d4472f7114cbacb6770fdabaa21b9e8e100aa', + url: 'https://example.com/qg-apps-typescript/commits/2f6d4472f7114cbacb6770fdabaa21b9e8e100aa', + html_url: + 'https:/example.com/qg-apps-typescript/commit/2f6d4472f7114cbacb6770fdabaa21b9e8e100aa', + }, + ], + }, + ], + }, + { + responseStatus: 200, + responseBody: [ + { + sha: 'd6a73ae06e781fe510090dc58f33cc7bcf8c9c11', + node_id: 'C_ksagkglwajklgfjkwoaggjksidkgjsgjskok12345JKT', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-05-17T14:22:46Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-05-30T12:48:08Z', + }, + message: + 'Adjust gitFetcher to fetch all pull requests and ignore pagination or limits.', + tree: { + sha: 'd7950bd787096772a6cabde0b24fd4c68ca3aa77', + url: 'https://example.com/qg-apps-typescript/git/trees/d7950bd787096772a6cabde0b24fd4c68ca3aa77', + }, + url: 'https://example.com/qg-apps-typescript/git/commits/d6a73ae06e781fe510090dc58f33cc7bcf8c9c11', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://example.com/qg-apps-typescript/commits/d6a73ae06e781fe510090dc58f33cc7bcf8c9c11', + html_url: + 'https:/example.com/qg-apps-typescript/commit/d6a73ae06e781fe510090dc58f33cc7bcf8c9c11', + comments_url: + 'https://example.com/qg-apps-typescript/commits/d6a73ae06e781fe510090dc58f33cc7bcf8c9c11/comments', + author: { + login: 'users/TxxxUxxGxxx', + id: 12345647865456416, + node_id: 'U_kshgsioajaRgashjg', + avatar_url: + 'https://avatars.githubusercontent.com/u/12345647865456416?v=4', + gravatar_id: '', + url: 'https://example.com/users/TxxxUxxGxxx', + html_url: 'https://github.com/users/TxxxUxxGxxx', + followers_url: + 'https://example.com/users/TxxxUxxGxxx/followers', + following_url: + 'https://example.com/users/TxxxUxxGxxx/following{/other_user}', + gists_url: + 'https://example.com/users/TxxxUxxGxxx/gists{/gist_id}', + starred_url: + 'https://example.com/users/TxxxUxxGxxx/starred{/owner}{/repo}', + subscriptions_url: + 'https://example.com/users/TxxxUxxGxxx/subscriptions', + organizations_url: + 'https://example.com/users/TxxxUxxGxxx/orgs', + repos_url: 'https://example.com/users/TxxxUxxGxxx/repos', + events_url: + 'https://example.com/users/TxxxUxxGxxx/events{/privacy}', + received_events_url: + 'https://example.com/users/TxxxUxxGxxx/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'users/TxxxUxxGxxx', + id: 12345647865456416, + node_id: 'U_kshgsioajaRgashjg', + avatar_url: + 'https://avatars.githubusercontent.com/u/12345647865456416?v=4', + gravatar_id: '', + url: 'https://example.com/users/TxxxUxxGxxx', + html_url: 'https://github.com/users/TxxxUxxGxxx', + followers_url: + 'https://example.com/users/TxxxUxxGxxx/followers', + following_url: + 'https://example.com/users/TxxxUxxGxxx/following{/other_user}', + gists_url: + 'https://example.com/users/TxxxUxxGxxx/gists{/gist_id}', + starred_url: + 'https://example.com/users/TxxxUxxGxxx/starred{/owner}{/repo}', + subscriptions_url: + 'https://example.com/users/TxxxUxxGxxx/subscriptions', + organizations_url: + 'https://example.com/users/TxxxUxxGxxx/orgs', + repos_url: 'https://example.com/users/TxxxUxxGxxx/repos', + events_url: + 'https://example.com/users/TxxxUxxGxxx/events{/privacy}', + received_events_url: + 'https://example.com/users/TxxxUxxGxxx/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: '8d59a30e6fbbd75320695c46c7221bab9fe07424', + url: 'https://example.com/qg-apps-typescript/commits/8d59a30e6fbbd75320695c46c7221bab9fe07424', + html_url: + 'https:/example.com/qg-apps-typescript/commit/8d59a30e6fbbd75320695c46c7221bab9fe07424', + }, + ], + }, + { + sha: '70c52ff0011d60ded7d438463ad44945306ddc7f', + node_id: 'C_kwD21245gsaglkjoaslsjaASHGSAGIAmbsdtr', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-04-18T14:54:22Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-04-28T11:27:36Z', + }, + message: 'Validate git fetcher input values', + tree: { + sha: '2f147a29812c08781b5be7eb01d7267fb1b157e7', + url: 'https://example.com/qg-apps-typescript/git/trees/2f147a29812c08781b5be7eb01d7267fb1b157e7', + }, + url: 'https://example.com/qg-apps-typescript/git/commits/70c52ff0011d60ded7d438463ad44945306ddc7f', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://example.com/qg-apps-typescript/commits/70c52ff0011d60ded7d438463ad44945306ddc7f', + html_url: + 'https:/example.com/qg-apps-typescript/commit/70c52ff0011d60ded7d438463ad44945306ddc7f', + comments_url: + 'https://example.com/qg-apps-typescript/commits/70c52ff0011d60ded7d438463ad44945306ddc7f/comments', + author: { + login: 'users/TxxxUxxGxxx', + id: 65498712345, + node_id: 'U_kgDOcasf', + avatar_url: + 'https://avatars.githubusercontent.com/u/65498712345?v=4', + gravatar_id: '', + url: 'https://example.com/users/TxxxUxxGxxx', + html_url: 'https://github.com/users/TxxxUxxGxxx', + followers_url: + 'https://example.com/users/TxxxUxxGxxx/followers', + following_url: + 'https://example.com/users/TxxxUxxGxxx/following{/other_user}', + gists_url: + 'https://example.com/users/TxxxUxxGxxx/gists{/gist_id}', + starred_url: + 'https://example.com/users/TxxxUxxGxxx/starred{/owner}{/repo}', + subscriptions_url: + 'https://example.com/users/TxxxUxxGxxx/subscriptions', + organizations_url: + 'https://example.com/users/TxxxUxxGxxx/orgs', + repos_url: 'https://example.com/users/TxxxUxxGxxx/repos', + events_url: + 'https://example.com/users/TxxxUxxGxxx/events{/privacy}', + received_events_url: + 'https://example.com/users/TxxxUxxGxxx/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'users/TxxxUxxGxxx', + id: 123456789123, + node_id: 'U_ab5dasEGh48', + avatar_url: + 'https://avatars.githubusercontent.com/u/123456789123?v=4', + gravatar_id: '', + url: 'https://example.com/users/TxxxUxxGxxx', + html_url: 'https://github.com/users/TxxxUxxGxxx', + followers_url: + 'https://example.com/users/TxxxUxxGxxx/followers', + following_url: + 'https://example.com/users/TxxxUxxGxxx/following{/other_user}', + gists_url: + 'https://example.com/users/TxxxUxxGxxx/gists{/gist_id}', + starred_url: + 'https://example.com/users/TxxxUxxGxxx/starred{/owner}{/repo}', + subscriptions_url: + 'https://example.com/users/TxxxUxxGxxx/subscriptions', + organizations_url: + 'https://example.com/users/TxxxUxxGxxx/orgs', + repos_url: 'https://example.com/users/TxxxUxxGxxx/repos', + events_url: + 'https://example.com/users/TxxxUxxGxxx/events{/privacy}', + received_events_url: + 'https://example.com/users/TxxxUxxGxxx/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: 'cc0a150e8c5585f6212abab78ceaf8bc2b4f94c1', + url: 'https://example.com/qg-apps-typescript/commits/cc0a150e8c5585f6212abab78ceaf8bc2b4f94c1', + html_url: + 'https:/example.com/qg-apps-typescript/commit/cc0a150e8c5585f6212abab78ceaf8bc2b4f94c1', + }, + ], + }, + ], + }, + { + responseStatus: 200, + responseBody: [ + { + sha: '92aa336413904e67151b6625dbaeb3fbe01ef132', + node_id: 'C_kaujsgjghsjaGAJSgasgwakfgawRGAJsagasKa21fasfg', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-03-20T15:03:41Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-03-20T15:03:41Z', + }, + message: + 'Improve log and error messages for better user readability + minor code clean up', + tree: { + sha: '97fe2161887bd14518dea8fbf308d5165b03987d', + url: 'https://example.com/qg-apps-typescript/git/trees/97fe2161887bd14518dea8fbf308d5165b03987d', + }, + url: 'https://example.com/qg-apps-typescript/git/commits/92aa336413904e67151b6625dbaeb3fbe01ef132', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://example.com/qg-apps-typescript/commits/92aa336413904e67151b6625dbaeb3fbe01ef132', + html_url: + 'https:/example.com/qg-apps-typescript/commit/92aa336413904e67151b6625dbaeb3fbe01ef132', + comments_url: + 'https://example.com/qg-apps-typescript/commits/92aa336413904e67151b6625dbaeb3fbe01ef132/comments', + author: { + login: 'users/TxxxUxxGxxx', + id: 98745631215486, + node_id: 'U_kg235352fasg2', + avatar_url: + 'https://avatars.githubusercontent.com/u/98745631215486?v=4', + gravatar_id: '', + url: 'https://example.com/users/TxxxUxxGxxx', + html_url: 'https://github.com/users/TxxxUxxGxxx', + followers_url: + 'https://example.com/users/TxxxUxxGxxx/followers', + following_url: + 'https://example.com/users/TxxxUxxGxxx/following{/other_user}', + gists_url: + 'https://example.com/users/TxxxUxxGxxx/gists{/gist_id}', + starred_url: + 'https://example.com/users/TxxxUxxGxxx/starred{/owner}{/repo}', + subscriptions_url: + 'https://example.com/users/TxxxUxxGxxx/subscriptions', + organizations_url: + 'https://example.com/users/TxxxUxxGxxx/orgs', + repos_url: 'https://example.com/users/TxxxUxxGxxx/repos', + events_url: + 'https://example.com/users/TxxxUxxGxxx/events{/privacy}', + received_events_url: + 'https://example.com/users/TxxxUxxGxxx/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'users/TxxxUxxGxxx', + id: 98745631215486, + node_id: 'U_kg235352fasg2', + avatar_url: + 'https://avatars.githubusercontent.com/u/98745631215486?v=4', + gravatar_id: '', + url: 'https://example.com/users/TxxxUxxGxxx', + html_url: 'https://github.com/users/TxxxUxxGxxx', + followers_url: + 'https://example.com/users/TxxxUxxGxxx/followers', + following_url: + 'https://example.com/users/TxxxUxxGxxx/following{/other_user}', + gists_url: + 'https://example.com/users/TxxxUxxGxxx/gists{/gist_id}', + starred_url: + 'https://example.com/users/TxxxUxxGxxx/starred{/owner}{/repo}', + subscriptions_url: + 'https://example.com/users/TxxxUxxGxxx/subscriptions', + organizations_url: + 'https://example.com/users/TxxxUxxGxxx/orgs', + repos_url: 'https://example.com/users/TxxxUxxGxxx/repos', + events_url: + 'https://example.com/users/TxxxUxxGxxx/events{/privacy}', + received_events_url: + 'https://example.com/users/TxxxUxxGxxx/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: '1f0f928abc84ba2d51b0da0cb40f4388d4a60f27', + url: 'https://example.com/qg-apps-typescript/commits/1f0f928abc84ba2d51b0da0cb40f4388d4a60f27', + html_url: + 'https:/example.com/qg-apps-typescript/commit/1f0f928abc84ba2d51b0da0cb40f4388d4a60f27', + }, + ], + }, + ], + }, + { + responseStatus: 200, + responseBody: [], + }, + ], + }, + }, + } +} + +export function getGitCommitsMetadataAndDiffErrorMockServerResponse( + port: number +): MockServerOptions { + return { + port, + https: true, + responses: { + [githubStartCommitEndpoint]: { + get: { + responseStatus: 404, + }, + }, + }, + } +} diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-authentication.int-spec.ts b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-authentication.int-spec.ts new file mode 100644 index 00000000..42e81059 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-authentication.int-spec.ts @@ -0,0 +1,253 @@ +import * as fs from 'fs' +import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { + MockServer, + MockServerOptions, + run, + RunProcessResult, +} from '../../../../integration-tests/src/util' +import { getGitPullRequestsMockOptions } from './fixtures/getGitPullRequestsMockServerResponse' +import { + SupportedAuthMethod, + supportedAuthMethods, +} from '../../src/model/git-server-config' +import { + defaultEnvironment, + gitFetcherExecutable, + MOCK_SERVER_PORT, + verifyErrorCase, + verifyOutputFile, + verifyPrRequest, +} from './utils' + +describe('Authentication', () => { + let mockServer: MockServer | undefined + + const requestUrlPullRequests = + '/projects/aquatest/repos/bitbucket-fetcher-test-repo/pull-requests' + + beforeAll(() => { + expect(fs.existsSync(gitFetcherExecutable)).toBe(true) + }) + + beforeEach(() => { + fs.mkdirSync(`${defaultEnvironment.evidence_path}`) + }) + + afterEach(async () => { + fs.rmSync(`${defaultEnvironment.evidence_path}`, { + recursive: true, + }) + await mockServer?.stop() + }) + + describe('Success Cases', () => { + describe('Authentication', () => { + const options: MockServerOptions = getGitPullRequestsMockOptions( + MOCK_SERVER_PORT, + 200 + ) + + beforeEach(() => { + mockServer = new MockServer(options) + }) + + it.each(supportedAuthMethods)( + `should fetch file from bitbucket and save it for auth method "%s"`, + async (authMethod: SupportedAuthMethod) => { + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: authMethod, + GIT_FETCHER_SERVER_TYPE: 'bitbucket', + GIT_FETCHER_API_TOKEN: + authMethod === 'token' ? 'someToken' : undefined, + GIT_FETCHER_USERNAME: authMethod === 'basic' ? 'john' : undefined, + GIT_FETCHER_PASSWORD: authMethod === 'basic' ? 'secret' : undefined, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-bitbucket.yml`, + } + + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: env, + } + ) + expect(mockServer!.getNumberOfRequests()).toEqual(1) + verifyPrRequest(mockServer!, requestUrlPullRequests, authMethod) + + await verifyOutputFile( + env.evidence_path, + true, + JSON.stringify([ + { id: 1, title: 'foo 1' }, + { id: 2, title: 'foo 2' }, + ]) + ) + expect(result.exitCode).to.equal(0) + } + ) + + + it.each(supportedAuthMethods)( + `should fetch file from github and save it for auth method "%s"`, + async (authMethod: SupportedAuthMethod) => { + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: authMethod, + GIT_FETCHER_SERVER_TYPE: 'github', + GIT_FETCHER_API_TOKEN: + authMethod === 'token' ? 'someToken' : undefined, + GIT_FETCHER_USERNAME: authMethod === 'basic' ? 'john' : undefined, + GIT_FETCHER_PASSWORD: authMethod === 'basic' ? 'secret' : undefined, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-github.yml`, + } + + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: env, + } + ) + + await verifyOutputFile( + env.evidence_path, + true, + JSON.stringify([ + { + id: 1, + title: 'Dummy PR', + state: 'open', + labels: [ + { + id: 1, + url: 'www.foo.bar', + name: 'ignore', + default: false, + }, + ], + }, + ]) + ) + expect(result.exitCode).to.equal(0) + } + ) + }) + }) + + describe('Error Cases', () => { + let options: MockServerOptions + beforeEach(() => { + options = getGitPullRequestsMockOptions(MOCK_SERVER_PORT, 400) + }) + + it('should throw error if env variable GIT_FETCHER_SERVER_AUTH_METHOD and GIT_FETCHER_API_TOKEN are undefined', async () => { + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: undefined, + GIT_FETCHER_SERVER_TYPE: 'github', + GIT_FETCHER_API_TOKEN: undefined, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-bitbucket.yml`, + } + + mockServer = new MockServer(options) + + await verifyErrorCase( + mockServer, + gitFetcherExecutable, + env, + '{"status":"FAILED","reason":"GIT_FETCHER_API_TOKEN environment variable is required for \\"token\\" authentication, but is not set or empty."}', + 'expected' + ) + expect(mockServer.getNumberOfRequests()).toEqual(0) + }) + + it.each([undefined, ' '])( + 'should throw error if env variable GIT_FETCHER_USERNAME is "%s" for auth method "basic"', + async (username) => { + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: 'basic', + GIT_FETCHER_USERNAME: username, + GIT_FETCHER_PASSWORD: 'secret', + GIT_FETCHER_SERVER_TYPE: 'github', + } + mockServer = new MockServer(options) + + await verifyErrorCase( + mockServer, + gitFetcherExecutable, + env, + '{"status":"FAILED","reason":"GIT_FETCHER_USERNAME environment variable is required for \\"basic\\" authentication, but is not set or empty."}', + 'expected' + ) + expect(mockServer.getNumberOfRequests()).toEqual(0) + } + ) + + it.each([undefined, ' '])( + 'should throw error if env variable GIT_FETCHER_PASSWORD is "%s" for auth method "basic"', + async (password) => { + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: 'basic', + GIT_FETCHER_USERNAME: 'John', + GIT_FETCHER_PASSWORD: password, + GIT_FETCHER_SERVER_TYPE: 'github', + } + mockServer = new MockServer(options) + + await verifyErrorCase( + mockServer, + gitFetcherExecutable, + env, + '{"status":"FAILED","reason":"GIT_FETCHER_PASSWORD environment variable is required for \\"basic\\" authentication, but is not set or empty."}', + 'expected' + ) + expect(mockServer.getNumberOfRequests()).toEqual(0) + } + ) + + it('should throw error if env variable GIT_FETCHER_SERVER_AUTH_METHOD has unsupported value', async () => { + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: 'invalid', + GIT_FETCHER_SERVER_TYPE: 'github', + GIT_FETCHER_API_TOKEN: 'someToken', + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-bitbucket.yml`, + } + + mockServer = new MockServer(options) + + await verifyErrorCase( + mockServer, + gitFetcherExecutable, + env, + '{"status":"FAILED","reason":"No valid authentication method provided. Valid authentication methods are: token,basic"}', + 'expected' + ) + expect(mockServer.getNumberOfRequests()).toEqual(0) + }) + + it('should throw error for auth method "token" if the token itself is empty', async () => { + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: 'token', + GIT_FETCHER_SERVER_TYPE: 'bitbucket', + GIT_FETCHER_API_TOKEN: ' ', + } + + mockServer = new MockServer(options) + + await verifyErrorCase( + mockServer, + gitFetcherExecutable, + env, + '{"status":"FAILED","reason":"GIT_FETCHER_API_TOKEN environment variable is required for \\"token\\" authentication, but is not set or empty."}', + 'expected' + ) + expect(mockServer.getNumberOfRequests()).toEqual(0) + }) + }) +}) diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-basic.int-spec.ts b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-basic.int-spec.ts new file mode 100644 index 00000000..685b739e --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-basic.int-spec.ts @@ -0,0 +1,225 @@ +import * as fs from 'fs' +import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { + MockServer, + MockServerOptions, + run, + RunProcessResult, +} from '../../../../integration-tests/src/util' +import { getGitPullRequestsMockOptions } from './fixtures/getGitPullRequestsMockServerResponse' +import { SupportedAuthMethod } from '../../src/model/git-server-config' +import { + defaultEnvironment, + gitFetcherExecutable, + MOCK_SERVER_PORT, + verifyErrorCase, + verifyOutputFile, + verifyPrRequest, +} from './utils' + +describe('Basic', () => { + let mockServer: MockServer | undefined + + const requestUrlPullRequests = + '/projects/aquatest/repos/bitbucket-fetcher-test-repo/pull-requests' + + beforeAll(() => { + expect(fs.existsSync(gitFetcherExecutable)).toBe(true) + }) + + beforeEach(() => { + fs.mkdirSync(`${defaultEnvironment.evidence_path}`) + }) + + afterEach(async () => { + fs.rmSync(`${defaultEnvironment.evidence_path}`, { + recursive: true, + }) + await mockServer?.stop() + }) + + describe('Success Cases', () => { + const options: MockServerOptions = getGitPullRequestsMockOptions( + MOCK_SERVER_PORT, + 200 + ) + + it('should fetch file from github but should return empty string, when label filter does not match pr-labels', async () => { + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: 'token', + GIT_FETCHER_SERVER_TYPE: 'github', + GIT_FETCHER_API_TOKEN: 'someToken', + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-github-wrong-label.yml`, + } + + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: env, + } + ) + + await verifyOutputFile(env.evidence_path, true, JSON.stringify([])) + expect(result.exitCode).to.equal(0) + }) + }) + + describe('Error Cases', () => { + it('git fetch response with status code 404', async () => { + const options: MockServerOptions = getGitPullRequestsMockOptions( + MOCK_SERVER_PORT, + 404 + ) + + const authMethod: SupportedAuthMethod = 'token' + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: authMethod, + GIT_FETCHER_SERVER_TYPE: 'bitbucket', + GIT_FETCHER_API_TOKEN: 'someToken', + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-bitbucket.yml`, + } + + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: env, + } + ) + + expect(mockServer.getNumberOfRequests()).toEqual(1) + verifyPrRequest(mockServer, requestUrlPullRequests, authMethod) + await verifyOutputFile(env.evidence_path, false) + expect(result.stdout).contain( + '{"status":"FAILED","reason":"Repository not found. Status code: 404"}' + ) + expect(result.exitCode).to.equal(0) + }) + + it.each([401, 403])( + 'git fetch response with status code %i', + async (statusCode) => { + const authMethod: SupportedAuthMethod = 'token' + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: authMethod, + GIT_FETCHER_SERVER_TYPE: 'bitbucket', + GIT_FETCHER_API_TOKEN: 'someToken', + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-bitbucket.yml`, + } + const options: MockServerOptions = getGitPullRequestsMockOptions( + MOCK_SERVER_PORT, + statusCode + ) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: env, + } + ) + + expect(mockServer.getNumberOfRequests()).toEqual(1) + verifyPrRequest(mockServer, requestUrlPullRequests, authMethod) + await verifyOutputFile(env.evidence_path, false) + expect(result.stdout).contain( + `{"status":"FAILED","reason":"Could not access the required repository. Status code: ${statusCode}"}` + ) + expect(result.exitCode).to.equal(0) + } + ) + + it(`git fetch response with status code 500 (other than 2xx and 4xx)`, async () => { + const authMethod: SupportedAuthMethod = 'token' + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: authMethod, + GIT_FETCHER_SERVER_TYPE: 'bitbucket', + GIT_FETCHER_API_TOKEN: 'someToken', + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-bitbucket.yml`, + } + const options: MockServerOptions = getGitPullRequestsMockOptions( + MOCK_SERVER_PORT, + 500 + ) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: env, + } + ) + + expect(mockServer.getNumberOfRequests()).toEqual(1) + verifyPrRequest(mockServer, requestUrlPullRequests, authMethod) + await verifyOutputFile(env.evidence_path, false) + expect(result.stderr).contain( + 'Error: Could not fetch data from git repository. Status code: 500' + ) + expect(result.exitCode).to.equal(1) + }) + + it('git fetcher config yaml has invalid structure', async () => { + const options: MockServerOptions = getGitPullRequestsMockOptions( + MOCK_SERVER_PORT, + 400 + ) + + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: 'token', + GIT_FETCHER_SERVER_TYPE: 'github', + GIT_FETCHER_API_TOKEN: 'someToken', + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-invalid-structure.yml`, + } + + mockServer = new MockServer(options) + + await verifyErrorCase( + mockServer, + gitFetcherExecutable, + env, + '{"status":"FAILED","reason":"Validation error: Required at \\"resource\\""}', + 'expected' + ) + expect(mockServer.getNumberOfRequests()).toEqual(0) + }) + + it('git fetcher config yaml has invalid values', async () => { + const options: MockServerOptions = getGitPullRequestsMockOptions( + MOCK_SERVER_PORT, + 400 + ) + + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: 'token', + GIT_FETCHER_SERVER_TYPE: 'github', + GIT_FETCHER_API_TOKEN: 'someToken', + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-invalid-values.yml`, + } + + mockServer = new MockServer(options) + + await verifyErrorCase( + mockServer, + gitFetcherExecutable, + env, + "{\"status\":\"FAILED\",\"reason\":\"Validation error: String must contain at least 1 character(s) at \\\"repo\\\"; Invalid enum value. Expected 'pull-request' | 'pull-requests' | 'pr' | 'prs' | 'pullrequest' | 'pullrequests' | 'pull' | 'pulls' | 'branches' | 'tags' | 'metadata-and-diff', received ' ' at \\\"resource\\\"\"}", + 'expected' + ) + expect(mockServer.getNumberOfRequests()).toEqual(0) + }) + }) +}) diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-bitbucket-branches.int-spec.ts b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-bitbucket-branches.int-spec.ts new file mode 100644 index 00000000..79b612b1 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-bitbucket-branches.int-spec.ts @@ -0,0 +1,140 @@ +import * as fs from 'fs' +import { IncomingHttpHeaders } from 'http' +import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { + MOCK_SERVER_CERT_PATH, + MockServer, + MockServerOptions, + ReceivedRequest, + run, + RunProcessResult, +} from '../../../../integration-tests/src/util' +import { BitbucketBranch } from '../../src/model/bitbucket-branch' +import { + getGitBranchesErrorMockServerResponse, + getGitBranchesMockServerResponse, +} from './fixtures/getGitBranchesMockServerResponse' + +describe('Fetch Branches from Bitbucket', () => { + const MOCK_SERVER_PORT = 8080 + const getBranchesEndpoint = + '/projects/aquatest/repos/bitbucket-fetcher-test-repo/branches' + + const environment = { + evidence_path: `${__dirname}/evidence`, + NODE_EXTRA_CA_CERTS: MOCK_SERVER_CERT_PATH, + GIT_FETCHER_SERVER_API_URL: `https://localhost:${MOCK_SERVER_PORT}`, + GIT_FETCHER_SERVER_TYPE: 'bitbucket', + GIT_FETCHER_API_TOKEN: 'someToken', + } as const + + const gitFetcherExecutable = `${__dirname}/../../dist/index.js` + let mockServerOptions: MockServerOptions + let mockServer: MockServer + + beforeAll(() => { + expect(fs.existsSync(gitFetcherExecutable)).toBe(true) + }) + + beforeEach(() => { + fs.mkdirSync(environment.evidence_path) + }) + + afterEach(async () => { + await mockServer?.stop() + + fs.rmSync(environment.evidence_path, { + recursive: true, + }) + }) + + it('should successfully fetch all three pages of branches', async () => { + mockServerOptions = getGitBranchesMockServerResponse(MOCK_SERVER_PORT) + mockServer = new MockServer(mockServerOptions) + + const result: RunProcessResult = await run(gitFetcherExecutable, [], { + env: { + ...environment, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-fetch-branches-from-bitbucket.yml`, + }, + }) + + // verify process result + expect(result.exitCode).toEqual(0) + expect(result.stdout).toHaveLength(3) + expect(result.stdout[0]).toEqual('Fetched 5 branches') + expect(result.stdout[1]).toEqual( + 'Fetch from https://localhost:8080 was successful with config {"org":"aquatest","repo":"bitbucket-fetcher-test-repo","resource":"branches"}' + ) + expect(result.stdout[2]).toEqual( + '{"output":{"git-fetcher-result":"git-fetcher-data.json"}}' + ) + expect(result.stderr).toHaveLength(0) + + // verify requests + expect(mockServer.getNumberOfRequests()).toEqual(3) + const requests: ReceivedRequest[] = mockServer.getRequests( + getBranchesEndpoint, + 'get' + ) + expect(requests).toHaveLength(3) + + const requestForFirstPage = requests[0] + verifyHeaders(requestForFirstPage.headers) + let expectedPageStart = '0' + expect(requestForFirstPage.query.start).toEqual(expectedPageStart) + + const requestForSecondPage = requests[1] + verifyHeaders(requestForSecondPage.headers) + expectedPageStart = `${mockServerOptions.responses[getBranchesEndpoint].get[0].responseBody.nextPageStart}` + expect(requestForSecondPage.query.start).toEqual(expectedPageStart) + + const requestForThirdPage = requests[2] + verifyHeaders(requestForThirdPage.headers) + expectedPageStart = `${mockServerOptions.responses[getBranchesEndpoint].get[1].responseBody.nextPageStart}` + expect(requestForThirdPage.query.start).toEqual(expectedPageStart) + + // verify output file + const outputFilePath = `${environment.evidence_path}/git-fetcher-data.json` + expect(fs.existsSync(outputFilePath)).toEqual(true) + const actualFileContent: string = fs.readFileSync(outputFilePath, { + encoding: 'utf-8', + }) + const expectedFileContent: BitbucketBranch[] = [ + ...mockServerOptions.responses[getBranchesEndpoint].get[0].responseBody + .values, + ...mockServerOptions.responses[getBranchesEndpoint].get[1].responseBody + .values, + ...mockServerOptions.responses[getBranchesEndpoint].get[2].responseBody + .values, + ] + expect(actualFileContent).toStrictEqual(JSON.stringify(expectedFileContent)) + }) + + it('should return failed, if bitbucket returns 404 NOT FOUND error', async () => { + mockServerOptions = getGitBranchesErrorMockServerResponse(MOCK_SERVER_PORT) + mockServer = new MockServer(mockServerOptions) + + const result: RunProcessResult = await run(gitFetcherExecutable, [], { + env: { + ...environment, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-fetch-branches-from-bitbucket.yml`, + }, + }) + + // gitfetcher should throw error + expect(result.exitCode).toEqual(0) + expect(result.stdout).toContain( + '{"status":"FAILED","reason":"Repository not found. Status code: 404"}' + ) + + // gitfetcher should not write an output file + const outputFilePath = `${environment.evidence_path}/git-fetcher-data.json` + expect(fs.existsSync(outputFilePath)).toEqual(false) + }) +}) + +function verifyHeaders(headers: IncomingHttpHeaders): void { + expect(headers.accept).toEqual('application/json') + expect(headers.authorization).toEqual('Bearer someToken') +} diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-bitbucket-commits-metadata-and-diff.int-spec.ts b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-bitbucket-commits-metadata-and-diff.int-spec.ts new file mode 100644 index 00000000..4326c28f --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-bitbucket-commits-metadata-and-diff.int-spec.ts @@ -0,0 +1,156 @@ +import * as fs from 'fs' +import { IncomingHttpHeaders } from 'http' +import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { + MOCK_SERVER_CERT_PATH, + MockServer, + MockServerOptions, + ReceivedRequest, + run, + RunProcessResult, +} from '../../../../integration-tests/src/util' +import { CommitsMetadataAndDiff } from '../../src/model/commits-metadata-and-diff' +import { + getGitCommitsMetadataAndDiffErrorMockServerResponse, + getGitCommitsMetadataAndDiffMockServerResponse, +} from './fixtures/getBitbucketCommitsMetadataAndDiffMockServerResponse' + +describe('Fetch Commits Metadata from Bitbucket', () => { + const MOCK_SERVER_PORT = 8080 + const getCommitsMetadataEndpoint = + '/projects/aquatest/repos/bitbucket-fetcher-test-repo/commits' + const getDiffEndpoint = + '/projects/aquatest/repos/bitbucket-fetcher-test-repo/diff/Somefolder/something.py' + + const environment = { + evidence_path: `${__dirname}/evidence`, + NODE_EXTRA_CA_CERTS: MOCK_SERVER_CERT_PATH, + GIT_FETCHER_SERVER_API_URL: `https://localhost:${MOCK_SERVER_PORT}`, + GIT_FETCHER_SERVER_TYPE: 'bitbucket', + GIT_FETCHER_API_TOKEN: 'someToken', + } as const + + const gitFetcherExecutable = `${__dirname}/../../dist/index.js` + let mockServerOptions: MockServerOptions + let mockServer: MockServer + + beforeAll(() => { + expect(fs.existsSync(gitFetcherExecutable)).toBe(true) + }) + + beforeEach(() => { + fs.mkdirSync(environment.evidence_path) + }) + + afterEach(async () => { + await mockServer?.stop() + + fs.rmSync(environment.evidence_path, { + recursive: true, + }) + }) + + it('should successfully fetch all three pages of commits', async () => { + mockServerOptions = + getGitCommitsMetadataAndDiffMockServerResponse(MOCK_SERVER_PORT) + mockServer = new MockServer(mockServerOptions) + const result: RunProcessResult = await run(gitFetcherExecutable, [], { + env: { + ...environment, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-fetch-commits-metadata-and-diff-from-bitbucket.yml`, + }, + }) + + // verify process result + expect(result.exitCode).toEqual(0) + expect(result.stdout).toHaveLength(4) + expect(result.stdout[0]).toEqual('Fetched medata about 5 commits') + expect(result.stdout[1]).toEqual('Fetched 1 diff') + expect(result.stdout[2]).toEqual( + 'Fetch from https://localhost:8080 was successful with config {"org":"aquatest","repo":"bitbucket-fetcher-test-repo","resource":"metadata-and-diff","filter":{"startHash":"35cc5eec543e69aed90503f21cf12666bcbfda4f"},"filePath":"Somefolder/something.py"}' + ) + expect(result.stdout[3]).toEqual( + '{"output":{"git-fetcher-result":"git-fetcher-data.json"}}' + ) + + expect(result.stderr).toHaveLength(0) + + // verify requests + expect(mockServer.getNumberOfRequests()).toEqual(4) + + let requests: ReceivedRequest[] = mockServer.getRequests( + getCommitsMetadataEndpoint, + 'get' + ) + expect(requests).toHaveLength(3) + + const requestForFirstPage = requests[0] + verifyHeaders(requestForFirstPage.headers) + let expectedPageStart = '0' + expect(requestForFirstPage.query.start).toEqual(expectedPageStart) + + const requestForSecondPage = requests[1] + verifyHeaders(requestForSecondPage.headers) + expectedPageStart = `${mockServerOptions.responses[getCommitsMetadataEndpoint].get[0].responseBody.nextPageStart}` + expect(requestForSecondPage.query.start).toEqual(expectedPageStart) + + const requestForThirdPage = requests[2] + verifyHeaders(requestForThirdPage.headers) + expectedPageStart = `${mockServerOptions.responses[getCommitsMetadataEndpoint].get[1].responseBody.nextPageStart}` + expect(requestForThirdPage.query.start).toEqual(expectedPageStart) + + requests = mockServer.getRequests(getDiffEndpoint, 'get') + expect(requests).toHaveLength(1) + + // verify output file + const outputFilePath = `${environment.evidence_path}/git-fetcher-data.json` + expect(fs.existsSync(outputFilePath)).toEqual(true) + const actualFileContent: string = fs.readFileSync(outputFilePath, { + encoding: 'utf-8', + }) + const expectedFileContent: CommitsMetadataAndDiff = { + commitsMetadata: [], + diff: [], + } + expectedFileContent.commitsMetadata = [ + ...mockServerOptions.responses[getCommitsMetadataEndpoint].get[0] + .responseBody.values, + ...mockServerOptions.responses[getCommitsMetadataEndpoint].get[1] + .responseBody.values, + ...mockServerOptions.responses[getCommitsMetadataEndpoint].get[2] + .responseBody.values, + ] + expectedFileContent.diff = [ + ...mockServerOptions.responses[getDiffEndpoint].get[0].responseBody.diffs, + ] + expect(actualFileContent).toStrictEqual(JSON.stringify(expectedFileContent)) + }) + + it('should throw an error, if bitbucket returns 404 NOT FOUND error', async () => { + mockServerOptions = + getGitCommitsMetadataAndDiffErrorMockServerResponse(MOCK_SERVER_PORT) + mockServer = new MockServer(mockServerOptions) + + const result: RunProcessResult = await run(gitFetcherExecutable, [], { + env: { + ...environment, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-fetch-commits-metadata-and-diff-from-bitbucket.yml`, + }, + }) + + // gitfetcher should throw error + expect(result.exitCode).toEqual(1) + expect(result.stderr).toContain( + 'Error: Repository not found. Status code: 404' + ) + + // gitfetcher should not write an output file + const outputFilePath = `${environment.evidence_path}/git-fetcher-data.json` + expect(fs.existsSync(outputFilePath)).toEqual(false) + }) +}) + +function verifyHeaders(headers: IncomingHttpHeaders): void { + expect(headers.accept).toEqual('application/json') + expect(headers.authorization).toEqual('Bearer someToken') +} diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-bitbucket-tags.int-spec.ts b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-bitbucket-tags.int-spec.ts new file mode 100644 index 00000000..083cc76d --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-bitbucket-tags.int-spec.ts @@ -0,0 +1,140 @@ +import * as fs from 'fs' +import { IncomingHttpHeaders } from 'http' +import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { + MOCK_SERVER_CERT_PATH, + MockServer, + MockServerOptions, + ReceivedRequest, + run, + RunProcessResult, +} from '../../../../integration-tests/src/util' +import { BitbucketTag } from '../../src/model/bitbucket-tag' +import { + getGitTagsErrorMockServerResponse, + getGitTagsSuccessMockServerResponse, +} from './fixtures/getGitTagsSuccessMockServerResponse' + +describe('Fetch Tags from Bitbucket', () => { + const MOCK_SERVER_PORT = 8080 + const getTagsEndpoint = + '/projects/aquatest/repos/bitbucket-fetcher-test-repo/tags' + + const environment = { + evidence_path: `${__dirname}/evidence`, + NODE_EXTRA_CA_CERTS: MOCK_SERVER_CERT_PATH, + GIT_FETCHER_SERVER_API_URL: `https://localhost:${MOCK_SERVER_PORT}`, + GIT_FETCHER_SERVER_TYPE: 'bitbucket', + GIT_FETCHER_API_TOKEN: 'someToken', + } as const + + const gitFetcherExecutable = `${__dirname}/../../dist/index.js` + let mockServerOptions: MockServerOptions + let mockServer: MockServer + + beforeAll(() => { + expect(fs.existsSync(gitFetcherExecutable)).toBe(true) + }) + + beforeEach(() => { + fs.mkdirSync(environment.evidence_path) + }) + + afterEach(async () => { + await mockServer?.stop() + + fs.rmSync(environment.evidence_path, { + recursive: true, + }) + }) + + it('should successfully fetch all three pages of tags', async () => { + mockServerOptions = getGitTagsSuccessMockServerResponse(MOCK_SERVER_PORT) + mockServer = new MockServer(mockServerOptions) + const result: RunProcessResult = await run(gitFetcherExecutable, [], { + env: { + ...environment, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-fetch-tags-from-bitbucket.yml`, + }, + }) + + // verify process result + expect(result.exitCode).toEqual(0) + expect(result.stdout).toHaveLength(3) + expect(result.stdout[0]).toEqual('Fetched 5 tags') + expect(result.stdout[1]).toEqual( + 'Fetch from https://localhost:8080 was successful with config {"org":"aquatest","repo":"bitbucket-fetcher-test-repo","resource":"tags"}' + ) + expect(result.stdout[2]).toEqual( + '{"output":{"git-fetcher-result":"git-fetcher-data.json"}}' + ) + + expect(result.stderr).toHaveLength(0) + + // verify requests + expect(mockServer.getNumberOfRequests()).toEqual(3) + const requests: ReceivedRequest[] = mockServer.getRequests( + getTagsEndpoint, + 'get' + ) + expect(requests).toHaveLength(3) + + const requestForFirstPage = requests[0] + verifyHeaders(requestForFirstPage.headers) + let expectedPageStart = '0' + expect(requestForFirstPage.query.start).toEqual(expectedPageStart) + + const requestForSecondPage = requests[1] + verifyHeaders(requestForSecondPage.headers) + expectedPageStart = `${mockServerOptions.responses[getTagsEndpoint].get[0].responseBody.nextPageStart}` + expect(requestForSecondPage.query.start).toEqual(expectedPageStart) + + const requestForThirdPage = requests[2] + verifyHeaders(requestForThirdPage.headers) + expectedPageStart = `${mockServerOptions.responses[getTagsEndpoint].get[1].responseBody.nextPageStart}` + expect(requestForThirdPage.query.start).toEqual(expectedPageStart) + + // verify output file + const outputFilePath = `${environment.evidence_path}/git-fetcher-data.json` + expect(fs.existsSync(outputFilePath)).toEqual(true) + const actualFileContent: string = fs.readFileSync(outputFilePath, { + encoding: 'utf-8', + }) + const expectedFileContent: BitbucketTag[] = [ + ...mockServerOptions.responses[getTagsEndpoint].get[0].responseBody + .values, + ...mockServerOptions.responses[getTagsEndpoint].get[1].responseBody + .values, + ...mockServerOptions.responses[getTagsEndpoint].get[2].responseBody + .values, + ] + expect(actualFileContent).toStrictEqual(JSON.stringify(expectedFileContent)) + }) + + it('should throw an error, if bitbucket returns 404 NOT FOUND error', async () => { + mockServerOptions = getGitTagsErrorMockServerResponse(MOCK_SERVER_PORT) + mockServer = new MockServer(mockServerOptions) + + const result: RunProcessResult = await run(gitFetcherExecutable, [], { + env: { + ...environment, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-fetch-tags-from-bitbucket.yml`, + }, + }) + + // gitfetcher should throw error + expect(result.exitCode).toEqual(0) + expect(result.stdout).toContain( + '{"status":"FAILED","reason":"Repository not found. Status code: 404"}' + ) + + // gitfetcher should not write an output file + const outputFilePath = `${environment.evidence_path}/git-fetcher-data.json` + expect(fs.existsSync(outputFilePath)).toEqual(false) + }) +}) + +function verifyHeaders(headers: IncomingHttpHeaders): void { + expect(headers.accept).toEqual('application/json') + expect(headers.authorization).toEqual('Bearer someToken') +} diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-date-filter.int-spec.ts b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-date-filter.int-spec.ts new file mode 100644 index 00000000..a58bb651 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-date-filter.int-spec.ts @@ -0,0 +1,292 @@ +import * as fs from 'fs' +import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { + MockServer, + MockServerOptions, + run, + RunProcessResult, +} from '../../../../integration-tests/src/util' +import { BitbucketPr } from '../../src/model/bitbucket-pr' +import { + getBitbucketResponseOptions, + PULL_REQUESTS_ENDPOINT, +} from './fixtures/getBitbucketResponseOptions' +import { + defaultEnvironment, + gitFetcherExecutable, + MOCK_SERVER_PORT, + verifyErrorCase, + verifyOutputFile, + verifyPrRequest, +} from './utils' + +describe('Date Filter', () => { + let mockServer: MockServer | undefined + + const requestUrlPullRequests = + '/projects/aquatest/repos/bitbucket-fetcher-test-repo/pull-requests' + + beforeAll(() => { + expect(fs.existsSync(gitFetcherExecutable)).toBe(true) + }) + + beforeEach(() => { + fs.mkdirSync(`${defaultEnvironment.evidence_path}`) + }) + + afterEach(async () => { + fs.rmSync(`${defaultEnvironment.evidence_path}`, { + recursive: true, + }) + await mockServer?.stop() + }) + + describe('Success Cases', () => { + describe('For Bitbucket', () => { + const authMethod = 'token' + const serverType = 'bitbucket' + + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: authMethod, + GIT_FETCHER_SERVER_TYPE: serverType, + GIT_FETCHER_API_TOKEN: 'someToken', + } + + const pullRequests: BitbucketPr[] = [ + { + id: 1, + state: 'OPEN', + updatedDate: 1580515200000, // 01-02-2020 00:00:00 + }, + { + id: 2, + state: 'MERGED', + updatedDate: 1609459200000, // 01-01-2021 00:00:00 + }, + { + id: 3, + state: 'OPEN', + updatedDate: 1678838400000, // 15-03-2023 00:00:00 + }, + ] + + let options: MockServerOptions + + beforeEach(() => { + options = getBitbucketResponseOptions({ + port: MOCK_SERVER_PORT, + pullRequestResponses: pullRequests, + }) + mockServer = new MockServer(options) + }) + + it('should store all pull requests for start date 01-02-2020', async () => { + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/date-filter/git-fetcher-config-valid-start-01-02-2020.yml`, + }, + } + ) + expect(mockServer.getNumberOfRequests()).toEqual(1) + verifyPrRequest(mockServer, requestUrlPullRequests, authMethod) + await verifyOutputFile( + env.evidence_path, + true, + JSON.stringify(pullRequests) + ) + expect(result.exitCode).to.equal(0) + }) + + it('should store only pull requests between 01-06-2020 and today', async () => { + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/date-filter/git-fetcher-config-valid-start-01-06-2020.yml`, + }, + } + ) + expect(mockServer.getNumberOfRequests()).toEqual(1) + verifyPrRequest(mockServer, requestUrlPullRequests, authMethod) + await verifyOutputFile( + env.evidence_path, + true, + JSON.stringify([pullRequests[1], pullRequests[2]]) + ) + expect(result.exitCode).to.equal(0) + }) + + it('should store pull requests between 01-06-2020 and 31-12-2022', async () => { + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/date-filter/git-fetcher-config-valid-start-01-06-2020-end-31-12-2022.yml`, + }, + } + ) + expect(mockServer.getNumberOfRequests()).toEqual(1) + verifyPrRequest(mockServer, requestUrlPullRequests, authMethod) + await verifyOutputFile( + env.evidence_path, + true, + JSON.stringify([pullRequests[1]]) + ) + expect(result.exitCode).to.equal(0) + }) + + it('should store pull requests between 01-06-2020 and 31-12-2023', async () => { + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/date-filter/git-fetcher-config-valid-start-01-06-2020-end-31-12-2023.yml`, + }, + } + ) + expect(mockServer.getNumberOfRequests()).toEqual(1) + verifyPrRequest(mockServer, requestUrlPullRequests, authMethod) + await verifyOutputFile( + env.evidence_path, + true, + JSON.stringify([pullRequests[1], pullRequests[2]]) + ) + expect(result.exitCode).to.equal(0) + }) + + it('should not store pull requests for date filter from 01-01-2019 to 31-12-2019', async () => { + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/date-filter/git-fetcher-config-valid-start-01-01-2019-end-31-12-2019.yml`, + }, + } + ) + expect(mockServer.getNumberOfRequests()).toEqual(1) + verifyPrRequest(mockServer, requestUrlPullRequests, authMethod) + await verifyOutputFile(env.evidence_path, true, JSON.stringify([])) + expect(result.exitCode).to.equal(0) + }) + + it('should not store pull requests for date filter from 01-01-2024 to 31-12-2024', async () => { + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/date-filter/git-fetcher-config-valid-start-01-01-2024-end-31-12-2024.yml`, + }, + } + ) + expect(mockServer.getNumberOfRequests()).toEqual(1) + verifyPrRequest(mockServer, requestUrlPullRequests, authMethod) + await verifyOutputFile(env.evidence_path, true, JSON.stringify([])) + expect(result.exitCode).to.equal(0) + }) + }) + }) + + describe('Error Cases', () => { + let options: MockServerOptions + + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: 'token', + GIT_FETCHER_SERVER_TYPE: 'bitbucket', + GIT_FETCHER_API_TOKEN: 'someToken', + } + + beforeEach(() => { + options = { + port: MOCK_SERVER_PORT, + https: true, + responses: { + [PULL_REQUESTS_ENDPOINT]: { + get: { + responseStatus: 400, + }, + }, + }, + } + mockServer = new MockServer(options) + }) + + it('should fail for invalid startDate', async () => { + await verifyErrorCase( + mockServer, + gitFetcherExecutable, + { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/date-filter/git-fetcher-config-invalid-start-date.yml`, + }, + '{"status":"FAILED","reason":"Validation error: date must match the format dd-mm-yyyy at \\"filter.startDate\\""}', + 'expected' + ) + expect(mockServer.getNumberOfRequests()).toEqual(0) + }) + + it('should fail for invalid endDate', async () => { + await verifyErrorCase( + mockServer, + gitFetcherExecutable, + { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/date-filter/git-fetcher-config-invalid-end-date.yml`, + }, + '{"status":"FAILED","reason":"Validation error: date must match the format dd-mm-yyyy at \\"filter.endDate\\""}', + 'expected' + ) + expect(mockServer.getNumberOfRequests()).toEqual(0) + }) + + it('should fail if startDate is after endDate', async () => { + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: 'token', + GIT_FETCHER_SERVER_TYPE: 'bitbucket', + GIT_FETCHER_API_TOKEN: 'someToken', + } + + await verifyErrorCase( + mockServer, + gitFetcherExecutable, + { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/date-filter/git-fetcher-config-invalid-start-date-after-end-date.yml`, + }, + '{"status":"FAILED","reason":"Validation error: filter.endDate must be after or equal filter.startDate at \\"filter\\""}', + 'expected' + ) + expect(mockServer.getNumberOfRequests()).toEqual(0) + }) + + it('should fail if endDate is provided but startDate is missing', async () => { + await verifyErrorCase( + mockServer, + gitFetcherExecutable, + { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/date-filter/git-fetcher-config-invalid-start-date-missing.yml`, + }, + '{"status":"FAILED","reason":"Validation error: Specify filter.startDate if filter.endDate is provided at \\"filter\\""}', + 'expected' + ) + expect(mockServer.getNumberOfRequests()).toEqual(0) + }) + }) +}) diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-environment.int-spec.ts b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-environment.int-spec.ts new file mode 100644 index 00000000..6eeac548 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-environment.int-spec.ts @@ -0,0 +1,167 @@ +import * as fs from 'fs' +import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { + MockServer, + MockServerOptions, +} from '../../../../integration-tests/src/util' +import { + defaultEnvironment, + gitFetcherExecutable, + MOCK_SERVER_PORT, + verifyErrorCase, +} from './utils' +import { getGitPullRequestsMockOptions } from './fixtures/getGitPullRequestsMockServerResponse' + +describe('Environment', () => { + let mockServer: MockServer | undefined + + beforeAll(() => { + expect(fs.existsSync(gitFetcherExecutable)).toBe(true) + }) + + beforeEach(() => { + fs.mkdirSync(`${defaultEnvironment.evidence_path}`) + }) + + afterEach(async () => { + fs.rmSync(`${defaultEnvironment.evidence_path}`, { + recursive: true, + }) + await mockServer?.stop() + }) + + describe('Error Cases', () => { + it('env variable NODE_TLS_REJECT_UNAUTHORIZED is set to 0', async () => { + const options: MockServerOptions = getGitPullRequestsMockOptions( + MOCK_SERVER_PORT, + 400 + ) + + const env = { + ...defaultEnvironment, + NODE_TLS_REJECT_UNAUTHORIZED: '0', + GIT_FETCHER_SERVER_AUTH_METHOD: 'token', + GIT_FETCHER_SERVER_TYPE: 'github', + GIT_FETCHER_API_TOKEN: 'someToken', + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-bitbucket.yml`, + } + + mockServer = new MockServer(options) + + await verifyErrorCase( + mockServer, + gitFetcherExecutable, + env, + '{"status":"FAILED","reason":"NODE_TLS_REJECT_UNAUTHORIZED environment variable is set to 0 which is not allowed"}', + 'expected' + ) + expect(mockServer.getNumberOfRequests()).toEqual(0) + }) + + it('env variable GIT_FETCHER_SERVER_TYPE is undefined', async () => { + const options: MockServerOptions = getGitPullRequestsMockOptions( + MOCK_SERVER_PORT, + 400 + ) + + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: 'token', + GIT_FETCHER_SERVER_TYPE: undefined, + GIT_FETCHER_API_TOKEN: 'someToken', + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-bitbucket.yml`, + } + + mockServer = new MockServer(options) + + await verifyErrorCase( + mockServer, + gitFetcherExecutable, + env, + '{"status":"FAILED","reason":"GIT_FETCHER_SERVER_TYPE environment variable is not set\\nThe server type \\"undefined\\" is not supported"}', + 'expected' + ) + expect(mockServer.getNumberOfRequests()).toEqual(0) + }) + + it('env variable GIT_FETCHER_SERVER_TYPE has unsupported type', async () => { + const options: MockServerOptions = getGitPullRequestsMockOptions( + MOCK_SERVER_PORT, + 400 + ) + + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: 'token', + GIT_FETCHER_SERVER_TYPE: 'unsupported_server_type', + GIT_FETCHER_API_TOKEN: 'someToken', + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-bitbucket.yml`, + } + + mockServer = new MockServer(options) + + await verifyErrorCase( + mockServer, + gitFetcherExecutable, + env, + '{"status":"FAILED","reason":"The server type \\"unsupported_server_type\\" is not supported"}', + 'expected' + ) + expect(mockServer.getNumberOfRequests()).toEqual(0) + }) + + it('env variable GIT_FETCHER_SERVER_API_URL is undefined', async () => { + const options: MockServerOptions = getGitPullRequestsMockOptions( + MOCK_SERVER_PORT, + 400 + ) + + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_API_URL: undefined, + GIT_FETCHER_SERVER_AUTH_METHOD: 'token', + GIT_FETCHER_SERVER_TYPE: 'github', + GIT_FETCHER_API_TOKEN: 'someToken', + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-bitbucket.yml`, + } + + mockServer = new MockServer(options) + + await verifyErrorCase( + mockServer, + gitFetcherExecutable, + env, + '{"status":"FAILED","reason":"GIT_FETCHER_SERVER_API_URL environment variable is not set.' + +'\\nGIT_FETCHER_SERVER_API_URL environment variable must use secured connections with https"}', + 'expected' + ) + expect(mockServer.getNumberOfRequests()).toEqual(0) + }) + + it('should fail for insecure http connection', async () => { + const options: MockServerOptions = getGitPullRequestsMockOptions( + MOCK_SERVER_PORT, + 400 + ) + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_API_URL: `http://localhost:${MOCK_SERVER_PORT}`, + GIT_FETCHER_SERVER_AUTH_METHOD: 'token', + GIT_FETCHER_SERVER_TYPE: 'bitbucket', + GIT_FETCHER_API_TOKEN: 'someToken', + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-bitbucket.yml`, + } + + mockServer = new MockServer(options) + + await verifyErrorCase( + mockServer, + gitFetcherExecutable, + env, + '{"status":"FAILED","reason":"GIT_FETCHER_SERVER_API_URL environment variable must use secured connections with https"}', + 'expected' + ) + expect(mockServer.getNumberOfRequests()).toEqual(0) + }) + }) +}) diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-github-commits-metadata-and-diff.int-spec.ts b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-github-commits-metadata-and-diff.int-spec.ts new file mode 100644 index 00000000..bd788fe1 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-github-commits-metadata-and-diff.int-spec.ts @@ -0,0 +1,272 @@ +import * as fs from 'fs' +import { IncomingHttpHeaders } from 'http' +import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { + MOCK_SERVER_CERT_PATH, + MockServer, + MockServerOptions, + ReceivedRequest, + run, + RunProcessResult, +} from '../../../../integration-tests/src/util' +import { CommitsMetadataAndDiff } from '../../src/model/commits-metadata-and-diff' +import { + getGitCommitsMetadataAndDiffErrorMockServerResponse, + getGitCommitsMetadataAndDiffMockServerResponse, +} from './fixtures/getGithubCommitsMetadataAndDiffMockServerResponse' + +describe('Fetch Commits Metadata from Github', () => { + const MOCK_SERVER_PORT = 8080 + const githubStartCommitEndpoint = + '/repos/aquatest/github-fetcher-test-repo/commits/afeaebf412c6d0b865a36cfdec37fdb46c0fab63' + const githubEndCommitEndpoint = + '/repos/aquatest/github-fetcher-test-repo/commits/8036cf75f4b7365efea76cbd716ef12d352d7d29' + const githubDiffEndpoint = + '/repos/aquatest/github-fetcher-test-repo/compare/afeaebf412c6d0b865a36cfdec37fdb46c0fab63...8036cf75f4b7365efea76cbd716ef12d352d7d29' + const githubCommitsMetadataEndpoint = + '/repos/aquatest/github-fetcher-test-repo/commits' + + const environment = { + evidence_path: `${__dirname}/evidence`, + NODE_EXTRA_CA_CERTS: MOCK_SERVER_CERT_PATH, + GIT_FETCHER_SERVER_API_URL: `https://localhost:${MOCK_SERVER_PORT}`, + GIT_FETCHER_SERVER_TYPE: 'github', + GIT_FETCHER_API_TOKEN: 'someToken', + } as const + + const gitFetcherExecutable = `${__dirname}/../../dist/index.js` + let mockServerOptions: MockServerOptions + let mockServer: MockServer + + beforeAll(() => { + expect(fs.existsSync(gitFetcherExecutable)).toBe(true) + }) + + beforeEach(() => { + fs.mkdirSync(environment.evidence_path) + }) + + afterEach(async () => { + await mockServer?.stop() + + fs.rmSync(environment.evidence_path, { + recursive: true, + }) + }) + + it('should successfully fetch all three pages of commits', async () => { + mockServerOptions = + getGitCommitsMetadataAndDiffMockServerResponse(MOCK_SERVER_PORT) + mockServer = new MockServer(mockServerOptions) + const result: RunProcessResult = await run(gitFetcherExecutable, [], { + env: { + ...environment, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-fetch-commits-metadata-and-diff-from-github.yml`, + }, + }) + + // verify process result + expect(result.exitCode).toEqual(0) + expect(result.stdout).toHaveLength(6) + expect(result.stdout[0]).toEqual( + 'Fetched metadata about starting commit at 2023-03-06T14:11:29Z' + ) + expect(result.stdout[1]).toEqual( + 'Fetched metadata about ending commit at 2023-07-12T10:46:50Z' + ) + expect(result.stdout[2]).toEqual( + 'Fetched 2 lines added and 98 lines removed' + ) + expect(result.stdout[3]).toEqual('Fetched metadata about 5 commits') + expect(result.stdout[4]).toEqual( + 'Fetch from https://localhost:8080 was successful with config {"org":"aquatest","repo":"github-fetcher-test-repo","resource":"metadata-and-diff","filter":{"startHash":"afeaebf412c6d0b865a36cfdec37fdb46c0fab63","endHash":"8036cf75f4b7365efea76cbd716ef12d352d7d29"},"filePath":"apps/git-fetcher/src/fetchers/git-fetcher.ts"}' + ) + expect(result.stdout[5]).toEqual( + '{"output":{"git-fetcher-result":"git-fetcher-data.json"}}' + ) + expect(result.stderr).toHaveLength(0) + + // verify requests + expect(mockServer.getNumberOfRequests()).toEqual(7) + + let requests: ReceivedRequest[] = mockServer.getRequests( + githubStartCommitEndpoint, + 'get' + ) + expect(requests).toHaveLength(1) + verifyHeaders(requests[0].headers) + + requests = mockServer.getRequests(githubEndCommitEndpoint, 'get') + expect(requests).toHaveLength(1) + verifyHeaders(requests[0].headers) + + requests = mockServer.getRequests(githubDiffEndpoint, 'get') + expect(requests).toHaveLength(1) + verifyHeaders(requests[0].headers) + + requests = mockServer.getRequests(githubCommitsMetadataEndpoint, 'get') + expect(requests).toHaveLength(4) + verifyHeaders(requests[0].headers) + verifyHeaders(requests[1].headers) + verifyHeaders(requests[2].headers) + verifyHeaders(requests[3].headers) + + // verify output file + const outputFilePath = `${environment.evidence_path}/git-fetcher-data.json` + expect(fs.existsSync(outputFilePath)).toEqual(true) + const actualFileContent: string = fs.readFileSync(outputFilePath, { + encoding: 'utf-8', + }) + const expectedFileContent: CommitsMetadataAndDiff = { + commitsMetadata: [], + diff: [], + } + expectedFileContent.commitsMetadata = [ + mockServerOptions.responses[githubCommitsMetadataEndpoint].get[0] + .responseBody[0].commit, + mockServerOptions.responses[githubCommitsMetadataEndpoint].get[0] + .responseBody[1].commit, + mockServerOptions.responses[githubCommitsMetadataEndpoint].get[1] + .responseBody[0].commit, + mockServerOptions.responses[githubCommitsMetadataEndpoint].get[1] + .responseBody[1].commit, + mockServerOptions.responses[githubCommitsMetadataEndpoint].get[2] + .responseBody[0].commit, + ] + expectedFileContent.diff = { + linesAdded: [ + '\n+export interface GitFetcher {', + '\n+ fetchResource(): Promise', + ], + linesRemoved: [ + "\n-import { GitServerConfig } from '../model/git-server-config'", + "\n-import { ConfigFileData } from '../model/config-file-data'", + "\n-import { handleResponseStatus } from '../utils/handle-response-status.js'", + '\n-', + '\n-export class GitFetcher {', + '\n- constructor(public env: GitServerConfig, public config: ConfigFileData) {}', + '\n-', + '\n- public pullRequestValidInputs = [', + "\n- 'pull-request',", + "\n- 'pull-requests',", + "\n- 'pr',", + "\n- 'prs',", + "\n- 'pullrequest',", + "\n- 'pullrequests',", + "\n- 'pull',", + "\n- 'pulls',", + '\n- ]', + '\n-', + '\n- public validateResourceName(resource: string) {', + "\n- if (this.env.gitServerType == 'bitbucket') {", + '\n- if (this.pullRequestValidInputs.includes(resource.toLocaleLowerCase())) {', + "\n- return 'pull-requests'", + '\n- } else {', + '\n- throw new Error(`${resource} resource name not valid`)', + '\n- }', + "\n- } else if (this.env.gitServerType == 'github') {", + '\n- if (this.pullRequestValidInputs.includes(resource.toLocaleLowerCase())) {', + "\n- return 'pulls'", + '\n- } else {', + '\n- throw new Error(`${resource} resource name not valid`)', + '\n- }', + '\n- } else {', + '\n- throw new Error(`${this.env.gitServerType} server type not supported`)', + '\n- }', + '\n- }', + '\n-', + '\n- public async getOptions() {', + "\n- if (this.env.gitServerAuthMethod.toLocaleLowerCase() == 'basic') {", + '\n- const options = {', + "\n- method: 'GET',", + '\n- auth: {', + '\n- username: this.env.gitServerUsername,', + '\n- password: this.env.gitServerPassword,', + '\n- },', + '\n- headers: {', + "\n- Accept: 'application/vnd.github+json',", + '\n- },', + '\n- }', + '\n- return options', + "\n- } else if (this.env.gitServerAuthMethod.toLocaleLowerCase() == 'token') {", + '\n- const options = {', + "\n- method: 'GET',", + '\n- headers: {', + "\n- Accept: 'application/json',", + '\n- Authorization: `Bearer ${this.env.gitServerApiToken}`,', + '\n- },', + '\n- }', + '\n- return options', + '\n- } else {', + "\n- throw new Error('No valid auth method provided')", + '\n- }', + '\n- }', + '\n-', + '\n- public async buildUrl(resource: string) {', + '\n- const resourceName = this.validateResourceName(resource)', + "\n- if (this.env.gitServerType == 'bitbucket') {", + '\n- const endpoint = `${this.env.gitServerApiUrl.replace(', + '\n- //*$/,', + "\n- ''", + '\n- )}/projects/${this.config.data.org}/repos/${', + '\n- this.config.data.repo', + '\n- }/${resourceName}?state=ALL`', + '\n- return endpoint', + "\n- } else if (this.env.gitServerType == 'github') {", + "\n- const endpoint = `${this.env.gitServerApiUrl.replace(//*$/, '')}/repos/${", + '\n- this.config.data.org', + '\n- }/${this.config.data.repo}/${resourceName}?state=all&per_page=100`', + '\n- return endpoint', + '\n- } else {', + '\n- throw new Error(`${this.env.gitServerType} server type not supported`)', + '\n- }', + '\n- }', + '\n-', + '\n- public async runQuery() {', + '\n- const url = await this.buildUrl(this.config.data.resource)', + '\n- const options = await this.getOptions()', + '\n- try {', + '\n- const response = await fetch(url, options)', + '\n- if (response.status != 200) {', + '\n- handleResponseStatus(response.status)', + '\n- }', + '\n- return response.json()', + '\n- } catch (error: any) {', + '\n- throw new Error(', + '\n- `Got the following error when running Git fetcher: ${error.message}`', + '\n- )', + '\n- }', + '\n- }', + ], + } + expect(actualFileContent).toStrictEqual(JSON.stringify(expectedFileContent)) + }) + + it('should throw an error, if bitbucket returns 404 NOT FOUND error', async () => { + mockServerOptions = + getGitCommitsMetadataAndDiffErrorMockServerResponse(MOCK_SERVER_PORT) + mockServer = new MockServer(mockServerOptions) + + const result: RunProcessResult = await run(gitFetcherExecutable, [], { + env: { + ...environment, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-fetch-commits-metadata-and-diff-from-github.yml`, + }, + }) + + // gitfetcher should throw error + expect(result.exitCode).toEqual(1) + expect(result.stderr).toContain( + 'Error: Repository not found. Status code: 404' + ) + + // gitfetcher should not write an output file + const outputFilePath = `${environment.evidence_path}/git-fetcher-data.json` + expect(fs.existsSync(outputFilePath)).toEqual(false) + }) +}) + +function verifyHeaders(headers: IncomingHttpHeaders): void { + expect(headers.accept).toEqual('application/json') + expect(headers.authorization).toEqual('Bearer someToken') +} diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-hash-filter.int-spec.ts b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-hash-filter.int-spec.ts new file mode 100644 index 00000000..85fadf8e --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-hash-filter.int-spec.ts @@ -0,0 +1,313 @@ +import * as fs from 'fs' +import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { + MockServer, + MockServerOptions, + run, + RunProcessResult, +} from '../../../../integration-tests/src/util' +import { BitbucketCommit } from '../../src/model/bitbucket-commit' +import { + bitBucketPrs, + createBitbucketCommits, + getBitbucketResponseOptions, + PULL_REQUESTS_ENDPOINT, + requestUrlCommit, +} from './fixtures/getBitbucketResponseOptions' +import { + defaultEnvironment, + gitFetcherExecutable, + MOCK_SERVER_PORT, + verifyCommitRequest, + verifyErrorCase, + verifyOutputFile, + verifyPrRequest, +} from './utils' + +describe('Hash Filter', () => { + let mockServer: MockServer | undefined + + beforeAll(() => { + expect(fs.existsSync(gitFetcherExecutable)).toBe(true) + }) + + beforeEach(() => { + fs.mkdirSync(`${defaultEnvironment.evidence_path}`) + }) + + afterEach(async () => { + fs.rmSync(`${defaultEnvironment.evidence_path}`, { + recursive: true, + }) + await mockServer?.stop() + }) + + describe('Success Cases', () => { + describe('For Bitbucket', () => { + const authMethod = 'token' + const serverType = 'bitbucket' + + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: authMethod, + GIT_FETCHER_SERVER_TYPE: serverType, + GIT_FETCHER_API_TOKEN: 'someToken', + } + + it('should filter pull requests by startHash and endHash - outputs all pull requests', async () => { + const commitResponses = createBitbucketCommits( + new Date('2020-02-01'), + new Date('2023-03-15') + ) + const options: MockServerOptions = getBitbucketResponseOptions({ + port: MOCK_SERVER_PORT, + pullRequestResponses: bitBucketPrs, + commitResponses: commitResponses, + }) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/hash-filter/git-fetcher-config-valid-start-and-end-hash.yml`, + }, + } + ) + + expect(mockServer.getNumberOfRequests()).toEqual(3) + verifyPrRequest(mockServer, PULL_REQUESTS_ENDPOINT, authMethod) + verifyCommitRequest( + mockServer, + requestUrlCommit(commitResponses[0].id), + authMethod + ) + verifyCommitRequest( + mockServer, + requestUrlCommit(commitResponses[1].id), + authMethod + ) + await verifyOutputFile( + env.evidence_path, + true, + JSON.stringify(bitBucketPrs) + ) + expect(result.exitCode).to.equal(0) + }) + + it('should filter pull requests by startHash and endHash - outputs some pull requests', async () => { + const commitResponses = createBitbucketCommits( + new Date('2020-12-01'), + new Date('2022-05-31') + ) + const options: MockServerOptions = getBitbucketResponseOptions({ + port: MOCK_SERVER_PORT, + pullRequestResponses: bitBucketPrs, + commitResponses: commitResponses, + }) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/hash-filter/git-fetcher-config-valid-start-and-end-hash.yml`, + }, + } + ) + + expect(mockServer.getNumberOfRequests()).toEqual(3) + verifyPrRequest(mockServer, PULL_REQUESTS_ENDPOINT, authMethod) + verifyCommitRequest( + mockServer, + requestUrlCommit(commitResponses[0].id), + authMethod + ) + verifyCommitRequest( + mockServer, + requestUrlCommit(commitResponses[1].id), + authMethod + ) + await verifyOutputFile( + env.evidence_path, + true, + JSON.stringify([bitBucketPrs[1], bitBucketPrs[2]]) + ) + expect(result.exitCode).to.equal(0) + }) + + it('should filter pull requests by startHash and endHash - outputs no pull requests because endHash is earlier than the oldest pull request', async () => { + const commitResponses = createBitbucketCommits( + new Date('2018-12-01'), + new Date('2019-05-31') + ) + const options: MockServerOptions = getBitbucketResponseOptions({ + port: MOCK_SERVER_PORT, + pullRequestResponses: bitBucketPrs, + commitResponses: commitResponses, + }) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/hash-filter/git-fetcher-config-valid-start-and-end-hash.yml`, + }, + } + ) + + expect(mockServer.getNumberOfRequests()).toEqual(3) + verifyPrRequest(mockServer, PULL_REQUESTS_ENDPOINT, authMethod) + verifyCommitRequest( + mockServer, + requestUrlCommit(commitResponses[0].id), + authMethod + ) + verifyCommitRequest( + mockServer, + requestUrlCommit(commitResponses[1].id), + authMethod + ) + await verifyOutputFile(env.evidence_path, true, JSON.stringify([])) + expect(result.exitCode).to.equal(0) + }) + + it('should filter pull requests by startHash and endHash - outputs no pull requests because startHash is after the latest pull request', async () => { + const commitResponses = createBitbucketCommits( + new Date('2023-04-01'), + new Date('2023-05-31') + ) + const options: MockServerOptions = getBitbucketResponseOptions({ + port: MOCK_SERVER_PORT, + pullRequestResponses: bitBucketPrs, + commitResponses: commitResponses, + }) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/hash-filter/git-fetcher-config-valid-start-and-end-hash.yml`, + }, + } + ) + + expect(mockServer.getNumberOfRequests()).toEqual(3) + verifyPrRequest(mockServer, PULL_REQUESTS_ENDPOINT, authMethod) + verifyCommitRequest( + mockServer, + requestUrlCommit(commitResponses[0].id), + authMethod + ) + verifyCommitRequest( + mockServer, + requestUrlCommit(commitResponses[1].id), + authMethod + ) + await verifyOutputFile(env.evidence_path, true, JSON.stringify([])) + expect(result.exitCode).to.equal(0) + }) + + it('should filter pull requests by startHash - outputs all pull requests', async () => { + const commitResponse: BitbucketCommit = createBitbucketCommits( + new Date('2020-01-01'), + new Date() + )[0] + const options: MockServerOptions = getBitbucketResponseOptions({ + port: MOCK_SERVER_PORT, + pullRequestResponses: bitBucketPrs, + commitResponses: [commitResponse], + }) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/hash-filter/git-fetcher-config-valid-start-hash.yml`, + }, + } + ) + + expect(mockServer.getNumberOfRequests()).toEqual(2) + verifyPrRequest(mockServer, PULL_REQUESTS_ENDPOINT, authMethod) + verifyCommitRequest( + mockServer, + requestUrlCommit(commitResponse.id), + authMethod + ) + await verifyOutputFile( + env.evidence_path, + true, + JSON.stringify(bitBucketPrs) + ) + expect(result.exitCode).to.equal(0) + }) + }) + }) + + describe('Error Cases', () => { + describe('For Bitbucket', async () => { + let options: MockServerOptions + const authMethod = 'token' + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: authMethod, + GIT_FETCHER_SERVER_TYPE: 'bitbucket', + GIT_FETCHER_API_TOKEN: 'someToken', + } + + it('should fail if commit hash could not be found', async () => { + options = getBitbucketResponseOptions({ + port: MOCK_SERVER_PORT, + pullRequestResponses: [ + { + id: 1, + state: 'MERGED', + updatedDate: 1559347200000, // 01-06-2019 , + }, + { + id: 2, + state: 'MERGED', + updatedDate: 1625097600000, // 01-07-2021 , + }, + ], + }) + options.responses[ + requestUrlCommit('c11631a0ddccb9579feae43b949b53c369528f43') + ] = { get: { responseStatus: 404 } } + mockServer = new MockServer(options) + + await verifyErrorCase( + mockServer, + gitFetcherExecutable, + { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/hash-filter/git-fetcher-config-valid-state-and-hash.yml`, + }, + '{"status":"FAILED","reason":"Could not retrieve the commit hash c11631a0ddccb9579feae43b949b53c369528f43 (status 404)"}', + 'expected' + ) + expect(mockServer.getNumberOfRequests()).toEqual(2) + await verifyPrRequest( + mockServer, + PULL_REQUESTS_ENDPOINT, + authMethod, + 'MERGED' + ) + }) + }) + }) +}) diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-label-filter.int-spec.ts b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-label-filter.int-spec.ts new file mode 100644 index 00000000..2ea98bbe --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-label-filter.int-spec.ts @@ -0,0 +1,64 @@ +import * as fs from 'fs' +import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { + MockServer, + MockServerOptions, + run, + RunProcessResult, +} from '../../../../integration-tests/src/util' +import { getGitPullRequestsMockOptions } from './fixtures/getGitPullRequestsMockServerResponse' +import { + defaultEnvironment, + gitFetcherExecutable, + MOCK_SERVER_PORT, + verifyOutputFile, +} from './utils' + +describe('Label Filter', () => { + let mockServer: MockServer | undefined + + beforeAll(() => { + expect(fs.existsSync(gitFetcherExecutable)).toBe(true) + }) + + beforeEach(() => { + fs.mkdirSync(`${defaultEnvironment.evidence_path}`) + }) + + afterEach(async () => { + fs.rmSync(`${defaultEnvironment.evidence_path}`, { + recursive: true, + }) + await mockServer?.stop() + }) + + describe('Success Cases', () => { + const options: MockServerOptions = getGitPullRequestsMockOptions( + MOCK_SERVER_PORT, + 200 + ) + + it('should fetch pull requests from GitHub, but should return empty string, when label filter does not match pr-labels', async () => { + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: 'token', + GIT_FETCHER_SERVER_TYPE: 'github', + GIT_FETCHER_API_TOKEN: 'someToken', + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/git-fetcher-config-github-wrong-label.yml`, + } + + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: env, + } + ) + + await verifyOutputFile(env.evidence_path, true, JSON.stringify([])) + expect(result.exitCode).to.equal(0) + }) + }) +}) diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-state-filter.int-spec.ts b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-state-filter.int-spec.ts new file mode 100644 index 00000000..fc477c00 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-state-filter.int-spec.ts @@ -0,0 +1,289 @@ +import * as fs from 'fs' +import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { + MockServer, + MockServerOptions, + run, + RunProcessResult, +} from '../../../../integration-tests/src/util' +import { + allowedFilterState, + AllowedFilterStateType, +} from '../../src/model/config-file-data' +import { BitbucketPr } from '../../src/model/bitbucket-pr' +import { + defaultEnvironment, + gitFetcherExecutable, + MOCK_SERVER_PORT, + verifyCommitRequest, + verifyErrorCase, + verifyOutputFile, + verifyPrRequest, +} from './utils' +import { BitbucketCommit } from '../../src/model/bitbucket-commit' +import { + getBitbucketResponseOptions, + PULL_REQUESTS_ENDPOINT, +} from './fixtures/getBitbucketResponseOptions' + +describe('State Filter', () => { + let mockServer: MockServer | undefined + + const requestUrlPullRequests = + '/projects/aquatest/repos/bitbucket-fetcher-test-repo/pull-requests' + + const requestUrlCommit = (hash: string) => + `/projects/aquatest/repos/bitbucket-fetcher-test-repo/commits/${hash}` + + beforeAll(() => { + expect(fs.existsSync(gitFetcherExecutable)).toBe(true) + }) + + beforeEach(() => { + fs.mkdirSync(`${defaultEnvironment.evidence_path}`) + }) + + afterEach(async () => { + fs.rmSync(`${defaultEnvironment.evidence_path}`, { + recursive: true, + }) + await mockServer?.stop() + }) + + describe('Success Cases', () => { + describe('For Bitbucket', () => { + const authMethod = 'token' + const serverType = 'bitbucket' + + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: authMethod, + GIT_FETCHER_SERVER_TYPE: serverType, + GIT_FETCHER_API_TOKEN: 'someToken', + } as const + + it.each(allowedFilterState)( + 'should store only pull requests with the state %s from bitbucket', + async (state: AllowedFilterStateType) => { + const responses: Record = { + DECLINED: [ + { id: 1, state: 'DECLINED', updatedDate: undefined }, + { id: 2, state: 'DECLINED', updatedDate: undefined }, + ], + MERGED: [ + { id: 3, state: 'MERGED', updatedDate: undefined }, + { id: 4, state: 'MERGED', updatedDate: undefined }, + { id: 5, state: 'MERGED', updatedDate: undefined }, + ], + OPEN: [{ id: 6, state: 'OPEN', updatedDate: undefined }], + ALL: [ + { id: 1, state: 'DECLINED', updatedDate: undefined }, + { id: 2, state: 'DECLINED', updatedDate: undefined }, + { id: 3, state: 'MERGED', updatedDate: undefined }, + { id: 4, state: 'MERGED', updatedDate: undefined }, + { id: 5, state: 'MERGED', updatedDate: undefined }, + { id: 6, state: 'OPEN', updatedDate: undefined }, + ], + } + + const options: MockServerOptions = getBitbucketResponseOptions({ + port: MOCK_SERVER_PORT, + pullRequestResponses: responses[state], + }) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/state-filter/git-fetcher-config-bitbucket-${state}.yml`, + }, + } + ) + + expect(mockServer.getNumberOfRequests()).toEqual(1) + verifyPrRequest(mockServer, requestUrlPullRequests, authMethod, state) + await verifyOutputFile( + env.evidence_path, + true, + JSON.stringify(responses[state]) + ) + expect(result.exitCode).to.equal(0) + } + ) + + it('should filter pull requests if state filter with date filter is used in combination ', async () => { + const filterState: AllowedFilterStateType = 'MERGED' + const responses: BitbucketPr[] = [ + { + id: 1, + state: 'MERGED', + updatedDate: 1559347200000 /* 01-06-2019 */, + }, + { + id: 2, + state: 'MERGED', + updatedDate: 1625097600000 /* 01-07-2021 */, + }, + { + id: 3, + state: 'MERGED', + updatedDate: 1647306000000 /* 15-03-2022 */, + }, + { + id: 4, + state: 'MERGED', + updatedDate: 1685577600000 /* 01-06-2023 */, + }, + ] + + const options: MockServerOptions = getBitbucketResponseOptions({ + port: MOCK_SERVER_PORT, + pullRequestResponses: responses, + }) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/date-filter/git-fetcher-config-valid-state-and-date.yml`, + }, + } + ) + + expect(mockServer.getNumberOfRequests()).toEqual(1) + verifyPrRequest( + mockServer, + requestUrlPullRequests, + authMethod, + filterState + ) + await verifyOutputFile( + env.evidence_path, + true, + JSON.stringify([responses[1], responses[2]]) + ) + expect(result.exitCode).to.equal(0) + }) + + it('should filter pull requests if state filter with hash filter is used in combination ', async () => { + const filterState: AllowedFilterStateType = 'MERGED' + + const pullRequestResponses: BitbucketPr[] = [ + { + id: 1, + state: filterState, + updatedDate: 1559347200000, // 01-06-2019 , + }, + { + id: 2, + state: filterState, + updatedDate: 1625097600000, // 01-07-2021 , + }, + { + id: 3, + state: filterState, + updatedDate: 1647306000000, // 15-03-2022 , + }, + { + id: 4, + state: filterState, + updatedDate: 1685577600000, // 01-06-2023 , + }, + ] + + const commitResponses: BitbucketCommit[] = [ + { + id: 'c11631a0ddccb9579feae43b949b53c369528f43', + committerTimestamp: 1643670000000, // 01-02-2022 + }, + { + id: 'a71631a0dcccb957afeae43b949b53c369528f4f', + committerTimestamp: 1686780000000, // 15-06-2023 + }, + ] + + const options: MockServerOptions = getBitbucketResponseOptions({ + port: MOCK_SERVER_PORT, + pullRequestResponses: pullRequestResponses, + commitResponses: commitResponses, + }) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/hash-filter/git-fetcher-config-valid-state-and-hash.yml`, + }, + } + ) + + expect(mockServer.getNumberOfRequests()).toEqual(3) + verifyPrRequest( + mockServer, + requestUrlPullRequests, + authMethod, + filterState + ) + verifyCommitRequest( + mockServer, + requestUrlCommit(commitResponses[0].id), + authMethod + ) + verifyCommitRequest( + mockServer, + requestUrlCommit(commitResponses[1].id), + authMethod + ) + await verifyOutputFile( + env.evidence_path, + true, + JSON.stringify([pullRequestResponses[2], pullRequestResponses[3]]) + ) + expect(result.exitCode).to.equal(0) + }) + }) + }) + + describe('Error Cases', () => { + it('should fail for invalid filter state', async () => { + const options: MockServerOptions = { + port: MOCK_SERVER_PORT, + https: true, + responses: { + [PULL_REQUESTS_ENDPOINT]: { + get: { + responseStatus: 400, + }, + }, + }, + } + mockServer = new MockServer(options) + + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: 'token', + GIT_FETCHER_SERVER_TYPE: 'bitbucket', + GIT_FETCHER_API_TOKEN: 'someToken', + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/state-filter/git-fetcher-config-bitbucket-INVALID.yml`, + } + + await verifyErrorCase( + mockServer, + gitFetcherExecutable, + env, + '{"status":"FAILED","reason":"Validation error: Invalid enum value. Expected \'DECLINED\' | \'MERGED\' | \'OPEN\' | \'ALL\', received \'INVALID_STATE\' at \\"filter.state\\""}', + 'expected' + ) + expect(mockServer.getNumberOfRequests()).toEqual(0) + }) + }) +}) diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-tag-filter.int-spec.ts b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-tag-filter.int-spec.ts new file mode 100644 index 00000000..bf177940 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/git-fetcher-tag-filter.int-spec.ts @@ -0,0 +1,419 @@ +import * as fs from 'fs' +import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { + MockServer, + MockServerOptions, + ReceivedRequest, + run, + RunProcessResult, +} from '../../../../integration-tests/src/util' +import { BitbucketCommit } from '../../src/model/bitbucket-commit' +import { BitbucketTag } from '../../src/model/bitbucket-tag' +import { + bitBucketPrs, + createBitbucketCommits, + createBitbucketTags, + getBitbucketResponseOptions, + PULL_REQUESTS_ENDPOINT, + requestUrlCommit, + requestUrlTag, +} from './fixtures/getBitbucketResponseOptions' +import { + defaultEnvironment, + gitFetcherExecutable, + MOCK_SERVER_PORT, + verifyAuthorizationHeader, + verifyOutputFile, +} from './utils' + +describe('Tag Filter', () => { + let mockServer: MockServer + + beforeAll(() => { + expect(fs.existsSync(gitFetcherExecutable)).toBe(true) + }) + + beforeEach(() => { + fs.mkdirSync(defaultEnvironment.evidence_path) + }) + + afterEach(async () => { + fs.rmSync(defaultEnvironment.evidence_path, { + recursive: true, + }) + await mockServer?.stop() + }) + + describe('Success Cases', () => { + describe('For Bitbucket', () => { + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: 'token', + GIT_FETCHER_API_TOKEN: 'someToken', + GIT_FETCHER_SERVER_TYPE: 'bitbucket', + } as const + + /** Contains mock tag mock responses from the mock server */ + let tagResponses: [BitbucketTag, BitbucketTag] + + /** Contains mock commit responses from the mock server */ + let commitResponses: [BitbucketCommit, BitbucketCommit] + + /** + * Starts a {@link MockServer}, which mocks PR responses, commit responses (where the commits have the respective + * dates) and tag responses (which link to those commits). + * Stores {@link tagResponses} and {@link commitResponses} for later use inside the test cases. + * @param firstCommitDate timestamp for the first mocked commit + * @param secondCommitDate timestamp for the second mocked commit + */ + function startBitbucketMockServer( + firstCommitDate: Date, + secondCommitDate: Date + ): void { + commitResponses = createBitbucketCommits( + firstCommitDate, + secondCommitDate + ) + tagResponses = createBitbucketTags(commitResponses) + const options: MockServerOptions = getBitbucketResponseOptions({ + port: MOCK_SERVER_PORT, + pullRequestResponses: bitBucketPrs, + commitResponses: commitResponses, + tagResponses: tagResponses, + }) + mockServer = new MockServer(options) + } + + /** + * Verifies if the expected requests were made to the {@link MockServer}. Makes use of {@link tagResponses} and + * {@link commitResponses} to recreate the correct request URLs so that it can check the requests sent to those + * URLs. + * @param filterByEndTag - if set to true or not defined, verifies that two requests for tags and + * likewise, two requests for commits were sent. If set to false, verifies that one request for tags + * and one request for commits was sent. + */ + function verifyBitbucketRequests( + filterByEndTag = true, + expectedStateFilter = 'ALL' + ): void { + const expectedNumberOfRequests: number = filterByEndTag ? 5 : 3 + expect(mockServer.getNumberOfRequests()).toEqual( + expectedNumberOfRequests + ) + + // verify requests for GET pull requests + let requests: ReceivedRequest[] = mockServer.getRequests( + PULL_REQUESTS_ENDPOINT, + 'get' + ) + expect(requests).length(1) + verifyAuthorizationHeader( + env.GIT_FETCHER_SERVER_AUTH_METHOD, + requests[0] + ) + expect(requests[0].query.state).toEqual(expectedStateFilter) + expect(requests[0].query.start).toEqual('0') + + // verify request for first tag + requests = mockServer.getRequests( + requestUrlTag(tagResponses[0].displayId), + 'get' + ) + expect(requests).length(1) + verifyAuthorizationHeader( + env.GIT_FETCHER_SERVER_AUTH_METHOD, + requests[0] + ) + + // verify request for second tag, if an endTag filter was provided + if (filterByEndTag) { + requests = mockServer.getRequests( + requestUrlTag(tagResponses[1].displayId), + 'get' + ) + expect(requests).length(1) + verifyAuthorizationHeader( + env.GIT_FETCHER_SERVER_AUTH_METHOD, + requests[0] + ) + } + + // verify request for first commit + requests = mockServer.getRequests( + requestUrlCommit(commitResponses[0].id), + 'get' + ) + expect(requests).length(1) + verifyAuthorizationHeader( + env.GIT_FETCHER_SERVER_AUTH_METHOD, + requests[0] + ) + + // verify request for second commit, if an endTag filter was provided + if (filterByEndTag) { + requests = mockServer.getRequests( + requestUrlCommit(commitResponses[1].id), + 'get' + ) + expect(requests).length(1) + verifyAuthorizationHeader( + env.GIT_FETCHER_SERVER_AUTH_METHOD, + requests[0] + ) + } + } + + it('should filter pull requests by startTag and endTag - fetches and writes all PRs', async () => { + startBitbucketMockServer( + new Date('2020-02-01'), + new Date('22023-03-15') + ) + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/tag-filter/git-fetcher-config-valid-start-and-end-tag.yml`, + }, + } + ) + + // verify result + expect(result.exitCode).toEqual(0) + expect(result.stderr).length(0) + expect(result.stdout[0]).toEqual('Fetched 4 pull requests') + + // should write all PRs to the output file + await verifyOutputFile( + env.evidence_path, + true, + JSON.stringify(bitBucketPrs) + ) + + // verify requests + verifyBitbucketRequests() + }) + + it('should filter pull requests by startTag and endTag - fetches and writes no PRs, because the endTag references a commit which is older than the oldest PR', async () => { + startBitbucketMockServer(new Date('2018-12-01'), new Date('2019-05-31')) + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/tag-filter/git-fetcher-config-valid-start-and-end-tag.yml`, + }, + } + ) + + // verify result + expect(result.exitCode).toEqual(0) + expect(result.stderr).length(0) + expect(result.stdout[0]).toEqual('Fetched 0 pull requests') + + // should write empty output file + await verifyOutputFile(env.evidence_path, true, '[]') + + // verify requests + verifyBitbucketRequests() + }) + + it('should filter pull requests by startTag and endTag - fetches and writes no PRs, because startTag references a commit which is younger than the youngest PR', async () => { + startBitbucketMockServer(new Date('2023-04-01'), new Date('2023-05-31')) + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/tag-filter/git-fetcher-config-valid-start-and-end-tag.yml`, + }, + } + ) + + // verify result + expect(result.exitCode).toEqual(0) + expect(result.stderr).length(0) + expect(result.stdout[0]).toEqual('Fetched 0 pull requests') + + // should write empty output file + await verifyOutputFile(env.evidence_path, true, '[]') + + // verify requests + verifyBitbucketRequests() + }) + + it('should filter pull requests by startTag - fetches and writes all PRs, because startTag references a commit which is older than the oldest PR', async () => { + // second date is older than oldest PR, so that the test proves it is not used for the filtering + startBitbucketMockServer(new Date('2020-01-01'), new Date('2020-01-02')) + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/tag-filter/git-fetcher-config-valid-start-tag.yml`, + }, + } + ) + + // verify result + expect(result.exitCode).toEqual(0) + expect(result.stderr).length(0) + expect(result.stdout[0]).toEqual('Fetched 4 pull requests') + + // should write all PRs to the output file + await verifyOutputFile( + env.evidence_path, + true, + JSON.stringify(bitBucketPrs) + ) + + // verify requests + verifyBitbucketRequests(false) + }) + + it('should filter pull requests by startTag and endTag - fetches and writes PRs from years 2021 and 2022', async () => { + startBitbucketMockServer(new Date('2021-01-01'), new Date('2022-12-31')) + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/tag-filter/git-fetcher-config-valid-start-and-end-tag.yml`, + }, + } + ) + + // verify result + expect(result.exitCode).toEqual(0) + expect(result.stderr).length(0) + expect(result.stdout[0]).toEqual('Fetched 2 pull requests') + + // should write two PRs to the output file + await verifyOutputFile( + env.evidence_path, + true, + JSON.stringify([bitBucketPrs[1], bitBucketPrs[2]]) + ) + + // verify requests + verifyBitbucketRequests() + }) + + it('should filter pull requests by startTag, endTag and state - fetches and writes OPEN PRs from year 2022', async () => { + // filtering by state is done in the GET request to bitbucket's PR endpoint, so this is tested by verifying + // the query parameter. For consistent results the tags filter one PR which is OPEN. + startBitbucketMockServer(new Date('2022-01-01'), new Date('2022-12-31')) + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/tag-filter/git-fetcher-config-valid-state-and-tag.yml`, + }, + } + ) + + // verify result + expect(result.exitCode).toEqual(0) + expect(result.stderr).length(0) + expect(result.stdout[0]).toEqual('Fetched 1 pull request') + + // should write two PRs to the output file + await verifyOutputFile( + env.evidence_path, + true, + JSON.stringify([bitBucketPrs[2]]) + ) + + // verify requests - checking on filter state 'OPEN' proves that tag and state filtering work together + verifyBitbucketRequests(true, 'OPEN') + }) + }) + }) + + describe('Error Cases', () => { + describe('For Bitbucket', () => { + const env = { + ...defaultEnvironment, + GIT_FETCHER_SERVER_AUTH_METHOD: 'token', + GIT_FETCHER_API_TOKEN: 'someToken', + GIT_FETCHER_SERVER_TYPE: 'bitbucket', + } as const + + it('should fail if tag could not be found', async () => { + const commitResponses = createBitbucketCommits( + new Date('2020-01-01'), + new Date('2023-05-31') + ) + const options: MockServerOptions = getBitbucketResponseOptions({ + port: MOCK_SERVER_PORT, + pullRequestResponses: bitBucketPrs, + commitResponses: commitResponses, + }) + // should be requested, but fail with error + options.responses[requestUrlTag('tag1')] = { + get: { + responseStatus: 404, + }, + } + // should not be requested + options.responses[requestUrlTag('tag2')] = { + get: { + responseStatus: 404, + }, + } + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + gitFetcherExecutable, + undefined, + { + env: { + ...env, + GIT_FETCHER_CONFIG_FILE_PATH: `${__dirname}/configs/tag-filter/git-fetcher-config-valid-start-and-end-tag.yml`, + }, + } + ) + + // verify result + expect(result.exitCode).toEqual(0) + expect(result.stdout).toContain( + '{"status":"FAILED","reason":"Could not retrieve the tag tag1"}' + ) + + // should not write an output file + await verifyOutputFile(env.evidence_path, false) + + // verify requests + expect(mockServer.getNumberOfRequests()).toEqual(2) + + // verify request for PRs + let requests: ReceivedRequest[] = mockServer.getRequests( + PULL_REQUESTS_ENDPOINT, + 'get' + ) + expect(requests).length(1) + verifyAuthorizationHeader( + env.GIT_FETCHER_SERVER_AUTH_METHOD, + requests[0] + ) + expect(requests[0].query.state).toEqual('ALL') + expect(requests[0].query.start).toEqual('0') + + // verify request for tag 1 + requests = mockServer.getRequests(requestUrlTag('tag1'), 'get') + expect(requests).length(1) + verifyAuthorizationHeader( + env.GIT_FETCHER_SERVER_AUTH_METHOD, + requests[0] + ) + }) + }) + }) +}) diff --git a/yaku-apps-typescript/apps/git-fetcher/test/integration/utils.ts b/yaku-apps-typescript/apps/git-fetcher/test/integration/utils.ts new file mode 100644 index 00000000..706f1d76 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/integration/utils.ts @@ -0,0 +1,93 @@ +import { SupportedAuthMethod } from '../../src/model/git-server-config' +import { AllowedFilterStateType } from '../../src/model/config-file-data' +import { expect } from 'vitest' +import { + MOCK_SERVER_CERT_PATH, + MockServer, + ReceivedRequest, + run, + RunProcessResult, +} from '../../../../integration-tests/src/util' +import { existsSync, promises as fs } from 'fs' + +export const gitFetcherExecutable = `${__dirname}/../../dist/index.js` +export const MOCK_SERVER_PORT = 8080 +export const defaultEnvironment = { + evidence_path: `${__dirname}/evidence`, + GIT_FETCHER_SERVER_API_URL: `https://localhost:${MOCK_SERVER_PORT}`, + NODE_EXTRA_CA_CERTS: MOCK_SERVER_CERT_PATH, +} + +export function verifyPrRequest( + mockServer: MockServer, + requestUrl: string, + authMethod: SupportedAuthMethod, + filterState: AllowedFilterStateType = 'ALL' +): void { + const request: ReceivedRequest = mockServer.getRequests(requestUrl, 'get')[0] + expect(request.query.state).toEqual(filterState) + verifyAuthorizationHeader(authMethod, request) +} + +export function verifyCommitRequest( + mockServer: MockServer, + requestUrl: string, + authMethod: SupportedAuthMethod, + expectedNumberOfRequests = 1 +) { + const requests: ReceivedRequest[] = mockServer.getRequests(requestUrl, 'get') + expect(requests.length).toBe(expectedNumberOfRequests) + requests.forEach((request) => verifyAuthorizationHeader(authMethod, request)) +} + +export function verifyAuthorizationHeader( + authMethod: SupportedAuthMethod, + request: ReceivedRequest +) { + if (authMethod === 'token') { + expect(request.headers.authorization).toEqual('Bearer someToken') + expect(request.headers.accept).toEqual('application/json') + } else if (authMethod === 'basic') { + expect(request.headers.authorization).toEqual('Basic am9objpzZWNyZXQ=') + expect(request.headers.accept).toEqual('application/vnd.github+json') + } +} + +export async function verifyOutputFile( + evidencePath: string | undefined, + shouldFileExist: boolean, + expectedContent?: string +): Promise { + const outputFilePath = `${evidencePath}/git-fetcher-data.json` + + const existsFile = existsSync(outputFilePath) + expect(existsFile).toBe(shouldFileExist) + + if (shouldFileExist) { + const file = await fs.readFile(outputFilePath, { encoding: 'utf8' }) + expect(file).toStrictEqual(expectedContent) + } +} + +export async function verifyErrorCase( + mockServer: MockServer, + gitFetcherExecutable, + env: NodeJS.ProcessEnv, + expectedErrorMessage: string | RegExp, + kind: 'expected' | 'unexpected' +) { + const result: RunProcessResult = await run(gitFetcherExecutable, undefined, { + env: env, + }) + await verifyOutputFile(env.evidence_path, false) + switch (kind) { + case 'expected': + expect(result.stdout[0]).toEqual(expectedErrorMessage) + expect(result.exitCode).to.equal(0) + break + case 'unexpected': + expect(result.stderr).toContain(expectedErrorMessage) + expect(result.exitCode).to.equal(1) + break + } +} diff --git a/yaku-apps-typescript/apps/git-fetcher/test/unit/compare-labels.test.ts b/yaku-apps-typescript/apps/git-fetcher/test/unit/compare-labels.test.ts new file mode 100644 index 00000000..3ab9f600 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/unit/compare-labels.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest' +import { GithubLabel } from '../../src/model/github-label' +import { compareLabels } from '../../src/utils/compare-labels' + +const requiredLabels = ['foo', 'bar'] + +describe('CompareLables', () => { + it('returns true, when all expected labels are part of the fetched labels', () => { + const fetchedLabels: GithubLabel[] = [ + { id: 1, name: 'foo' }, + { id: 2, name: 'bar' }, + ] + + const result: boolean = compareLabels(requiredLabels, fetchedLabels) + expect(result).toBe(true) + }) + + it('returns true, when all expected labels are part of the fetched labels, also when more labels are returned as required.', () => { + const fetchedLabels: GithubLabel[] = [ + { id: 1, name: 'foo' }, + { id: 2, name: 'bar' }, + { id: 3, name: 'fooBar' }, + ] + + const result: boolean = compareLabels(requiredLabels, fetchedLabels) + expect(result).toBe(true) + }) + + it('returns false, when none of the expected labels have been fetched.', () => { + const fetchedLabels: GithubLabel[] = [{ id: 1, name: 'fooBar' }] + + const result: boolean = compareLabels(requiredLabels, fetchedLabels) + expect(result).toBe(false) + }) + + it('returns false, when required labels array is empty.', () => { + const fetchedLabels: GithubLabel[] = [{ id: 1, name: 'fooBar' }] + const result: boolean = compareLabels([], fetchedLabels) + expect(result).toBe(false) + }) + + it('returns false, when fetched labels object is empty.', () => { + const result = compareLabels(requiredLabels, []) + expect(result).toBe(false) + }) +}) diff --git a/yaku-apps-typescript/apps/git-fetcher/test/unit/fixtures/bitbucket-responses-branches.ts b/yaku-apps-typescript/apps/git-fetcher/test/unit/fixtures/bitbucket-responses-branches.ts new file mode 100644 index 00000000..4b75c332 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/unit/fixtures/bitbucket-responses-branches.ts @@ -0,0 +1,82 @@ +import { BitbucketBranch } from '../../../src/model/bitbucket-branch' +import { BitBucketResponse } from '../../../src/model/BitBucketResponse' +import { BitbucketTag } from '../../../src/model/bitbucket-tag' + +export const bitBucketBranchEmptyResponse: BitBucketResponse = { + size: 0, + limit: 25, + start: 0, + isLastPage: true, + values: [], +} + +export const bitBucketBranchSinglePageResponse: BitBucketResponse = + { + size: 1, + limit: 25, + start: 0, + isLastPage: true, + values: [ + { + id: 'refs/heads/main', + displayId: 'main', + type: 'BRANCH', + latestCommit: 'b2e71587157e15201589790ee1c8a17455b967aa', + latestChangeset: 'b2e71587157e15201589790ee1c8a17455b967aa', + isDefault: true, + }, + ], + } + +export const bitBucketBranchMultiPageResponse: BitBucketResponse[] = + [ + { + size: 2, + limit: 2, + start: 0, + nextPageStart: 2, + isLastPage: false, + values: [ + { + id: 'refs/heads/main', + displayId: 'main', + type: 'BRANCH', + latestCommit: 'b2e71587157e15201589790ee1c8a17455b967aa', + latestChangeset: 'b2e71587157e15201589790ee1c8a17455b967aa', + isDefault: true, + }, + { + id: 'refs/heads/AQUATEST-5-update-jira-evaluator-1', + displayId: 'AQUATEST-5-update-jira-evaluator-1', + type: 'BRANCH', + latestCommit: 'e386f3482f18d174d3478164d9217025d86d0655', + latestChangeset: 'e386f3482f18d174d3478164d9217025d86d0655', + isDefault: false, + }, + ], + }, + { + size: 2, + limit: 2, + start: 2, + isLastPage: true, + values: [ + { + id: 'refs/heads/test-branch-31-01', + displayId: 'test-branch-31-01', + type: 'BRANCH', + latestCommit: '68521d211c5e38c27381718149014c5ec20b1f8e', + latestChangeset: '68521d211c5e38c27381718149014c5ec20b1f8e', + isDefault: false, + }, + { + id: 'refs/heads/test-branch-1', + displayId: 'test-branch-1', + type: 'BRANCH', + latestCommit: '539328a0427477d38c06a336a3b1f908238c1b6a', + latestChangeset: '539328a0427477d38c06a336a3b1f908238c1b6a', + isDefault: false, + }, + ], + }, + ] diff --git a/yaku-apps-typescript/apps/git-fetcher/test/unit/fixtures/bitbucket-responses-commits-and-diff.ts b/yaku-apps-typescript/apps/git-fetcher/test/unit/fixtures/bitbucket-responses-commits-and-diff.ts new file mode 100644 index 00000000..0bf6a950 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/unit/fixtures/bitbucket-responses-commits-and-diff.ts @@ -0,0 +1,353 @@ +import { BitbucketResponse } from '../../../src/model/bitbucket-response' +import { BitbucketCommit } from '../../../src/model/bitbucket-commit' +import { BitbucketDiffResponse } from '../../../src/model/bitbucket-diff-response' + +export const bitBucketCommitsEmptyResponse: BitbucketResponse = + { + size: 0, + limit: 25, + start: 0, + isLastPage: true, + values: [], + } + +export const bitBucketDiffEmptyResponse: BitbucketDiffResponse = { + fromHash: 'master', + toHash: 'master', + contextLines: 10, + whitespace: 'SHOW', + diffs: [], + truncated: false, +} + +export const bitBucketCommitsSinglePageResponse: BitbucketResponse = + { + size: 5, + limit: 25, + start: 0, + isLastPage: true, + nextPageStart: null, + values: [ + { + id: 'f5d0053ff3879c01edfd268e4d88e46747e99370', + displayId: 'f5d0053ff38', + author: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + authorTimestamp: 1690382590000, + committer: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + committerTimestamp: 1690382590000, + message: 'commit for integration tests 1', + parents: [ + { + id: 'ba414b6a0eede7338bd0a971b0c0b6076342e7a4', + displayId: 'ba414b6a0ee', + }, + ], + }, + { + id: 'ba414b6a0eede7338bd0a971b0c0b6076342e7a4', + displayId: 'ba414b6a0ee', + author: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + authorTimestamp: 1690367674000, + committer: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + committerTimestamp: 1690367674000, + message: 'add new commit for integration tests', + parents: [ + { + id: 'b94be7709aecbb65d5cd69f760c61a8fe740eda4', + displayId: 'b94be7709ae', + }, + ], + }, + { + id: 'b94be7709aecbb65d5cd69f760c61a8fe740eda4', + displayId: 'b94be7709ae', + author: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + authorTimestamp: 1690367578000, + committer: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + committerTimestamp: 1690367578000, + message: 'modify file for integration tests', + parents: [ + { + id: '19e27ca09fb986d1810b531dfca18dbfc927f906', + displayId: '19e27ca09fb', + }, + ], + }, + { + id: '19e27ca09fb986d1810b531dfca18dbfc927f906', + displayId: '19e27ca09fb', + author: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + authorTimestamp: 1688124368000, + committer: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + committerTimestamp: 1688124368000, + message: 'modify file to test api functionality', + parents: [ + { + id: '2844c40eea52eb6868679415e017e16a1c4d5a31', + displayId: '2844c40eea5', + }, + ], + }, + { + id: '2844c40eea52eb6868679415e017e16a1c4d5a31', + displayId: '2844c40eea5', + author: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + authorTimestamp: 1688046420000, + committer: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + committerTimestamp: 1688046420000, + message: 'add changes to test the api', + parents: [ + { + id: '35cc5eec543e69aed90503f21cf12666bcbfda4f', + displayId: '35cc5eec543', + }, + ], + }, + ], + } + +export const bitBucketDiffNonEmptyPageResponse: BitbucketDiffResponse = { + fromHash: '35cc5eec543e69aed90503f21cf12666bcbfda4f', + toHash: 'master', + contextLines: 10, + whitespace: 'SHOW', + diffs: [ + { + source: { + components: ['Some folder', 'something.py'], + parent: 'Some folder', + name: 'something.py', + extension: 'py', + toString: 'Some folder/something.py', + }, + destination: { + components: ['Some folder', 'something.py'], + parent: 'Some folder', + name: 'something.py', + extension: 'py', + toString: 'Some folder/something.py', + }, + hunks: [ + { + sourceLine: 1, + sourceSpan: 1, + destinationLine: 1, + destinationSpan: 4, + segments: [ + { + type: 'REMOVED', + lines: [ + { + source: 1, + destination: 1, + line: 'print("Hello world!")', + truncated: false, + }, + ], + truncated: false, + }, + { + type: 'ADDED', + lines: [ + { + source: 2, + destination: 1, + line: 'print("Hello world! + some changes")', + truncated: false, + }, + { + source: 2, + destination: 2, + line: 'print("another line to test")', + truncated: false, + }, + { + source: 2, + destination: 3, + line: 'print("delete previous line and addd this one for integration tests")', + truncated: false, + }, + { + source: 2, + destination: 4, + line: 'print("another line")', + truncated: false, + }, + ], + truncated: false, + }, + ], + truncated: false, + }, + ], + truncated: false, + }, + ], + truncated: false, +} + +export const bitBucketCommitsMultiPageResponse: BitbucketResponse[] = + [ + { + size: 2, + limit: 2, + start: 0, + isLastPage: false, + nextPageStart: 2, + values: [ + { + id: 'f5d0053ff3879c01edfd268e4d88e46747e99370', + displayId: 'f5d0053ff38', + author: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + authorTimestamp: 1690382590000, + committer: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + committerTimestamp: 1690382590000, + message: 'commit for integration tests 1', + parents: [ + { + id: 'ba414b6a0eede7338bd0a971b0c0b6076342e7a4', + displayId: 'ba414b6a0ee', + }, + ], + }, + { + id: 'ba414b6a0eede7338bd0a971b0c0b6076342e7a4', + displayId: 'ba414b6a0ee', + author: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + authorTimestamp: 1690367674000, + committer: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + committerTimestamp: 1690367674000, + message: 'add new commit for integration tests', + parents: [ + { + id: 'b94be7709aecbb65d5cd69f760c61a8fe740eda4', + displayId: 'b94be7709ae', + }, + ], + }, + ], + }, + { + size: 2, + limit: 2, + start: 2, + isLastPage: false, + nextPageStart: 4, + values: [ + { + id: 'b94be7709aecbb65d5cd69f760c61a8fe740eda4', + displayId: 'b94be7709ae', + author: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + authorTimestamp: 1690367578000, + committer: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + committerTimestamp: 1690367578000, + message: 'modify file for integration tests', + parents: [ + { + id: '19e27ca09fb986d1810b531dfca18dbfc927f906', + displayId: '19e27ca09fb', + }, + ], + }, + { + id: '19e27ca09fb986d1810b531dfca18dbfc927f906', + displayId: '19e27ca09fb', + author: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + authorTimestamp: 1688124368000, + committer: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + committerTimestamp: 1688124368000, + message: 'modify file to test api functionality', + parents: [ + { + id: '2844c40eea52eb6868679415e017e16a1c4d5a31', + displayId: '2844c40eea5', + }, + ], + }, + ], + }, + { + size: 1, + limit: 2, + start: 4, + isLastPage: true, + nextPageStart: null, + values: [ + { + id: '2844c40eea52eb6868679415e017e16a1c4d5a31', + displayId: '2844c40eea5', + author: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + authorTimestamp: 1688046420000, + committer: { + name: 'Tech User', + emailAddress: 'tech.user@example.com', + }, + committerTimestamp: 1688046420000, + message: 'add changes to test the api', + parents: [ + { + id: '35cc5eec543e69aed90503f21cf12666bcbfda4f', + displayId: '35cc5eec543', + }, + ], + }, + ], + }, + ] diff --git a/yaku-apps-typescript/apps/git-fetcher/test/unit/fixtures/bitbucket-responses-prs.ts b/yaku-apps-typescript/apps/git-fetcher/test/unit/fixtures/bitbucket-responses-prs.ts new file mode 100644 index 00000000..c8a01f2c --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/unit/fixtures/bitbucket-responses-prs.ts @@ -0,0 +1,61 @@ +import { BitbucketPr } from '../../../src/model/bitbucket-pr' +import { BitbucketResponse } from '../../../src/model/bitbucket-response' + +export const singlePageResponse: BitbucketResponse = { + isLastPage: true, + limit: 25, + size: 2, + start: 0, + values: [ + { + id: 1, + updatedDate: 123456, + state: 'OPEN', + }, + { + id: 2, + updatedDate: 123456, + state: 'OPEN', + }, + ], +} + +export const multiPageResponse: BitbucketResponse[] = [ + { + nextPageStart: 2, + isLastPage: false, + limit: 2, + size: 2, + start: 0, + values: [ + { + id: 1, + updatedDate: 123456, + state: 'OPEN', + }, + { + id: 2, + updatedDate: 123456, + state: 'OPEN', + }, + ], + }, + { + isLastPage: true, + limit: 2, + size: 2, + start: 2, + values: [ + { + id: 3, + updatedDate: 123456, + state: 'OPEN', + }, + { + id: 4, + updatedDate: 123456, + state: 'OPEN', + }, + ], + }, +] diff --git a/yaku-apps-typescript/apps/git-fetcher/test/unit/fixtures/bitbucket-responses-tags.ts b/yaku-apps-typescript/apps/git-fetcher/test/unit/fixtures/bitbucket-responses-tags.ts new file mode 100644 index 00000000..f33e3317 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/unit/fixtures/bitbucket-responses-tags.ts @@ -0,0 +1,64 @@ +import { BitBucketResponse } from '../../../src/model/BitBucketResponse' +import { BitbucketTag } from '../../../src/model/bitbucket-tag' + +export const bitBucketTagEmptyResponse: BitBucketResponse = { + size: 0, + limit: 25, + start: 0, + isLastPage: true, + values: [], +} + +export const bitBucketTagSinglePageResponse: BitBucketResponse = { + size: 1, + limit: 25, + start: 0, + isLastPage: true, + values: [ + { + id: 'refs/tags/initial', + displayId: 'initial', + type: 'TAG', + latestCommit: '9da28cfda7344149d18e36b69279565373a8dfb9', + latestChangeset: '9da28cfda7344149d18e36b69279565373a8dfb9', + hash: 'a13dbf0d42971adfc592103941cc7898652f3cbb', + }, + ], +} + +export const bitBucketTagMultiPageResponse: BitBucketResponse[] = + [ + { + size: 1, + limit: 1, + start: 0, + isLastPage: false, + nextPageStart: 1, + values: [ + { + id: 'refs/tags/initial', + displayId: 'initial', + type: 'TAG', + latestCommit: '9da28cfda7344149d18e36b69279565373a8dfb9', + latestChangeset: '9da28cfda7344149d18e36b69279565373a8dfb9', + hash: 'a13dbf0d42971adfc592103941cc7898652f3cbb', + }, + ], + }, + { + size: 1, + limit: 1, + start: 1, + isLastPage: true, + values: [ + { + id: 'refs/tags/tag-for-readme', + displayId: 'tag-for-readme', + type: 'TAG', + latestCommit: '68521d211c5e38c27381718149014c5ec20b1f8e', + latestChangeset: '68521d211c5e38c27381718149014c5ec20b1f8e', + hash: 'd1994e10bd134e7b1cf25213d3efcb0004226e6f', + }, + ], + }, + ] diff --git a/yaku-apps-typescript/apps/git-fetcher/test/unit/fixtures/github-responses-commits-and-diff.ts b/yaku-apps-typescript/apps/git-fetcher/test/unit/fixtures/github-responses-commits-and-diff.ts new file mode 100644 index 00000000..5311f6c2 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/unit/fixtures/github-responses-commits-and-diff.ts @@ -0,0 +1,1555 @@ +import { GithubSingleCommitResponse } from '../../../src/model/github-single-commit-response' +import { GithubDiffResponse } from '../../../src/model/github-diff-response' +import { GithubMultipleCommitsResponse } from '../../../src/model/github-multiple-commits-response' + +export const githubStartCommitResponse: GithubSingleCommitResponse = { + sha: 'afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + node_id: + 'X_kwDOJEBj4toAKGFmXXFlYmY0MTJjNmQwYjg2NWEzNxXxXXXxMzdmZGI0NmMwZmFiNjM', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-03-06T14:11:29Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-03-06T14:11:29Z', + }, + message: + 'Add git fetcher app\n\nSigned-off-by: Tech User ', + tree: { + sha: '347a2c4dc61cb708a1067675fd6dbc1f0ea74608', + url: 'https://api.example.com/trees/347a2c4dc61cb708a1067675fd6dbc1f0ea74608', + }, + url: 'https://api.example.com/commits/afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://api.example.com/afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + html_url: 'https://example.com/afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + comments_url: + 'https://api.example.com/afeaebf412c6d0b865a36cfdec37fdb46c0fab63/comments', + author: { + login: 'TechUser', + id: 1234567891, + node_id: 'XXXXXXXlcjMwMTIxNzU5', + avatar_url: 'https://example.com/u/xxxxxxxxx?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'TechUser', + id: 1234567891, + node_id: 'XXXXXXXlcjMwMTIxNzU5', + avatar_url: 'https://example.com/u/xxxxxxxxx?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: '097079eba1e7749d6d86d324fa530f8e89e55595', + url: 'https://api.example.com/097079eba1e7749d6d86d324fa530f8e89e55595', + html_url: 'https://example.com/097079eba1e7749d6d86d324fa530f8e89e55595', + }, + ], + stats: { + total: 472, + additions: 472, + deletions: 0, + }, + files: [ + { + sha: 'b6702e99ebb354a6500eb19e24e29a134abdd80c', + filename: 'apps/git-fetcher/package.json', + status: 'added', + additions: 42, + deletions: 0, + changes: 42, + blob_url: + 'https://example.com/blob/afeaebf412c6d0b865a36cfdec37fdb46c0fab63/apps%2Fgit-fetcher%2Fpackage.json', + raw_url: + 'https://example.com/raw/afeaebf412c6d0b865a36cfdec37fdb46c0fab63/apps%2Fgit-fetcher%2Fpackage.json', + contents_url: + 'https://api.example.com/repos/contents/apps%2Fgit-fetcher%2Fpackage.json?ref=afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + patch: + '@@ -0,0 +1,42 @@\n+{\n+ "name": "@B-S-F/git-fetcher",\n+ "version": "0.1.1",\n+ "description": "",\n+ "type": "module",\n+ "main": "dist/index.js",\n+ "scripts": {\n+ "login-artifactory": "npm login --registry https://example.com/api/npm --scope @top99",\n+ "prepare": "npm run build",\n+ "build": "tsup",\n+ "dev": "nodemon --watch \\"src/**\\" --exec npm run start",\n+ "start": "npm run build && node dist/index.js",\n+ "lint": "eslint \'**/*.ts\'",\n+ "setup": "npm install && npm run build",\n+ "format": "prettier src --write"\n+ },\n+ "keywords": [],\n+ "author": "",\n+ "files": [\n+ "dist"\n+ ],\n+ "license": "",\n+ "dependencies": {\n+ "fs-extra": "^10.1.0",\n+ "node-fetch": "^3.2.10",\n+ "process": "^0.11.10",\n+ "tsup": "^6.5.0"\n+ },\n+ "devDependencies": {\n+ "@B-S-F/eslint-config": "*",\n+ "@B-S-F/typescript-config": "*",\n+ "@types/node": "*",\n+ "@typescript-eslint/eslint-plugin": "*",\n+ "@typescript-eslint/parser": "*",\n+ "eslint": "*",\n+ "eslint-config-prettier": "*",\n+ "typescript": "*"\n+ },\n+ "bin": {\n+ "git-fetcher": "dist/index.js"\n+ }\n+}', + }, + { + sha: '4438322ba0497ba85a170bd065ed9050a63ff830', + filename: 'apps/git-fetcher/src/fetchers/git-fetcher.ts', + status: 'added', + additions: 99, + deletions: 0, + changes: 99, + blob_url: + 'https://example.com/blob/afeaebf412c6d0b865a36cfdec37fdb46c0fab63/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher.ts', + raw_url: + 'https://example.com/raw/afeaebf412c6d0b865a36cfdec37fdb46c0fab63/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher.ts', + contents_url: + 'https://api.example.com/repos/contents/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher.ts?ref=afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + patch: + "@@ -0,0 +1,99 @@\n+import { GitServerConfig } from '../model/git-server-config'\n+import { ConfigFileData } from '../model/config-file-data'\n+import { handleResponseStatus } from '../utils/handle-response-status.js'\n+\n+export class GitFetcher {\n+ constructor(public env: GitServerConfig, public config: ConfigFileData) {}\n+\n+ public pullRequestValidInputs = [\n+ 'pull-request',\n+ 'pull-requests',\n+ 'pr',\n+ 'prs',\n+ 'pullrequest',\n+ 'pullrequests',\n+ 'pull',\n+ 'pulls',\n+ ]\n+\n+ public validateResourceName(resource: string) {\n+ if (this.env.gitServerType == 'bitbucket') {\n+ if (this.pullRequestValidInputs.includes(resource.toLocaleLowerCase())) {\n+ return 'pull-requests'\n+ } else {\n+ throw new Error(`${resource} resource name not valid`)\n+ }\n+ } else if (this.env.gitServerType == 'github') {\n+ if (this.pullRequestValidInputs.includes(resource.toLocaleLowerCase())) {\n+ return 'pulls'\n+ } else {\n+ throw new Error(`${resource} resource name not valid`)\n+ }\n+ } else {\n+ throw new Error(`${this.env.gitServerType} server type not supported`)\n+ }\n+ }\n+\n+ public async getOptions() {\n+ if (this.env.gitServerAuthMethod.toLocaleLowerCase() == 'basic') {\n+ const options = {\n+ method: 'GET',\n+ auth: {\n+ username: this.env.gitServerUsername,\n+ password: this.env.gitServerPassword,\n+ },\n+ headers: {\n+ Accept: 'application/vnd.github+json',\n+ },\n+ }\n+ return options\n+ } else if (this.env.gitServerAuthMethod.toLocaleLowerCase() == 'token') {\n+ const options = {\n+ method: 'GET',\n+ headers: {\n+ Accept: 'application/json',\n+ Authorization: `Bearer ${this.env.gitServerApiToken}`,\n+ },\n+ }\n+ return options\n+ } else {\n+ throw new Error('No valid auth method provided')\n+ }\n+ }\n+\n+ public async buildUrl(resource: string) {\n+ const resourceName = this.validateResourceName(resource)\n+ if (this.env.gitServerType == 'bitbucket') {\n+ const endpoint = `${this.env.gitServerApiUrl.replace(\n+ //*$/,\n+ ''\n+ )}/projects/${this.config.data.org}/repos/${\n+ this.config.data.repo\n+ }/${resourceName}?state=ALL`\n+ return endpoint\n+ } else if (this.env.gitServerType == 'github') {\n+ const endpoint = `${this.env.gitServerApiUrl.replace(//*$/, '')}/repos/${\n+ this.config.data.org\n+ }/${this.config.data.repo}/${resourceName}?state=all&per_page=100`\n+ return endpoint\n+ } else {\n+ throw new Error(`${this.env.gitServerType} server type not supported`)\n+ }\n+ }\n+\n+ public async runQuery() {\n+ const url = await this.buildUrl(this.config.data.resource)\n+ const options = await this.getOptions()\n+ try {\n+ const response = await fetch(url, options)\n+ if (response.status != 200) {\n+ handleResponseStatus(response.status)\n+ }\n+ return response.json()\n+ } catch (error: any) {\n+ throw new Error(\n+ `Got the following error when running Git fetcher: ${error.message}`\n+ )\n+ }\n+ }\n+}", + }, + { + sha: 'b89b727221e34f5bed6c5f2e81ee9eb0c8f48a74', + filename: 'apps/git-fetcher/src/index.ts', + status: 'added', + additions: 13, + deletions: 0, + changes: 13, + blob_url: + 'https://example.com/blob/afeaebf412c6d0b865a36cfdec37fdb46c0fab63/apps%2Fgit-fetcher%2Fsrc%2Findex.ts', + raw_url: + 'https://example.com/raw/afeaebf412c6d0b865a36cfdec37fdb46c0fab63/apps%2Fgit-fetcher%2Fsrc%2Findex.ts', + contents_url: + 'https://api.example.com/repos/contents/apps%2Fgit-fetcher%2Fsrc%2Findex.ts?ref=afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + patch: + "@@ -0,0 +1,13 @@\n+#!/usr/bin/env node\n+\n+import { run } from './run.js'\n+try {\n+ await run()\n+} catch (error: any) {\n+ console.log(\n+ JSON.stringify({\n+ comment: `Could not fetch data: ${error.message}`,\n+ })\n+ )\n+ process.exit(1)\n+}", + }, + ], +} + +export const githubEndCommitResponse: GithubSingleCommitResponse = { + sha: '8036cf75f4b7365efea76cbd716ef12d352d7d29', + node_id: 'C_kwDOJEBj4toAKDgxXXXxxXXXXXXXxXXXxxxXXxxXxxNmVmMTJkMzUyZDdkMjk', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-07-12T10:46:50Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-07-12T10:46:50Z', + }, + message: + 'add commits and diff retrieval of target file for both bitbucket and github', + tree: { + sha: '9450ecc9597185ad82f9c9b61df5337f5ad4a286', + url: 'https://api.example.com/trees/9450ecc9597185ad82f9c9b61df5337f5ad4a286', + }, + url: 'https://api.example.com/commits/8036cf75f4b7365efea76cbd716ef12d352d7d29', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://api.example.com/8036cf75f4b7365efea76cbd716ef12d352d7d29', + html_url: 'https://example.com/8036cf75f4b7365efea76cbd716ef12d352d7d29', + comments_url: + 'https://api.example.com/8036cf75f4b7365efea76cbd716ef12d352d7d29/comments', + author: { + login: 'TechUser', + id: 112345678, + node_id: 'XXXXXXXlcjMwMTIxNzU5', + avatar_url: 'https://example.com/u/40821471?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'TechUser', + id: 112345678, + node_id: 'XXXXXXXlcjMwMTIxNzU5', + avatar_url: 'https://example.com/u/40821471?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: 'dc7e626d6da5b0b4911a1b11f0d5dcf6009f827f', + url: 'https://api.example.com/dc7e626d6da5b0b4911a1b11f0d5dcf6009f827f', + html_url: 'https://example.com/dc7e626d6da5b0b4911a1b11f0d5dcf6009f827f', + }, + ], + stats: { + total: 210, + additions: 208, + deletions: 2, + }, + files: [ + { + sha: 'c274acc18f53be6f0e14e8f787a28e8ba20af89d', + filename: + 'apps/git-fetcher/src/fetchers/git-fetcher-github-commits-and-diff.ts', + status: 'added', + additions: 114, + deletions: 0, + changes: 114, + blob_url: + 'https://example.com/blob/8036cf75f4b7365efea76cbd716ef12d352d7d29/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher-github-commits-and-diff.ts', + raw_url: + 'https://example.com/raw/8036cf75f4b7365efea76cbd716ef12d352d7d29/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher-github-commits-and-diff.ts', + contents_url: + 'https://api.example.com/repos/contents/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher-github-commits-and-diff.ts?ref=8036cf75f4b7365efea76cbd716ef12d352d7d29', + patch: + "@@ -0,0 +1,114 @@\n+import { CommitsMetadataAndDiff } from '../model/bitbucket-commits-metadata-and-diff'\n+import { ConfigFileData } from '../model/config-file-data'\n+import { GitServerConfig } from '../model/git-server-config'\n+import { GitFetcher } from './git-fetcher'\n+import { getRequestOptions } from './utils/get-request-options.js'\n+\n+export class GitFetcherGithubCommitsAndDiff\n+ implements GitFetcher\n+{\n+ constructor(\n+ private readonly gitServerConfig: GitServerConfig,\n+ private readonly config: ConfigFileData\n+ ) {}\n+\n+ private incrementSecondsAndReturnIsoFormat = (startDate: string) => {\n+ const newDate = new Date(\n+ new Date(startDate).setUTCSeconds(new Date(startDate).getUTCSeconds() + 1)\n+ )\n+ return (\n+ newDate.getUTCFullYear() +\n+ '-' +\n+ (newDate.getUTCMonth() + 1) +\n+ '-' +\n+ newDate.getUTCDate() +\n+ 'T' +\n+ newDate.getUTCHours() +\n+ ':' +\n+ newDate.getUTCMinutes() +\n+ ':' +\n+ newDate.getUTCSeconds() +\n+ 'Z'\n+ )\n+ }\n+\n+ public async fetchResource(): Promise {\n+ const requestOptions: RequestInit = await getRequestOptions(\n+ this.gitServerConfig\n+ )\n+ let result: CommitsMetadataAndDiff = {\n+ commitsMetadata: [],\n+ diff: {},\n+ }\n+ try {\n+ const serverName = this.gitServerConfig.gitServerApiUrl\n+ const projectKey = this.config.data.org\n+ const repositorySlug = this.config.data.repo\n+ const filePath = this.config.data.filePath\n+ const startHash = this.config.data.filter?.startHash\n+ let endHash = this.config.data.filter?.endHash\n+ if (!filePath) {\n+ throw new Error(\n+ `Please define the 'filePath' parameter in the config and try again`\n+ )\n+ }\n+ if (!startHash) {\n+ throw new Error(\n+ `Please define the 'filter.startHash' parameter in the config and try again`\n+ )\n+ }\n+ let startDate\n+ let startCommitUrl = `${serverName}/repos/${projectKey}/${repositorySlug}/commits/${startHash}`\n+ let response: Response = await fetch(startCommitUrl, requestOptions)\n+ let responseBody = await response.json()\n+ startDate = responseBody.commit.committer.date\n+ //function below is required in order to have consistency between the '/compare' and '/commits' API calls\n+ startDate = this.incrementSecondsAndReturnIsoFormat(startDate)\n+\n+ if (!endHash) {\n+ endHash = 'master'\n+ }\n+ let endDate\n+ let endCommitUrl = `${serverName}/repos/${projectKey}/${repositorySlug}/commits/${endHash}`\n+ response = await fetch(endCommitUrl, requestOptions)\n+ responseBody = await response.json()\n+ endDate = responseBody.commit.committer.date\n+\n+ let diffUrl = `${serverName}/repos/${projectKey}/${repositorySlug}/compare/${startHash}...${endHash}`\n+ response = await fetch(diffUrl, requestOptions)\n+ responseBody = await response.json()\n+ for (const file of responseBody.files) {\n+ if (file.filename === filePath) {\n+ const linesAdded = file.patch.match(/\\n\\+[\\S\\s]*?(?=\\n)/g)\n+ result.diff['linesAdded'] = linesAdded\n+\n+ const linesRemoved = file.patch.match(/\\n\\-[\\S\\s]*?(?=\\n)/g)\n+ result.diff['linesRemoved'] = linesRemoved\n+ break\n+ }\n+ }\n+\n+ let currentPage: number | null = 1\n+ while (currentPage != null) {\n+ let commitsUrl = `${serverName}/repos/${projectKey}/${repositorySlug}/commits?path=${filePath}&until=${endDate}&page=${currentPage}&per_page=100`\n+ if (startDate) {\n+ commitsUrl = commitsUrl + `&since=${startDate}`\n+ }\n+ let response: Response = await fetch(commitsUrl, requestOptions)\n+ let responseBody = await response.json()\n+\n+ if (responseBody.length > 0) {\n+ for (const data of responseBody) {\n+ result.commitsMetadata.push(data.commit)\n+ }\n+ currentPage = currentPage + 1\n+ } else {\n+ currentPage = null\n+ }\n+ }\n+ } catch (error: any) {\n+ throw new Error(`github commits and diffs: ${error.message}`)\n+ }\n+ return result\n+ }\n+}", + }, + { + sha: '73491746fc3d4c675726060b73df7dd8ccf72309', + filename: 'apps/git-fetcher/src/fetchers/git-fetcher.ts', + status: 'modified', + additions: 1, + deletions: 1, + changes: 2, + blob_url: + 'https://example.com/blob/8036cf75f4b7365efea76cbd716ef12d352d7d29/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher.ts', + raw_url: + 'https://example.com/raw/8036cf75f4b7365efea76cbd716ef12d352d7d29/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher.ts', + contents_url: + 'https://api.example.com/repos/contents/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher.ts?ref=8036cf75f4b7365efea76cbd716ef12d352d7d29', + patch: + '@@ -1,3 +1,3 @@\n export interface GitFetcher {\n- fetchResource(): Promise\n+ fetchResource(): Promise\n }', + }, + { + sha: '9f8469934f0143966ac39eadd0b7c88b51dc5e6e', + filename: + 'apps/git-fetcher/src/model/bitbucket-commits-metadata-and-diff.ts', + status: 'added', + additions: 4, + deletions: 0, + changes: 4, + blob_url: + 'https://example.com/blob/8036cf75f4b7365efea76cbd716ef12d352d7d29/apps%2Fgit-fetcher%2Fsrc%2Fmodel%2Fbitbucket-commits-metadata-and-diff.ts', + raw_url: + 'https://example.com/raw/8036cf75f4b7365efea76cbd716ef12d352d7d29/apps%2Fgit-fetcher%2Fsrc%2Fmodel%2Fbitbucket-commits-metadata-and-diff.ts', + contents_url: + 'https://api.example.com/repos/contents/apps%2Fgit-fetcher%2Fsrc%2Fmodel%2Fbitbucket-commits-metadata-and-diff.ts?ref=8036cf75f4b7365efea76cbd716ef12d352d7d29', + patch: + '@@ -0,0 +1,4 @@\n+export type CommitsMetadataAndDiff = {\n+ commitsMetadata: any\n+ diff: any\n+}', + }, + ], +} + +export const githubDiffResponse: GithubDiffResponse = { + url: 'https://api.example.com/repos/compare/afeaebf412c6d0b865a36cfdec37fdb46c0fab63...8036cf75f4b7365efea76cbd716ef12d352d7d29', + html_url: + 'https://example.com/compare/afeaebf412c6d0b865a36cfdec37fdb46c0fab63...8036cf75f4b7365efea76cbd716ef12d352d7d29', + permalink_url: + 'https://example.com/compare/B-S-F:afeaebf...B-S-F:8036cf7', + diff_url: + 'https://example.com/compare/afeaebf412c6d0b865a36cfdec37fdb46c0fab63...8036cf75f4b7365efea76cbd716ef12d352d7d29.diff', + patch_url: + 'https://example.com/compare/afeaebf412c6d0b865a36cfdec37fdb46c0fab63...8036cf75f4b7365efea76cbd716ef12d352d7d29.patch', + base_commit: { + sha: 'afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + node_id: 'C_kwDOJEBj4toAKDgxXXXxxXXXXXXXxXXXxxxXXxxXxxNmVmMTJkMzUyZDdkMjk', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-03-06T14:11:29Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-03-06T14:11:29Z', + }, + message: + 'Add git fetcher app\n\nSigned-off-by: Tech User ', + tree: { + sha: '347a2c4dc61cb708a1067675fd6dbc1f0ea74608', + url: 'https://api.example.com/trees/347a2c4dc61cb708a1067675fd6dbc1f0ea74608', + }, + url: 'https://api.example.com/commits/afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://api.example.com/afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + html_url: 'https://example.com/afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + comments_url: + 'https://api.example.com/afeaebf412c6d0b865a36cfdec37fdb46c0fab63/comments', + author: { + login: 'TechUser', + id: 1234567891, + node_id: 'XXXXXXXlcjMwMTIxNzU5', + avatar_url: 'https://example.com/u/xxxxxxxxx?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'TechUser', + id: 1234567891, + node_id: 'XXXXXXXlcjMwMTIxNzU5', + avatar_url: 'https://example.com/u/xxxxxxxxx?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: '097079eba1e7749d6d86d324fa530f8e89e55595', + url: 'https://api.example.com/097079eba1e7749d6d86d324fa530f8e89e55595', + html_url: + 'https://example.com/097079eba1e7749d6d86d324fa530f8e89e55595', + }, + ], + }, + merge_base_commit: { + sha: 'afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + node_id: 'C_kwDOJEBj4toAKDgxXXXxxXXXXXXXxXXXxxxXXxxXxxNmVmMTJkMzUyZDdkMjk', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-03-06T14:11:29Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-03-06T14:11:29Z', + }, + message: + 'Add git fetcher app\n\nSigned-off-by: Tech User ', + tree: { + sha: '347a2c4dc61cb708a1067675fd6dbc1f0ea74608', + url: 'https://api.example.com/trees/347a2c4dc61cb708a1067675fd6dbc1f0ea74608', + }, + url: 'https://api.example.com/commits/afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://api.example.com/afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + html_url: 'https://example.com/afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + comments_url: + 'https://api.example.com/afeaebf412c6d0b865a36cfdec37fdb46c0fab63/comments', + author: { + login: 'TechUser', + id: 1234567891, + node_id: 'XXXXXXXlcjMwMTIxNzU5', + avatar_url: 'https://example.com/u/xxxxxxxxx?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'TechUser', + id: 1234567891, + node_id: 'XXXXXXXlcjMwMTIxNzU5', + avatar_url: 'https://example.com/u/xxxxxxxxx?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: '097079eba1e7749d6d86d324fa530f8e89e55595', + url: 'https://api.example.com/097079eba1e7749d6d86d324fa530f8e89e55595', + html_url: + 'https://example.com/097079eba1e7749d6d86d324fa530f8e89e55595', + }, + ], + }, + status: 'ahead', + ahead_by: 324, + behind_by: 0, + total_commits: 324, + commits: [ + { + sha: 'fbf45173f2c1fbf4f6f2439abba88946a2cc8360', + node_id: + 'C_kwDOJEBj4toAKDgxXXXxxXXXXXXXxXXXxxxXXxxXxxNmVmMTJkMzUyZDdkMjk', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-03-06T14:15:43Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-03-06T14:15:43Z', + }, + message: + 'Add oneq-finalizer and git-fetcher to release workflow\n\nSigned-off-by: Tech User ', + tree: { + sha: '84152e149262f4883b1c724d9c9bb8e4e5d23fad', + url: 'https://api.example.com/trees/84152e149262f4883b1c724d9c9bb8e4e5d23fad', + }, + url: 'https://api.example.com/commits/fbf45173f2c1fbf4f6f2439abba88946a2cc8360', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://api.example.com/fbf45173f2c1fbf4f6f2439abba88946a2cc8360', + html_url: 'https://example.com/fbf45173f2c1fbf4f6f2439abba88946a2cc8360', + comments_url: + 'https://api.example.com/fbf45173f2c1fbf4f6f2439abba88946a2cc8360/comments', + author: { + login: 'TechUser', + id: 1234567891, + node_id: 'XXXXXXXlcjMwMTIxNzU5', + avatar_url: 'https://example.com/u/xxxxxxxxx?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: + 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'TechUser', + id: 1234567891, + node_id: 'XXXXXXXlcjMwMTIxNzU5', + avatar_url: 'https://example.com/u/xxxxxxxxx?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: + 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: 'afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + url: 'https://api.example.com/afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + html_url: + 'https://example.com/afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + }, + ], + }, + ], + files: [ + { + sha: 'ab49e18dfe46bfe03d9fa77837cff8ccd175de53', + filename: 'apps/git-fetcher/src/fetchers/git-fetcher-github-prs.ts', + status: 'added', + additions: 106, + deletions: 0, + changes: 106, + blob_url: + 'https://example.com/blob/8036cf75f4b7365efea76cbd716ef12d352d7d29/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher-github-prs.ts', + raw_url: + 'https://example.com/raw/8036cf75f4b7365efea76cbd716ef12d352d7d29/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher-github-prs.ts', + contents_url: + 'https://api.example.com/repos/contents/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher-github-prs.ts?ref=8036cf75f4b7365efea76cbd716ef12d352d7d29', + patch: + "@@ -0,0 +1,106 @@\n+import { GitFetcher } from './git-fetcher'\n+import { handleResponseStatus } from '../utils/handle-response-status.js'\n+import { getRequestOptions } from './utils/get-request-options.js'\n+import { GitServerConfig } from '../model/git-server-config.js'\n+import { ConfigFileData } from '../model/config-file-data.js'\n+import { compareLabels } from '../utils/compare-labels.js'\n+import { GithubPr } from '../model/github-pr'\n+\n+/**\n+ * @description Creates a GitFetcher which is able to fetch pull requests from GitHub\n+ */\n+export class GitFetcherGithubPrs implements GitFetcher {\n+ /**\n+ * @constructor\n+ * @param {GitServerConfig} gitServerConfig\n+ * @param {ConfigFileData} config\n+ */\n+ constructor(\n+ private gitServerConfig: GitServerConfig,\n+ private config: ConfigFileData\n+ ) {}\n+\n+ /**\n+ * Builds the request url as well as the request options to fetch pull requests from GitHub\n+ * and calls the fetch-method. As long as the response body includes pull requests objects, the\n+ * currentPage variable will be incremented and pull requests are pushed to the array, that's eventually\n+ * returned. If the response body is empty and no pull requests were received, the loop ends,\n+ * and the collected pull requests are returned.\n+ * @returns an a promise for an array of GitHub pull requests, which have been filtered according to the configuration.\n+ * @throws {Error} when either fetch response is not successful or response can't be parsed.\n+ */\n+ public async fetchResource(): Promise {\n+ const requestOptions: RequestInit = await getRequestOptions(\n+ this.gitServerConfig\n+ )\n+\n+ let pullRequests: GithubPr[] = []\n+\n+ let currentPage: number | null = 1\n+ let responseBody: GithubPr[]\n+\n+ while (currentPage != null) {\n+ try {\n+ const response: Response = await fetch(\n+ this.composeUrl(currentPage),\n+ requestOptions\n+ )\n+\n+ if (response.status != 200) {\n+ handleResponseStatus(response.status)\n+ }\n+\n+ responseBody = (await response.json()) as GithubPr[]\n+ } catch (error: any) {\n+ throw new Error(\n+ `Got the following error when running git fetcher: ${error.message}`\n+ )\n+ }\n+\n+ if (responseBody.length > 0) {\n+ pullRequests.push(...responseBody)\n+ currentPage++\n+ } else {\n+ currentPage = null\n+ }\n+ }\n+\n+ if (\n+ pullRequests.length > 0 &&\n+ this.config.data.labels &&\n+ this.config.data.labels.length > 0\n+ ) {\n+ const filteredPrs: GithubPr[] = []\n+\n+ pullRequests.forEach((pr: GithubPr) => {\n+ if (compareLabels(this.config.data.labels, pr.labels)) {\n+ filteredPrs.push(pr)\n+ }\n+ })\n+\n+ pullRequests = filteredPrs\n+ }\n+\n+ console.log(\n+ `Fetched ${pullRequests.length} pull request${\n+ pullRequests.length === 1 ? '' : 's'\n+ }`\n+ )\n+ return pullRequests\n+ }\n+\n+ private composeUrl(startPage?: number): string {\n+ const strippedApiUrl: string = this.gitServerConfig.gitServerApiUrl.replace(\n+ //*$/,\n+ ''\n+ )\n+ const stateFilter = 'all' //will allow to set other state filters, introduced in future tickets\n+\n+ let baseUrl = `${strippedApiUrl}/repos/${this.config.data.org}/${this.config.data.repo}/pulls?state=${stateFilter}&per_page=100`\n+ if (startPage != null) {\n+ baseUrl += `&page=${startPage}`\n+ }\n+\n+ return baseUrl\n+ }\n+}", + }, + { + sha: '73491746fc3d4c675726060b73df7dd8ccf72309', + filename: 'apps/git-fetcher/src/fetchers/git-fetcher.ts', + status: 'modified', + additions: 2, + deletions: 98, + changes: 100, + blob_url: + 'https://example.com/blob/8036cf75f4b7365efea76cbd716ef12d352d7d29/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher.ts', + raw_url: + 'https://example.com/raw/8036cf75f4b7365efea76cbd716ef12d352d7d29/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher.ts', + contents_url: + 'https://api.example.com/repos/contents/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Fgit-fetcher.ts?ref=8036cf75f4b7365efea76cbd716ef12d352d7d29', + patch: + "@@ -1,99 +1,3 @@\n-import { GitServerConfig } from '../model/git-server-config'\n-import { ConfigFileData } from '../model/config-file-data'\n-import { handleResponseStatus } from '../utils/handle-response-status.js'\n-\n-export class GitFetcher {\n- constructor(public env: GitServerConfig, public config: ConfigFileData) {}\n-\n- public pullRequestValidInputs = [\n- 'pull-request',\n- 'pull-requests',\n- 'pr',\n- 'prs',\n- 'pullrequest',\n- 'pullrequests',\n- 'pull',\n- 'pulls',\n- ]\n-\n- public validateResourceName(resource: string) {\n- if (this.env.gitServerType == 'bitbucket') {\n- if (this.pullRequestValidInputs.includes(resource.toLocaleLowerCase())) {\n- return 'pull-requests'\n- } else {\n- throw new Error(`${resource} resource name not valid`)\n- }\n- } else if (this.env.gitServerType == 'github') {\n- if (this.pullRequestValidInputs.includes(resource.toLocaleLowerCase())) {\n- return 'pulls'\n- } else {\n- throw new Error(`${resource} resource name not valid`)\n- }\n- } else {\n- throw new Error(`${this.env.gitServerType} server type not supported`)\n- }\n- }\n-\n- public async getOptions() {\n- if (this.env.gitServerAuthMethod.toLocaleLowerCase() == 'basic') {\n- const options = {\n- method: 'GET',\n- auth: {\n- username: this.env.gitServerUsername,\n- password: this.env.gitServerPassword,\n- },\n- headers: {\n- Accept: 'application/vnd.github+json',\n- },\n- }\n- return options\n- } else if (this.env.gitServerAuthMethod.toLocaleLowerCase() == 'token') {\n- const options = {\n- method: 'GET',\n- headers: {\n- Accept: 'application/json',\n- Authorization: `Bearer ${this.env.gitServerApiToken}`,\n- },\n- }\n- return options\n- } else {\n- throw new Error('No valid auth method provided')\n- }\n- }\n-\n- public async buildUrl(resource: string) {\n- const resourceName = this.validateResourceName(resource)\n- if (this.env.gitServerType == 'bitbucket') {\n- const endpoint = `${this.env.gitServerApiUrl.replace(\n- //*$/,\n- ''\n- )}/projects/${this.config.data.org}/repos/${\n- this.config.data.repo\n- }/${resourceName}?state=ALL`\n- return endpoint\n- } else if (this.env.gitServerType == 'github') {\n- const endpoint = `${this.env.gitServerApiUrl.replace(//*$/, '')}/repos/${\n- this.config.data.org\n- }/${this.config.data.repo}/${resourceName}?state=all&per_page=100`\n- return endpoint\n- } else {\n- throw new Error(`${this.env.gitServerType} server type not supported`)\n- }\n- }\n-\n- public async runQuery() {\n- const url = await this.buildUrl(this.config.data.resource)\n- const options = await this.getOptions()\n- try {\n- const response = await fetch(url, options)\n- if (response.status != 200) {\n- handleResponseStatus(response.status)\n- }\n- return response.json()\n- } catch (error: any) {\n- throw new Error(\n- `Got the following error when running Git fetcher: ${error.message}`\n- )\n- }\n- }\n+export interface GitFetcher {\n+ fetchResource(): Promise\n }", + }, + { + sha: '62e232b894cc96e6bfb3be758e94e1ac57df1b99', + filename: 'apps/git-fetcher/src/fetchers/index.ts', + status: 'added', + additions: 2, + deletions: 0, + changes: 2, + blob_url: + 'https://example.com/blob/8036cf75f4b7365efea76cbd716ef12d352d7d29/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Findex.ts', + raw_url: + 'https://example.com/raw/8036cf75f4b7365efea76cbd716ef12d352d7d29/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Findex.ts', + contents_url: + 'https://api.example.com/repos/contents/apps%2Fgit-fetcher%2Fsrc%2Ffetchers%2Findex.ts?ref=8036cf75f4b7365efea76cbd716ef12d352d7d29', + patch: + "@@ -0,0 +1,2 @@\n+export * from './git-fetcher.js'\n+export * from './generate-git-fetcher.js'", + }, + ], +} + +export const githubMultipleCommitsResponse: GithubMultipleCommitsResponse[] = [ + { + sha: '8036cf75f4b7365efea76cbd716ef12d352d7d29', + node_id: 'C_kwDOJEBj4toAKDgxxxXXXxxZZZzzZZXXzzmVmMTJkMzUyZDdkMjk', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-07-12T10:46:50Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-07-12T10:46:50Z', + }, + message: + 'add commits and diff retrieval of target file for both bitbucket and github', + tree: { + sha: '9450ecc9597185ad82f9c9b61df5337f5ad4a286', + url: 'https://api.example.com/trees/9450ecc9597185ad82f9c9b61df5337f5ad4a286', + }, + url: 'https://api.example.com/commits/8036cf75f4b7365efea76cbd716ef12d352d7d29', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://api.example.com/8036cf75f4b7365efea76cbd716ef12d352d7d29', + html_url: 'https://example.com/8036cf75f4b7365efea76cbd716ef12d352d7d29', + comments_url: + 'https://api.example.com/8036cf75f4b7365efea76cbd716ef12d352d7d29/comments', + author: { + login: 'TechUser', + id: 112345678, + node_id: 'XXXXXXXlcjMwMTIxNzU5', + avatar_url: 'https://example.com/u/40821471?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'TechUser', + id: 112345678, + node_id: 'XXXXXXXlcjMwMTIxNzU5', + avatar_url: 'https://example.com/u/40821471?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: 'dc7e626d6da5b0b4911a1b11f0d5dcf6009f827f', + url: 'https://api.example.com/dc7e626d6da5b0b4911a1b11f0d5dcf6009f827f', + html_url: + 'https://example.com/dc7e626d6da5b0b4911a1b11f0d5dcf6009f827f', + }, + ], + }, + { + sha: '8cf0dafa2bfcc3104aa0d69ae7cf9ff55ceab8cd', + node_id: + 'C_kwDOJEBj4toAKDhjZjBkYWZhxxxXXxxXXXXXXXxZZZzzzzxXXZXxZmY1NWNlYWI4Y2Q', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-05-31T07:20:01Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-06-14T12:05:11Z', + }, + message: + 'Add functionality for fetching branches and tags from bitbucket', + tree: { + sha: '1a2539623b501741ccec196ad673071570a35dbe', + url: 'https://api.example.com/trees/1a2539623b501741ccec196ad673071570a35dbe', + }, + url: 'https://api.example.com/commits/8cf0dafa2bfcc3104aa0d69ae7cf9ff55ceab8cd', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://api.example.com/8cf0dafa2bfcc3104aa0d69ae7cf9ff55ceab8cd', + html_url: 'https://example.com/8cf0dafa2bfcc3104aa0d69ae7cf9ff55ceab8cd', + comments_url: + 'https://api.example.com/8cf0dafa2bfcc3104aa0d69ae7cf9ff55ceab8cd/comments', + author: { + login: 'TechUser', + id: 1234567482, + node_id: 'U_kgxXXxxXXxxxg', + avatar_url: 'https://example.com/u/1234567482?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'TechUser', + id: 1234567482, + node_id: 'U_kgxXXxxXXxxxg', + avatar_url: 'https://example.com/u/1234567482?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: '2f6d4472f7114cbacb6770fdabaa21b9e8e100aa', + url: 'https://api.example.com/2f6d4472f7114cbacb6770fdabaa21b9e8e100aa', + html_url: + 'https://example.com/2f6d4472f7114cbacb6770fdabaa21b9e8e100aa', + }, + ], + }, + { + sha: 'd6a73ae06e781fe510090dc58f33cc7bcf8c9c11', + node_id: 'C_kwDOJEBj4toAKGQ2YTcxxxxxxxxxxxxxxxxxxxxxxxxdiY2Y4YzljMTE', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-05-17T14:22:46Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-05-30T12:48:08Z', + }, + message: + 'Adjust gitFetcher to fetch all pull requests and ignore pagination or limits.', + tree: { + sha: 'd7950bd787096772a6cabde0b24fd4c68ca3aa77', + url: 'https://api.example.com/trees/d7950bd787096772a6cabde0b24fd4c68ca3aa77', + }, + url: 'https://api.example.com/commits/d6a73ae06e781fe510090dc58f33cc7bcf8c9c11', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://api.example.com/d6a73ae06e781fe510090dc58f33cc7bcf8c9c11', + html_url: 'https://example.com/d6a73ae06e781fe510090dc58f33cc7bcf8c9c11', + comments_url: + 'https://api.example.com/d6a73ae06e781fe510090dc58f33cc7bcf8c9c11/comments', + author: { + login: 'TechUser', + id: 1234567482, + node_id: 'U_kgxXXxxXXxxxg', + avatar_url: 'https://example.com/u/1234567482?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'TechUser', + id: 1234567482, + node_id: 'U_kgxXXxxXXxxxg', + avatar_url: 'https://example.com/u/1234567482?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: '8d59a30e6fbbd75320695c46c7221bab9fe07424', + url: 'https://api.example.com/8d59a30e6fbbd75320695c46c7221bab9fe07424', + html_url: + 'https://example.com/8d59a30e6fbbd75320695c46c7221bab9fe07424', + }, + ], + }, + { + sha: '70c52ff0011d60ded7d438463ad44945306ddc7f', + node_id: + 'C_kwDOJEBj4toAKDcwYzUyZmYwMDExZDYwZGVkN2Q0Mzg0NjNhZDQ0OTQ1MzA2ZGRjN2Y', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-04-18T14:54:22Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-04-28T11:27:36Z', + }, + message: 'Validate git fetcher input values', + tree: { + sha: '2f147a29812c08781b5be7eb01d7267fb1b157e7', + url: 'https://api.example.com/trees/2f147a29812c08781b5be7eb01d7267fb1b157e7', + }, + url: 'https://api.example.com/commits/70c52ff0011d60ded7d438463ad44945306ddc7f', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://api.example.com/70c52ff0011d60ded7d438463ad44945306ddc7f', + html_url: 'https://example.com/70c52ff0011d60ded7d438463ad44945306ddc7f', + comments_url: + 'https://api.example.com/70c52ff0011d60ded7d438463ad44945306ddc7f/comments', + author: { + login: 'TechUser', + id: 1234567482, + node_id: 'U_kgxXXxxXXxxxg', + avatar_url: 'https://example.com/u/1234567482?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'TechUser', + id: 564897364, + node_id: 'U_kgxxxxxQg', + avatar_url: 'https://example.com/u/564897364?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: 'cc0a150e8c5585f6212abab78ceaf8bc2b4f94c1', + url: 'https://api.example.com/cc0a150e8c5585f6212abab78ceaf8bc2b4f94c1', + html_url: + 'https://example.com/cc0a150e8c5585f6212abab78ceaf8bc2b4f94c1', + }, + ], + }, + { + sha: '92aa336413904e67151b6625dbaeb3fbe01ef132', + node_id: + 'C_kwDOJEBj4toAKDkyYWEzMzY0MTM5MDRlNjcxNTFiNjYyNWRiYWViM2ZiZTAxZWYxMzI', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-03-20T15:03:41Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-03-20T15:03:41Z', + }, + message: + 'Improve log and error messages for better user readability + minor code clean up', + tree: { + sha: '97fe2161887bd14518dea8fbf308d5165b03987d', + url: 'https://api.example.com/trees/97fe2161887bd14518dea8fbf308d5165b03987d', + }, + url: 'https://api.example.com/commits/92aa336413904e67151b6625dbaeb3fbe01ef132', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://api.example.com/92aa336413904e67151b6625dbaeb3fbe01ef132', + html_url: 'https://example.com/92aa336413904e67151b6625dbaeb3fbe01ef132', + comments_url: + 'https://api.example.com/92aa336413904e67151b6625dbaeb3fbe01ef132/comments', + author: { + login: 'TechUser', + id: 1234567482, + node_id: 'U_kgxXXxxXXxxxg', + avatar_url: 'https://example.com/u/1234567482?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'TechUser', + id: 1234567482, + node_id: 'U_kgxXXxxXXxxxg', + avatar_url: 'https://example.com/u/1234567482?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: '1f0f928abc84ba2d51b0da0cb40f4388d4a60f27', + url: 'https://api.example.com/1f0f928abc84ba2d51b0da0cb40f4388d4a60f27', + html_url: + 'https://example.com/1f0f928abc84ba2d51b0da0cb40f4388d4a60f27', + }, + ], + }, +] + +export const githubMultipleCommitsResponsePage1: GithubMultipleCommitsResponse[] = + [ + { + sha: '8036cf75f4b7365efea76cbd716ef12d352d7d29', + node_id: + 'C_kwDOJEBj4toAKDgwMzZjZjc1ZjRiNzM2NWVmZWE3NmNiZDcxNmVmMTJkMzUyZDdkMjk', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-07-12T10:46:50Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-07-12T10:46:50Z', + }, + message: + 'add commits and diff retrieval of target file for both bitbucket and github', + tree: { + sha: '9450ecc9597185ad82f9c9b61df5337f5ad4a286', + url: 'https://api.example.com/trees/9450ecc9597185ad82f9c9b61df5337f5ad4a286', + }, + url: 'https://api.example.com/commits/8036cf75f4b7365efea76cbd716ef12d352d7d29', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://api.example.com/8036cf75f4b7365efea76cbd716ef12d352d7d29', + html_url: 'https://example.com/8036cf75f4b7365efea76cbd716ef12d352d7d29', + comments_url: + 'https://api.example.com/8036cf75f4b7365efea76cbd716ef12d352d7d29/comments', + author: { + login: 'TechUser', + id: 112345678, + node_id: 'XXXXXXXlcjMwMTIxNzU5', + avatar_url: 'https://example.com/u/40821471?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: + 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'TechUser', + id: 112345678, + node_id: 'XXXXXXXlcjMwMTIxNzU5', + avatar_url: 'https://example.com/u/40821471?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: + 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: 'dc7e626d6da5b0b4911a1b11f0d5dcf6009f827f', + url: 'https://api.example.com/dc7e626d6da5b0b4911a1b11f0d5dcf6009f827f', + html_url: + 'https://example.com/dc7e626d6da5b0b4911a1b11f0d5dcf6009f827f', + }, + ], + }, + { + sha: '8cf0dafa2bfcc3104aa0d69ae7cf9ff55ceab8cd', + node_id: + 'C_kwDOJEBj4toAKDhjZjBkYWZhMmJmY2MzMTA0YWEwZDY5YWU3Y2Y5ZmY1NWNlYWI4Y2Q', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-05-31T07:20:01Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-06-14T12:05:11Z', + }, + message: + 'Add functionality for fetching branches and tags from bitbucket', + tree: { + sha: '1a2539623b501741ccec196ad673071570a35dbe', + url: 'https://api.example.com/trees/1a2539623b501741ccec196ad673071570a35dbe', + }, + url: 'https://api.example.com/commits/8cf0dafa2bfcc3104aa0d69ae7cf9ff55ceab8cd', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://api.example.com/8cf0dafa2bfcc3104aa0d69ae7cf9ff55ceab8cd', + html_url: 'https://example.com/8cf0dafa2bfcc3104aa0d69ae7cf9ff55ceab8cd', + comments_url: + 'https://api.example.com/8cf0dafa2bfcc3104aa0d69ae7cf9ff55ceab8cd/comments', + author: { + login: 'TechUser', + id: 564897364, + node_id: 'U_kgxxxxxQg', + avatar_url: 'https://example.com/u/564897364?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: + 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'TechUser', + id: 564897364, + node_id: 'U_kgxxxxxQg', + avatar_url: 'https://example.com/u/564897364?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: + 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: '2f6d4472f7114cbacb6770fdabaa21b9e8e100aa', + url: 'https://api.example.com/2f6d4472f7114cbacb6770fdabaa21b9e8e100aa', + html_url: + 'https://example.com/2f6d4472f7114cbacb6770fdabaa21b9e8e100aa', + }, + ], + }, + ] + +export const githubMultipleCommitsResponsePage2: GithubMultipleCommitsResponse[] = + [ + { + sha: 'd6a73ae06e781fe510090dc58f33cc7bcf8c9c11', + node_id: 'C_kwDOJEBj4toAKGQ2YTczYWUwNmU3ODxxxxxxxxXXXXXxxxxxxxx4YzljMTE', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-05-17T14:22:46Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-05-30T12:48:08Z', + }, + message: + 'Adjust gitFetcher to fetch all pull requests and ignore pagination or limits.', + tree: { + sha: 'd7950bd787096772a6cabde0b24fd4c68ca3aa77', + url: 'https://api.example.com/trees/d7950bd787096772a6cabde0b24fd4c68ca3aa77', + }, + url: 'https://api.example.com/commits/d6a73ae06e781fe510090dc58f33cc7bcf8c9c11', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://api.example.com/d6a73ae06e781fe510090dc58f33cc7bcf8c9c11', + html_url: 'https://example.com/d6a73ae06e781fe510090dc58f33cc7bcf8c9c11', + comments_url: + 'https://api.example.com/d6a73ae06e781fe510090dc58f33cc7bcf8c9c11/comments', + author: { + login: 'TechUser', + id: 1234567482, + node_id: 'U_kgDOxxxxxxx', + avatar_url: 'https://example.com/u/1234567482?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: + 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'TechUser', + id: 1234567482, + node_id: 'U_kgDOxxxxxxx', + avatar_url: 'https://example.com/u/1234567482?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: + 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: '8d59a30e6fbbd75320695c46c7221bab9fe07424', + url: 'https://api.example.com/8d59a30e6fbbd75320695c46c7221bab9fe07424', + html_url: + 'https://example.com/8d59a30e6fbbd75320695c46c7221bab9fe07424', + }, + ], + }, + { + sha: '70c52ff0011d60ded7d438463ad44945306ddc7f', + node_id: + 'C_kwDOJEBj4toAKDxxxXXXxXxxXXXXxxXxXXxxxXXXXxxXXXXxxxTQ1MzA2ZGRjN2Y', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-04-18T14:54:22Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-04-28T11:27:36Z', + }, + message: 'Validate git fetcher input values', + tree: { + sha: '2f147a29812c08781b5be7eb01d7267fb1b157e7', + url: 'https://api.example.com/trees/2f147a29812c08781b5be7eb01d7267fb1b157e7', + }, + url: 'https://api.example.com/commits/70c52ff0011d60ded7d438463ad44945306ddc7f', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://api.example.com/70c52ff0011d60ded7d438463ad44945306ddc7f', + html_url: 'https://example.com/70c52ff0011d60ded7d438463ad44945306ddc7f', + comments_url: + 'https://api.example.com/70c52ff0011d60ded7d438463ad44945306ddc7f/comments', + author: { + login: 'TechUser', + id: 1234567482, + node_id: 'U_kgxXXxxXXxxxg', + avatar_url: 'https://example.com/u/1234567482?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: + 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'TechUser', + id: 564897364, + node_id: 'U_kgxxxxxQg', + avatar_url: 'https://example.com/u/564897364?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: + 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: 'cc0a150e8c5585f6212abab78ceaf8bc2b4f94c1', + url: 'https://api.example.com/cc0a150e8c5585f6212abab78ceaf8bc2b4f94c1', + html_url: + 'https://example.com/cc0a150e8c5585f6212abab78ceaf8bc2b4f94c1', + }, + ], + }, + ] + +export const githubMultipleCommitsResponsePage3: GithubMultipleCommitsResponse[] = + [ + { + sha: '92aa336413904e67151b6625dbaeb3fbe01ef132', + node_id: + 'C_kwDOJEBj4toAKDkyYWEzMzY0MTM5MDRlNjcxNTFiNjYyNWRiYWViM2ZiZTAxZWYxMzI', + commit: { + author: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-03-20T15:03:41Z', + }, + committer: { + name: 'Tech User', + email: 'tech.user@example.com', + date: '2023-03-20T15:03:41Z', + }, + message: + 'Improve log and error messages for better user readability + minor code clean up', + tree: { + sha: '97fe2161887bd14518dea8fbf308d5165b03987d', + url: 'https://api.example.com/trees/97fe2161887bd14518dea8fbf308d5165b03987d', + }, + url: 'https://api.example.com/commits/92aa336413904e67151b6625dbaeb3fbe01ef132', + comment_count: 0, + verification: { + verified: false, + reason: 'unsigned', + signature: null, + payload: null, + }, + }, + url: 'https://api.example.com/92aa336413904e67151b6625dbaeb3fbe01ef132', + html_url: 'https://example.com/92aa336413904e67151b6625dbaeb3fbe01ef132', + comments_url: + 'https://api.example.com/92aa336413904e67151b6625dbaeb3fbe01ef132/comments', + author: { + login: 'TechUser', + id: 1234567482, + node_id: 'U_kgxXXxxXXxxxg', + avatar_url: 'https://example.com/u/1234567482?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: + 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + committer: { + login: 'TechUser', + id: 1234567482, + node_id: 'U_kgxXXxxXXxxxg', + avatar_url: 'https://example.com/u/1234567482?v=4', + gravatar_id: '', + url: 'https://api.example.com/users/TechUser', + html_url: 'https://example.com/TechUser', + followers_url: 'https://api.example.com/users/TechUser/followers', + following_url: + 'https://api.example.com/users/TechUser/following{/other_user}', + gists_url: 'https://api.example.com/users/TechUser/gists{/gist_id}', + starred_url: + 'https://api.example.com/users/TechUser/starred{/owner}{/repo}', + subscriptions_url: + 'https://api.example.com/users/TechUser/subscriptions', + organizations_url: 'https://api.example.com/users/TechUser/orgs', + repos_url: 'https://api.example.com/users/TechUser/repos', + events_url: 'https://api.example.com/users/TechUser/events{/privacy}', + received_events_url: + 'https://api.example.com/users/TechUser/received_events', + type: 'User', + site_admin: false, + }, + parents: [ + { + sha: '1f0f928abc84ba2d51b0da0cb40f4388d4a60f27', + url: 'https://api.example.com/1f0f928abc84ba2d51b0da0cb40f4388d4a60f27', + html_url: + 'https://example.com/1f0f928abc84ba2d51b0da0cb40f4388d4a60f27', + }, + ], + }, + ] + +export const githubCommitsEmptyResponse: GithubMultipleCommitsResponse[] = [] + +export const githubNoDiffResponse: GithubDiffResponse = { + url: '', + html_url: '', + permalink_url: '', + diff_url: '', + patch_url: '', + base_commit: '', + merge_base_commit: {}, + status: '', + ahead_by: 0, + behind_by: 0, + total_commits: 0, + commits: [], + files: [], +} diff --git a/yaku-apps-typescript/apps/git-fetcher/test/unit/fixtures/github-responses-prs.ts b/yaku-apps-typescript/apps/git-fetcher/test/unit/fixtures/github-responses-prs.ts new file mode 100644 index 00000000..4cc02047 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/unit/fixtures/github-responses-prs.ts @@ -0,0 +1,40 @@ +import { GithubLabel } from '../../../src/model/github-label' +import { GithubPr } from '../../../src/model/github-pr' + +const gitHubLabel: GithubLabel = { + id: 1, + name: 'foo', +} + +const gitHubPrResponse1: GithubPr = { + number: 1, + state: 'open', + labels: [gitHubLabel], +} + +const gitHubPrResponse2: GithubPr = { + number: 2, + state: 'closed', + labels: [gitHubLabel], +} + +const gitHubPrResponse3: GithubPr = { + number: 3, + state: 'open', + labels: [gitHubLabel], +} + +const gitHubPrResponse4: GithubPr = { + number: 4, + state: 'closed', + labels: [gitHubLabel], +} + +const singleResponse1: GithubPr[] = [gitHubPrResponse1, gitHubPrResponse2] +const singleResponse2: GithubPr[] = [gitHubPrResponse3, gitHubPrResponse4] + +export const multiPageResponse: GithubPr[][] = [ + singleResponse1, + singleResponse2, + [], +] diff --git a/yaku-apps-typescript/apps/git-fetcher/test/unit/generate-git-fetcher.test.ts b/yaku-apps-typescript/apps/git-fetcher/test/unit/generate-git-fetcher.test.ts new file mode 100644 index 00000000..2fc6cbfc --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/unit/generate-git-fetcher.test.ts @@ -0,0 +1,114 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import { GitFetcher, GitResource } from '../../src/fetchers' +import { GitFetcherBitbucketTagsAndBranches } from '../../src/fetchers/git-fetcher-bitbucket-tags-and-branches' +import { GitFetcherBitbucketPrs } from '../../src/fetchers/git-fetcher-bitbucket-prs' +import { GitFetcherGithubPrs } from '../../src/fetchers/git-fetcher-github-prs' +import { + ConfigFileData, + GitConfigResource, + gitFetcherPullRequests, +} from '../../src/model/config-file-data' +import { + GitServerConfig, + SupportedGitServerType, +} from '../../src/model/git-server-config' +import generateGitFetcher from '../../src/fetchers/generate-git-fetcher' + +describe('Git Fetcher Factory', () => { + let gitServerConfig: GitServerConfig + let configFileData: ConfigFileData + + beforeEach(() => { + gitServerConfig = { + gitServerType: 'bitbucket', + gitServerApiUrl: 'https://www.foo.bar', + gitServerAuthMethod: 'token', + gitServerApiToken: 'someToken', + gitFetcherConfigFilePath: './config.yaml', + gitFetcherOutputFilePath: './output.json', + } + configFileData = { + data: { + org: 'someOrg', + repo: 'someRepo', + resource: 'prs', + }, + } + }) + + it('should return fetcher for Bitbucket branches', () => { + expectFetcherType( + 'bitbucket', + 'branches', + GitFetcherBitbucketTagsAndBranches.name + ) + }) + + it('should return fetcher for Bitbucket tags', () => { + expectFetcherType( + 'bitbucket', + 'tags', + GitFetcherBitbucketTagsAndBranches.name + ) + }) + + it.each(gitFetcherPullRequests)( + 'should return fetcher for Bitbucket PRs, when passing %s as resource', + (resource) => { + expectFetcherType('bitbucket', resource, GitFetcherBitbucketPrs.name) + } + ) + + it.each(gitFetcherPullRequests)( + 'should return fetcher for Github PRs, when passing %s as resource', + (resource) => { + expectFetcherType('github', resource, GitFetcherGithubPrs.name) + } + ) + + it('should throw exception when passing an unknown git server type', () => { + gitServerConfig.gitServerType = 'ado' as 'bitbucket' // explicit type cast for error case + let errorWasThrownAsExpected = false + + try { + generateGitFetcher(gitServerConfig, configFileData) + } catch (e) { + console.log(e) + expect(e.toString()).toContain( + 'Unsupported git server / git resource combination: ado/prs' + ) + errorWasThrownAsExpected = true + } + + expect(errorWasThrownAsExpected).toBe(true) + }) + + it('should throw exception when passing an unknown resource', () => { + configFileData.data.resource = 'unknownResource' as 'prs' // explicit type cast for error case + let errorWasThrownAsExpected = false + + try { + const result = generateGitFetcher(gitServerConfig, configFileData) + console.log(result) + } catch (e) { + console.log(e) + errorWasThrownAsExpected = true + } + + expect(errorWasThrownAsExpected).toBe(true) + }) + + function expectFetcherType( + gitServerType: SupportedGitServerType, + gitConfigResource: GitConfigResource, + expectedFetcherName: string + ): void { + gitServerConfig.gitServerType = gitServerType + configFileData.data.resource = gitConfigResource + const result: GitFetcher = generateGitFetcher( + gitServerConfig, + configFileData + ) + expect(result.constructor.name).toEqual(expectedFetcherName) + } +}) diff --git a/yaku-apps-typescript/apps/git-fetcher/test/unit/git-fetcher-bitbucket-branches.test.ts b/yaku-apps-typescript/apps/git-fetcher/test/unit/git-fetcher-bitbucket-branches.test.ts new file mode 100644 index 00000000..2efa1c3c --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/unit/git-fetcher-bitbucket-branches.test.ts @@ -0,0 +1,146 @@ +import { fail } from 'assert' +import { beforeEach, describe, expect, it, SpyInstance, vi } from 'vitest' +import { GitFetcherBitbucketTagsAndBranches } from '../../src/fetchers/git-fetcher-bitbucket-tags-and-branches' +import { BitbucketResponse } from '../../src/model/bitbucket-response' +import { BitbucketBranch } from '../../src/model/bitbucket-branch' +import { ConfigFileData } from '../../src/model/config-file-data' +import { GitServerConfig } from '../../src/model/git-server-config' +import { + bitBucketBranchEmptyResponse, + bitBucketBranchMultiPageResponse, + bitBucketBranchSinglePageResponse, +} from './fixtures/bitbucket-responses-branches' +import * as responseHandler from '../../src/utils/handle-response-status' + +const gitServerConfigDefault: GitServerConfig = { + gitServerType: 'bitbucket', + gitServerApiUrl: ' https://www.foo.bar', + gitServerAuthMethod: 'basic', +} as const as GitServerConfig + +const configDefault: ConfigFileData = { + data: { + org: 'foo_org', + repo: 'foo_repo', + resource: 'branches', + }, +} as const + +let responseStatusHandlerSpy: SpyInstance +let consoleSpy: SpyInstance +let fetchMock: SpyInstance +let gitFetcherBitBucketBranches: GitFetcherBitbucketTagsAndBranches + +const fetchUrlMatcher = /\/branches\?start=\d+$/ + +describe('Git Fetcher BitBucket Branches', () => { + beforeEach(() => { + responseStatusHandlerSpy = vi + .spyOn(responseHandler, 'handleResponseStatus') + .mockImplementation(() => { + throw new Error('MockError') + }) + consoleSpy = vi.spyOn(console, 'log') + fetchMock = vi.spyOn(global, 'fetch') + gitFetcherBitBucketBranches = new GitFetcherBitbucketTagsAndBranches( + gitServerConfigDefault, + configDefault, + 'branches' + ) + }) + + it('should return empty array when no branches were returned by BitBucket', async () => { + fetchMock.mockImplementation(async (url: string) => { + expect(url).toMatch(fetchUrlMatcher) + return { + status: 200, + json: async () => bitBucketBranchEmptyResponse, + } + }) + + const result: BitbucketBranch[] = + (await gitFetcherBitBucketBranches.fetchResource()) as BitbucketBranch[] + expect(fetchMock).toHaveBeenCalledOnce() + expect(consoleSpy).toHaveBeenCalledWith('Fetched 0 branches') + expect(result).toHaveLength(0) + + expect(responseStatusHandlerSpy).toBeCalledTimes(0) + }) + + it('should send exactly one request, if the first response contains all branches', async () => { + fetchMock.mockImplementation(async (url: string) => { + expect(url).toMatch(fetchUrlMatcher) + return { + status: 200, + json: async () => bitBucketBranchSinglePageResponse, + } + }) + + const result: BitbucketBranch[] = + (await gitFetcherBitBucketBranches.fetchResource()) as BitbucketBranch[] + expect(fetchMock).toHaveBeenCalledOnce() + expect(consoleSpy).toHaveBeenCalledWith('Fetched 1 branch') + expect(result).toHaveLength(1) + expect(result).toEqual(bitBucketBranchSinglePageResponse.values) + + expect(responseStatusHandlerSpy).toBeCalledTimes(0) + }) + + it('should send multiple requests, until all branches have been fetched', async () => { + let queriedFirstPage = false + let queriedSecondPage = false + fetchMock.mockImplementation(async (url: string) => { + expect(url).toMatch(fetchUrlMatcher) + let responseBodyToReturn: BitbucketResponse + if (url.endsWith('start=0')) { + responseBodyToReturn = bitBucketBranchMultiPageResponse[0] + queriedFirstPage = true + } else if (url.endsWith('start=1')) { + responseBodyToReturn = bitBucketBranchMultiPageResponse[1] + queriedSecondPage = true + } else { + fail(`unexpected url: ${url}`) + } + return { + status: 200, + json: async () => responseBodyToReturn, + } + + const result: BitbucketBranch[] = + (await gitFetcherBitBucketBranches.fetchResource()) as BitbucketBranch[] + expect(fetchMock).toBeCalledTimes(2) + expect(consoleSpy).toBeCalledWith('Fetched 2 branches') + expect(queriedFirstPage).toBe(true) + expect(queriedSecondPage).toBe(true) + expect(result).toHaveLength(2) + expect(result).toEqual([ + ...bitBucketBranchMultiPageResponse[0].values, + ...bitBucketBranchMultiPageResponse[1].values, + ]) + + expect(responseStatusHandlerSpy).toBeCalledTimes(0) + }) + }) + + it.each([400, 401, 403, 500])( + 'should throw error when BitBucket responds with non-200 code (%d)', + async (responseCode: number) => { + fetchMock.mockImplementation(async (url: string) => { + expect(url).toMatch(fetchUrlMatcher) + return { + status: responseCode, + } + }) + + let errorWasThrown = false + try { + await gitFetcherBitBucketBranches.fetchResource() + } catch (e) { + errorWasThrown = true + } + expect(errorWasThrown).toBe(true) + expect(responseStatusHandlerSpy).toHaveBeenCalledOnce() + expect(responseStatusHandlerSpy).toHaveBeenCalledWith(responseCode) + } + ) +}) diff --git a/yaku-apps-typescript/apps/git-fetcher/test/unit/git-fetcher-bitbucket-commits-metadata-and-diff.test.ts b/yaku-apps-typescript/apps/git-fetcher/test/unit/git-fetcher-bitbucket-commits-metadata-and-diff.test.ts new file mode 100644 index 00000000..6afdf0d4 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/unit/git-fetcher-bitbucket-commits-metadata-and-diff.test.ts @@ -0,0 +1,189 @@ +import { fail } from 'assert' +import { beforeEach, describe, expect, it, SpyInstance, vi } from 'vitest' +import { BitbucketResponse } from '../../src/model/bitbucket-response.js' +import { CommitsMetadataAndDiff } from '../../src/model/commits-metadata-and-diff.js' +import { BitbucketCommit } from '../../src/model/bitbucket-commit.js' +import { ConfigFileData } from '../../src/model/config-file-data.js' +import { GitServerConfig } from '../../src/model/git-server-config.js' +import * as responseHandler from '../../src/utils/handle-response-status.js' +import { + bitBucketCommitsEmptyResponse, + bitBucketDiffEmptyResponse, + bitBucketCommitsSinglePageResponse, + bitBucketDiffNonEmptyPageResponse, + bitBucketCommitsMultiPageResponse, +} from './fixtures/bitbucket-responses-commits-and-diff.js' +import { GitFetcherBitbucketCommitsAndDiff } from '../../src/fetchers/git-fetcher-bitbucket-commits-and-diff.js' + +const gitServerConfigDefault: GitServerConfig = { + gitServerType: 'bitbucket', + gitServerApiUrl: ' https://www.foo.bar', + gitServerAuthMethod: 'basic', +} as const as GitServerConfig + +const configDefault: ConfigFileData = { + data: { + org: 'foo_org', + repo: 'foo_repo', + resource: 'metadata-and-diff', + filter: { + startHash: '35cc5eec543e69aed90503f21cf12666bcbfda4f', + }, + filePath: 'Somefolder/something.py', + }, +} as const + +let responseStatusHandlerSpy: SpyInstance +let consoleSpy: SpyInstance +let fetchMock: SpyInstance +let gitFetcherBitbucketCommitsAndDiff: GitFetcherBitbucketCommitsAndDiff + +const fetchCommitsUrlMatcher = /\/commits\?path=/ +const fetchDiffUrlMatcher = /\/diff\// +const fetchCommitsOrDiffUrlMatcher = /\/commits\?path=|\/diff\// + +describe('Git Fetcher Metadata And Diff', () => { + beforeEach(() => { + responseStatusHandlerSpy = vi + .spyOn(responseHandler, 'handleResponseStatus') + .mockImplementation(() => { + throw new Error('Mock Error') + }) + consoleSpy = vi.spyOn(console, 'log') + fetchMock = vi.spyOn(global, 'fetch') + gitFetcherBitbucketCommitsAndDiff = new GitFetcherBitbucketCommitsAndDiff( + gitServerConfigDefault, + configDefault + ) + }) + + it('should return empty array when no commits were returned by BitBucket', async () => { + fetchMock.mockImplementation(async (url: string) => { + expect(url).toMatch(fetchCommitsOrDiffUrlMatcher) + if (url.match(fetchCommitsUrlMatcher)) { + return { + status: 200, + json: async () => bitBucketCommitsEmptyResponse, + } + } + if (url.match(fetchDiffUrlMatcher)) { + return { + status: 200, + json: async () => bitBucketDiffEmptyResponse, + } + } + }) + + const result: CommitsMetadataAndDiff = + (await gitFetcherBitbucketCommitsAndDiff.fetchResource()) as CommitsMetadataAndDiff + expect(fetchMock).toBeCalledTimes(2) + expect(consoleSpy).toHaveBeenCalledWith('Fetched medata about 0 commits') + expect(consoleSpy).toHaveBeenCalledWith('Fetched 0 diff') + expect(result.commitsMetadata).toHaveLength(0) + expect(result.diff).toHaveLength(0) + + expect(responseStatusHandlerSpy).toBeCalledTimes(0) + }) + + it('should send one request for commits and one request for diff, if the first commits response contains all commits', async () => { + fetchMock.mockImplementation(async (url: string) => { + expect(url).toMatch(fetchCommitsOrDiffUrlMatcher) + if (url.match(fetchCommitsUrlMatcher)) { + return { + status: 200, + json: async () => bitBucketCommitsSinglePageResponse, + } + } + if (url.match(fetchDiffUrlMatcher)) { + return { + status: 200, + json: async () => bitBucketDiffNonEmptyPageResponse, + } + } + }) + + const result: CommitsMetadataAndDiff = + (await gitFetcherBitbucketCommitsAndDiff.fetchResource()) as CommitsMetadataAndDiff + expect(fetchMock).toBeCalledTimes(2) + expect(consoleSpy).toHaveBeenCalledWith('Fetched medata about 5 commits') + expect(consoleSpy).toHaveBeenCalledWith('Fetched 1 diff') + expect(result.commitsMetadata).toHaveLength(5) + expect(result.diff).toHaveLength(1) + + expect(responseStatusHandlerSpy).toBeCalledTimes(0) + }) + + it('should send multiple requests, until all commits have been fetched', async () => { + let queriedFirstPage = false + let queriedSecondPage = false + let queriedThirdPage = false + fetchMock.mockImplementation(async (url: string) => { + expect(url).toMatch(fetchCommitsOrDiffUrlMatcher) + if (url.match(fetchCommitsUrlMatcher)) { + let responseBodyToReturn: BitbucketResponse + if (url.endsWith('start=0')) { + responseBodyToReturn = bitBucketCommitsMultiPageResponse[0] + queriedFirstPage = true + } else if (url.endsWith('start=2')) { + responseBodyToReturn = bitBucketCommitsMultiPageResponse[1] + queriedSecondPage = true + } else if (url.endsWith('start=4')) { + responseBodyToReturn = bitBucketCommitsMultiPageResponse[2] + queriedThirdPage = true + } else { + fail(`unexpected url: ${url}`) + } + return { + status: 200, + json: async () => responseBodyToReturn, + } + } + if (url.match(fetchDiffUrlMatcher)) { + return { + status: 200, + json: async () => bitBucketDiffNonEmptyPageResponse, + } + } + }) + + const result: CommitsMetadataAndDiff = + (await gitFetcherBitbucketCommitsAndDiff.fetchResource()) as CommitsMetadataAndDiff + expect(fetchMock).toBeCalledTimes(4) + expect(consoleSpy).toBeCalledWith('Fetched medata about 5 commits') + expect(consoleSpy).toHaveBeenCalledWith('Fetched 1 diff') + expect(queriedFirstPage).toBe(true) + expect(queriedSecondPage).toBe(true) + expect(queriedThirdPage).toBe(true) + expect(result.commitsMetadata).toHaveLength(5) + expect(result.diff).toHaveLength(1) + expect(result.commitsMetadata).toEqual([ + ...bitBucketCommitsMultiPageResponse[0].values, + ...bitBucketCommitsMultiPageResponse[1].values, + ...bitBucketCommitsMultiPageResponse[2].values, + ]) + + expect(responseStatusHandlerSpy).toBeCalledTimes(0) + }) + + it.each([400, 401, 403, 500])( + 'should throw error when BitBucket responds with non-200 code (%d)', + async (responseCode: number) => { + fetchMock.mockImplementation(async (url: string) => { + expect(url).toMatch(fetchCommitsOrDiffUrlMatcher) + return { + status: responseCode, + } + }) + + let errorWasThrown = false + try { + await gitFetcherBitbucketCommitsAndDiff.fetchResource() + } catch (e) { + errorWasThrown = true + } + expect(errorWasThrown).toBe(true) + expect(responseStatusHandlerSpy).toHaveBeenCalledOnce() + expect(responseStatusHandlerSpy).toHaveBeenCalledWith(responseCode) + } + ) +}) diff --git a/yaku-apps-typescript/apps/git-fetcher/test/unit/git-fetcher-bitbucket-prs.test.ts b/yaku-apps-typescript/apps/git-fetcher/test/unit/git-fetcher-bitbucket-prs.test.ts new file mode 100644 index 00000000..7f41defd --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/unit/git-fetcher-bitbucket-prs.test.ts @@ -0,0 +1,209 @@ +import { beforeEach, describe, expect, it, SpyInstance, vi } from 'vitest' +import { + multiPageResponse, + singlePageResponse, +} from './fixtures/bitbucket-responses-prs' +import { GitFetcherBitbucketPrs } from '../../src/fetchers/git-fetcher-bitbucket-prs' +import * as responseHandler from '../../src/utils/handle-response-status' +import { BitbucketResponse } from '../../src/model/bitbucket-response' +import { GitServerConfig } from '../../src/model/git-server-config' +import { BitbucketPr } from '../../src/model/bitbucket-pr' +import { + allowedFilterState, + AllowedFilterStateType, + ConfigFileData, +} from '../../src/model/config-file-data' + +const gitServerConfigDefault: GitServerConfig = { + gitServerType: 'bitbucket', + gitServerApiUrl: 'www.foo.bar', + gitServerAuthMethod: 'basic', +} as GitServerConfig + +let consoleSpy: SpyInstance | undefined = undefined +let fetchMock: SpyInstance | undefined = undefined +let gitFetcherBitbucketPrs: GitFetcherBitbucketPrs | undefined = undefined +let handleResponseStatusSpy: SpyInstance | undefined = undefined + +describe('GitFetcherBitBucket', () => { + let configDefault: ConfigFileData + + beforeEach(() => { + consoleSpy = vi.spyOn(console, 'log') + fetchMock = vi.spyOn(global, 'fetch') + handleResponseStatusSpy = vi + .spyOn(responseHandler, 'handleResponseStatus') + .mockImplementation(() => { + throw new Error('Mock Error') + }) + configDefault = { + data: { + org: 'foo_org', + repo: 'foo_repo', + resource: 'prs', + }, + } + + gitFetcherBitbucketPrs = new GitFetcherBitbucketPrs( + gitServerConfigDefault, + configDefault + ) + }) + + it('should throw an error message, when fetch-method is unable to get response', async () => { + await expect(gitFetcherBitbucketPrs.fetchResource()).rejects.toThrow( + 'Failed to parse URL from www.foo.bar/projects/foo_org/repos/foo_repo/pull-requests?state=ALL&start=0' + ) + }) + + it(`should call the fetch method only once and return array with prs, when it return body's attribute "isLastPage" is true`, async () => { + const responseFixture: BitbucketResponse> = + singlePageResponse + + fetchMock.mockImplementation(async () => { + return { + status: 200, + json: async () => { + return responseFixture + }, + } as Response + }) + + const result = await gitFetcherBitbucketPrs.fetchResource() + + expect(fetchMock).toBeCalledTimes(1) + const expectedNumberOfPrs = responseFixture.values.length + expect(consoleSpy).toHaveBeenCalledWith( + `Fetched ${responseFixture.values.length} pull request${ + expectedNumberOfPrs === 1 ? '' : 's' + }` + ) + expect(result).toEqual(responseFixture.values) + }) + + it(`should call the fetch method twice and return an array that with prs from both calls, first response body's attribute 'isLastPage' is false but second is true`, async () => { + const responseFixture: BitbucketResponse[] = multiPageResponse + + const expectedResultArray: BitbucketPr[] = responseFixture.flatMap( + (response) => response.values + ) + + fetchMock.mockImplementation(async (url: string) => { + return { + status: 200, + json: async () => { + const page = url.split('=').slice(-1)[0] + return responseFixture[ + Number(page) === 0 ? Number(page) : Number(page) - 1 + ] + }, + } as Response + }) + + const result = await gitFetcherBitbucketPrs.fetchResource() + + expect(fetchMock).toHaveBeenCalledTimes(2) + const expectedLength = expectedResultArray.length + expect(consoleSpy).toHaveBeenCalledWith( + `Fetched ${expectedLength} pull request${expectedLength === 1 ? '' : 's'}` + ) + expect(result).toEqual(expectedResultArray) + }) + + it(`should throw an error, when the response status is not 200.`, async () => { + const responseFixture: BitbucketResponse>[] = + multiPageResponse + + fetchMock.mockImplementation(async (url: string) => { + return { + status: 404, + json: async () => { + const page = url.split('=').slice(-1)[0] + return responseFixture[ + Number(page) === 0 ? Number(page) : Number(page) - 1 + ] + }, + } as Response + }) + + let errorCaught = false + try { + await gitFetcherBitbucketPrs.fetchResource() + } catch (error) { + errorCaught = true + expect(error.message).toEqual('Mock Error') + } + + expect(errorCaught).toBe(true) + expect(fetchMock).toHaveBeenCalledTimes(1) + expect(handleResponseStatusSpy).toHaveBeenCalledTimes(1) + }) + + it('should set state filter to "ALL" in the URL if no state is given in the configuration', async () => { + const config: ConfigFileData = configDefault + config.data.filter = undefined + + gitFetcherBitbucketPrs = new GitFetcherBitbucketPrs( + gitServerConfigDefault, + config + ) + const responseFixture: BitbucketResponse> = + singlePageResponse + fetchMock.mockImplementation(async () => { + return { + status: 200, + json: async () => { + return responseFixture + }, + } as Response + }) + + // eslint-disable-next-line + // @ts-ignore + const composeUrlSpy = vi.spyOn(gitFetcherBitbucketPrs, 'composePrUrl') + + await gitFetcherBitbucketPrs.fetchResource() + + expect(composeUrlSpy).toBeCalledTimes(1) + expect(composeUrlSpy).toBeCalledWith(undefined, 0) + expect(composeUrlSpy).toReturnWith( + 'www.foo.bar/projects/foo_org/repos/foo_repo/pull-requests?state=ALL&start=0' + ) + }) + + it.each(allowedFilterState)( + 'should set the state filter to %s in the URL', + async (state: AllowedFilterStateType) => { + const config: ConfigFileData = { + data: { ...configDefault.data, filter: {} }, + } + config.data.filter.state = state + + gitFetcherBitbucketPrs = new GitFetcherBitbucketPrs( + gitServerConfigDefault, + config + ) + const responseFixture: BitbucketResponse> = + singlePageResponse + fetchMock.mockImplementation(async () => { + return { + status: 200, + json: async () => { + return responseFixture + }, + } as Response + }) + + // eslint-disable-next-line + // @ts-ignore + const composeUrlSpy = vi.spyOn(gitFetcherBitbucketPrs, 'composePrUrl') + await gitFetcherBitbucketPrs.fetchResource() + + expect(composeUrlSpy).toBeCalledTimes(1) + expect(composeUrlSpy).toBeCalledWith(state, 0) + expect(composeUrlSpy).toReturnWith( + `www.foo.bar/projects/foo_org/repos/foo_repo/pull-requests?state=${state}&start=0` + ) + } + ) +}) diff --git a/yaku-apps-typescript/apps/git-fetcher/test/unit/git-fetcher-bitbucket-tags.test.ts b/yaku-apps-typescript/apps/git-fetcher/test/unit/git-fetcher-bitbucket-tags.test.ts new file mode 100644 index 00000000..98769ea5 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/unit/git-fetcher-bitbucket-tags.test.ts @@ -0,0 +1,146 @@ +import { fail } from 'assert' +import { beforeEach, describe, expect, it, SpyInstance, vi } from 'vitest' +import { BitbucketResponse } from '../../src/model/bitbucket-response.js' +import { BitbucketTag } from '../../src/model/bitbucket-tag' +import { ConfigFileData } from '../../src/model/config-file-data' +import { GitServerConfig } from '../../src/model/git-server-config' +import * as responseHandler from '../../src/utils/handle-response-status' +import { + bitBucketTagEmptyResponse, + bitBucketTagMultiPageResponse, + bitBucketTagSinglePageResponse, +} from './fixtures/bitbucket-responses-tags' +import { GitFetcherBitbucketTagsAndBranches } from '../../src/fetchers/git-fetcher-bitbucket-tags-and-branches' + +const gitServerConfigDefault: GitServerConfig = { + gitServerType: 'bitbucket', + gitServerApiUrl: ' https://www.foo.bar', + gitServerAuthMethod: 'basic', +} as const as GitServerConfig + +const configDefault: ConfigFileData = { + data: { + org: 'foo_org', + repo: 'foo_repo', + resource: 'tags', + }, +} as const + +let responseStatusHandlerSpy: SpyInstance +let consoleSpy: SpyInstance +let fetchMock: SpyInstance +let gitFetcherBitBucketTags: GitFetcherBitbucketTagsAndBranches + +const fetchUrlMatcher = /\/tags\?start=\d+$/ + +describe('Git Fetcher BitBucket Tags', () => { + beforeEach(() => { + responseStatusHandlerSpy = vi + .spyOn(responseHandler, 'handleResponseStatus') + .mockImplementation(() => { + throw new Error('Mock Error') + }) + consoleSpy = vi.spyOn(console, 'log') + fetchMock = vi.spyOn(global, 'fetch') + gitFetcherBitBucketTags = new GitFetcherBitbucketTagsAndBranches( + gitServerConfigDefault, + configDefault, + 'tags' + ) + }) + + it('should return empty array when no tags were returned by BitBucket', async () => { + fetchMock.mockImplementation(async (url: string) => { + expect(url).toMatch(fetchUrlMatcher) + return { + status: 200, + json: async () => bitBucketTagEmptyResponse, + } + }) + + const result: BitbucketTag[] = + (await gitFetcherBitBucketTags.fetchResource()) as BitbucketTag[] + expect(fetchMock).toHaveBeenCalledOnce() + expect(consoleSpy).toHaveBeenCalledWith('Fetched 0 tags') + expect(result).toHaveLength(0) + + expect(responseStatusHandlerSpy).toBeCalledTimes(0) + }) + + it('should send exactly one request, if the first response contains all tags', async () => { + fetchMock.mockImplementation(async (url: string) => { + expect(url).toMatch(fetchUrlMatcher) + return { + status: 200, + json: async () => bitBucketTagSinglePageResponse, + } + }) + + const result: BitbucketTag[] = + (await gitFetcherBitBucketTags.fetchResource()) as BitbucketTag[] + expect(fetchMock).toHaveBeenCalledOnce() + expect(consoleSpy).toBeCalledWith('Fetched 1 tag') + expect(result).toHaveLength(1) + expect(result).toEqual(bitBucketTagSinglePageResponse.values) + + expect(responseStatusHandlerSpy).toBeCalledTimes(0) + }) + + it('should send multiple requests, until all tags have been fetched', async () => { + let queriedFirstPage = false + let queriedSecondPage = false + fetchMock.mockImplementation(async (url: string) => { + expect(url).toMatch(fetchUrlMatcher) + let responseBodyToReturn: BitbucketResponse + if (url.endsWith('start=0')) { + responseBodyToReturn = bitBucketTagMultiPageResponse[0] + queriedFirstPage = true + } else if (url.endsWith('start=1')) { + responseBodyToReturn = bitBucketTagMultiPageResponse[1] + queriedSecondPage = true + } else { + fail(`unexpected url: ${url}`) + } + return { + status: 200, + json: async () => responseBodyToReturn, + } + }) + + const result: BitbucketTag[] = + (await gitFetcherBitBucketTags.fetchResource()) as BitbucketTag[] + expect(fetchMock).toBeCalledTimes(2) + expect(consoleSpy).toBeCalledWith('Fetched 2 tags') + expect(queriedFirstPage).toBe(true) + expect(queriedSecondPage).toBe(true) + expect(result).toHaveLength(2) + expect(result).toEqual([ + ...bitBucketTagMultiPageResponse[0].values, + ...bitBucketTagMultiPageResponse[1].values, + ]) + + expect(responseStatusHandlerSpy).toBeCalledTimes(0) + }) + + it.each([400, 401, 403, 500])( + 'should throw error when BitBucket responds with non-200 code (%d)', + async (responseCode: number) => { + fetchMock.mockImplementation(async (url: string) => { + expect(url).toMatch(fetchUrlMatcher) + return { + status: responseCode, + } + }) + + let errorWasThrown = false + try { + await gitFetcherBitBucketTags.fetchResource() + } catch (e) { + errorWasThrown = true + } + expect(errorWasThrown).toBe(true) + expect(responseStatusHandlerSpy).toHaveBeenCalledOnce() + expect(responseStatusHandlerSpy).toHaveBeenCalledWith(responseCode) + } + ) +}) diff --git a/yaku-apps-typescript/apps/git-fetcher/test/unit/git-fetcher-github-commits-metadata-and-diff.test.ts b/yaku-apps-typescript/apps/git-fetcher/test/unit/git-fetcher-github-commits-metadata-and-diff.test.ts new file mode 100644 index 00000000..f7cc7071 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/unit/git-fetcher-github-commits-metadata-and-diff.test.ts @@ -0,0 +1,271 @@ +import { beforeEach, describe, expect, it, SpyInstance, vi } from 'vitest' +import { CommitsMetadataAndDiff } from '../../src/model/commits-metadata-and-diff.js' +import { ConfigFileData } from '../../src/model/config-file-data.js' +import { GitServerConfig } from '../../src/model/git-server-config.js' +import * as responseHandler from '../../src/utils/handle-response-status.js' +import { + githubStartCommitResponse, + githubEndCommitResponse, + githubDiffResponse, + githubMultipleCommitsResponse, + githubMultipleCommitsResponsePage1, + githubMultipleCommitsResponsePage2, + githubMultipleCommitsResponsePage3, + githubCommitsEmptyResponse, + githubNoDiffResponse, +} from './fixtures/github-responses-commits-and-diff.js' +import { GitFetcherGithubCommitsAndDiff } from '../../src/fetchers/git-fetcher-github-commits-and-diff.js' + +const gitServerConfigDefault: GitServerConfig = { + gitServerType: 'bitbucket', + gitServerApiUrl: ' https://www.foo.bar', + gitServerAuthMethod: 'basic', +} as const as GitServerConfig + +const configDefault: ConfigFileData = { + data: { + org: 'foo_org', + repo: 'foo_repo', + resource: 'metadata-and-diff', + filter: { + startHash: 'afeaebf412c6d0b865a36cfdec37fdb46c0fab63', + endHash: '8036cf75f4b7365efea76cbd716ef12d352d7d29', + }, + filePath: 'apps/git-fetcher/src/fetchers/git-fetcher.ts', + }, +} as const + +let responseStatusHandlerSpy: SpyInstance +let consoleSpy: SpyInstance +let fetchMock: SpyInstance +let gitFetcherGithubCommitsAndDiff: GitFetcherGithubCommitsAndDiff + +const fetchAllCommitsUrlMatcher = /\/commits\?path=/ +const fetchIndividualCommitUrlMatcher = /\/commits\// +const fetchDiffUrlMatcher = /\/compare\// +const fetchCommitsOrDiffUrlMatcher = /\/commits\?path=|\/commits\/|\/compare\// +const startCommitIdMatcher = /afeaebf412c6d0b865a36cfdec37fdb46c0fab63/ + +describe('Git Fetcher Metadata And Diff', () => { + beforeEach(() => { + responseStatusHandlerSpy = vi + .spyOn(responseHandler, 'handleResponseStatus') + .mockImplementation(() => { + throw new Error('Mock Error') + }) + consoleSpy = vi.spyOn(console, 'log') + fetchMock = vi.spyOn(global, 'fetch') + gitFetcherGithubCommitsAndDiff = new GitFetcherGithubCommitsAndDiff( + gitServerConfigDefault, + configDefault + ) + }) + + it('should return empty array when no commits were returned by Github', async () => { + fetchMock.mockImplementation(async (url: string) => { + expect(url).toMatch(fetchCommitsOrDiffUrlMatcher) + if (url.match(fetchIndividualCommitUrlMatcher)) { + return { + status: 200, + json: async () => githubStartCommitResponse, + } + } + if (url.match(fetchAllCommitsUrlMatcher)) { + return { + status: 200, + json: async () => githubCommitsEmptyResponse, + } + } + if (url.match(fetchDiffUrlMatcher)) { + return { + status: 200, + json: async () => githubNoDiffResponse, + } + } + }) + + const result: CommitsMetadataAndDiff = + (await gitFetcherGithubCommitsAndDiff.fetchResource()) as CommitsMetadataAndDiff + expect(fetchMock).toBeCalledTimes(4) + expect(consoleSpy).toHaveBeenCalledWith( + 'Fetched metadata about starting commit at 2023-03-06T14:11:29Z' + ) + expect(consoleSpy).toHaveBeenCalledWith( + 'Fetched metadata about ending commit at 2023-03-06T14:11:29Z' + ) + expect(consoleSpy).toHaveBeenCalledWith( + 'Fetched 0 lines added and 0 lines removed' + ) + expect(consoleSpy).toHaveBeenCalledWith('Fetched metadata about 0 commits') + expect(result.commitsMetadata).toHaveLength(0) + expect(result.diff.linesAdded).toHaveLength(0) + expect(result.diff.linesRemoved).toHaveLength(0) + + expect(responseStatusHandlerSpy).toBeCalledTimes(0) + }) + + it('should send two request for commits and one request for diff, if the first commits response contains all commits', async () => { + fetchMock.mockImplementation(async (url: string) => { + expect(url).toMatch(fetchCommitsOrDiffUrlMatcher) + if (url.match(fetchIndividualCommitUrlMatcher)) { + if (url.match(startCommitIdMatcher)) { + return { + status: 200, + json: async () => githubStartCommitResponse, + } + } else { + return { + status: 200, + json: async () => githubEndCommitResponse, + } + } + } + if (url.match(fetchAllCommitsUrlMatcher)) { + if (url.match(/&page=1/)) { + return { + status: 200, + json: async () => githubMultipleCommitsResponse, + } + } else { + return { + status: 200, + json: async () => githubCommitsEmptyResponse, + } + } + } + if (url.match(fetchDiffUrlMatcher)) { + return { + status: 200, + json: async () => githubDiffResponse, + } + } + }) + + const result: CommitsMetadataAndDiff = + (await gitFetcherGithubCommitsAndDiff.fetchResource()) as CommitsMetadataAndDiff + expect(fetchMock).toBeCalledTimes(5) + expect(consoleSpy).toHaveBeenCalledWith( + 'Fetched metadata about starting commit at 2023-03-06T14:11:29Z' + ) + expect(consoleSpy).toHaveBeenCalledWith( + 'Fetched metadata about ending commit at 2023-07-12T10:46:50Z' + ) + expect(consoleSpy).toHaveBeenCalledWith( + 'Fetched 2 lines added and 98 lines removed' + ) + expect(consoleSpy).toHaveBeenCalledWith('Fetched metadata about 5 commits') + expect(result.commitsMetadata).toHaveLength(5) + expect(result.diff.linesAdded).toHaveLength(2) + expect(result.diff.linesRemoved).toHaveLength(98) + + expect(responseStatusHandlerSpy).toBeCalledTimes(0) + }) + + it('should send multiple requests, until all commits have been fetched', async () => { + let queriedFirstPage = false + let queriedSecondPage = false + let queriedThirdPage = false + fetchMock.mockImplementation(async (url: string) => { + expect(url).toMatch(fetchCommitsOrDiffUrlMatcher) + if (url.match(fetchIndividualCommitUrlMatcher)) { + if (url.match(startCommitIdMatcher)) { + return { + status: 200, + json: async () => githubStartCommitResponse, + } + } else { + return { + status: 200, + json: async () => githubEndCommitResponse, + } + } + } + if (url.match(fetchAllCommitsUrlMatcher)) { + if (url.match(/&page=1/)) { + queriedFirstPage = true + return { + status: 200, + json: async () => githubMultipleCommitsResponsePage1, + } + } + if (url.match(/&page=2/)) { + queriedSecondPage = true + return { + status: 200, + json: async () => githubMultipleCommitsResponsePage2, + } + } + if (url.match(/&page=3/)) { + queriedThirdPage = true + return { + status: 200, + json: async () => githubMultipleCommitsResponsePage3, + } + } else { + return { + status: 200, + json: async () => githubCommitsEmptyResponse, + } + } + } + if (url.match(fetchDiffUrlMatcher)) { + return { + status: 200, + json: async () => githubDiffResponse, + } + } + }) + + const result: CommitsMetadataAndDiff = + (await gitFetcherGithubCommitsAndDiff.fetchResource()) as CommitsMetadataAndDiff + + expect(fetchMock).toBeCalledTimes(7) + expect(consoleSpy).toHaveBeenCalledWith( + 'Fetched metadata about starting commit at 2023-03-06T14:11:29Z' + ) + expect(consoleSpy).toHaveBeenCalledWith( + 'Fetched metadata about ending commit at 2023-07-12T10:46:50Z' + ) + expect(consoleSpy).toHaveBeenCalledWith( + 'Fetched 2 lines added and 98 lines removed' + ) + expect(consoleSpy).toHaveBeenCalledWith('Fetched metadata about 5 commits') + expect(queriedFirstPage).toBe(true) + expect(queriedSecondPage).toBe(true) + expect(queriedThirdPage).toBe(true) + expect(result.commitsMetadata).toHaveLength(5) + expect(result.diff.linesAdded).toHaveLength(2) + expect(result.diff.linesRemoved).toHaveLength(98) + expect(result.commitsMetadata).toEqual([ + githubMultipleCommitsResponsePage1[0].commit, + githubMultipleCommitsResponsePage1[1].commit, + githubMultipleCommitsResponsePage2[0].commit, + githubMultipleCommitsResponsePage2[1].commit, + githubMultipleCommitsResponsePage3[0].commit, + ]) + + expect(responseStatusHandlerSpy).toBeCalledTimes(0) + }) + + it.each([400, 401, 403, 500])( + 'should throw error when Github responds with non-200 code (%d)', + async (responseCode: number) => { + fetchMock.mockImplementation(async (url: string) => { + expect(url).toMatch(fetchCommitsOrDiffUrlMatcher) + return { + status: responseCode, + } + }) + + let errorWasThrown = false + try { + await gitFetcherGithubCommitsAndDiff.fetchResource() + } catch (e) { + errorWasThrown = true + } + expect(errorWasThrown).toBe(true) + expect(responseStatusHandlerSpy).toHaveBeenCalledOnce() + expect(responseStatusHandlerSpy).toHaveBeenCalledWith(responseCode) + } + ) +}) diff --git a/yaku-apps-typescript/apps/git-fetcher/test/unit/git-fetcher-github.test.ts b/yaku-apps-typescript/apps/git-fetcher/test/unit/git-fetcher-github.test.ts new file mode 100644 index 00000000..b9b10b45 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/unit/git-fetcher-github.test.ts @@ -0,0 +1,131 @@ +import { beforeEach, describe, expect, it, SpyInstance, vi } from 'vitest' +import { GitFetcherGithubPrs } from '../../src/fetchers/git-fetcher-github-prs' +import * as responseHandler from '../../src/utils/handle-response-status' +import * as compareLabels from '../../src/utils/compare-labels' +import { GitServerConfig } from '../../src/model/git-server-config' +import { ConfigFileData } from '../../src/model/config-file-data' +import { multiPageResponse } from './fixtures/github-responses-prs' +import { GithubPr } from '../../src/model/github-pr' + +const gitServerConfigDefault: GitServerConfig = { + gitServerType: 'bitbucket', + gitServerApiUrl: 'www.foo.bar', + gitServerAuthMethod: 'basic', +} as GitServerConfig + +const configDefault: ConfigFileData = { + data: { + org: 'foo_org', + repo: 'foo_repo', + resource: 'prs', + labels: ['foo'], + }, +} + +let gitFetcherGithub: GitFetcherGithubPrs | undefined = undefined +let consoleSpy: SpyInstance | undefined = undefined +let fetchMock: SpyInstance | undefined = undefined +let handleResponseStatusSpy: SpyInstance | undefined = undefined +let compareLabelMock: SpyInstance | undefined = undefined + +describe('GitFetcherGitHub', () => { + beforeEach(() => { + consoleSpy = vi.spyOn(console, 'log') + fetchMock = vi.spyOn(global, 'fetch') + compareLabelMock = vi.spyOn(compareLabels, 'compareLabels') + handleResponseStatusSpy = vi + .spyOn(responseHandler, 'handleResponseStatus') + .mockImplementation(() => { + throw new Error('Mock Error') + }) + + gitFetcherGithub = new GitFetcherGithubPrs( + gitServerConfigDefault, + configDefault + ) + }) + + it('should throw an error message, when fetch-method is unable to get response.', async () => { + await expect(gitFetcherGithub.fetchResource()).rejects.toThrow( + 'Failed to parse URL from www.foo.bar/repos/foo_org/foo_repo/pulls?state=all&per_page=100&page=1' + ) + }) + + it('should create a correct url to fetch the prs, based on the information given in the config files.', async () => { + const config: ConfigFileData = { + data: { ...configDefault.data }, + } + + gitFetcherGithub = new GitFetcherGithubPrs(gitServerConfigDefault, config) + fetchMock.mockImplementation(async () => { + return { + status: 200, + json: async () => { + return {} + }, + } as Response + }) + + // eslint-disable-next-line + // @ts-ignore + const composeUrlSpy = vi.spyOn(gitFetcherGithub, 'composeUrl') + await gitFetcherGithub.fetchResource() + + expect(composeUrlSpy).toBeCalledTimes(1) + expect(composeUrlSpy).toBeCalledWith(1) + expect(composeUrlSpy).toReturnWith( + `www.foo.bar/repos/foo_org/foo_repo/pulls?state=all&per_page=100&page=1` + ) + }) + + it(`should call the fetch method three times and return array with prs, when the response body of the last call, returns an empty array.`, async () => { + const responseFixture: GithubPr[][] = multiPageResponse + const expectedResultArray: GithubPr[] = responseFixture.flat() + + fetchMock.mockImplementation(async (url: string) => { + return { + status: 200, + json: async () => { + const page = url.split('=').slice(-1)[0] + return responseFixture[Number(page) - 1] + }, + } as Response + }) + + const result = await gitFetcherGithub.fetchResource() + + expect(fetchMock).toBeCalledTimes(3) + expect(compareLabelMock).toBeCalledTimes(4) + const expectedLength = expectedResultArray.length + expect(consoleSpy).toHaveBeenCalledWith( + `Fetched ${expectedLength} pull request${expectedLength === 1 ? '' : 's'}` + ) + expect(result).toEqual(expectedResultArray) + }) + + it(`should throw an error, when response status is not 200`, async () => { + const responseFixture: GithubPr[][] = multiPageResponse + let errorThrown = false + + fetchMock.mockImplementation(async (url: string) => { + return { + status: 400, + json: async () => { + const page = url.split('=').slice(-1)[0] + return responseFixture[Number(page) - 1] + }, + } as Response + }) + + try { + await gitFetcherGithub.fetchResource() + } catch (error) { + errorThrown = true + expect(error.message).toEqual('Mock Error') + } + + expect(errorThrown).toBeTruthy() + expect(handleResponseStatusSpy).toHaveBeenCalledTimes(1) + expect(fetchMock).toBeCalledTimes(1) + }) +}) diff --git a/yaku-apps-typescript/apps/git-fetcher/test/unit/handle-response-status.test.ts b/yaku-apps-typescript/apps/git-fetcher/test/unit/handle-response-status.test.ts new file mode 100644 index 00000000..ff53a2cd --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/unit/handle-response-status.test.ts @@ -0,0 +1,62 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { handleResponseStatus } from '../../src/utils/handle-response-status' + +const statusMessage: { status: number; serverType: string; message: string }[] = + [ + { + status: 404, + serverType: 'bitbucket', + message: 'Repository not found. Status code: 404', + }, + { + status: 404, + serverType: 'github', + message: 'Repository not found. Status code: 404', + }, + { + status: 401, + serverType: 'github', + message: + 'Could not access the required repository, SSO Token might not be authorized for the required organization. Status code: 401', + }, + { + status: 401, + serverType: 'bitbucket', + message: 'Could not access the required repository. Status code: 401', + }, + { + status: 403, + serverType: 'github', + message: + 'Could not access the required repository, SSO Token might not be authorized for the required organization. Status code: 403', + }, + { + status: 403, + serverType: 'bitbucket', + message: 'Could not access the required repository. Status code: 403', + }, + { + status: 500, + serverType: 'github', + message: 'Could not fetch data from git repository. Status code: 500', + }, + { + status: 500, + serverType: 'bitbucket', + message: 'Could not fetch data from git repository. Status code: 500', + }, + ] + +describe('HandleResponseStatus', () => { + beforeEach(() => { + vi.unstubAllEnvs() + }) + + it.each(statusMessage)( + 'throws an error with the corresponding error message, when status Code $status is returned.', + ({ status, serverType, message }) => { + vi.stubEnv('GIT_FETCHER_SERVER_TYPE', serverType) + expect(() => handleResponseStatus(status)).toThrowError(message) + } + ) +}) diff --git a/yaku-apps-typescript/apps/git-fetcher/test/unit/run.test.ts b/yaku-apps-typescript/apps/git-fetcher/test/unit/run.test.ts new file mode 100644 index 00000000..4b5c7a43 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/unit/run.test.ts @@ -0,0 +1,266 @@ +import { AppOutput } from '@B-S-F/autopilot-utils' +import { accessSync, existsSync } from 'fs' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import * as generateGitFetcher from '../../src/fetchers/generate-git-fetcher' +import { GitFetcherBitbucketPrs } from '../../src/fetchers/git-fetcher-bitbucket-prs' +import { GitFetcherGithubPrs } from '../../src/fetchers/git-fetcher-github-prs' +import { BitbucketPr } from '../../src/model/bitbucket-pr' +import { GitFetcherConfig } from '../../src/model/config-file-data' +import { GitServerConfig } from '../../src/model/git-server-config' +import { GithubLabel } from '../../src/model/github-label' +import * as run from '../../src/run' +import * as compareLabels from '../../src/utils/compare-labels' +import * as validation from '../../src/utils/validation' + +vi.mock('fs') +vi.mock('../src/utils/validation') +vi.mock('../../src/fetchers/git-fetcher-bitbucket-prs') +vi.mock('../../src/fetchers/git-fetcher-github-prs') + +describe('Run', () => { + beforeEach(() => { + vi.unstubAllEnvs() + }) + + describe('Validate Environment Variables', () => { + let output: AppOutput + beforeEach(() => { + vi.mocked(accessSync).mockReturnValue(undefined) + vi.mocked(existsSync).mockReturnValue(true) + + vi.stubEnv('evidence_path', 'foo/bar') + output = new AppOutput() + }) + + afterEach(() => { + vi.unstubAllEnvs() + }) + + it('throws an error with corresponding error message, if expected environments are not satisfied.', async () => { + vi.stubEnv('NODE_TLS_REJECT_UNAUTHORIZED', '0') + vi.stubEnv('GIT_FETCHER_SERVER_TYPE', '') + vi.stubEnv('GIT_FETCHER_SERVER_API_URL', '') + vi.stubEnv('GIT_FETCHER_API_TOKEN', '') + await expect(run.run(output)).rejects.toThrowError() + }) + + it('throws an error with corresponding error message, when env_var GIT_FETCHER_SERVER_TYPE is undefined', async () => { + vi.stubEnv('GIT_FETCHER_SERVER_TYPE', '') + await expect(run.run(output)).rejects.toThrowError( + 'GIT_FETCHER_SERVER_TYPE environment variable is not set' + ) + }) + + it('throws an error with corresponding error message, when env_var GIT_FETCHER_SERVER_TYPE is not a supported git Server Type', async () => { + vi.stubEnv('GIT_FETCHER_SERVER_TYPE', 'foo') + + await expect(run.run(output)).rejects.toThrowError( + 'The server type "foo" is not supported' + ) + }) + + it('throws an error with corresponding error message, when env_var GIT_FETCHER_SERVER_API_URL is not set', async () => { + vi.stubEnv('GIT_FETCHER_SERVER_TYPE', 'bitbucket') + vi.stubEnv('GIT_FETCHER_SERVER_API_URL', '') + + await expect(run.run(output)).rejects.toThrowError( + 'GIT_FETCHER_SERVER_API_URL environment variable is not set.' + ) + }) + + it('throws an error with corresponding error message, when env_var GIT_FETCHER_SERVER_API_URL is no https url', async () => { + vi.stubEnv('GIT_FETCHER_SERVER_TYPE', 'bitbucket') + vi.stubEnv('GIT_FETCHER_SERVER_API_URL', 'http://www.foo.bar') + + await expect(run.run(output)).rejects.toThrowError( + 'GIT_FETCHER_SERVER_API_URL environment variable must use secured connections with https' + ) + }) + + it('throws an error with corresponding error message, when env_var GIT_FETCHER_SERVER_AUTH_METHOD is undefined', async () => { + vi.stubEnv('GIT_FETCHER_SERVER_TYPE', 'bitbucket') + vi.stubEnv('GIT_FETCHER_SERVER_API_URL', 'https://www.foo.bar') + vi.stubEnv('GIT_FETCHER_API_TOKEN', '') + + await expect(run.run(output)).rejects.toThrowError( + 'GIT_FETCHER_API_TOKEN environment variable is required for "token" authentication, but is not set or empty.' + ) + }) + + it('throws an error with corresponding error message, when env_var GIT_FETCHER_SERVER_AUTH_METHOD is no valid auth method', async () => { + vi.stubEnv('GIT_FETCHER_SERVER_TYPE', 'bitbucket') + vi.stubEnv('GIT_FETCHER_SERVER_API_URL', 'https://www.foo.bar') + vi.stubEnv('GIT_FETCHER_SERVER_AUTH_METHOD', 'fooBar') + + await expect(run.run(output)).rejects.toThrowError( + 'No valid authentication method provided. Valid authentication methods are: token,basic' + ) + }) + + it('throws an error with corresponding error message, when auth method is basic an no password is set', async () => { + vi.stubEnv('GIT_FETCHER_SERVER_TYPE', 'bitbucket') + vi.stubEnv('GIT_FETCHER_SERVER_API_URL', 'https://www.foo.bar') + vi.stubEnv('GIT_FETCHER_SERVER_AUTH_METHOD', 'basic') + vi.stubEnv('GIT_FETCHER_USERNAME', 'foo') + + await expect(run.run(output)).rejects.toThrowError( + 'GIT_FETCHER_PASSWORD environment variable is required for "basic" authentication, but is not set or empty.' + ) + }) + + it('throws an error with corresponding error message, when auth method is basic an no username is set', async () => { + vi.stubEnv('evidence_path', 'foo/bar') + vi.stubEnv('GIT_FETCHER_SERVER_TYPE', 'bitbucket') + vi.stubEnv('GIT_FETCHER_SERVER_API_URL', 'https://www.foo.bar') + vi.stubEnv('GIT_FETCHER_SERVER_AUTH_METHOD', 'basic') + vi.stubEnv('GIT_FETCHER_PASSWORD', 'foo') + + await expect(run.run(output)).rejects.toThrowError( + 'GIT_FETCHER_USERNAME environment variable is required for "basic" authentication, but is not set or empty.' + ) + }) + }) + + describe('Run', () => { + let gitServerConfig: GitServerConfig + beforeEach(async () => { + vi.mocked(accessSync).mockReturnValue(undefined) + vi.mocked(existsSync).mockReturnValue(true) + + vi.stubEnv('evidence_path', 'foo/bar') + vi.stubEnv('GIT_FETCHER_SERVER_API_URL', 'https://www.foo.bar') + vi.stubEnv('GIT_FETCHER_SERVER_AUTH_METHOD', 'basic') + vi.stubEnv('GIT_FETCHER_USERNAME', 'foo') + vi.stubEnv('GIT_FETCHER_PASSWORD', 'bar') + + gitServerConfig = { + gitServerType: 'bitbucket', + gitServerApiUrl: 'https://www.foo.bar', + gitFetcherConfigFilePath: './gitfetcher-config.yaml', + gitFetcherOutputFilePath: './output.json', + gitServerAuthMethod: 'token', + gitServerApiToken: 'someToken', + } + }) + + it('prompts 1 message to the console and compareLabels method was not called, when everything is set properly and bitbucket is set as server type.', async () => { + vi.stubEnv('GIT_FETCHER_SERVER_TYPE', 'bitbucket') + const logSpy = vi.spyOn(global.console, 'log') + const compareLabelsSpy = vi.spyOn(compareLabels, 'compareLabels') + const generateGitFetcherSpy = vi.spyOn(generateGitFetcher, 'default') + const gitFetcherConfig: GitFetcherConfig = { + org: 'fooOrg', + repo: 'barRepo', + resource: 'prs', + labels: ['foo'], + } + generateGitFetcherSpy.mockReturnValue( + new GitFetcherBitbucketPrs(gitServerConfig, { data: gitFetcherConfig }) + ) + + const gitFetcherConfigPromise: Promise = new Promise( + (resolve) => resolve(gitFetcherConfig) + ) + vi.spyOn(validation, 'validateFetcherConfig').mockReturnValue( + gitFetcherConfigPromise + ) + + Object.defineProperty(GitFetcherBitbucketPrs.prototype, 'fetchUrl', { + value: 'www.foo.bar', + }) + + vi.mocked( + GitFetcherBitbucketPrs.prototype.fetchResource + ).mockImplementation(async () => { + return [ + { + id: 1, + state: 'OPEN', + updatedDate: 123456, + }, + ] as BitbucketPr[] + }) + + await run.run(new AppOutput()) + + expect(logSpy).toBeCalledTimes(1) + expect(logSpy).toBeCalledWith( + 'Fetch from https://www.foo.bar was successful with config {"org":"fooOrg","repo":"barRepo","resource":"prs","labels":["foo"]}' + ) + expect(compareLabelsSpy).not.toHaveBeenCalled() + }) + + it('prompts 1 message to the console and compareLabels method has not been called, when everything is set properly, gitFetcher config does not include labels and github is set as server type', async () => { + vi.stubEnv('GIT_FETCHER_SERVER_TYPE', 'github') + const logSpy = vi.spyOn(global.console, 'log') + const compareLabelsSpy = vi.spyOn(compareLabels, 'compareLabels') + const generateGitFetcherSpy = vi.spyOn(generateGitFetcher, 'default') + gitServerConfig.gitServerType = 'github' + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo org', + repo: 'bar repo', + resource: 'pr', + } + generateGitFetcherSpy.mockReturnValue( + new GitFetcherGithubPrs(gitServerConfig, { data: gitFetcherConfig }) + ) + + const gitFetcherConfigPromise: Promise = new Promise( + (resolve) => resolve(gitFetcherConfig) + ) + vi.spyOn(validation, 'validateFetcherConfig').mockReturnValue( + gitFetcherConfigPromise + ) + + Object.defineProperty(GitFetcherGithubPrs.prototype, 'fetchUrl', { + value: 'www.foo.bar', + }) + + vi.mocked(GitFetcherGithubPrs.prototype.fetchResource).mockImplementation( + async () => { + const labels: GithubLabel = { + id: 1, + name: 'wontfix', + } + + return [ + { + number: 1, + state: 'open', + labels: [labels], + }, + ] + } + ) + + await run.run(new AppOutput()) + + expect(logSpy).toBeCalledTimes(1) + expect(logSpy).toBeCalledWith( + 'Fetch from https://www.foo.bar was successful with config {"org":"foo org","repo":"bar repo","resource":"pr"}' + ) + expect(compareLabelsSpy).not.toHaveBeenCalled() + }) + + it('should throw error, when generateGitFetcher throws error', async () => { + const generateGitFetcherSpy = vi.spyOn(generateGitFetcher, 'default') + generateGitFetcherSpy.mockImplementation(() => { + throw new Error('mock error') + }) + + vi.stubEnv('GIT_FETCHER_SERVER_TYPE', 'github') + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo org', + repo: 'bar repo', + resource: 'pr', + } + const gitFetcherConfigPromise: Promise = new Promise( + (resolve) => resolve(gitFetcherConfig) + ) + vi.spyOn(validation, 'validateFetcherConfig').mockReturnValue( + gitFetcherConfigPromise + ) + expect(run.run(new AppOutput())).rejects.toThrowError('mock error') + }) + }) +}) diff --git a/yaku-apps-typescript/apps/git-fetcher/test/unit/validation.test.ts b/yaku-apps-typescript/apps/git-fetcher/test/unit/validation.test.ts new file mode 100644 index 00000000..d0d0e3c5 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/test/unit/validation.test.ts @@ -0,0 +1,563 @@ +import { + allowedFilterState, + AllowedFilterStateType, + GitFetcherConfig, +} from '../../src/model/config-file-data' +import { validateFetcherConfig } from '../../src/utils/validation' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { readFileSync } from 'fs' + +const testPath = 'foo/bar' + +vi.mock('fs') + +describe('ValidateFetcherConfig', async () => { + afterEach(() => { + vi.clearAllMocks() + }) + + it('throws ENOENT error, when file path points to no file', async () => { + vi.mocked(readFileSync).mockImplementation(() => { + throw new Error('ENOENT: no such file or directory') + }) + + await expect(validateFetcherConfig(testPath)).rejects.toThrowError( + 'ENOENT: no such file or directory' + ) + }) + + it('throws error, when readFiles returns format that cannot be parsed into yaml', async () => { + vi.mocked(readFileSync).mockReturnValue('foo: bar, foo: bar') + + await expect(validateFetcherConfig(testPath)).rejects.toThrowError( + 'Nested mappings are not allowed' + ) + }) + + it('throws an error with the corresponding error message, when the config has an invalid structure', async () => { + vi.mocked(readFileSync).mockReturnValue('foo: bar') + await expect(validateFetcherConfig(testPath)).rejects.toThrowError( + 'Validation error: Required at "org"; Required at "repo"; Required at "resource"' + ) + }) + + it('returns git fetcher config, when file content is valid', async () => { + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + } + + vi.mocked(readFileSync).mockReturnValue(JSON.stringify(gitFetcherConfig)) + + const result = await validateFetcherConfig(testPath) + expect(result).toEqual(gitFetcherConfig) + }) + + describe('Filter by State', async () => { + describe('Success Cases', async () => { + it.each(allowedFilterState)( + 'does not throw an error for valid state filter %s', + async (stateFilter: AllowedFilterStateType) => { + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: { state: stateFilter }, + } + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + + const result = await validateFetcherConfig(testPath) + expect(result).toEqual(gitFetcherConfig) + } + ) + }) + + describe('Error Cases', async () => { + it('throws an error, when an invalid state filter is given', async () => { + const invalidRequestStatus = 'invalid' + + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: { state: invalidRequestStatus as any }, + } + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + await expect(validateFetcherConfig(testPath)).rejects.toThrowError( + `Validation error: Invalid enum value. Expected 'DECLINED' | 'MERGED' | 'OPEN' | 'ALL', received '${invalidRequestStatus}' at "filter.state"` + ) + }) + }) + }) + + describe('Filter by Date', async () => { + describe('Success Cases', async () => { + it('does not throw an error, when filter startDate equals filter endDate', async () => { + const filterStartDate = '01-02-2023' + + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: { + startDate: filterStartDate as any, + endDate: filterStartDate as any, + }, + } + + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + + const result = await validateFetcherConfig(testPath) + expect(result).toEqual({ + ...gitFetcherConfig, + filter: { + startDate: new Date(new Date('2023-02-01').setHours(0, 0, 0, 0)), + endDate: new Date(new Date('2023-02-01').setHours(23, 59, 59, 999)), + }, + }) + }) + + it('does not throw an error, when filter endDate is greater than filter startDate', async () => { + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: { + startDate: '01-02-2023' as any, + endDate: '01-03-2023' as any, + }, + } + + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + + const result = await validateFetcherConfig(testPath) + expect(result).toEqual({ + ...gitFetcherConfig, + filter: { + startDate: new Date(new Date('2023-02-01').setHours(0, 0, 0, 0)), + endDate: new Date(new Date('2023-03-01').setHours(23, 59, 59, 999)), + }, + }) + }) + + it('does not throw an error, when filter startDate is provided but filter endDate is not', async () => { + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: { + startDate: '01-01-2023' as any, + }, + } + + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + + const result = await validateFetcherConfig(testPath) + expect(result).toEqual({ + ...gitFetcherConfig, + filter: { + startDate: new Date(new Date('2023-01-01').setHours(0, 0, 0, 0)), + }, + }) + }) + }) + + describe('Error Cases', async () => { + it('throws an error, when filter endDate is provided but filter startDate is not', async () => { + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: { endDate: '01-01-2023' as any }, + } + + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + await expect(validateFetcherConfig(testPath)).rejects.toThrowError( + 'Validation error: Specify filter.startDate if filter.endDate is provided at "filter"' + ) + }) + + it('throws an error, when filter endDate is before filter startDate', async () => { + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: { + startDate: '01-02-2023' as any, + endDate: '01-01-2023' as any, + }, + } + + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + await expect(validateFetcherConfig(testPath)).rejects.toThrowError( + 'Validation error: filter.endDate must be after or equal filter.startDate at "filter"' + ) + }) + + it('throws an error, when filter startDate is empty', async () => { + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: { startDate: '' as any }, + } + + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + await expect(validateFetcherConfig(testPath)).rejects.toThrowError( + 'Validation error: date must match the format dd-mm-yyyy at "filter.startDate"' + ) + }) + + it('throws an error, when filter endDate is empty', async () => { + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: { + startDate: '01-01-2023' as any, + endDate: '' as any, + }, + } + + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + await expect(validateFetcherConfig(testPath)).rejects.toThrowError( + 'Validation error: date must match the format dd-mm-yyyy at "filter.endDate"' + ) + }) + + it('throws an error, when filter startDate does not match the required format', async () => { + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: { + startDate: '5-01-2023' as any, + endDate: '01-01-2023' as any, + }, + } + + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + await expect(validateFetcherConfig(testPath)).rejects.toThrowError( + 'Validation error: date must match the format dd-mm-yyyy at "filter.startDate"' + ) + }) + + it('throws an error, when filter endDate does not match the required format', async () => { + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: { + startDate: '01-01-2023' as any, + endDate: '01-1-2024' as any, + }, + } + + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + await expect(validateFetcherConfig(testPath)).rejects.toThrowError( + 'Validation error: date must match the format dd-mm-yyyy at "filter.endDate"' + ) + }) + }) + }) + + describe('Filter by Hash', async () => { + describe('Success Cases', async () => { + it('does not throw an error, when only startHash is given', async () => { + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: { + startHash: 'ad897ad8b76ad4c7b6', + }, + } + + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + + const result = await validateFetcherConfig(testPath) + expect(result).toEqual(gitFetcherConfig) + }) + + it('does not throw an error, when startHash and state filter are combined', async () => { + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: { + state: 'ALL', + startHash: 'ad897ad8b76ad4c7b6', + }, + } + + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + + const result = await validateFetcherConfig(testPath) + expect(result).toEqual(gitFetcherConfig) + }) + + it('does not throw an error, when startHash, endHash and state filter are combined', async () => { + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: { + state: 'ALL', + startHash: 'ad897ad8b76ad4c7b6', + endHash: 'ad897ad8b76ad4c7b6', + }, + } + + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + + const result = await validateFetcherConfig(testPath) + expect(result).toEqual(gitFetcherConfig) + }) + }) + + describe('Error Cases', async () => { + it.each(['startHash', 'endHash'])( + 'throws an error, when %s is empty', + async (hash) => { + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: { + startHash: 'ad897ad8b76ad4c7b6', + [hash]: '', + }, + } + + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + await expect(validateFetcherConfig(testPath)).rejects.toThrowError( + `Validation error: String must contain at least 1 character(s) at "filter.${hash}"` + ) + } + ) + + it('throws an error, when endHash is used without startHash', async () => { + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: { endHash: 'c3d9087cd0a7b' }, + } + + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + await expect(validateFetcherConfig(testPath)).rejects.toThrowError( + 'Validation error: Specify filter.startHash if filter.endHash is provided at "filter"' + ) + }) + }) + }) + + describe('Filter by Tag', async () => { + describe('Success Cases', async () => { + it('does not throw an error, when only startTag is given', async () => { + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: { + startTag: '13dbf0d42971a', + }, + } + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + const result = await validateFetcherConfig(testPath) + expect(result).toEqual(gitFetcherConfig) + }) + + it('does not throw an error, when startTag and endTag are given', async () => { + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: { + startTag: '13dbf0d42971a', + endTag: '9da28cfda7344', + }, + } + + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + + const result = await validateFetcherConfig(testPath) + expect(result).toEqual(gitFetcherConfig) + }) + + it('does not throw an error, when startTag and state filter are combined', async () => { + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: { + state: 'ALL', + startTag: '13dbf0d42971a', + }, + } + + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + + const result = await validateFetcherConfig(testPath) + expect(result).toEqual(gitFetcherConfig) + }) + + it('does not throw an error, when startTag, endTag and state filter are combined', async () => { + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: { + state: 'ALL', + startTag: '13dbf0d42971a', + endTag: '9da28cfda7344', + }, + } + + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + + const result = await validateFetcherConfig(testPath) + expect(result).toEqual(gitFetcherConfig) + }) + }) + + describe('Error Cases', async () => { + it.each(['startTag', 'endTag'])( + 'throws an error, when %s is empty', + async (tag) => { + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: { + startTag: '13dbf0d42971a', + [tag]: '', + }, + } + + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + await expect(validateFetcherConfig(testPath)).rejects.toThrowError( + `Validation error: String must contain at least 1 character(s) at "filter.${tag}` + ) + } + ) + it('throws an error, when endTag is used without startTag', async () => { + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: { + endTag: '9da28cfda7344', + }, + } + + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + + await expect(validateFetcherConfig(testPath)).rejects.toThrowError( + 'Validation error: Specify filter.startTag if filter.endTag is provided at "filter"' + ) + }) + }) + }) + + describe('Filter Invalid Combinations', function () { + it.each([ + // date + hash filter + ['startDate', 'startHash', undefined], + ['startDate', 'endHash', undefined], + ['endDate', 'startHash', undefined], + ['endDate', 'endHash', undefined], + // date + tag filter + ['startDate', undefined, 'startTag'], + ['startDate', undefined, 'endTag'], + ['endDate', undefined, 'startTag'], + ['endDate', undefined, 'endTag'], + // hash + tag filter + [undefined, 'startHash', 'startTag'], + [undefined, 'startHash', 'endTag'], + [undefined, 'endHash', 'startTag'], + [undefined, 'endHash', 'endTag'], + // date + tag + hash filter + ['startDate', 'startHash', 'startTag'], + ['startDate', 'startHash', 'endTag'], + ['startDate', 'endHash', 'startTag'], + ['startDate', 'endHash', 'endTag'], + ['endDate', 'startHash', 'startTag'], + ['endDate', 'startHash', 'endTag'], + ['endDate', 'endHash', 'startTag'], + ['endDate', 'endHash', 'endTag'], + ])( + 'throws an error, when date: %s, hash: %s, tag: %s are used in combination', + async (date, hash, tag) => { + const gitFetcherConfig: GitFetcherConfig = { + org: 'foo', + repo: 'bar repo', + resource: 'pr', + filter: {}, + } + + if (date !== undefined) { + gitFetcherConfig.filter[date] = '01-01-2023' + } + if (hash !== undefined) { + gitFetcherConfig.filter[hash] = 'c3d9087cd0a7b' + } + if (tag !== undefined) { + gitFetcherConfig.filter[tag] = '13dbf0d42971a' + } + + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify(gitFetcherConfig) + ) + await expect(validateFetcherConfig(testPath)).rejects.toThrowError( + 'Validation error: Combining the date, hash and/or tag filter is not possible at "filter"' + ) + } + ) + }) +}) diff --git a/yaku-apps-typescript/apps/git-fetcher/tsconfig.json b/yaku-apps-typescript/apps/git-fetcher/tsconfig.json new file mode 100644 index 00000000..0b3bd614 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "@B-S-F/typescript-config/base.json", + "ts-node": { + "compilerOptions": { + "module": "ESNext" + } + }, + "compilerOptions": { + "esModuleInterop": true, + "moduleResolution": "node", + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/yaku-apps-typescript/apps/git-fetcher/tsup.config.json b/yaku-apps-typescript/apps/git-fetcher/tsup.config.json new file mode 100644 index 00000000..f35b9ed3 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/tsup.config.json @@ -0,0 +1,6 @@ +{ + "entry": ["src/index.ts"], + "splitting": false, + "sourcemap": true, + "clean": true +} diff --git a/yaku-apps-typescript/apps/git-fetcher/tsup.config.ts b/yaku-apps-typescript/apps/git-fetcher/tsup.config.ts new file mode 100644 index 00000000..a1f42d9c --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/**/*.ts'], + splitting: false, + clean: true, + target: 'node18', + format: ['esm'], + bundle: false, + sourcemap: true, +}) diff --git a/yaku-apps-typescript/apps/git-fetcher/vitest-integration.config.ts b/yaku-apps-typescript/apps/git-fetcher/vitest-integration.config.ts new file mode 100644 index 00000000..2accf96c --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/vitest-integration.config.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + testTimeout: 15000, + include: ['test/integration/**/*.int-spec.ts'], + pool: 'threads', + poolOptions: { + threads: { + maxThreads: 1, + minThreads: 1, + }, + }, + typecheck: { + tsconfig: 'tsconfig.json', + }, + reporters: ['junit', 'default'], + outputFile: 'reports/integration-test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/git-fetcher/vitest.config.ts b/yaku-apps-typescript/apps/git-fetcher/vitest.config.ts new file mode 100644 index 00000000..97b8b1d1 --- /dev/null +++ b/yaku-apps-typescript/apps/git-fetcher/vitest.config.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + typecheck: { + tsconfig: 'tsconfig.json', + }, + coverage: { + provider: 'v8', + all: true, + enabled: true, + reporter: ['cobertura', 'json-summary', 'text-summary'], + include: ['src'], + exclude: ['**/src/index.ts', 'src/model'], + }, + reporters: ['junit', 'default'], + outputFile: 'reports/test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/html-finalizer/.eslintrc.cjs b/yaku-apps-typescript/apps/html-finalizer/.eslintrc.cjs new file mode 100644 index 00000000..e446520f --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/.eslintrc.cjs @@ -0,0 +1,37 @@ +module.exports = { + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'prettier', + ], + // parser: '@typescript-eslint/parser', + // parserOptions: { + // ecmaVersion: 12, + // sourceType: module, + // }, + plugins: ['@typescript-eslint'], + settings: { + next: { + rootDir: ['apps/*/', 'packages/*/'], + }, + }, + env: { + browser: true, + es2021: true, + }, + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + 'no-sparse-arrays': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { destructuredArrayIgnorePattern: '^([.]{3})?_' }, + ], + }, + ignorePatterns: [ + 'node_modules/', + '*/node_modules', + '**/tsconfig.json', + '**/dist', + ], + } \ No newline at end of file diff --git a/yaku-apps-typescript/apps/html-finalizer/.gitignore b/yaku-apps-typescript/apps/html-finalizer/.gitignore new file mode 100644 index 00000000..5dc4e178 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/.gitignore @@ -0,0 +1,3 @@ +test/integration/input/v0/*.html +test/integration/input/v1/*.html +test/integration/input/v1-with-logs/*.html diff --git a/yaku-apps-typescript/apps/html-finalizer/README.md b/yaku-apps-typescript/apps/html-finalizer/README.md new file mode 100644 index 00000000..78051482 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/README.md @@ -0,0 +1,53 @@ +# html-finalizer + +## Setup + +As developer, use turbo to install and setup entire mono-repo. + +## How to use + +### Run + +Inputs are read via environment variables and listed below. + +```shell +html-finalizer +``` + +The finalizer will generate following files: + +- qg-result.html: An overview report that contains a list of all requirements and their final status. The status of a requirement depends on the status of its underlying checks. +- qg-evidence.html: This report contains a detailed list of all checks and components and their status. It also contains links to the generated output folder of each run in evidence folder so they could be accessed. +- qg-dashboard.html: A graphical overview that shows statics of the current run. +- qg-full-report.html: A file that contains the three previous html reports. + +### Env + +#### result_path + +The path of the evidence folder provided by the qg cli + +- string + +#### [optional] HIDE_UNANSWERED + +If this environment is provided and set to true, all unanswered questions will be hidden in the html result. + +- boolean + +## How to update integration tests snapshots + +To update the snapshot file of the integration tests, run the following command in the app folder: + +```shell +npx vitest --config vitest-integration.config.ts +``` + +You'll get the following message: + +```shell +Tests failed. Watching for file changes... + press u to update snapshot, press h to show help +``` + +Press **u** to update the snapshot file, then press **q** to quit. diff --git a/yaku-apps-typescript/apps/html-finalizer/package.json b/yaku-apps-typescript/apps/html-finalizer/package.json new file mode 100644 index 00000000..92a7e815 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/package.json @@ -0,0 +1,41 @@ +{ + "name": "@B-S-F/html-finalizer", + "version": "0.33.0", + "description": "", + "main": "dist/index.js", + "type": "module", + "scripts": { + "prebuild": "npx rimraf ./dist", + "build": "tsup && npm run copy-files && npm link", + "copy-files": "npx copyfiles -u 1 ./src/*.ejs dist", + "dev": "nodemon --watch \"src/**\" --exec npm run start", + "lint": "eslint '**/*.ts'", + "test": "npx vitest run --config vitest-integration.config.ts --coverage", + "test:integration:local": "npx rimraf dist && npm run build && npm run test:integration:ci", + "test:integration:ci": "npx vitest run --config vitest-integration.config.ts", + "start": "npm run build && node ./dist/run.js" + }, + "keywords": [], + "author": "", + "devDependencies": { + "@types/ejs": "^3.1.1", + "@types/node": "*", + "eslint": "*", + "nodemon": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + }, + "dependencies": { + "@B-S-F/markdown-utils": "~0.2.0", + "ejs": "^3.1.10", + "fs-extra": "^10.1.0", + "yaml": "^2.4.1" + }, + "bin": { + "html-finalizer": "dist/run.js" + }, + "files": [ + "dist" + ] +} diff --git a/yaku-apps-typescript/apps/html-finalizer/sample/qg-config.yaml b/yaku-apps-typescript/apps/html-finalizer/sample/qg-config.yaml new file mode 100644 index 00000000..6979399e --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/sample/qg-config.yaml @@ -0,0 +1,286 @@ +metadata: + version: v1 +header: + name: ${{ vars.TITLE }} + version: ${{ vars.VERSION }} +default: + vars: + VAR_4: 'some value' + VAR_3: 'some value that will be overridden' +env: + ENV_1: global-env-1 + ENV_2: global-env-2 + ENV_3: global-env-3 +autopilots: + status-provider: + run: | + echo '{"status": "${{ env.STATUS }}"}' + echo '{"reason": "Some reason"}' + echo '{"result": {"criterion": "I am a criterion", "fulfilled": false, "justification": "I am the justification"}}' + reason-provider: + run: | + echo '{"reason": "${{ env.REASON }}"}' + echo '{"status": "FAILED"}' + outputs-provider: + run: | + echo '{"output": {"output1": "${{ env.OUTPUT1 }}"}}' + echo '{"output": {"output2": "${{ env.OUTPUT2 }}"}}' + echo '{"status": "GREEN", "reason": "This is a reason"}' + echo '{"result": {"criterion": "I am a criterion", "fulfilled": false, "justification": "I am the justification"}}' + base-interface: + run: | + echo '{"reason": "This is a reason"}' + echo '{"output": {"output1": "output1_value", "output2": "output2_value"}}' + echo '{"status": "FAILED", "reason": "This is a reason"}' + echo '{"result": {"criterion": "I am a criterion", "fulfilled": false, "justification": "I am the justification"}}' + combined-json-lines: + run: | + echo '{"status": "GREEN", "reason": "This is a reason", "output": {"output1": "output1_value", "output2": "output2_value"}}' + findings-interface: + run: | + echo '{"result": {"criterion": "I am a criterion", "fulfilled": false, "justification": "I am the reason"}}' + echo '{"result": {"criterion": "I am a criterion 2", "fulfilled": false, "justification": "I am another reason"}}' + echo '{"result": {"criterion": "I am a criterion 3", "fulfilled": false, "justification": "I am yet another reason", "metadata": {"customer": "I am customer in metadata", "package": "I am a package", "severity": "I am a severity"}}}' + echo '{"status": "GREEN", "reason": "This is a reason"}' + env-provider: + run: | + echo "$ENV_1" + echo "${{ env.ENV_1 }}" + echo "$ENV_2" + echo "${{ env.ENV_2 }}" + echo "$ENV_3" + echo "${{ env.ENV_3 }}" + echo '{"status": "FAILED", "reason": "This is a reason"}' + env: + ENV_3: autopilot-env-3 + secrets-provider: + run: | + echo "$SECRET_1" + echo "$SECRET_2" + echo "${{ secrets.SECRET_3 }}" + echo '{"status": "FAILED", "reason": "This is a reason"}' + env: + SECRET_2: ${{ secrets.SECRET_2}} + vars-provider: + run: | + echo "$VAR_1" + echo "$VAR_2" + echo "${{ vars.VAR_3}}" + echo "${{ vars.VAR_4 }}" + echo '{"status": "FAILED", "reason": "This is a reason"}' + env: + VAR_2: ${{ vars.VAR_2}} + additional-config-provider: + run: | + echo '{"status": "FAILED", "reason": "This is a reason"}' + echo "This autopilot has an additional config" + cat $evidence_path/additional-config.yaml + config: + - additional-config.yaml + escape-characters-autopilot: + run: | + echo '{"result": {"criterion": "criterion is \b \f \n \r \t \u000A \\ \" \\n", "fulfilled": true, "justification": "reason is \b \f \n \r \t \u000A \\ \" \\n"}}' + echo '{"status": "RED"}' + new-line-autopilot: + run: | + echo '{"status": "GREEN"}' + echo '{"reason": "reas\non"}' + echo '{"result": {"criterion": "crit\nerion", "fulfilled": true, "justification": "reas\non", "metadata": {"cust\tomer": "cust\nomer metadata"}}}' + echo '{"output": {"outputkeywith\tinit": "Output value with\nin it"}}' +finalize: + run: | + echo $FINALIZER_ENV_1 + echo ${{ env.FINALIZER_ENV_1 }} + echo $FINALIZER_ENV_2 + echo ${{ env.FINALIZER_ENV_2 }} + echo $FINALIZER_ENV_3 + echo ${{ env.FINALIZER_ENV_3 }} + if [ -f ${result_path}/qg-result.yaml ]; then + echo "qg-result.yaml exists" + else + echo "qg-result.yaml does not exist" + exit 1 + fi + env: + FINALIZER_ENV_1: ${{ env.ENV_1 }} + FINALIZER_ENV_2: ${{ secrets.SECRET_1 }} + FINALIZER_ENV_3: ${{ vars.VAR_1 }} +chapters: + '1': + title: Manual Answers have to be supported + requirements: + '1': + title: GREEN answer + checks: + '1': + title: GREEN answer check + manual: + status: GREEN + reason: It should be GREEN + '2': + title: YELLOW answer + checks: + '1': + title: YELLOW answer check + manual: + status: YELLOW + reason: It should be YELLOW + '3': + title: RED answer + checks: + '1': + title: RED answer check + manual: + status: RED + reason: It should be RED + '4': + title: NA answer + checks: + '1': + title: NA answer check + manual: + status: NA + reason: It should be NA + '5': + title: UNANSWERED answer + checks: + '1': + title: UNANSWERED answer check + manual: + status: UNANSWERED + reason: It should be UNANSWERED + '2': + title: Base Interface + requirements: + '1': + title: Base Interface has to be supported + text: | + The base interface should be supported to retrieve the status from an autopilot + The base interface consists of the following properties: + - status + - reason + - outputs + checks: + '1a': + title: Status GREEN should be supported + automation: + autopilot: status-provider + env: + STATUS: GREEN + '1b': + title: Status YELLOW should be supported + automation: + autopilot: status-provider + env: + STATUS: YELLOW + '1c': + title: Status RED should be supported + automation: + autopilot: status-provider + env: + STATUS: RED + '1d': + title: Status NA should be supported + automation: + autopilot: status-provider + env: + STATUS: NA + '1e': + title: Status UNANSWERED should be supported + automation: + autopilot: status-provider + env: + STATUS: UNANSWERED + '1f': + title: If a status is not supported, it should be set to ERROR + automation: + autopilot: status-provider + env: + STATUS: UNKNOWN + '1g': + title: If a status is empty, it should be set to ERROR + automation: + autopilot: status-provider + env: + STATUS: '' + '3': + title: Reason should be supported + automation: + autopilot: reason-provider + env: + REASON: This is a reason + '4': + title: Outputs should be supported + automation: + autopilot: outputs-provider + env: + OUTPUT1: output1_value + OUTPUT2: output2_value + '5': + title: Combined json lines with status, reason, and outputs should be supported + automation: + autopilot: combined-json-lines + '6': + title: Findings should be supported + automation: + autopilot: findings-interface + '7': + title: Can provide handle escape characters in a string + automation: + autopilot: escape-characters-autopilot + '8': + title: Can provide handle new line characters in a string + automation: + autopilot: new-line-autopilot + '3': + title: Parameter Replacement + requirements: + '1': + title: Should replace parameters in autopilots + checks: + '1': + title: Replace environments + automation: + autopilot: env-provider + env: + ENV_2: autopilot-ref-env-2 + '2': + title: Replace secrets + automation: + autopilot: secrets-provider + env: + SECRET_1: autopilot-ref-secret-1 + '3': + title: Replace variables + automation: + autopilot: vars-provider + env: + VAR_1: autopilot-ref-var-1 + '2': + title: Should replace parameters in manual answers like ${{ vars.REQUIREMENT_TITLE }} + text: ${{ vars.REQUIREMENT_TEXT }} + checks: + '1': + title: check for var replacement in manual answer + manual: + status: GREEN + reason: ${{ vars.REQUIREMENT_REASON }} + '3': + title: Should replace parameters in additional config + checks: + '1': + title: Replace parameters in additional config + automation: + autopilot: additional-config-provider + env: + ADDITIONAL_CONFIG_ENV: autopilot-ref-additional-config-env + '4': + title: Should hide secrets + requirements: + '1': + title: Hide secrets in logs + checks: + '1a': + title: Check 1 + automation: + autopilot: secrets-provider diff --git a/yaku-apps-typescript/apps/html-finalizer/sample/qg-result.yaml b/yaku-apps-typescript/apps/html-finalizer/sample/qg-result.yaml new file mode 100644 index 00000000..3c7ec945 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/sample/qg-result.yaml @@ -0,0 +1,473 @@ +metadata: + version: v1 +header: + name: '' + version: '' + date: 2023-11-16 18:05 + toolVersion: 0.6.1 +overallStatus: ERROR +statistics: + counted-checks: 24 + counted-automated-checks: 18 + counted-manual-check: 5 + counted-unanswered-checks: 1 + counted-skipped-checks: 0 + degree-of-automation: 75 + degree-of-completion: 95.83 +chapters: + '1': + title: Manual Answers have to be supported + status: RED + requirements: + '1': + title: GREEN answer + status: GREEN + checks: + '1': + title: GREEN answer check + status: GREEN + type: Manual + evaluation: + status: GREEN + reason: It should be GREEN + '2': + title: YELLOW answer + status: YELLOW + checks: + '1': + title: YELLOW answer check + status: YELLOW + type: Manual + evaluation: + status: YELLOW + reason: It should be YELLOW + '3': + title: RED answer + status: RED + checks: + '1': + title: RED answer check + status: RED + type: Manual + evaluation: + status: RED + reason: It should be RED + '4': + title: NA answer + status: NA + checks: + '1': + title: NA answer check + status: NA + type: Manual + evaluation: + status: NA + reason: It should be NA + '5': + title: UNANSWERED answer + status: UNANSWERED + checks: + '1': + title: UNANSWERED answer check + status: UNANSWERED + type: Manual + evaluation: + status: UNANSWERED + reason: It should be UNANSWERED + '2': + title: Base Interface + status: ERROR + requirements: + '1': + title: Base Interface has to be supported + text: | + The base interface should be supported to retrieve the status from an autopilot + The base interface consists of the following properties: + - status + - reason + - outputs + status: ERROR + checks: + 1a: + title: Status GREEN should be supported + status: GREEN + type: Automation + evaluation: + autopilot: status-provider + status: GREEN + reason: Some reason + results: + - criterion: I am a criterion + fulfilled: false + justification: I am the justification + execution: + logs: + - '{"status": "GREEN"}' + - '{"reason": "Some reason"}' + - '{"result": {"criterion": "I am a criterion", "fulfilled": false, "justification": "I am the justification"}}' + evidencePath: 2_1_1a + exitCode: 0 + 1b: + title: Status YELLOW should be supported + status: YELLOW + type: Automation + evaluation: + autopilot: status-provider + status: YELLOW + reason: Some reason + results: + - criterion: I am a criterion + fulfilled: false + justification: I am the justification + execution: + logs: + - '{"status": "YELLOW"}' + - '{"reason": "Some reason"}' + - '{"result": {"criterion": "I am a criterion", "fulfilled": false, "justification": "I am the justification"}}' + evidencePath: 2_1_1b + exitCode: 0 + 1c: + title: Status RED should be supported + status: RED + type: Automation + evaluation: + autopilot: status-provider + status: RED + reason: Some reason + results: + - criterion: I am a criterion + fulfilled: false + justification: I am the justification + execution: + logs: + - '{"status": "RED"}' + - '{"reason": "Some reason"}' + - '{"result": {"criterion": "I am a criterion", "fulfilled": false, "justification": "I am the justification"}}' + evidencePath: 2_1_1c + exitCode: 0 + 1d: + title: Status NA should be supported + status: ERROR + type: Automation + evaluation: + autopilot: status-provider + status: ERROR + reason: "autopilot 'status-provider' provided an invalid 'status': 'NA'" + results: + - criterion: I am a criterion + fulfilled: false + justification: I am the justification + execution: + logs: + - '{"status": "NA"}' + - '{"reason": "Some reason"}' + - '{"result": {"criterion": "I am a criterion", "fulfilled": false, "justification": "I am the justification"}}' + evidencePath: 2_1_1d + exitCode: 0 + 1e: + title: Status UNANSWERED should be supported + status: ERROR + type: Automation + evaluation: + autopilot: status-provider + status: ERROR + reason: "autopilot 'status-provider' provided an invalid 'status': 'UNANSWERED'" + results: + - criterion: I am a criterion + fulfilled: false + justification: I am the justification + execution: + logs: + - '{"status": "UNANSWERED"}' + - '{"reason": "Some reason"}' + - '{"result": {"criterion": "I am a criterion", "fulfilled": false, "justification": "I am the justification"}}' + evidencePath: 2_1_1e + exitCode: 0 + 1f: + title: If a status is not supported, it should be set to ERROR + status: ERROR + type: Automation + evaluation: + autopilot: status-provider + status: ERROR + reason: "autopilot 'status-provider' provided an invalid 'status': 'UNKNOWN'" + results: + - criterion: I am a criterion + fulfilled: false + justification: I am the justification + execution: + logs: + - '{"status": "UNKNOWN"}' + - '{"reason": "Some reason"}' + - '{"result": {"criterion": "I am a criterion", "fulfilled": false, "justification": "I am the justification"}}' + evidencePath: 2_1_1f + exitCode: 0 + 1g: + title: If a status is empty, it should be set to ERROR + status: ERROR + type: Automation + evaluation: + autopilot: status-provider + status: ERROR + reason: "autopilot 'status-provider' provided an invalid 'status': ''" + results: + - criterion: I am a criterion + fulfilled: false + justification: I am the justification + execution: + logs: + - '{"status": ""}' + - '{"reason": "Some reason"}' + - '{"result": {"criterion": "I am a criterion", "fulfilled": false, "justification": "I am the justification"}}' + evidencePath: 2_1_1g + exitCode: 0 + '3': + title: Reason should be supported + status: FAILED + type: Automation + evaluation: + autopilot: reason-provider + status: FAILED + reason: This is a reason + execution: + logs: + - '{"reason": "This is a reason"}' + - '{"status": "FAILED"}' + evidencePath: '2_1_3' + exitCode: 0 + '4': + title: Outputs should be supported + status: GREEN + type: Automation + evaluation: + autopilot: outputs-provider + status: GREEN + reason: This is a reason + results: + - criterion: I am a criterion + fulfilled: false + justification: I am the justification + outputs: + output1: output1_value + output2: output2_value + execution: + logs: + - '{"output": {"output1": "output1_value"}}' + - '{"output": {"output2": "output2_value"}}' + - '{"status": "GREEN", "reason": "This is a reason"}' + - '{"result": {"criterion": "I am a criterion", "fulfilled": false, "justification": "I am the justification"}}' + evidencePath: '2_1_4' + exitCode: 0 + '5': + title: Combined json lines with status, reason, and outputs should be supported + status: GREEN + type: Automation + evaluation: + autopilot: combined-json-lines + status: GREEN + reason: This is a reason + outputs: + output1: output1_value + output2: output2_value + execution: + logs: + - '{"status": "GREEN", "reason": "This is a reason", "output": {"output1": "output1_value", "output2": "output2_value"}}' + evidencePath: '2_1_5' + exitCode: 0 + '6': + title: Findings should be supported + status: GREEN + type: Automation + evaluation: + autopilot: findings-interface + status: GREEN + reason: This is a reason + results: + - criterion: I am a criterion + fulfilled: false + justification: I am the reason + - criterion: I am a criterion 2 + fulfilled: false + justification: I am another reason + - criterion: I am a criterion 3 + fulfilled: false + justification: I am yet another reason + metadata: + customer: I am customer in metadata + package: I am a package + severity: I am a severity + execution: + logs: + - '{"result": {"criterion": "I am a criterion", "fulfilled": false, "justification": "I am the reason"}}' + - '{"result": {"criterion": "I am a criterion 2", "fulfilled": false, "justification": "I am another reason"}}' + - '{"result": {"criterion": "I am a criterion 3", "fulfilled": false, "justification": "I am yet another reason", "metadata": {"customer": "I am customer in metadata", "package": "I am a package", "severity": "I am a severity"}}}' + - '{"status": "GREEN", "reason": "This is a reason"}' + evidencePath: '2_1_6' + exitCode: 0 + '7': + title: Can provide handle escape characters in a string + status: RED + type: Automation + evaluation: + autopilot: escape-characters-autopilot + status: RED + reason: '' + results: + - criterion: "criterion is \b \f \n \r \t \n \\ \" \\n" + fulfilled: true + justification: "reason is \b \f \n \r \t \n \\ \" \\n" + execution: + logs: + - '{"result": {"criterion": "criterion is \b \f \n \r \t \u000A \\ \" \\n", "fulfilled": true, "justification": "reason is \b \f \n \r \t \u000A \\ \" \\n"}}' + - '{"status": "RED"}' + evidencePath: '2_1_7' + exitCode: 0 + '8': + title: Can provide handle new line characters in a string + status: GREEN + type: Automation + evaluation: + autopilot: new-line-autopilot + status: GREEN + reason: |- + reas + on + results: + - criterion: |- + crit + erion + fulfilled: true + justification: |- + reas + on + metadata: + "cust\tomer": |- + cust + omer metadata + outputs: + "outputkeywith\tinit": |- + Output value with + in it + execution: + logs: + - '{"status": "GREEN"}' + - '{"reason": "reas\non"}' + - '{"result": {"criterion": "crit\nerion", "fulfilled": true, "justification": "reas\non", "metadata": {"cust\tomer": "cust\nomer metadata"}}}' + - '{"output": {"outputkeywith\tinit": "Output value with\nin it"}}' + evidencePath: '2_1_8' + exitCode: 0 + '3': + title: Parameter Replacement + status: FAILED + requirements: + '1': + title: Should replace parameters in autopilots + status: FAILED + checks: + '1': + title: Replace environments + status: FAILED + type: Automation + evaluation: + autopilot: env-provider + status: FAILED + reason: This is a reason + execution: + logs: + - global-env-1 + - global-env-1 + - autopilot-ref-env-2 + - autopilot-ref-env-2 + - autopilot-env-3 + - autopilot-env-3 + - '{"status": "FAILED", "reason": "This is a reason"}' + evidencePath: '3_1_1' + exitCode: 0 + '2': + title: Replace secrets + status: FAILED + type: Automation + evaluation: + autopilot: secrets-provider + status: FAILED + reason: This is a reason + execution: + logs: + - autopilot-ref-secret-1 + - '{"status": "FAILED", "reason": "This is a reason"}' + evidencePath: '3_1_2' + exitCode: 0 + '3': + title: Replace variables + status: FAILED + type: Automation + evaluation: + autopilot: vars-provider + status: FAILED + reason: This is a reason + execution: + logs: + - autopilot-ref-var-1 + - some value that will be overridden + - some value + - '{"status": "FAILED", "reason": "This is a reason"}' + evidencePath: '3_1_3' + exitCode: 0 + '2': + title: 'Should replace parameters in manual answers like ' + status: GREEN + checks: + '1': + title: check for var replacement in manual answer + status: GREEN + type: Manual + evaluation: + status: GREEN + reason: '' + '3': + title: Should replace parameters in additional config + status: FAILED + checks: + '1': + title: Replace parameters in additional config + status: FAILED + type: Automation + evaluation: + autopilot: additional-config-provider + status: FAILED + reason: This is a reason + execution: + logs: + - '{"status": "FAILED", "reason": "This is a reason"}' + - This autopilot has an additional config + evidencePath: '3_3_1' + exitCode: 0 + '4': + title: Should hide secrets + status: FAILED + requirements: + '1': + title: Hide secrets in logs + status: FAILED + checks: + 1a: + title: Check 1 + status: FAILED + type: Automation + evaluation: + autopilot: secrets-provider + status: FAILED + reason: This is a reason + execution: + logs: + - '{"status": "FAILED", "reason": "This is a reason"}' + evidencePath: 4_1_1a + exitCode: 0 +finalize: + execution: + logs: + - global-env-1 + - global-env-1 + - qg-result.yaml exists + evidencePath: . + exitCode: 0 diff --git a/yaku-apps-typescript/apps/html-finalizer/src/bar-chart.ejs b/yaku-apps-typescript/apps/html-finalizer/src/bar-chart.ejs new file mode 100644 index 00000000..80407fb3 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/bar-chart.ejs @@ -0,0 +1,5 @@ +
+<%_ for (const [name, value] of data) { _%> +
+<%_ } _%> +
diff --git a/yaku-apps-typescript/apps/html-finalizer/src/chart-legend.ejs b/yaku-apps-typescript/apps/html-finalizer/src/chart-legend.ejs new file mode 100644 index 00000000..7f43331a --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/chart-legend.ejs @@ -0,0 +1,27 @@ +<%_ +let sumOfPercentages = 0; +// Calculate percentages for each entry +const initialPercentages = data.map(([name, value]) => (value / count) * 100); +const roundedPercentages = initialPercentages.map((percentage) => Math.floor(percentage)); +// Calculate the sum of percentages +sumOfPercentages = roundedPercentages.reduce((sum, percentage) => sum + percentage, 0); +// Calculate the remaining percentage to reach 100% +const remainingPercentage = 100 - sumOfPercentages; +// Identify the entries with their decimal parts +const entriesWithDecimalParts = initialPercentages.map((percentage, index) => ({ index, decimalPart: percentage % 1 })); +// Sort entries in decreasing order of their decimal parts +entriesWithDecimalParts.sort((a, b) => b.decimalPart - a.decimalPart); +// Distribute the remaining percentage by adding 1 to items in decreasing order of their decimal parts +for (let i = 0; i < remainingPercentage; i++) { + roundedPercentages[entriesWithDecimalParts[i].index]++; +} +// Render the legend entries +for (let i = 0; i < data.length; i++) { + const [name, value] = data[i]; + const percentage = roundedPercentages[i]; +%> + + + <%- percentage %> % <%= labels[name] %> + +<%_ } _%> diff --git a/yaku-apps-typescript/apps/html-finalizer/src/ejs-utils.ts b/yaku-apps-typescript/apps/html-finalizer/src/ejs-utils.ts new file mode 100644 index 00000000..059c60aa --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/ejs-utils.ts @@ -0,0 +1,199 @@ +import markdown from '@B-S-F/markdown-utils' + +const statusIconMap: { [status: string]: string } = { + RED: 'error', + YELLOW: 'alert-warning', + GREEN: 'check-circle', + NA: 'denied', + UNANSWERED: 'text-unanswered', + FAILED: 'report', + ERROR: 'bug', + SKIPPED: 'text-skipped', +} + +const statusTooltipMap: { [status: string]: string } = { + RED: 'Check did not pass', + YELLOW: 'Check passed with warning', + GREEN: 'Check passed', + NA: 'Check not applicable', + UNANSWERED: 'No check configured', + FAILED: 'Autopilot failed with user error', + ERROR: 'Autopilot failed with runtime error', + SKIPPED: 'Check was skipped', +} + +const headerDisplayOrder = [ + { + key: 'name', + label: 'Name', + }, + { + key: 'version', + label: 'Version', + }, + { + key: 'date', + label: 'Date', + }, + { + key: 'qgCliVersion', + label: 'QG CLI Version', + }, + { + key: 'toolVersion', + label: 'Tool Version', + }, +] + +function sortedObjectEntries( + object: + | ArrayLike + | { + [s: string]: unknown + } +) { + return Object.entries(object).sort((a, b) => { + return a[0].localeCompare(b[0], undefined, { numeric: true }) + }) +} + +function sortedObjectValues( + object: + | ArrayLike + | { + [s: string]: unknown + } +) { + return sortedObjectEntries(object).map(([_, entries]) => entries) +} + +const COLOR_CODES = new Map([ + [0, 'black'], + [1, '#cc0000'], + [2, '#4e9a06'], + [3, '#c4a000'], + [4, '#729fcf'], + [5, '#75507b'], + [6, '#06989a'], + [7, '#d3d7cf'], +]) + +function escapeHtmlCharacters(text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') +} + +function applyStyle(style: Map, text: string) { + if (text === '') { + return '' + } + const stylePairs = [] + for (const [prop, val] of style.entries()) { + stylePairs.push(`${prop}:${val}`) + } + if (stylePairs.length > 0) { + return `${text}` + } else { + return text + } +} + +function setStyleFromAnsiCode(style: Map, code: number) { + switch (true) { + case code === 0: { + style.clear() + break + } + case code === 1: { + style.set('font-weight', 'bold') + break + } + case code === 2: { + style.set('font-weight', 'lighter') + break + } + case code === 3: { + style.set('font-style', 'italic') + break + } + case code === 4: { + style.set('text-decoration', 'underline') + break + } + case code === 22: { + style.delete('font-weight') + break + } + case code === 24: { + style.delete('text-decoration') + break + } + case code >= 30 && code <= 37: { + const foregroundColor = COLOR_CODES.get(code % 10) + if (foregroundColor !== undefined) { + style.set('color', foregroundColor) + } + break + } + case code === 39: { + style.delete('color') + break + } + case code >= 40 && code <= 47: { + const backgroundColor = COLOR_CODES.get(code % 10) + if (backgroundColor !== undefined) { + style.set('background-color', backgroundColor) + } + break + } + case code === 49: { + style.delete('background-color') + break + } + default: { + console.log(`Ignoring unknown ANSI SGR code ${code}`) + break + } + } +} + +function formatLogs(lines: string[]) { + const style = new Map() + const logs = escapeHtmlCharacters(lines.join('\n')) + '\n' + const chunks = logs.split(/\033\[(.*?)m/).flatMap((chunk, i) => { + if (i % 2 == 0) { + // even elements are just chunks of text + return applyStyle(style, chunk) + } else { + // odd elements are ANSI codes to interpret + // these change the current style but do not produce any text + for (const ansiCode of chunk.split(';').map(Number)) { + setStyleFromAnsiCode(style, ansiCode) + } + return [] + } + }) + return chunks.join('') +} + +export const utils = { + markdown, + mapStatus(status: string) { + if (!status) return null + return statusIconMap[status] || '' + }, + mapTooltip(status: string) { + if (!status) return null + return statusTooltipMap[status] || '' + }, + formatLogs, + sortedObjectEntries, + sortedObjectValues, + headerDisplayOrder, + hideUnanswered: false, + filterCount: 0, + statuses: Object.keys(statusIconMap), +} diff --git a/yaku-apps-typescript/apps/html-finalizer/src/footer-legend.ejs b/yaku-apps-typescript/apps/html-finalizer/src/footer-legend.ejs new file mode 100644 index 00000000..a789004a --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/footer-legend.ejs @@ -0,0 +1,6 @@ +
+ <%_ for (const status of utils.statuses) { _%> + <%- include('status-icon.ejs', { status }) %>
<%= utils.mapTooltip(status) %>
+ <%_ } _%> + *
Manual status
+
diff --git a/yaku-apps-typescript/apps/html-finalizer/src/index.ts b/yaku-apps-typescript/apps/html-finalizer/src/index.ts new file mode 100644 index 00000000..3b57be26 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/index.ts @@ -0,0 +1,52 @@ +import { readFile } from 'fs/promises' +import { utils } from './ejs-utils.js' +import FileRenderer, { OutputFile } from './render-file.js' +import YAML from 'yaml' +import path from 'path' + +const outputFiles: OutputFile[] = [ + { + template: './qg-dashboard-template.ejs', + output: 'qg-dashboard.html', + default: true, + }, + { + template: './qg-result-template.ejs', + output: 'qg-result.html', + default: true, + }, + { + template: './qg-evidence-template.ejs', + output: 'qg-evidence.html', + default: true, + }, + { + template: './qg-full-report-template.ejs', + output: 'qg-full-report.html', + additionalConfig: { pdfExport: true, silent: true }, + default: true, + }, +] + +export async function renderHtmlFiles(fileList?: string[]) { + const { result_path: resultPath, HIDE_UNANSWERED: hideUnanswered } = + process.env + if (!resultPath) { + throw new Error('result_path environment variable is not set') + } + utils.hideUnanswered = Boolean(hideUnanswered) + + const result = await YAML.parse( + await readFile(path.join(resultPath, 'qg-result.yaml'), { + encoding: 'utf8', + }) + ) + const resultWithUtils = { ...result, utils } + const renderFile = FileRenderer(resultWithUtils, resultPath) + const requestedOutputFiles = fileList + ? outputFiles.filter((file) => fileList.includes(file.output)) + : outputFiles.filter((file) => file.default) + for (const outputFile of requestedOutputFiles) { + await renderFile(outputFile) + } +} diff --git a/yaku-apps-typescript/apps/html-finalizer/src/qg-check-template-old.ejs b/yaku-apps-typescript/apps/html-finalizer/src/qg-check-template-old.ejs new file mode 100644 index 00000000..e1af437e --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/qg-check-template-old.ejs @@ -0,0 +1,51 @@ +<%_ if (check.checks && Object.keys(check.checks).length !== 0) { _%> + +

<%= check.id %>

+

<%= check.title %>

+ + <%_ for (const subCheck of utils.sortedObjectValues(check.checks)) { _%> + <%- include('qg-check-template-old.ejs', { check: subCheck }) %> + <%_ } _%> +<%_ } else { + _%> + <%_ for (const report of check.reports) { _%> + <%_ const rowspan = report.componentResults.length _%> + + <%= check.id %> + <%= check.title %> + <%_ for (const componentResult of report.componentResults) { _%> + <%= componentResult.component.id %> <%= componentResult.component.version %> + + <%- include('status-icon.ejs', { status: componentResult.status }) %> + + class="centered"<%_ } _%>> + <%_ if (componentResult.evidencePath) { _%> + <%_ if (typeof pdfExport !=='undefined' && pdfExport) { _%> + <%= componentResult.evidencePath %> + <%_ } else { _%> + + + + + + <%_ } _%> + <%_ } _%> + <%_ if (componentResult.comments?.length) { _%> + <%_ for (const comment of componentResult.comments) { _%> + <%- utils.markdown.render(comment) %> + <%_ } _%> + <%_ } _%> + <%_ if (componentResult.sources?.length) { _%> +

Sources:

+
    + <%_ for (const source of componentResult.sources) { _%> + <%_ if (typeof source === 'string') { _%> +
  • <%= source %>
  • + <%_ } _%> + <%_ } _%> +
+ <%_ } _%> + + +<%_ }} _%> +<%_ } _%> diff --git a/yaku-apps-typescript/apps/html-finalizer/src/qg-check-template.ejs b/yaku-apps-typescript/apps/html-finalizer/src/qg-check-template.ejs new file mode 100644 index 00000000..46828aa5 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/qg-check-template.ejs @@ -0,0 +1,106 @@ +<%_ const rowspan = 1 _%> +<%_ const hasLogs = (check.type == "Automation" && (check.evaluation.execution.logs || check.evaluation.execution.errorLogs)) _%> + + <%= id %> + <%- utils.markdown.render(check.title) -%> + + <%_ const status = check.status == "" ? "FAILED" : check.status _%> + <%_ const extraTooltip = check.type == "Manual" ? '*Manual status' : undefined _%> + <%- include('status-icon.ejs', { status, extraTooltip }) %> + + <%_ if (check.evaluation.reason != "") { _%> + + <%- utils.markdown.render(check.evaluation.reason) %> + + <%_ } else { _%> + None + <%_ } _%> + <%_ if (check.type == "Manual") { _%> + + + <%_ } _%> + <%_ if (check.type == "Automation") { _%> + class="centered"<%_ } _%>> + <%_ if (check.evaluation.execution.evidencePath) { _%> + <%_ if (typeof pdfExport !=='undefined' && pdfExport) { _%> + <%= check.evaluation.execution.evidencePath %> + <%_ } else { _%> + + + + + + <%_ } _%> + <%_ } _%> + + <%_ if (check.evaluation.results?.length > 0) { _%> + + + + + + + + + + + <%_ for (const result of check.evaluation.results) { _%> + + + + + + <%_ } _%> + +
CriterionFulfilledJustification
<%- utils.markdown.render(result.criterion) %><%- result.fulfilled ? "Yes" : "No" %><%- utils.markdown.render(result.justification) %>
+ + <%_ } else { _%> + + <%_ } _%> + <%_ } _%> +<%_ if (hasLogs) { _%> + + + +

Log output

+ + + +<%_ } _%> +<%_ if (check.evaluation.outputs !== undefined && Object.keys(check.evaluation.outputs).length > 0) { _%> + + + +

Script outputs

+ + + + + + + + + <%_ for (const name of Object.keys(check.evaluation.outputs)) { _%> + + + + + <%_ } _%> + + + + +<%_ } _%> \ No newline at end of file diff --git a/yaku-apps-typescript/apps/html-finalizer/src/qg-dashboard-section-old.ejs b/yaku-apps-typescript/apps/html-finalizer/src/qg-dashboard-section-old.ejs new file mode 100644 index 00000000..fab81d02 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/qg-dashboard-section-old.ejs @@ -0,0 +1,75 @@ +<%_ +const labels = { + auto: 'Automatically', + manual: 'Manually', + unanswered: 'Unanswered', + NA: 'N/A' +} + +const requirements = [] + +for (const allocation of Object.values(allocations)) { + requirements.push(...Object.values(allocation.requirements)) +} + +const answerStatuses = { + auto: 0, + manual: 0, + unanswered: 0, +} + +const results = { + GREEN: 0, + YELLOW: 0, + RED: 0, + NA: 0 +} + +for (const requirement of requirements) { + if (requirement.status === 'PENDING') { + ++answerStatuses.unanswered + } else { + const status = (requirement.status === 'FAILED' || requirement.status === 'ERROR') ? 'RED' : requirement.status + if (requirement.manualStatus) { + ++answerStatuses.manual + } else { + ++answerStatuses.auto + } + ++results[status] + } +} + +const totalAnswered = answerStatuses.auto + answerStatuses.manual +_%> +

Status of Results

+

+ Result of answered: + <%- include('chart-legend.ejs', { + data: Object.entries(results).filter(([name, value]) => value), + count: totalAnswered, + labels + }) %> +

+

+ <%- include('bar-chart.ejs', { + data: [ + ...Object.entries(results).filter(([name, value]) => value), + ['empty', answerStatuses.unanswered] + ], + count: requirements.length + }) %> +

+

+ <%- include('bar-chart.ejs', { + data: Object.entries(answerStatuses), + count: requirements.length + }) %> +

+

+ Answered: + <%- include('chart-legend.ejs', { + data: Object.entries(answerStatuses), + count: requirements.length, + labels + }) %> +

diff --git a/yaku-apps-typescript/apps/html-finalizer/src/qg-dashboard-section.ejs b/yaku-apps-typescript/apps/html-finalizer/src/qg-dashboard-section.ejs new file mode 100644 index 00000000..227e4c2f --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/qg-dashboard-section.ejs @@ -0,0 +1,72 @@ +<%_ +const labels = { + auto: 'Automatically', + manual: 'Manually', + unanswered: 'Unanswered', + NA: 'N/A' +} + +const totalChecks = statistics["counted-checks"] +const checkStatistics = { + auto: statistics["counted-automated-checks"], + manual: statistics["counted-manual-check"], + unanswered: statistics["counted-unanswered-checks"], +} + +const checks = [] +for (const chapter of Object.values(chapters)) { + for (const requirement of Object.values(chapter.requirements)) { + if (requirement.checks) { + checks.push(...Object.values(requirement.checks)) + } + } +} + +const statuses = { + GREEN: 0, + YELLOW: 0, + RED: 0, + NA: 0, +} + +for (const check of checks) { + if (check.status != 'UNANSWERED') { + const status = (check.status == 'FAILED' || check.status == 'ERROR') ? 'RED' : check.status + ++statuses[status] + } +} + +const totalAnswered = totalChecks - checkStatistics.unanswered +_%> +

Status

+

+ Result of answered: + <%- include('chart-legend.ejs', { + data: Object.entries(statuses).filter(([name, value]) => value), + count: totalAnswered, + labels + }) %> +

+

+ <%- include('bar-chart.ejs', { + data: [ + ...Object.entries(statuses).filter(([name, value]) => value), + ['empty', checkStatistics.unanswered] + ], + count: totalChecks + }) %> +

+

+ <%- include('bar-chart.ejs', { + data: Object.entries(checkStatistics), + count: totalChecks + }) %> +

+

+ Answered: + <%- include('chart-legend.ejs', { + data: Object.entries(checkStatistics), + count: totalChecks, + labels + }) %> +

diff --git a/yaku-apps-typescript/apps/html-finalizer/src/qg-dashboard-template.ejs b/yaku-apps-typescript/apps/html-finalizer/src/qg-dashboard-template.ejs new file mode 100644 index 00000000..34c89799 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/qg-dashboard-template.ejs @@ -0,0 +1,30 @@ + + + + QG <%= header.name %> + <%- include('qg-report-head.ejs') %> + + + + <%- include('qg-report-body.ejs') %> + +

<%= header.name %>

+ + <%- include('qg-header.ejs') %> + + <% if (locals.metadata && locals.metadata.version) { %> + <%- include('qg-dashboard-section.ejs') %> + <% } else { %> + <%- include('qg-dashboard-section-old.ejs') %> + <% } %> + +

+ + + + + Breakdown of results + +

+ + diff --git a/yaku-apps-typescript/apps/html-finalizer/src/qg-evidence-section-old.ejs b/yaku-apps-typescript/apps/html-finalizer/src/qg-evidence-section-old.ejs new file mode 100644 index 00000000..de6f1063 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/qg-evidence-section-old.ejs @@ -0,0 +1,31 @@ +<%_ for (const allocation of utils.sortedObjectValues(allocations)) { _%> +

<%= allocation.id %> <%= allocation.title %>

+<%_ for (const requirement of utils.sortedObjectValues(allocation.requirements)) { _%> +

+ + <%- include('status-icon.ejs', { status: requirement.status, extraTooltip: requirement.manualStatus ? '*Manual status' : undefined }) %> + <%_ if (requirement.manualStatus) { _%>*<%_ } _%> + + <%= requirement.id %> <%= requirement.title %>

+ <%_ if (requirement.reason) { _%>

Manual status: <%- utils.markdown.render(requirement.reason) %>

<%_ } _%> +<%_ if (requirement.checks && Object.keys(requirement.checks).length !== 0) { _%> + + + + + + + + + + + + + <%_ for (const check of utils.sortedObjectValues(requirement.checks)) { _%> + <%- include('qg-check-template-old.ejs', { check }) %> + <%_ } _%> + +
IDCheckComponentResult class="centered"<%_ } _%>>EvidenceComment
+<%_ } _%> +<%_ } _%> +<%_ } _%> diff --git a/yaku-apps-typescript/apps/html-finalizer/src/qg-evidence-section.ejs b/yaku-apps-typescript/apps/html-finalizer/src/qg-evidence-section.ejs new file mode 100644 index 00000000..0849fe13 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/qg-evidence-section.ejs @@ -0,0 +1,29 @@ +<%_ for (const [chapter_id, chapter] of utils.sortedObjectEntries(chapters)) { _%> +

<%= chapter_id %> <%= chapter.title %>

+ <%_ for (const [id, requirement] of utils.sortedObjectEntries(chapter.requirements)) { _%> +

+ + <%- include('status-icon.ejs', { status: requirement.status }) %> + + <%= id %> <%= requirement.title %>

+ <%_ if (requirement.checks && Object.keys(requirement.checks).length !== 0) { _%> + + + + + + + + + + + + + <%_ for (const [id, check] of utils.sortedObjectEntries(requirement.checks)) { _%> + <%- include('qg-check-template.ejs', { id, check }) %> + <%_ } _%> + +
IDCheckStatusReason class="centered"<%_ } _%>>EvidenceResult
+ <%_ } _%> + <%_ } _%> +<%_ } _%> diff --git a/yaku-apps-typescript/apps/html-finalizer/src/qg-evidence-template.ejs b/yaku-apps-typescript/apps/html-finalizer/src/qg-evidence-template.ejs new file mode 100644 index 00000000..b2339cf9 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/qg-evidence-template.ejs @@ -0,0 +1,23 @@ + + + + QG Evidence: <%= header.name %> + <%- include('qg-report-head.ejs') %> + + + + <%- include('qg-report-body.ejs') %> + +

<%= header.name %>

+ + <%- include('qg-header.ejs') %> + + <% if (locals.metadata && locals.metadata.version) { %> + <%- include('qg-evidence-section.ejs') %> + <% } else { %> + <%- include('qg-evidence-section-old.ejs') %> + <% } %> + + <%- include('footer-legend.ejs') %> + + diff --git a/yaku-apps-typescript/apps/html-finalizer/src/qg-full-report-template.ejs b/yaku-apps-typescript/apps/html-finalizer/src/qg-full-report-template.ejs new file mode 100644 index 00000000..758e0482 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/qg-full-report-template.ejs @@ -0,0 +1,27 @@ + + + + + QG <%= header.name %> + <%- include('qg-report-head.ejs') %> + + + + <%- include('qg-report-body.ejs') %> + +

<%= header.name %>

+ + <%- include('qg-header.ejs') %> + + <% if (locals.metadata && locals.metadata.version) { %> + <%- include('qg-dashboard-section.ejs') %> + <%- include('qg-result-section.ejs') %> + <%- include('qg-evidence-section.ejs') %> + <% } else { %> + <%- include('qg-dashboard-section-old.ejs') %> + <%- include('qg-result-section-old.ejs') %> + <%- include('qg-evidence-section-old.ejs') %> + <% } %> + <%- include('footer-legend.ejs') %> + + diff --git a/yaku-apps-typescript/apps/html-finalizer/src/qg-header.ejs b/yaku-apps-typescript/apps/html-finalizer/src/qg-header.ejs new file mode 100644 index 00000000..2b247e18 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/qg-header.ejs @@ -0,0 +1,26 @@ + + + <% if (locals.metadata && locals.metadata.version) { %> + + + + + <% } %> + <%_ for (const { key, label } of utils.headerDisplayOrder) { _%> + <% if (header[key]) { %> + + + + + <% } %> + <%_ } _%> + +
+ Configuration + + <%= metadata.version %> +
+ <%= label %> + + <%= header[key] %> +
diff --git a/yaku-apps-typescript/apps/html-finalizer/src/qg-report-body.ejs b/yaku-apps-typescript/apps/html-finalizer/src/qg-report-body.ejs new file mode 100644 index 00000000..ecb9e81b --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/qg-report-body.ejs @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + PENDING + + + + + SKIPPED + + + + diff --git a/yaku-apps-typescript/apps/html-finalizer/src/qg-report-head.ejs b/yaku-apps-typescript/apps/html-finalizer/src/qg-report-head.ejs new file mode 100644 index 00000000..bca83b8b --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/qg-report-head.ejs @@ -0,0 +1,383 @@ + + + \ No newline at end of file diff --git a/yaku-apps-typescript/apps/html-finalizer/src/qg-result-section-old.ejs b/yaku-apps-typescript/apps/html-finalizer/src/qg-result-section-old.ejs new file mode 100644 index 00000000..15200e8f --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/qg-result-section-old.ejs @@ -0,0 +1,16 @@ +

Summary of Findings

+
+<%- include('qg-result-table-old.ejs', { + emptyListMessage: 'There are no findings.', + sortedAllocations: utils.sortedObjectValues(allocations).map((allocation) => ({ + ...allocation, + requirements: Object.fromEntries(Object.entries(allocation.requirements).filter(([, requirement]) => requirement.status === 'RED' || requirement.status === 'FAILED' || (requirement.status === 'PENDING' && !utils.hideUnanswered))) + })).filter((allocation) => Object.entries(allocation.requirements).length > 0) +}) %> +
+ +

Details

+<%- include('qg-result-table-old.ejs', { + sortedAllocations: utils.sortedObjectValues(allocations), + showRequirementText: true +}) %> diff --git a/yaku-apps-typescript/apps/html-finalizer/src/qg-result-section.ejs b/yaku-apps-typescript/apps/html-finalizer/src/qg-result-section.ejs new file mode 100644 index 00000000..a56798fb --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/qg-result-section.ejs @@ -0,0 +1,21 @@ +

Summary

+
+

Overall Status: <%- include('status-icon.ejs', { status: overallStatus }) %>

+<%- include('qg-result-table.ejs', { + emptyListMessage: 'All checks passed.', + sortedChapters: utils.sortedObjectEntries(chapters).map(([id, chapter]) => ({ + ...chapter, + id, + requirements: Object.fromEntries(Object.entries(chapter.requirements).filter(([, requirement]) => ['RED', 'FAILED', 'ERROR', 'SKIPPED', 'YELLOW'].includes(requirement.status) || (requirement.status === 'UNANSWERED' && !utils.hideUnanswered))) + })).filter((chapter) => Object.entries(chapter.requirements).length > 0) +}) %> +
+ +

Details

+<%- include('qg-result-table.ejs', { + sortedChapters: utils.sortedObjectEntries(chapters).map(([id, chapter]) => ({ + ...chapter, + id, + })), + showRequirementText: true +}) %> diff --git a/yaku-apps-typescript/apps/html-finalizer/src/qg-result-table-old.ejs b/yaku-apps-typescript/apps/html-finalizer/src/qg-result-table-old.ejs new file mode 100644 index 00000000..193a62d0 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/qg-result-table-old.ejs @@ -0,0 +1,140 @@ +<%_ const filterPrefix = `filter${++utils.filterCount}` + const filterIcons = [] + for (const allocation of sortedAllocations) { + for (const requirement of Object.values(allocation.requirements)) { + if (filterIcons.indexOf(requirement.status) === -1) { + filterIcons.push(requirement.status) + } + } + } +_%> +<%_ if (typeof emptyListMessage === 'undefined' || !emptyListMessage || sortedAllocations.length) { _%> + +<%_ for (const status of filterIcons) { _%> + +<%_ } _%> + + + + + + + + + + + + + + <%_ for (const allocation of sortedAllocations) { _%> + + + + + <%_ for (const requirement of utils.sortedObjectValues(allocation.requirements)) { _%> + + + + + + + <%_ } _%> + <%_ } _%> + +
IDRequirementResult + <%_ if (filterIcons.length > 1) { _%> + + +
+ <%_ for (const status of filterIcons) { _%> + + <%_ } _%> + <%_ } _%> +
Evidence
+

+ <%= allocation.id %> +

+
+

+ <%= allocation.title %> +

+
+ <%= requirement.id %> + +

+ <%= requirement.title %> +

+ <%- typeof showRequirementText !== 'undefined' && showRequirementText ? utils.markdown.render(requirement.text) : null %> +
+ <%- include('status-icon.ejs', { status: requirement.status, extraTooltip: requirement.manualStatus ? '*Manual status' : undefined }) %> + <%_ if (requirement.manualStatus) { _%>*<%_ } _%> + + <%_ if (requirement.manualStatus) { _%> + Manual status: <%- utils.markdown.render(requirement.reason) %> + <%_ } else if (requirement.checks) { _%> + <%_ if (typeof pdfExport !== 'undefined' && pdfExport) { _%> + result + <%_ } else { _%> + result + <%_ } _%> + <%_ } _%> +
+ +<%_ } else { _%> +

<%= emptyListMessage %>

+<%_ } _%> diff --git a/yaku-apps-typescript/apps/html-finalizer/src/qg-result-table.ejs b/yaku-apps-typescript/apps/html-finalizer/src/qg-result-table.ejs new file mode 100644 index 00000000..8f1ff34f --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/qg-result-table.ejs @@ -0,0 +1,140 @@ +<%_ const filterPrefix = `filter${++utils.filterCount}` + const filterIcons = [] + for (const chapter of sortedChapters) { + for (const requirement of Object.values(chapter.requirements)) { + if (filterIcons.indexOf(requirement.status) === -1) { + filterIcons.push(requirement.status) + } + } + } +_%> +<%_ if (typeof emptyListMessage === 'undefined' || !emptyListMessage || sortedChapters.length) { _%> + +<%_ for (const status of filterIcons) { _%> + +<%_ } _%> + + + + + + + + + + + + + + <%_ for (const chapter of sortedChapters) { _%> + + + + + <%_ for (const [id, requirement] of utils.sortedObjectEntries(chapter.requirements)) { _%> + + + + + + + <%_ } _%> + <%_ } _%> + +
IDRequirementResult + <%_ if (filterIcons.length > 1) { _%> + + +
+ <%_ for (const status of filterIcons) { _%> + + <%_ } _%> + <%_ } _%> +
Evidence
+

+ <%= chapter.id %> +

+
+

+ <%= chapter.title %> +

+
+ <%= id %> + +

+ <%= requirement.title %> +

+ <%- typeof showRequirementText !== 'undefined' && showRequirementText ? utils.markdown.render(requirement.text) : null %> +
+ <%- include('status-icon.ejs', { status: requirement.status, extraTooltip: requirement.manualEvaluation ? '*Manual status' : undefined }) %> + <%_ if (requirement.manualEvaluation) { _%>*<%_ } _%> + + <%_ if (requirement.manualEvaluation) { _%> + Manual status: <%- utils.markdown.render(requirement.manualEvaluation.reason) %> + <%_ } else if (requirement.checks) { _%> + <%_ if (typeof pdfExport !== 'undefined' && pdfExport) { _%> + result + <%_ } else { _%> + result + <%_ } _%> + <%_ } _%> +
+ +<%_ } else { _%> +

<%= emptyListMessage %>

+<%_ } _%> diff --git a/yaku-apps-typescript/apps/html-finalizer/src/qg-result-template.ejs b/yaku-apps-typescript/apps/html-finalizer/src/qg-result-template.ejs new file mode 100644 index 00000000..ebf42f54 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/qg-result-template.ejs @@ -0,0 +1,25 @@ + + + + QG <%= header.name %> + <%- include('qg-report-head.ejs') %> + + + + <%- include('qg-report-body.ejs') %> + +

<%= header.name %>

+ + <%- include('qg-header.ejs') %> + + <% if (locals.metadata && locals.metadata.version) { %> + <%- include('qg-dashboard-section.ejs') %> + <%- include('qg-result-section.ejs') %> + <% } else { %> + <%- include('qg-dashboard-section-old.ejs') %> + <%- include('qg-result-section-old.ejs') %> + <% } %> + + <%- include('footer-legend.ejs') %> + + diff --git a/yaku-apps-typescript/apps/html-finalizer/src/render-file.ts b/yaku-apps-typescript/apps/html-finalizer/src/render-file.ts new file mode 100644 index 00000000..18cd0af5 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/render-file.ts @@ -0,0 +1,41 @@ +import ejs, { Data } from 'ejs' +import resolve from './resolve.js' +import markdown from '@B-S-F/markdown-utils' +import { outputFile } from 'fs-extra' +import path from 'path' +export interface OutputFile { + /** Name of the template file */ + template: string + /** Name of the output file to generate */ + output: string + /** Defines if the file is default output or not */ + default?: boolean + /** Additional data to pass to the ejs renderer */ + additionalConfig?: Data +} + +/** + * Escapes characters for XML and converts quotes to typography quotes + * @param mdText Markdown formatted text + */ +function escape(mdText: any) { + return ejs.escapeXML(markdown.smartquotes(mdText)) +} + +export default function FileRenderer(data: Data, resultPath: string) { + return async function ({ template, output, additionalConfig }: OutputFile) { + const mergedData = additionalConfig + ? { ...data, ...additionalConfig } + : data + const html = await ejs.renderFile( + resolve(import.meta.url, template), + mergedData, + { + escape, + } + ) + await outputFile(path.join(resultPath, output), html) + if (!(additionalConfig && additionalConfig.silent)) + console.info(`${output} generated successfully.`) + } +} diff --git a/yaku-apps-typescript/apps/html-finalizer/src/resolve.ts b/yaku-apps-typescript/apps/html-finalizer/src/resolve.ts new file mode 100644 index 00000000..a6794cfc --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/resolve.ts @@ -0,0 +1,12 @@ +import path from 'path' +import url from 'url' + +/** + * Starting from a file url, resolves a relative path to an absolute file path + * @param metaUrl file url of the file to start from, typically `import.meta.url` + * @param relativePath relative path to a file + * @returns the absolute path to the file + */ +export default function resolve(metaUrl: string | URL, relativePath: string) { + return path.join(path.dirname(url.fileURLToPath(metaUrl)), relativePath) +} diff --git a/yaku-apps-typescript/apps/html-finalizer/src/run.ts b/yaku-apps-typescript/apps/html-finalizer/src/run.ts new file mode 100755 index 00000000..9b4a65d9 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/run.ts @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { renderHtmlFiles } from './index.js' + +renderHtmlFiles() diff --git a/yaku-apps-typescript/apps/html-finalizer/src/status-icon.ejs b/yaku-apps-typescript/apps/html-finalizer/src/status-icon.ejs new file mode 100644 index 00000000..ef15939e --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/src/status-icon.ejs @@ -0,0 +1,6 @@ +<%_ const tooltip = typeof extraTooltip === 'undefined' ? '' : ' ' + extraTooltip _%> + + + + + diff --git a/yaku-apps-typescript/apps/html-finalizer/test/integration/__snapshots__/render-html-files.int-spec.ts.snap b/yaku-apps-typescript/apps/html-finalizer/test/integration/__snapshots__/render-html-files.int-spec.ts.snap new file mode 100644 index 00000000..a473232c --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/test/integration/__snapshots__/render-html-files.int-spec.ts.snap @@ -0,0 +1,13660 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`renderHtmlFiles > should render result %p test/integration/input/v0 1`] = ` +" + + +QG XXXXX + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +PENDING + + + + +SKIPPED + + + + + + +

XXXXX

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+Name + +XXXXX +
+Version + +1.16.0 +
+Date + +2022-05-16 9:48:45 CEST +
+QG CLI Version + +0.1.8:4b50ee26239bb9e7f6084dcb1c005e05b7b79a88 +
+ + + +

Status of Results

+

+Result of answered: + + + +40 % + + + + +20 % + + + + +40 % + + +

+

+

+
+
+
+
+
+ +

+

+

+
+
+
+
+ +

+

+Answered: + + + +50 % Automatically + + + + +33 % Manually + + + + +17 % Unanswered + + +

+ + + +

+ + + + +Breakdown of results + +

+ + +" +`; + +exports[`renderHtmlFiles > should render result %p test/integration/input/v0 2`] = ` +" + + + +QG XXXXX + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +PENDING + + + + +SKIPPED + + + + + + +

XXXXX

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+Name + +XXXXX +
+Version + +1.16.0 +
+Date + +2022-05-16 9:48:45 CEST +
+QG CLI Version + +0.1.8:4b50ee26239bb9e7f6084dcb1c005e05b7b79a88 +
+ + + +

Status of Results

+

+Result of answered: + + + +40 % + + + + +20 % + + + + +40 % + + +

+

+

+
+
+
+
+
+ +

+

+

+
+
+
+
+ +

+

+Answered: + + + +50 % Automatically + + + + +33 % Manually + + + + +17 % Unanswered + + +

+ +

Summary of Findings

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDRequirementResult + + +
+ + +
Evidence
+

+1 +

+
+

+title +

+
+1.3 + +

+title +

+ +
+ + + + + + + +result +
+

+2 +

+
+

+title +

+
+2.6 + +

+title +

+ +
+ + + + + + + +result +
+5.1 + +

+title +

+ +
+ + + + + + + +result +
+ + +
+ +

Details

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDRequirementResult + + +
+ + + + +
Evidence
+

+1 +

+
+

+title +

+
+1.2 + +

+title +

+

text text

+ +
+ + + + + + +* +Manual status:

Not relevant

+ +
+1.3 + +

+title +

+

text

+ +
+ + + + + + + +result +
+1.15 + +

+title +

+

text

+ +
+ + + + + + + +result +
+

+2 +

+
+

+title +

+
+2.3 + +

+title +

+

text

+ +
+ + + + + + +* +Manual status:

Need verification

+ +
+2.6 + +

+title +

+

text

+ +
+ + + + + + + +result +
+5.1 + +

+title +

+

text

+ +
+ + + + + + + +result +
+ + + +

1 title

+

+ + + + + + + +* +1.2 title

+

Manual status:

Not relevant

+

+ + + + + + + + +1.3 title

+

+ + + + + + + + +1.15 title

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDCheckComponentResultEvidenceComment

1

title

1.1titlewebApp 1.16.0 + + + + + + + +fossid-autopilot/webApp +
KeyCloak 1.16.5 + + + + + + + +fossid-autopilot/KeyCloak +
1.2titlewebApp 1.16.0 + + + + + + + +fossid-autopilot/webApp +
KeyCloak 1.16.5 + + + + + + + +fossid-autopilot/KeyCloak +

2

title

2.1titleKeyCloak 1.16.5 + + + + + + + +

reason

+ +
webApp 1.16.0 + + + + + + + +whitesource-autopilot/webApp +

exitStatus.properties exists in /home/xxx2xx/repos/xxxx/qg-cli/evidence/run_2022-05-16T074845.501Z/whitesource-autopilot/webApp

+ +
2.2titleKeyCloak 1.16.5 + + + + + + + +

comments

+ +
webApp 1.16.0 + + + + + + + +whitesource-autopilot/webApp +

status.value=OK found in exitStatus.properties file

+ +
+

2 title

+

+ + + + + + + +* +2.3 title

+

Manual status:

Need verification

+

+ + + + + + + + +2.6 title

+ + + + + + + + + + + + + + + + + + + + + + +
IDCheckComponentResultEvidenceComment
1.1titlewebApp 1.16.0 + + + + + + + +docupedia-autopilot/webApp +
+

+ + + + + + + + +5.1 title

+ + + + + + + + + + + + + + + + + + + + + + +
IDCheckComponentResultEvidenceComment
1.1titlewebApp 1.16.0 + + + + + + + +sonarqube-autopilot/webApp +
+ + +
+ + + + + +
Check did not pass
+ + + + + +
Check passed with warning
+ + + + + +
Check passed
+ + + + + +
Check not applicable
+ + + + + +
No check configured
+ + + + + +
Autopilot failed with user error
+ + + + + +
Autopilot failed with runtime error
+ + + + + +
Check was skipped
+*
Manual status
+
+ + + +" +`; + +exports[`renderHtmlFiles > should render result %p test/integration/input/v0 3`] = ` +" + + +QG XXXXX + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +PENDING + + + + +SKIPPED + + + + + + +

XXXXX

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+Name + +XXXXX +
+Version + +1.16.0 +
+Date + +2022-05-16 9:48:45 CEST +
+QG CLI Version + +0.1.8:4b50ee26239bb9e7f6084dcb1c005e05b7b79a88 +
+ + + +

Status of Results

+

+Result of answered: + + + +40 % + + + + +20 % + + + + +40 % + + +

+

+

+
+
+
+
+
+ +

+

+

+
+
+
+
+ +

+

+Answered: + + + +50 % Automatically + + + + +33 % Manually + + + + +17 % Unanswered + + +

+ +

Summary of Findings

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDRequirementResult + + +
+ + +
Evidence
+

+1 +

+
+

+title +

+
+1.3 + +

+title +

+ +
+ + + + + + + +result +
+

+2 +

+
+

+title +

+
+2.6 + +

+title +

+ +
+ + + + + + + +result +
+5.1 + +

+title +

+ +
+ + + + + + + +result +
+ + +
+ +

Details

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDRequirementResult + + +
+ + + + +
Evidence
+

+1 +

+
+

+title +

+
+1.2 + +

+title +

+

text text

+ +
+ + + + + + +* +Manual status:

Not relevant

+ +
+1.3 + +

+title +

+

text

+ +
+ + + + + + + +result +
+1.15 + +

+title +

+

text

+ +
+ + + + + + + +result +
+

+2 +

+
+

+title +

+
+2.3 + +

+title +

+

text

+ +
+ + + + + + +* +Manual status:

Need verification

+ +
+2.6 + +

+title +

+

text

+ +
+ + + + + + + +result +
+5.1 + +

+title +

+

text

+ +
+ + + + + + + +result +
+ + + + + +
+ + + + + +
Check did not pass
+ + + + + +
Check passed with warning
+ + + + + +
Check passed
+ + + + + +
Check not applicable
+ + + + + +
No check configured
+ + + + + +
Autopilot failed with user error
+ + + + + +
Autopilot failed with runtime error
+ + + + + +
Check was skipped
+*
Manual status
+
+ + + +" +`; + +exports[`renderHtmlFiles > should render result %p test/integration/input/v0 4`] = ` +" + + +QG Evidence: XXXXX + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +PENDING + + + + +SKIPPED + + + + + + +

XXXXX

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+Name + +XXXXX +
+Version + +1.16.0 +
+Date + +2022-05-16 9:48:45 CEST +
+QG CLI Version + +0.1.8:4b50ee26239bb9e7f6084dcb1c005e05b7b79a88 +
+ + + +

1 title

+

+ + + + + + + +* +1.2 title

+

Manual status:

Not relevant

+

+ + + + + + + + +1.3 title

+

+ + + + + + + + +1.15 title

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDCheckComponentResultEvidenceComment

1

title

1.1titlewebApp 1.16.0 + + + + + + + + + + + + +
KeyCloak 1.16.5 + + + + + + + + + + + + +
1.2titlewebApp 1.16.0 + + + + + + + + + + + + +
KeyCloak 1.16.5 + + + + + + + + + + + + +

2

title

2.1titleKeyCloak 1.16.5 + + + + + + + +

reason

+ +
webApp 1.16.0 + + + + + + + + + + + + +

exitStatus.properties exists in /home/xxx2xx/repos/xxxx/qg-cli/evidence/run_2022-05-16T074845.501Z/whitesource-autopilot/webApp

+ +
2.2titleKeyCloak 1.16.5 + + + + + + + +

comments

+ +
webApp 1.16.0 + + + + + + + + + + + + +

status.value=OK found in exitStatus.properties file

+ +
+

2 title

+

+ + + + + + + +* +2.3 title

+

Manual status:

Need verification

+

+ + + + + + + + +2.6 title

+ + + + + + + + + + + + + + + + + + + + + + +
IDCheckComponentResultEvidenceComment
1.1titlewebApp 1.16.0 + + + + + + + + + + + + +
+

+ + + + + + + + +5.1 title

+ + + + + + + + + + + + + + + + + + + + + + +
IDCheckComponentResultEvidenceComment
1.1titlewebApp 1.16.0 + + + + + + + + + + + + +
+ + + +
+ + + + + +
Check did not pass
+ + + + + +
Check passed with warning
+ + + + + +
Check passed
+ + + + + +
Check not applicable
+ + + + + +
No check configured
+ + + + + +
Autopilot failed with user error
+ + + + + +
Autopilot failed with runtime error
+ + + + + +
Check was skipped
+*
Manual status
+
+ + + +" +`; + +exports[`renderHtmlFiles > should render result %p test/integration/input/v1 1`] = ` +" + + +QG + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +PENDING + + + + +SKIPPED + + + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
+Configuration + +v1 +
+Date + +2023-11-16 18:05 +
+Tool Version + +0.6.1 +
+ + + +

Status

+

+Result of answered: + + + +30 % + + + + +9 % + + + + +57 % + + + + +4 % N/A + + +

+

+

+
+
+
+
+
+
+ +

+

+

+
+
+
+
+ +

+

+Answered: + + + +75 % Automatically + + + + +21 % Manually + + + + +4 % Unanswered + + +

+ + + +

+ + + + +Breakdown of results + +

+ + +" +`; + +exports[`renderHtmlFiles > should render result %p test/integration/input/v1 2`] = ` +" + + + +QG + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +PENDING + + + + +SKIPPED + + + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
+Configuration + +v1 +
+Date + +2023-11-16 18:05 +
+Tool Version + +0.6.1 +
+ + + +

Status

+

+Result of answered: + + + +30 % + + + + +9 % + + + + +57 % + + + + +4 % N/A + + +

+

+

+
+
+
+
+
+
+ +

+

+

+
+
+
+
+ +

+

+Answered: + + + +75 % Automatically + + + + +21 % Manually + + + + +4 % Unanswered + + +

+ +

Summary

+
+

Overall Status: + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDRequirementResult + + +
+ + + + + +
Evidence
+

+1 +

+
+

+Manual Answers have to be supported +

+
+2 + +

+YELLOW answer +

+ +
+ + + + + + + +result +
+3 + +

+RED answer +

+ +
+ + + + + + + +result +
+5 + +

+UNANSWERED answer +

+ +
+ + + + + + + +result +
+

+2 +

+
+

+Base Interface +

+
+1 + +

+Base Interface has to be supported +

+ +
+ + + + + + + +result +
+

+3 +

+
+

+Parameter Replacement +

+
+1 + +

+Should replace parameters in autopilots +

+ +
+ + + + + + + +result +
+3 + +

+Should replace parameters in additional config +

+ +
+ + + + + + + +result +
+

+4 +

+
+

+Should hide secrets +

+
+1 + +

+Hide secrets in logs +

+ +
+ + + + + + + +result +
+ + +
+ +

Details

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDRequirementResult + + +
+ + + + + + + +
Evidence
+

+1 +

+
+

+Manual Answers have to be supported +

+
+1 + +

+GREEN answer +

+ +
+ + + + + + + +result +
+2 + +

+YELLOW answer +

+ +
+ + + + + + + +result +
+3 + +

+RED answer +

+ +
+ + + + + + + +result +
+4 + +

+NA answer +

+ +
+ + + + + + + +result +
+5 + +

+UNANSWERED answer +

+ +
+ + + + + + + +result +
+

+2 +

+
+

+Base Interface +

+
+1 + +

+Base Interface has to be supported +

+

The base interface should be supported to retrieve the status from an autopilot
+The base interface consists of the following properties:

+
    +
  • status
  • +
  • reason
  • +
  • outputs
  • +
+ +
+ + + + + + + +result +
+

+3 +

+
+

+Parameter Replacement +

+
+1 + +

+Should replace parameters in autopilots +

+ +
+ + + + + + + +result +
+2 + +

+Should replace parameters in manual answers like +

+ +
+ + + + + + + +result +
+3 + +

+Should replace parameters in additional config +

+ +
+ + + + + + + +result +
+

+4 +

+
+

+Should hide secrets +

+
+1 + +

+Hide secrets in logs +

+ +
+ + + + + + + +result +
+ + + +

1 Manual Answers have to be supported

+

+ + + + + + + + +1 GREEN answer

+ + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
1

GREEN answer check

+
+ + + + + + + +

It should be GREEN

+ +
+

+ + + + + + + + +2 YELLOW answer

+ + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
1

YELLOW answer check

+
+ + + + + + + +

It should be YELLOW

+ +
+

+ + + + + + + + +3 RED answer

+ + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
1

RED answer check

+
+ + + + + + + +

It should be RED

+ +
+

+ + + + + + + + +4 NA answer

+ + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
1

NA answer check

+
+ + + + + + + +

It should be NA

+ +
+

+ + + + + + + + +5 UNANSWERED answer

+ + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
1

UNANSWERED answer check

+
+ + + + + + + +

It should be UNANSWERED

+ +
+

2 Base Interface

+

+ + + + + + + + +1 Base Interface has to be supported

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
1a

Status GREEN should be supported

+
+ + + + + + + +

Some reason

+ +
+2_1_1a + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

I am a criterion

+
No

I am the justification

+
+
+

Log output

+ +
1b

Status YELLOW should be supported

+
+ + + + + + + +

Some reason

+ +
+2_1_1b + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

I am a criterion

+
No

I am the justification

+
+
+

Log output

+ +
1c

Status RED should be supported

+
+ + + + + + + +

Some reason

+ +
+2_1_1c + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

I am a criterion

+
No

I am the justification

+
+
+

Log output

+ +
1d

Status NA should be supported

+
+ + + + + + + +

autopilot ‘status-provider’ provided an invalid ‘status’: ‘NA’

+ +
+2_1_1d + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

I am a criterion

+
No

I am the justification

+
+
+

Log output

+ +
1e

Status UNANSWERED should be supported

+
+ + + + + + + +

autopilot ‘status-provider’ provided an invalid ‘status’: ‘UNANSWERED’

+ +
+2_1_1e + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

I am a criterion

+
No

I am the justification

+
+
+

Log output

+ +
1f

If a status is not supported, it should be set to ERROR

+
+ + + + + + + +

autopilot ‘status-provider’ provided an invalid ‘status’: ‘UNKNOWN’

+ +
+2_1_1f + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

I am a criterion

+
No

I am the justification

+
+
+

Log output

+ +
1g

If a status is empty, it should be set to ERROR

+
+ + + + + + + +

autopilot ‘status-provider’ provided an invalid ‘status’: ″

+ +
+2_1_1g + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

I am a criterion

+
No

I am the justification

+
+
+

Log output

+ +
3

Reason should be supported

+
+ + + + + + + +

This is a reason

+ +
+2_1_3 +
+

Log output

+ +
4

Outputs should be supported

+
+ + + + + + + +

This is a reason

+ +
+2_1_4 + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

I am a criterion

+
No

I am the justification

+
+
+

Log output

+ +
+

Script outputs

+ + + + + + + + + + + + + + + + + + +
5

Combined json lines with status, reason, and outputs should be supported

+
+ + + + + + + +

This is a reason

+ +
+2_1_5 +
+

Log output

+ +
+

Script outputs

+ + + + + + + + + + + + + + + + + + +
6

Findings should be supported

+
+ + + + + + + +

This is a reason

+ +
+2_1_6 + + + + + + + + + + + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

I am a criterion

+
No

I am the reason

+

I am a criterion 2

+
No

I am another reason

+

I am a criterion 3

+
No

I am yet another reason

+
+
+

Log output

+ +
7

Can provide handle escape characters in a string

+
+ + + + + + +None +2_1_7 + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

criterion is 

+

\\ ” \\n

+
Yes

reason is 

+

\\ ” \\n

+
+
+

Log output

+ +
8

Can provide handle new line characters in a string

+
+ + + + + + + +

reas
+on

+ +
+2_1_8 + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

crit
+erion

+
Yes

reas
+on

+
+
+

Log output

+ +
+

Script outputs

+ + + + + + + + + + + + + + +
+

3 Parameter Replacement

+

+ + + + + + + + +1 Should replace parameters in autopilots

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
1

Replace environments

+
+ + + + + + + +

This is a reason

+ +
+3_1_1 +
+

Log output

+ +
2

Replace secrets

+
+ + + + + + + +

This is a reason

+ +
+3_1_2 +
+

Log output

+ +
3

Replace variables

+
+ + + + + + + +

This is a reason

+ +
+3_1_3 +
+

Log output

+ +
+

+ + + + + + + + +2 Should replace parameters in manual answers like

+ + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
1

check for var replacement in manual answer

+
+ + + + + + +None
+

+ + + + + + + + +3 Should replace parameters in additional config

+ + + + + + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
1

Replace parameters in additional config

+
+ + + + + + + +

This is a reason

+ +
+3_3_1 +
+

Log output

+ +
+

4 Should hide secrets

+

+ + + + + + + + +1 Hide secrets in logs

+ + + + + + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
1a

Check 1

+
+ + + + + + + +

This is a reason

+ +
+4_1_1a +
+

Log output

+ +
+ + +
+ + + + + +
Check did not pass
+ + + + + +
Check passed with warning
+ + + + + +
Check passed
+ + + + + +
Check not applicable
+ + + + + +
No check configured
+ + + + + +
Autopilot failed with user error
+ + + + + +
Autopilot failed with runtime error
+ + + + + +
Check was skipped
+*
Manual status
+
+ + + +" +`; + +exports[`renderHtmlFiles > should render result %p test/integration/input/v1 3`] = ` +" + + +QG + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +PENDING + + + + +SKIPPED + + + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
+Configuration + +v1 +
+Date + +2023-11-16 18:05 +
+Tool Version + +0.6.1 +
+ + + +

Status

+

+Result of answered: + + + +30 % + + + + +9 % + + + + +57 % + + + + +4 % N/A + + +

+

+

+
+
+
+
+
+
+ +

+

+

+
+
+
+
+ +

+

+Answered: + + + +75 % Automatically + + + + +21 % Manually + + + + +4 % Unanswered + + +

+ +

Summary

+
+

Overall Status: + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDRequirementResult + + +
+ + + + + +
Evidence
+

+1 +

+
+

+Manual Answers have to be supported +

+
+2 + +

+YELLOW answer +

+ +
+ + + + + + + +result +
+3 + +

+RED answer +

+ +
+ + + + + + + +result +
+5 + +

+UNANSWERED answer +

+ +
+ + + + + + + +result +
+

+2 +

+
+

+Base Interface +

+
+1 + +

+Base Interface has to be supported +

+ +
+ + + + + + + +result +
+

+3 +

+
+

+Parameter Replacement +

+
+1 + +

+Should replace parameters in autopilots +

+ +
+ + + + + + + +result +
+3 + +

+Should replace parameters in additional config +

+ +
+ + + + + + + +result +
+

+4 +

+
+

+Should hide secrets +

+
+1 + +

+Hide secrets in logs +

+ +
+ + + + + + + +result +
+ + +
+ +

Details

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDRequirementResult + + +
+ + + + + + + +
Evidence
+

+1 +

+
+

+Manual Answers have to be supported +

+
+1 + +

+GREEN answer +

+ +
+ + + + + + + +result +
+2 + +

+YELLOW answer +

+ +
+ + + + + + + +result +
+3 + +

+RED answer +

+ +
+ + + + + + + +result +
+4 + +

+NA answer +

+ +
+ + + + + + + +result +
+5 + +

+UNANSWERED answer +

+ +
+ + + + + + + +result +
+

+2 +

+
+

+Base Interface +

+
+1 + +

+Base Interface has to be supported +

+

The base interface should be supported to retrieve the status from an autopilot
+The base interface consists of the following properties:

+
    +
  • status
  • +
  • reason
  • +
  • outputs
  • +
+ +
+ + + + + + + +result +
+

+3 +

+
+

+Parameter Replacement +

+
+1 + +

+Should replace parameters in autopilots +

+ +
+ + + + + + + +result +
+2 + +

+Should replace parameters in manual answers like +

+ +
+ + + + + + + +result +
+3 + +

+Should replace parameters in additional config +

+ +
+ + + + + + + +result +
+

+4 +

+
+

+Should hide secrets +

+
+1 + +

+Hide secrets in logs +

+ +
+ + + + + + + +result +
+ + + + + +
+ + + + + +
Check did not pass
+ + + + + +
Check passed with warning
+ + + + + +
Check passed
+ + + + + +
Check not applicable
+ + + + + +
No check configured
+ + + + + +
Autopilot failed with user error
+ + + + + +
Autopilot failed with runtime error
+ + + + + +
Check was skipped
+*
Manual status
+
+ + + +" +`; + +exports[`renderHtmlFiles > should render result %p test/integration/input/v1 4`] = ` +" + + +QG Evidence: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +PENDING + + + + +SKIPPED + + + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
+Configuration + +v1 +
+Date + +2023-11-16 18:05 +
+Tool Version + +0.6.1 +
+ + + +

1 Manual Answers have to be supported

+

+ + + + + + + + +1 GREEN answer

+ + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
1

GREEN answer check

+
+ + + + + + + +

It should be GREEN

+ +
+

+ + + + + + + + +2 YELLOW answer

+ + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
1

YELLOW answer check

+
+ + + + + + + +

It should be YELLOW

+ +
+

+ + + + + + + + +3 RED answer

+ + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
1

RED answer check

+
+ + + + + + + +

It should be RED

+ +
+

+ + + + + + + + +4 NA answer

+ + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
1

NA answer check

+
+ + + + + + + +

It should be NA

+ +
+

+ + + + + + + + +5 UNANSWERED answer

+ + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
1

UNANSWERED answer check

+
+ + + + + + + +

It should be UNANSWERED

+ +
+

2 Base Interface

+

+ + + + + + + + +1 Base Interface has to be supported

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
1a

Status GREEN should be supported

+
+ + + + + + + +

Some reason

+ +
+ + + + + + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

I am a criterion

+
No

I am the justification

+
+
+

Log output

+ +
1b

Status YELLOW should be supported

+
+ + + + + + + +

Some reason

+ +
+ + + + + + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

I am a criterion

+
No

I am the justification

+
+
+

Log output

+ +
1c

Status RED should be supported

+
+ + + + + + + +

Some reason

+ +
+ + + + + + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

I am a criterion

+
No

I am the justification

+
+
+

Log output

+ +
1d

Status NA should be supported

+
+ + + + + + + +

autopilot ‘status-provider’ provided an invalid ‘status’: ‘NA’

+ +
+ + + + + + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

I am a criterion

+
No

I am the justification

+
+
+

Log output

+ +
1e

Status UNANSWERED should be supported

+
+ + + + + + + +

autopilot ‘status-provider’ provided an invalid ‘status’: ‘UNANSWERED’

+ +
+ + + + + + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

I am a criterion

+
No

I am the justification

+
+
+

Log output

+ +
1f

If a status is not supported, it should be set to ERROR

+
+ + + + + + + +

autopilot ‘status-provider’ provided an invalid ‘status’: ‘UNKNOWN’

+ +
+ + + + + + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

I am a criterion

+
No

I am the justification

+
+
+

Log output

+ +
1g

If a status is empty, it should be set to ERROR

+
+ + + + + + + +

autopilot ‘status-provider’ provided an invalid ‘status’: ″

+ +
+ + + + + + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

I am a criterion

+
No

I am the justification

+
+
+

Log output

+ +
3

Reason should be supported

+
+ + + + + + + +

This is a reason

+ +
+ + + + + +
+

Log output

+ +
4

Outputs should be supported

+
+ + + + + + + +

This is a reason

+ +
+ + + + + + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

I am a criterion

+
No

I am the justification

+
+
+

Log output

+ +
+

Script outputs

+ + + + + + + + + + + + + + + + + + +
5

Combined json lines with status, reason, and outputs should be supported

+
+ + + + + + + +

This is a reason

+ +
+ + + + + +
+

Log output

+ +
+

Script outputs

+ + + + + + + + + + + + + + + + + + +
6

Findings should be supported

+
+ + + + + + + +

This is a reason

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

I am a criterion

+
No

I am the reason

+

I am a criterion 2

+
No

I am another reason

+

I am a criterion 3

+
No

I am yet another reason

+
+
+

Log output

+ +
7

Can provide handle escape characters in a string

+
+ + + + + + +None + + + + + + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

criterion is 

+

\\ ” \\n

+
Yes

reason is 

+

\\ ” \\n

+
+
+

Log output

+ +
8

Can provide handle new line characters in a string

+
+ + + + + + + +

reas
+on

+ +
+ + + + + + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

crit
+erion

+
Yes

reas
+on

+
+
+

Log output

+ +
+

Script outputs

+ + + + + + + + + + + + + + +
+

3 Parameter Replacement

+

+ + + + + + + + +1 Should replace parameters in autopilots

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
1

Replace environments

+
+ + + + + + + +

This is a reason

+ +
+ + + + + +
+

Log output

+ +
2

Replace secrets

+
+ + + + + + + +

This is a reason

+ +
+ + + + + +
+

Log output

+ +
3

Replace variables

+
+ + + + + + + +

This is a reason

+ +
+ + + + + +
+

Log output

+ +
+

+ + + + + + + + +2 Should replace parameters in manual answers like

+ + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
1

check for var replacement in manual answer

+
+ + + + + + +None
+

+ + + + + + + + +3 Should replace parameters in additional config

+ + + + + + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
1

Replace parameters in additional config

+
+ + + + + + + +

This is a reason

+ +
+ + + + + +
+

Log output

+ +
+

4 Should hide secrets

+

+ + + + + + + + +1 Hide secrets in logs

+ + + + + + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
1a

Check 1

+
+ + + + + + + +

This is a reason

+ +
+ + + + + +
+

Log output

+ +
+ + + +
+ + + + + +
Check did not pass
+ + + + + +
Check passed with warning
+ + + + + +
Check passed
+ + + + + +
Check not applicable
+ + + + + +
No check configured
+ + + + + +
Autopilot failed with user error
+ + + + + +
Autopilot failed with runtime error
+ + + + + +
Check was skipped
+*
Manual status
+
+ + + +" +`; + +exports[`renderHtmlFiles > should render result %p test/integration/input/v1-with-logs 1`] = ` +" + + +QG XX_DEMO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +PENDING + + + + +SKIPPED + + + + + + +

XX_DEMO

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+Configuration + +v1 +
+Name + +XX_DEMO +
+Version + +1.2.3 +
+Date + +2023-10-09 15:58 +
+Tool Version + +0.4.7 +
+ + + +

Status

+

+Result of answered: + + + +67 % + + + + +33 % + + +

+

+

+
+
+
+
+ +

+

+

+
+
+
+
+ +

+

+Answered: + + + +100 % Automatically + + + + +0 % Manually + + + + +0 % Unanswered + + +

+ + + +

+ + + + +Breakdown of results + +

+ + +" +`; + +exports[`renderHtmlFiles > should render result %p test/integration/input/v1-with-logs 2`] = ` +" + + + +QG XX_DEMO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +PENDING + + + + +SKIPPED + + + + + + +

XX_DEMO

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+Configuration + +v1 +
+Name + +XX_DEMO +
+Version + +1.2.3 +
+Date + +2023-10-09 15:58 +
+Tool Version + +0.4.7 +
+ + + +

Status

+

+Result of answered: + + + +67 % + + + + +33 % + + +

+

+

+
+
+
+
+ +

+

+

+
+
+
+
+ +

+

+Answered: + + + +100 % Automatically + + + + +0 % Manually + + + + +0 % Unanswered + + +

+ +

Summary

+
+

Overall Status: + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
IDRequirementResult +Evidence
+

+2 +

+
+

+title +

+
+2.1 + +

+title +

+ +
+ + + + + + + +result +
+ + +
+ +

Details

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDRequirementResult + + +
+ + +
Evidence
+

+2 +

+
+

+title +

+
+2.1 + +

+title +

+

text

+ +
+ + + + + + + +result +
+2.2 + +

+title +

+

text text

+ +
+ + + + + + + +result +
+ + + +

2 title

+

+ + + + + + + + +2.1 title

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
test-plan

title

+
+ + + + + + + +

File filter Testplan.pdf(1)/* mentioned in the config file sharepoint-test-plan-evaluator.yaml did not match any files!

+ +
+2_2.1_test-plan +
+

Log output

+ +
+

Script outputs

+ + + + + + + + + + + + + + +
+

+ + + + + + + + +2.2 title

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
ram

title

+
+ + + + + + + +

RAM usage is 78% (yellow=80%, red=90%)

+ +
+2_2.2_ram + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

RAM usage is less than 80% for GREEN and less than 90% for YELLOW

+
No

RAM usage is 78%

+
+
+

Log output

+ +
+

Script outputs

+ + + + + + + + + + + + + + + + + + +
rom

title

+
+ + + + + + + +

ROM usage is 78% (yellow=80%, red=90%)

+ +
+2_2.2_rom + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

ROM usage is less than 80% for GREEN and less than 90% for YELLOW

+
No

ROM usage is 78%

+
+
+

Log output

+ +
+

Script outputs

+ + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + +
Check did not pass
+ + + + + +
Check passed with warning
+ + + + + +
Check passed
+ + + + + +
Check not applicable
+ + + + + +
No check configured
+ + + + + +
Autopilot failed with user error
+ + + + + +
Autopilot failed with runtime error
+ + + + + +
Check was skipped
+*
Manual status
+
+ + + +" +`; + +exports[`renderHtmlFiles > should render result %p test/integration/input/v1-with-logs 3`] = ` +" + + +QG XX_DEMO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +PENDING + + + + +SKIPPED + + + + + + +

XX_DEMO

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+Configuration + +v1 +
+Name + +XX_DEMO +
+Version + +1.2.3 +
+Date + +2023-10-09 15:58 +
+Tool Version + +0.4.7 +
+ + + +

Status

+

+Result of answered: + + + +67 % + + + + +33 % + + +

+

+

+
+
+
+
+ +

+

+

+
+
+
+
+ +

+

+Answered: + + + +100 % Automatically + + + + +0 % Manually + + + + +0 % Unanswered + + +

+ +

Summary

+
+

Overall Status: + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
IDRequirementResult +Evidence
+

+2 +

+
+

+title +

+
+2.1 + +

+title +

+ +
+ + + + + + + +result +
+ + +
+ +

Details

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDRequirementResult + + +
+ + +
Evidence
+

+2 +

+
+

+title +

+
+2.1 + +

+title +

+

text

+ +
+ + + + + + + +result +
+2.2 + +

+title +

+

text text

+ +
+ + + + + + + +result +
+ + + + + +
+ + + + + +
Check did not pass
+ + + + + +
Check passed with warning
+ + + + + +
Check passed
+ + + + + +
Check not applicable
+ + + + + +
No check configured
+ + + + + +
Autopilot failed with user error
+ + + + + +
Autopilot failed with runtime error
+ + + + + +
Check was skipped
+*
Manual status
+
+ + + +" +`; + +exports[`renderHtmlFiles > should render result %p test/integration/input/v1-with-logs 4`] = ` +" + + +QG Evidence: XX_DEMO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +PENDING + + + + +SKIPPED + + + + + + +

XX_DEMO

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+Configuration + +v1 +
+Name + +XX_DEMO +
+Version + +1.2.3 +
+Date + +2023-10-09 15:58 +
+Tool Version + +0.4.7 +
+ + + +

2 title

+

+ + + + + + + + +2.1 title

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
test-plan

title

+
+ + + + + + + +

File filter Testplan.pdf(1)/* mentioned in the config file sharepoint-test-plan-evaluator.yaml did not match any files!

+ +
+ + + + + +
+

Log output

+ +
+

Script outputs

+ + + + + + + + + + + + + + +
+

+ + + + + + + + +2.2 title

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDCheckStatusReasonEvidenceResult
ram

title

+
+ + + + + + + +

RAM usage is 78% (yellow=80%, red=90%)

+ +
+ + + + + + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

RAM usage is less than 80% for GREEN and less than 90% for YELLOW

+
No

RAM usage is 78%

+
+
+

Log output

+ +
+

Script outputs

+ + + + + + + + + + + + + + + + + + +
rom

title

+
+ + + + + + + +

ROM usage is 78% (yellow=80%, red=90%)

+ +
+ + + + + + + + + + + + + + + + + + + + + +
CriterionFulfilledJustification

ROM usage is less than 80% for GREEN and less than 90% for YELLOW

+
No

ROM usage is 78%

+
+
+

Log output

+ +
+

Script outputs

+ + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + +
Check did not pass
+ + + + + +
Check passed with warning
+ + + + + +
Check passed
+ + + + + +
Check not applicable
+ + + + + +
No check configured
+ + + + + +
Autopilot failed with user error
+ + + + + +
Autopilot failed with runtime error
+ + + + + +
Check was skipped
+*
Manual status
+
+ + + +" +`; diff --git a/yaku-apps-typescript/apps/html-finalizer/test/integration/input/v1-with-logs/qg-result.yaml b/yaku-apps-typescript/apps/html-finalizer/test/integration/input/v1-with-logs/qg-result.yaml new file mode 100644 index 00000000..ae916eb2 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/test/integration/input/v1-with-logs/qg-result.yaml @@ -0,0 +1,133 @@ +metadata: + version: v1 +header: + name: XX_DEMO + version: 1.2.3 + date: 2023-10-09 15:58 + toolVersion: 0.4.7 +overallStatus: GREEN +statistics: + counted-checks: 3 + counted-automated-checks: 3 + counted-manual-check: 0 + counted-unanswered-checks: 0 + degree-of-automation: 100 + degree-of-completion: 100 +chapters: + '2': + title: title + status: ERROR + requirements: + '2.2': + title: title + text: text + text + status: GREEN + checks: + ram: + title: title + status: GREEN + type: Automation + evaluation: + autopilot: ram-autopilot + status: 'GREEN' + reason: 'RAM usage is 78% (yellow=80%, red=90%)' + results: + - criterion: 'RAM usage is less than 80% for GREEN and less than 90% for YELLOW' + fulfilled: false + justification: 'RAM usage is 78%' + outputs: + 'fetched': '/tmp/onyx-evidence-2023-10-09T15-56-10-2837/2_2.2_ram/ram.csv' + 'ram.csv': '/tmp/onyx-evidence-2023-10-09T15-56-10-2837/2_2.2_ram/ram.csv' + execution: + logs: + - "\e[1mINFO | Output path is: ram.json\e[0m" + - "\e[1mINFO | Fetching Splunk data...\e[0m" + - "\r100.0% | 572 scanned | 6 matched | 1 results" + - "\e[1mINFO | Writing Splunk data to file\e[0m" + - "\e[1mINFO | Output path is: ram.csv\e[0m" + - "\e[1mINFO | Fetching Splunk data...\e[0m" + - "\r100.0% | 572 scanned | 6 matched | 1 results" + - "\e[1mINFO | Writing Splunk data to file\e[0m" + - 'ram.csv is attached' + evidencePath: 2_2.2_ram + exitCode: 0 + rom: + title: title + status: GREEN + type: Automation + evaluation: + autopilot: rom-autopilot + status: 'GREEN' + reason: 'ROM usage is 78% (yellow=80%, red=90%)' + results: + - criterion: 'ROM usage is less than 80% for GREEN and less than 90% for YELLOW' + fulfilled: false + justification: 'ROM usage is 78%' + outputs: + 'fetched': '/tmp/onyx-evidence-2023-10-09T15-56-10-2837/2_2.2_rom/rom.csv' + 'rom.csv': '/tmp/onyx-evidence-2023-10-09T15-56-10-2837/2_2.2_rom/rom.csv' + execution: + logs: + - "\e[1mINFO | Output path is: rom.json\e[0m" + - "\e[1mINFO | Fetching Splunk data...\e[0m" + - "\r100.0% < 572 scanned < 6 matched < 1 results" + - "\e[1mINFO | Writing Splunk data to file\e[0m" + - "\e[1mINFO | Output path is: rom.csv\e[0m" + - "\e[1mINFO | Fetching Splunk data...\e[0m" + - "\r100.0% > 572 scanned > 6 matched > 1 results" + - "\e[1mINFO | Writing Splunk data to file\e[0m" + - 'rom.csv is attached' + evidencePath: 2_2.2_rom + exitCode: 0 + '2.1': + title: title + text: text + status: ERROR + checks: + test-plan: + title: title + status: FAILED + type: Automation + evaluation: + autopilot: sharepoint-test-plan-autopilot + status: 'FAILED' + reason: 'File filter `Testplan.pdf(1)/*` mentioned in the config file `sharepoint-test-plan-evaluator.yaml` did not match any files!' + outputs: + 'fetched': 'https://example.com' + execution: + logs: + - "\e[1mINFO | Configuring SharePoint Fetcher\e[0m" + - "\e[1mINFO | File `__custom_property_definitions__.json` was saved in path `/tmp/onyx-evidence-2023-10-09T15-56-10-2837/2_2.1_test-plan`\e[0m" + - "\e[31m\e[1mERROR | An error has occurred.\e[0m" + - "\e[33m\e[1mTraceback (most recent call last):\e[0m" + - " File \"\e[32m/home/qguser/.pex/unzipped_pexes//grow/autopilot_utils/\e[0m\e[32m\e[1mcli_base.py\e[0m\", line \e[33m383\e[0m, in \e[35merror_handler\e[0m" + - " \e[1mf\e[0m\e[1m(\e[0m\e[35m\e[1m*\e[0m\e[1margs\e[0m\e[1m,\e[0m \e[35m\e[1m**\e[0m\e[1mkwargs\e[0m\e[1m)\e[0m" + - " File \"\e[32m/home/qguser/.pex/unzipped_pexes//grow/autopilot_utils/\e[0m\e[32m\e[1mcli_base.py\e[0m\", line \e[33m362\e[0m, in \e[35mresult_handler_decorator\e[0m" + - " \e[1mf\e[0m\e[1m(\e[0m\e[35m\e[1m*\e[0m\e[1margs\e[0m\e[1m,\e[0m \e[35m\e[1m**\e[0m\e[1mkwargs\e[0m\e[1m)\e[0m" + - " File \"\e[32m/home/qguser/.pex/unzipped_pexes//grow/autopilot_utils/\e[0m\e[32m\e[1mcli_base.py\e[0m\", line \e[33m202\e[0m, in \e[35mmain_cli_entrypoint_wrapper\e[0m" + - " \e[1mclick_command\e[0m\e[1m(\e[0m\e[35m\e[1m*\e[0m\e[1margs\e[0m\e[1m,\e[0m \e[35m\e[1m**\e[0m\e[1mkwargs\e[0m\e[1m)\e[0m" + - " File \"\e[32m/home/qguser/.pex/unzipped_pexes//grow/sharepoint_evaluator/\e[0m\e[32m\e[1mcli.py\e[0m\", line \e[33m90\e[0m, in \e[35mclick_command\e[0m" + - " \e[1msharepoint_evaluator\e[0m\e[1m(\e[0m\e[1msettings\e[0m\e[1m,\e[0m \e[1mparsed_config_file\e[0m\e[1m)\e[0m" + - " File \"\e[32m/home/qguser/.pex/unzipped_pexes//grow/sharepoint_evaluator/\e[0m\e[32m\e[1mcli.py\e[0m\", line \e[33m128\e[0m, in \e[35msharepoint_evaluator\e[0m" + - " \e[35m\e[1mraise\e[0m \e[1mAutopilotConfigurationError\e[0m\e[1m(\e[0m" + - "\e[31m\e[1mgrow.autopilot_utils.errors.AutopilotConfigurationError\e[0m:\e[1m File filter `Testplan.pdf(1)/*` mentioned in the config file `sharepoint-test-plan-evaluator.yaml` did not match any files!\e[0m" + evidencePath: 2_2.1_test-plan + exitCode: 0 +finalize: + execution: + logs: + - 'ℹ️ Using passed assessment Id over the one from environment. To use the environment variable, do not pass the argument "--assessment-id".' + - "\U0001F680 Updating the assessment..." + - "\U0001F680 Updating criteria..." + - '✅ Criteria successfully updated' + - "\U0001F680 Adding approvers..." + - '✅ Approvers successfully added' + - '✅ Assessment updated with id: 2d39364a-5acb-4447-9c19-9f8968b14d7a' + - '✅ Assessment URL: https://example.com/2d39364a-5acb-4447-9c19-9f8968b14d7a' + - 'qg-dashboard.html generated successfully.' + - 'qg-result.html generated successfully.' + - 'qg-evidence.html generated successfully.' + - 'Archive created successfully: /tmp/onyx-evidence-2023-10-09T15-56-10-2837.zip' + evidencePath: . + exitCode: 0 diff --git a/yaku-apps-typescript/apps/html-finalizer/test/integration/input/v1/qg-result.yaml b/yaku-apps-typescript/apps/html-finalizer/test/integration/input/v1/qg-result.yaml new file mode 100644 index 00000000..3c7ec945 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/test/integration/input/v1/qg-result.yaml @@ -0,0 +1,473 @@ +metadata: + version: v1 +header: + name: '' + version: '' + date: 2023-11-16 18:05 + toolVersion: 0.6.1 +overallStatus: ERROR +statistics: + counted-checks: 24 + counted-automated-checks: 18 + counted-manual-check: 5 + counted-unanswered-checks: 1 + counted-skipped-checks: 0 + degree-of-automation: 75 + degree-of-completion: 95.83 +chapters: + '1': + title: Manual Answers have to be supported + status: RED + requirements: + '1': + title: GREEN answer + status: GREEN + checks: + '1': + title: GREEN answer check + status: GREEN + type: Manual + evaluation: + status: GREEN + reason: It should be GREEN + '2': + title: YELLOW answer + status: YELLOW + checks: + '1': + title: YELLOW answer check + status: YELLOW + type: Manual + evaluation: + status: YELLOW + reason: It should be YELLOW + '3': + title: RED answer + status: RED + checks: + '1': + title: RED answer check + status: RED + type: Manual + evaluation: + status: RED + reason: It should be RED + '4': + title: NA answer + status: NA + checks: + '1': + title: NA answer check + status: NA + type: Manual + evaluation: + status: NA + reason: It should be NA + '5': + title: UNANSWERED answer + status: UNANSWERED + checks: + '1': + title: UNANSWERED answer check + status: UNANSWERED + type: Manual + evaluation: + status: UNANSWERED + reason: It should be UNANSWERED + '2': + title: Base Interface + status: ERROR + requirements: + '1': + title: Base Interface has to be supported + text: | + The base interface should be supported to retrieve the status from an autopilot + The base interface consists of the following properties: + - status + - reason + - outputs + status: ERROR + checks: + 1a: + title: Status GREEN should be supported + status: GREEN + type: Automation + evaluation: + autopilot: status-provider + status: GREEN + reason: Some reason + results: + - criterion: I am a criterion + fulfilled: false + justification: I am the justification + execution: + logs: + - '{"status": "GREEN"}' + - '{"reason": "Some reason"}' + - '{"result": {"criterion": "I am a criterion", "fulfilled": false, "justification": "I am the justification"}}' + evidencePath: 2_1_1a + exitCode: 0 + 1b: + title: Status YELLOW should be supported + status: YELLOW + type: Automation + evaluation: + autopilot: status-provider + status: YELLOW + reason: Some reason + results: + - criterion: I am a criterion + fulfilled: false + justification: I am the justification + execution: + logs: + - '{"status": "YELLOW"}' + - '{"reason": "Some reason"}' + - '{"result": {"criterion": "I am a criterion", "fulfilled": false, "justification": "I am the justification"}}' + evidencePath: 2_1_1b + exitCode: 0 + 1c: + title: Status RED should be supported + status: RED + type: Automation + evaluation: + autopilot: status-provider + status: RED + reason: Some reason + results: + - criterion: I am a criterion + fulfilled: false + justification: I am the justification + execution: + logs: + - '{"status": "RED"}' + - '{"reason": "Some reason"}' + - '{"result": {"criterion": "I am a criterion", "fulfilled": false, "justification": "I am the justification"}}' + evidencePath: 2_1_1c + exitCode: 0 + 1d: + title: Status NA should be supported + status: ERROR + type: Automation + evaluation: + autopilot: status-provider + status: ERROR + reason: "autopilot 'status-provider' provided an invalid 'status': 'NA'" + results: + - criterion: I am a criterion + fulfilled: false + justification: I am the justification + execution: + logs: + - '{"status": "NA"}' + - '{"reason": "Some reason"}' + - '{"result": {"criterion": "I am a criterion", "fulfilled": false, "justification": "I am the justification"}}' + evidencePath: 2_1_1d + exitCode: 0 + 1e: + title: Status UNANSWERED should be supported + status: ERROR + type: Automation + evaluation: + autopilot: status-provider + status: ERROR + reason: "autopilot 'status-provider' provided an invalid 'status': 'UNANSWERED'" + results: + - criterion: I am a criterion + fulfilled: false + justification: I am the justification + execution: + logs: + - '{"status": "UNANSWERED"}' + - '{"reason": "Some reason"}' + - '{"result": {"criterion": "I am a criterion", "fulfilled": false, "justification": "I am the justification"}}' + evidencePath: 2_1_1e + exitCode: 0 + 1f: + title: If a status is not supported, it should be set to ERROR + status: ERROR + type: Automation + evaluation: + autopilot: status-provider + status: ERROR + reason: "autopilot 'status-provider' provided an invalid 'status': 'UNKNOWN'" + results: + - criterion: I am a criterion + fulfilled: false + justification: I am the justification + execution: + logs: + - '{"status": "UNKNOWN"}' + - '{"reason": "Some reason"}' + - '{"result": {"criterion": "I am a criterion", "fulfilled": false, "justification": "I am the justification"}}' + evidencePath: 2_1_1f + exitCode: 0 + 1g: + title: If a status is empty, it should be set to ERROR + status: ERROR + type: Automation + evaluation: + autopilot: status-provider + status: ERROR + reason: "autopilot 'status-provider' provided an invalid 'status': ''" + results: + - criterion: I am a criterion + fulfilled: false + justification: I am the justification + execution: + logs: + - '{"status": ""}' + - '{"reason": "Some reason"}' + - '{"result": {"criterion": "I am a criterion", "fulfilled": false, "justification": "I am the justification"}}' + evidencePath: 2_1_1g + exitCode: 0 + '3': + title: Reason should be supported + status: FAILED + type: Automation + evaluation: + autopilot: reason-provider + status: FAILED + reason: This is a reason + execution: + logs: + - '{"reason": "This is a reason"}' + - '{"status": "FAILED"}' + evidencePath: '2_1_3' + exitCode: 0 + '4': + title: Outputs should be supported + status: GREEN + type: Automation + evaluation: + autopilot: outputs-provider + status: GREEN + reason: This is a reason + results: + - criterion: I am a criterion + fulfilled: false + justification: I am the justification + outputs: + output1: output1_value + output2: output2_value + execution: + logs: + - '{"output": {"output1": "output1_value"}}' + - '{"output": {"output2": "output2_value"}}' + - '{"status": "GREEN", "reason": "This is a reason"}' + - '{"result": {"criterion": "I am a criterion", "fulfilled": false, "justification": "I am the justification"}}' + evidencePath: '2_1_4' + exitCode: 0 + '5': + title: Combined json lines with status, reason, and outputs should be supported + status: GREEN + type: Automation + evaluation: + autopilot: combined-json-lines + status: GREEN + reason: This is a reason + outputs: + output1: output1_value + output2: output2_value + execution: + logs: + - '{"status": "GREEN", "reason": "This is a reason", "output": {"output1": "output1_value", "output2": "output2_value"}}' + evidencePath: '2_1_5' + exitCode: 0 + '6': + title: Findings should be supported + status: GREEN + type: Automation + evaluation: + autopilot: findings-interface + status: GREEN + reason: This is a reason + results: + - criterion: I am a criterion + fulfilled: false + justification: I am the reason + - criterion: I am a criterion 2 + fulfilled: false + justification: I am another reason + - criterion: I am a criterion 3 + fulfilled: false + justification: I am yet another reason + metadata: + customer: I am customer in metadata + package: I am a package + severity: I am a severity + execution: + logs: + - '{"result": {"criterion": "I am a criterion", "fulfilled": false, "justification": "I am the reason"}}' + - '{"result": {"criterion": "I am a criterion 2", "fulfilled": false, "justification": "I am another reason"}}' + - '{"result": {"criterion": "I am a criterion 3", "fulfilled": false, "justification": "I am yet another reason", "metadata": {"customer": "I am customer in metadata", "package": "I am a package", "severity": "I am a severity"}}}' + - '{"status": "GREEN", "reason": "This is a reason"}' + evidencePath: '2_1_6' + exitCode: 0 + '7': + title: Can provide handle escape characters in a string + status: RED + type: Automation + evaluation: + autopilot: escape-characters-autopilot + status: RED + reason: '' + results: + - criterion: "criterion is \b \f \n \r \t \n \\ \" \\n" + fulfilled: true + justification: "reason is \b \f \n \r \t \n \\ \" \\n" + execution: + logs: + - '{"result": {"criterion": "criterion is \b \f \n \r \t \u000A \\ \" \\n", "fulfilled": true, "justification": "reason is \b \f \n \r \t \u000A \\ \" \\n"}}' + - '{"status": "RED"}' + evidencePath: '2_1_7' + exitCode: 0 + '8': + title: Can provide handle new line characters in a string + status: GREEN + type: Automation + evaluation: + autopilot: new-line-autopilot + status: GREEN + reason: |- + reas + on + results: + - criterion: |- + crit + erion + fulfilled: true + justification: |- + reas + on + metadata: + "cust\tomer": |- + cust + omer metadata + outputs: + "outputkeywith\tinit": |- + Output value with + in it + execution: + logs: + - '{"status": "GREEN"}' + - '{"reason": "reas\non"}' + - '{"result": {"criterion": "crit\nerion", "fulfilled": true, "justification": "reas\non", "metadata": {"cust\tomer": "cust\nomer metadata"}}}' + - '{"output": {"outputkeywith\tinit": "Output value with\nin it"}}' + evidencePath: '2_1_8' + exitCode: 0 + '3': + title: Parameter Replacement + status: FAILED + requirements: + '1': + title: Should replace parameters in autopilots + status: FAILED + checks: + '1': + title: Replace environments + status: FAILED + type: Automation + evaluation: + autopilot: env-provider + status: FAILED + reason: This is a reason + execution: + logs: + - global-env-1 + - global-env-1 + - autopilot-ref-env-2 + - autopilot-ref-env-2 + - autopilot-env-3 + - autopilot-env-3 + - '{"status": "FAILED", "reason": "This is a reason"}' + evidencePath: '3_1_1' + exitCode: 0 + '2': + title: Replace secrets + status: FAILED + type: Automation + evaluation: + autopilot: secrets-provider + status: FAILED + reason: This is a reason + execution: + logs: + - autopilot-ref-secret-1 + - '{"status": "FAILED", "reason": "This is a reason"}' + evidencePath: '3_1_2' + exitCode: 0 + '3': + title: Replace variables + status: FAILED + type: Automation + evaluation: + autopilot: vars-provider + status: FAILED + reason: This is a reason + execution: + logs: + - autopilot-ref-var-1 + - some value that will be overridden + - some value + - '{"status": "FAILED", "reason": "This is a reason"}' + evidencePath: '3_1_3' + exitCode: 0 + '2': + title: 'Should replace parameters in manual answers like ' + status: GREEN + checks: + '1': + title: check for var replacement in manual answer + status: GREEN + type: Manual + evaluation: + status: GREEN + reason: '' + '3': + title: Should replace parameters in additional config + status: FAILED + checks: + '1': + title: Replace parameters in additional config + status: FAILED + type: Automation + evaluation: + autopilot: additional-config-provider + status: FAILED + reason: This is a reason + execution: + logs: + - '{"status": "FAILED", "reason": "This is a reason"}' + - This autopilot has an additional config + evidencePath: '3_3_1' + exitCode: 0 + '4': + title: Should hide secrets + status: FAILED + requirements: + '1': + title: Hide secrets in logs + status: FAILED + checks: + 1a: + title: Check 1 + status: FAILED + type: Automation + evaluation: + autopilot: secrets-provider + status: FAILED + reason: This is a reason + execution: + logs: + - '{"status": "FAILED", "reason": "This is a reason"}' + evidencePath: 4_1_1a + exitCode: 0 +finalize: + execution: + logs: + - global-env-1 + - global-env-1 + - qg-result.yaml exists + evidencePath: . + exitCode: 0 diff --git a/yaku-apps-typescript/apps/html-finalizer/test/integration/render-html-files.int-spec.ts b/yaku-apps-typescript/apps/html-finalizer/test/integration/render-html-files.int-spec.ts new file mode 100644 index 00000000..e91157d8 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/test/integration/render-html-files.int-spec.ts @@ -0,0 +1,36 @@ +import { readFile } from 'fs/promises' +import path from 'path' +import { describe, expect, it } from 'vitest' +import { renderHtmlFiles } from '../../src/index' + +const resultPaths = [ + 'test/integration/input/v0', + 'test/integration/input/v1', + 'test/integration/input/v1-with-logs', +] + +describe('renderHtmlFiles', () => { + it.each(resultPaths)( + 'should render result %p', + async (resultPath: string) => { + process.env['result_path'] = resultPath + const files = [ + 'qg-dashboard.html', + 'qg-full-report.html', + 'qg-result.html', + 'qg-evidence.html', + ] + for (const file of files) { + await renderHtmlFiles([file]) + const html = await readFile(path.join(resultPath, file), { + encoding: 'utf8', + }) + const strippedHtml: string[] = [] + html.split('\n').forEach((line) => { + strippedHtml.push(line.trim()) + }) + expect(strippedHtml.join('\n')).toMatchSnapshot() + } + } + ) +}) diff --git a/yaku-apps-typescript/apps/html-finalizer/tsconfig.json b/yaku-apps-typescript/apps/html-finalizer/tsconfig.json new file mode 100644 index 00000000..b46b52ce --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/tsconfig.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Default", + "compilerOptions": { + "target": "es2021", + "module": "es2022", + "moduleResolution": "node", + "resolveJsonModule": true, + "sourceMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "types": ["vitest/importMeta"], + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/yaku-apps-typescript/apps/html-finalizer/tsup.config.json b/yaku-apps-typescript/apps/html-finalizer/tsup.config.json new file mode 100644 index 00000000..f35b9ed3 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/tsup.config.json @@ -0,0 +1,6 @@ +{ + "entry": ["src/index.ts"], + "splitting": false, + "sourcemap": true, + "clean": true +} diff --git a/yaku-apps-typescript/apps/html-finalizer/tsup.config.ts b/yaku-apps-typescript/apps/html-finalizer/tsup.config.ts new file mode 100644 index 00000000..a1f42d9c --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/**/*.ts'], + splitting: false, + clean: true, + target: 'node18', + format: ['esm'], + bundle: false, + sourcemap: true, +}) diff --git a/yaku-apps-typescript/apps/html-finalizer/update-results.sh b/yaku-apps-typescript/apps/html-finalizer/update-results.sh new file mode 100755 index 00000000..48d86cb5 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/update-results.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +# This script updates the qg-result.yaml files for th html-finalizer app + +HTML_FINALIZER_PATH=$(dirname $0) +RESULT_FILE_NAME="qg-result.yaml" +CONFIG_FILE_PATH="${HTML_FINALIZER_PATH}/sample/qg-config.yaml" +FIND_ALL_RESULT_FILES=$(find ${HTML_FINALIZER_PATH} -name ${RESULT_FILE_NAME}) +IGNORED_PATHS="test/integration/input/v1-with-logs" +RELEVANT_PATHS=$(echo ${FIND_ALL_RESULT_FILES} | tr " " "\n" | grep -v ${IGNORED_PATHS}) + +# Test if onyx is installed + +which onyx >/dev/null 2>&1 +if [ $? -ne 0 ]; then + echo "Onyx is not installed. Please install it first." + echo "https://github.com/B-S-F/onyx" + exit 1 +fi + +# Test if yq is installed + +which yq >/dev/null 2>&1 +if [ $? -ne 0 ]; then + echo "yq is not installed. Please install it first." + echo "brew install yq" + exit 1 +fi + +# Test if CONFIG_FILE_PATH exists + +if [ ! -f "${CONFIG_FILE_PATH}" ]; then + echo "The config file does not exist: ${CONFIG_FILE_PATH}" + exit 1 +fi + +# Create a tmp directory to store the new result file + +TMP_DIR=$(mktemp -d -t html-finalizer-XXXXXXXXXX) +echo "Created tmp directory: ${TMP_DIR}" +cp ${CONFIG_FILE_PATH} ${TMP_DIR} +cd ${TMP_DIR} +touch .secrets +touch .vars +onyx exec . +cd - +NEW_RESULT_FILE=$(find ${TMP_DIR} -name ${RESULT_FILE_NAME}) + +ONYX_VERSION=$(onyx --version) + +echo "Updating result files..." +echo "" +for result_file in ${RELEVANT_PATHS}; do + # compare onyx version with header.toolversion + result_file_tool_version=$(yq -r '.header.toolVersion' ${result_file}) + echo "Checking ${result_file}" + echo "Onyx version: ${ONYX_VERSION}" + echo "Result file version: ${result_file_tool_version}" + if [ "${result_file_tool_version}" == "null" ]; then + # yq returns null if the key is not found + echo "The result file is missing the header.toolVersion, skipping as it seems to be a legacy qg result file..." + elif [ "${result_file_tool_version}" != "${ONYX_VERSION}" ]; then + echo "The result file is outdated with your local onyx version, updating..." + cp ${NEW_RESULT_FILE} ${result_file} + else + echo "The result file is up to date with your local onyx version, skipping..." + continue + fi + echo "" +done diff --git a/yaku-apps-typescript/apps/html-finalizer/vitest-integration.config.ts b/yaku-apps-typescript/apps/html-finalizer/vitest-integration.config.ts new file mode 100644 index 00000000..8bec0725 --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/vitest-integration.config.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['test/integration/**/*.int-spec.ts'], + pool: 'threads', + poolOptions: { + threads: { + maxThreads: 1, + minThreads: 1, + }, + }, + typecheck: { + tsconfig: 'tsconfig.json', + }, + reporters: ['junit', 'default'], + outputFile: 'reports/integration-test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/html-finalizer/vitest.config.ts b/yaku-apps-typescript/apps/html-finalizer/vitest.config.ts new file mode 100644 index 00000000..1cf5e21f --- /dev/null +++ b/yaku-apps-typescript/apps/html-finalizer/vitest.config.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + typecheck: { + tsconfig: 'tsconfig.json', + }, + coverage: { + provider: 'v8', + all: true, + enabled: true, + reporter: ['cobertura', 'json-summary', 'text-summary'], + include: ['src'], + }, + reporters: ['junit', 'default'], + outputFile: 'reports/test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/jira-evaluator/.env.sample b/yaku-apps-typescript/apps/jira-evaluator/.env.sample new file mode 100644 index 00000000..54a4ebe9 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/.env.sample @@ -0,0 +1 @@ +JIRA_CONFIG_FILE_PATH="../sample/config.yaml" diff --git a/yaku-apps-typescript/apps/jira-evaluator/.eslintrc.cjs b/yaku-apps-typescript/apps/jira-evaluator/.eslintrc.cjs new file mode 100644 index 00000000..ab87ddba --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/.eslintrc.cjs @@ -0,0 +1,5 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +module.exports = require('@B-S-F/eslint-config/eslint-preset') diff --git a/yaku-apps-typescript/apps/jira-evaluator/.prettierrc b/yaku-apps-typescript/apps/jira-evaluator/.prettierrc new file mode 100644 index 00000000..b2095be8 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": false, + "singleQuote": true +} diff --git a/yaku-apps-typescript/apps/jira-evaluator/README.md b/yaku-apps-typescript/apps/jira-evaluator/README.md new file mode 100644 index 00000000..add4c1ff --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/README.md @@ -0,0 +1,3 @@ +# jira-evaluator + +This evaluator checks the response returned by the Jira fetcher, according to some rules defined in a custom yaml file. diff --git a/yaku-apps-typescript/apps/jira-evaluator/mnt/data.json b/yaku-apps-typescript/apps/jira-evaluator/mnt/data.json new file mode 100644 index 00000000..f53e42c4 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/mnt/data.json @@ -0,0 +1,83 @@ +[ + { + "url": "https://tracker.example.com/tracker/browseXXXXX297", + "summary": "Release v1.5.3", + "issuetype": { + "self": "https://tracker.example.com/tracker/rest/api/2/issuetype/2", + "id": "2", + "description": "A new feature of the product, which has yet to be developed.", + "iconUrl": "https://tracker.example.com/tracker/secure/viewavatar?size=xsmall&avatarId=24711&avatarType=issuetype", + "name": "New Feature", + "subtask": false, + "avatarId": 24711 + }, + "status": { + "self": "https://tracker.example.com/tracker/rest/api/2/status/6", + "description": "The issue is considered finished, the resolution is correct. Issues which are closed can be reopened.", + "iconUrl": "https://tracker.example.com/tracker/images/icons/statuses/closed.png", + "name": "Closed", + "id": "6", + "statusCategory": { + "self": "https://tracker.example.com/tracker/rest/api/2/statuscategory/3", + "id": 3, + "key": "done", + "colorName": "green", + "name": "Done" + } + } + }, + { + "url": "https://tracker.example.com/tracker/browseXXXXX295", + "summary": "Implement index files with search and filter functionality for IDs and DOCs", + "issuetype": { + "self": "https://tracker.example.com/tracker/rest/api/2/issuetype/2", + "id": "2", + "description": "A new feature of the product, which has yet to be developed.", + "iconUrl": "https://tracker.example.com/tracker/secure/viewavatar?size=xsmall&avatarId=24711&avatarType=issuetype", + "name": "New Feature", + "subtask": false, + "avatarId": 24711 + }, + "status": { + "self": "https://tracker.example.com/tracker/rest/api/2/status/6", + "description": "The issue is considered finished, the resolution is correct. Issues which are closed can be reopened.", + "iconUrl": "https://tracker.example.com/tracker/images/icons/statuses/closed.png", + "name": "Closed", + "id": "6", + "statusCategory": { + "self": "https://tracker.example.com/tracker/rest/api/2/statuscategory/3", + "id": 3, + "key": "done", + "colorName": "green", + "name": "Done" + } + } + }, + { + "url": "https://tracker.example.com/tracker/browseXXXXX294", + "summary": "Release v1.5.2", + "issuetype": { + "self": "https://tracker.example.com/tracker/rest/api/2/issuetype/2", + "id": "2", + "description": "A new feature of the product, which has yet to be developed.", + "iconUrl": "https://tracker.example.com/tracker/secure/viewavatar?size=xsmall&avatarId=24711&avatarType=issuetype", + "name": "New Feature", + "subtask": false, + "avatarId": 24711 + }, + "status": { + "self": "https://tracker.example.com/tracker/rest/api/2/status/6", + "description": "The issue is considered finished, the resolution is correct. Issues which are closed can be reopened.", + "iconUrl": "https://tracker.example.com/tracker/images/icons/statuses/closed.png", + "name": "Closed", + "id": "6", + "statusCategory": { + "self": "https://tracker.example.com/tracker/rest/api/2/statuscategory/3", + "id": 3, + "key": "done", + "colorName": "green", + "name": "Done" + } + } + } +] diff --git a/yaku-apps-typescript/apps/jira-evaluator/mnt/qg-answers.schema.json b/yaku-apps-typescript/apps/jira-evaluator/mnt/qg-answers.schema.json new file mode 100644 index 00000000..6fc972fe --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/mnt/qg-answers.schema.json @@ -0,0 +1,189 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QG Config", + "description": "Configuration of a QG with fixed requirements structure. Auto-generated, do not change!", + "$id": "qg-config", + "allOf": [ + { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json" + }, + { + "type": "object", + "required": ["allocations"], + "properties": { + "allocations": { + "type": "object", + "required": ["1", "2", "3"], + "properties": { + "1": { + "type": "object", + "additionalProperties": false, + "required": ["title", "requirements"], + "properties": { + "title": { + "const": "Environment should work" + }, + "requirements": { + "type": "object", + "additionalProperties": false, + "required": ["1.1", "1.2"], + "properties": { + "1.1": { + "type": "object", + "required": ["title", "text"], + "properties": { + "title": { + "const": "dummy-evaluator should return SUCCESS if env is provided" + }, + "text": { + "const": "No time to die" + }, + "report": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/ReportOverride" + }, + "checks": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/Checks" + } + }, + "additionalProperties": false + }, + "1.2": { + "type": "object", + "required": ["title", "text"], + "properties": { + "title": { + "const": "dummy-evaluator should return FAILED if env is missing" + }, + "text": { + "const": "No time to die" + }, + "report": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/ReportOverride" + }, + "checks": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/Checks" + } + }, + "additionalProperties": false + } + } + } + } + }, + "2": { + "type": "object", + "additionalProperties": false, + "required": ["title", "requirements"], + "properties": { + "title": { + "const": "Manual comments should work" + }, + "requirements": { + "type": "object", + "additionalProperties": false, + "required": ["2.1", "2.2"], + "properties": { + "2.1": { + "type": "object", + "required": ["title", "text"], + "properties": { + "title": { + "const": "Manual comment" + }, + "text": { + "const": "Some comment that should be available in the reports" + }, + "report": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/ReportOverride" + }, + "checks": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/Checks" + } + }, + "additionalProperties": false + }, + "2.2": { + "type": "object", + "required": ["title", "text"], + "properties": { + "title": { + "const": "Manual RED comment" + }, + "text": { + "const": "Some comment that should be available in the reports" + }, + "report": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/ReportOverride" + }, + "checks": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/Checks" + } + }, + "additionalProperties": false + } + } + } + } + }, + "3": { + "type": "object", + "additionalProperties": false, + "required": ["title", "requirements"], + "properties": { + "title": { + "const": "Components should be selectable on checks" + }, + "requirements": { + "type": "object", + "additionalProperties": false, + "required": ["3.1", "3.2"], + "properties": { + "3.1": { + "type": "object", + "required": ["title", "text"], + "properties": { + "title": { + "const": "Check only needed in app1" + }, + "text": { + "const": "No time to die" + }, + "report": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/ReportOverride" + }, + "checks": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/Checks" + } + }, + "additionalProperties": false + }, + "3.2": { + "type": "object", + "required": ["title", "text"], + "properties": { + "title": { + "const": "Check needed in all components" + }, + "text": { + "const": "No time to die" + }, + "report": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/ReportOverride" + }, + "checks": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/Checks" + } + }, + "additionalProperties": false + } + } + } + } + } + }, + "additionalProperties": false + } + } + } + ] +} diff --git a/yaku-apps-typescript/apps/jira-evaluator/mnt/qg-config.yaml b/yaku-apps-typescript/apps/jira-evaluator/mnt/qg-config.yaml new file mode 100644 index 00000000..a5c0fd20 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/mnt/qg-config.yaml @@ -0,0 +1,60 @@ +header: + name: name + version: 1.2.3 +globals: + varName: https://some.url +components: + jira: + jiraConfigFilePath: '../sample/config.yaml' + jiraUrl: 'https://tracker.example.com/tracker' +autopilots: + jira-autopilot: + run: | + jira-fetcher + jira-evaluator + env: + JIRA_URL: ${component.jiraUrl} + JIRA_CONFIG_FILE_PATH: ${component.jiraConfigFilePath} +reports: + jira: jira-autopilot +finalize: + run: html-finalizer +allocations: + '1': + title: Environment should work + requirements: + '1.1': + title: dummy-evaluator should return SUCCESS if env is provided + text: No time to die + checks: + '1.1': + title: Evaluate legal/official requirements/restrictions + components: + - jira + reports: + - jira + + '1.2': + title: dummy-evaluator should return FAILED if env is missing + text: No time to die + + '2': + title: Manual comments should work + requirements: + '2.1': + title: Manual comment + text: >- + Some comment that should be available in the reports + '2.2': + title: Manual RED comment + text: >- + Some comment that should be available in the reports + '3': + title: Components should be selectable on checks + requirements: + '3.1': + title: Check only needed in app1 + text: No time to die + '3.2': + title: Check needed in all components + text: No time to die diff --git a/yaku-apps-typescript/apps/jira-evaluator/mnt/qg-result.yaml b/yaku-apps-typescript/apps/jira-evaluator/mnt/qg-result.yaml new file mode 100644 index 00000000..3a9f9f25 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/mnt/qg-result.yaml @@ -0,0 +1,400 @@ +allocations: + '1': + title: Project management + requirements: + '1.2': + title: >- + 1.2 title + text: >- + 1.2 text + id: '1.2' + checks: {} + overallStatus: PENDING + '1.3': + title: >- + 1.3 title + text: >- + 1.3 text + id: '1.3' + checks: {} + overallStatus: PENDING + '1.4': + title: 1.4 title + text: 1.4 text + id: '1.4' + checks: {} + overallStatus: PENDING + '1.6': + title: 1.6 title + text: >- + 1.6 text + id: '1.6' + checks: {} + overallStatus: PENDING + '1.7': + title: 1.7 title + text: >- + 1.7 text + id: '1.7' + checks: {} + overallStatus: PENDING + '1.15': + title: >- + 1.15 title + text: >- + 1.15 text + id: '1.15' + checks: {} + overallStatus: PENDING + '1.20': + title: >- + 1.20 title + text: >- + 1.20 text + id: '1.20' + checks: {} + overallStatus: PENDING + '1.21': + title: >- + 1.21 title + text: Sprint Planning + id: '1.21' + checks: {} + overallStatus: PENDING + '1.22': + title: >- + 1.22 title + text: >- + 1.22 text + id: '1.22' + checks: {} + overallStatus: PENDING + id: '1' + '2': + title: Security + requirements: + '2.4': + title: |- + 2.4 title + text: |- + 2.4 text + id: '2.4' + checks: {} + overallStatus: PENDING + '2.5': + title: |- + 2.5 title + text: |- + 2.5 text + id: '2.5' + checks: {} + overallStatus: PENDING + '2.6': + title: 2.6 title + text: >- + 2.6 text + id: '2.6' + checks: {} + overallStatus: PENDING + id: '2' + '3': + title: 3 title + requirements: + '3.4': + title: >- + 3.4 title + text: >- + 3.4 text + checks: + '1.1': + title: 3.1.1 title + components: + - AzureDevops + reports: + - reportType: ado-work-items + componentResults: + - component: + version: 1.2.3 + adoApiOrg: abcdef-bci + adoApiProject: Nx_Base + adoConfigFilePath: /workspaces/qg-cli/mnt/ado-work-items-config.yaml + adoCpplyProxySettings: false + id: AzureDevops + evidencePath: ado-work-items-autopilot/AzureDevops + status: RED + comments: + - "**Results\n" + sources: [] + id: '1.1' + checks: {} + id: '3.4' + overallStatus: RED + '3.5': + title: >- + 3.5 title + text: >- + 3.5 text + id: '3.5' + checks: {} + overallStatus: PENDING + '3.7': + title: 3.7 title + text: >- + 3.7 text + id: '3.7' + checks: {} + overallStatus: PENDING + '3.8': + title: >- + 3.8 title + text: >- + 3.8 text + id: '3.8' + checks: {} + overallStatus: PENDING + '3.9': + title: >- + 3.9 title + text: >- + 3.9 text + id: '3.9' + checks: {} + overallStatus: PENDING + '3.11': + title: >- + 3.11 title + text: >- + 3.11 text + id: '3.11' + checks: {} + overallStatus: PENDING + id: '3' + '4': + title: Product development + requirements: + '4.4': + title: >- + 4.4 title + text: >- + 4.4 text + id: '4.4' + checks: {} + overallStatus: PENDING + '4.6': + title: |- + 4.6 title + text: 4.6 text + id: '4.6' + checks: {} + overallStatus: PENDING + '4.7': + title: >- + 4.7 title + text: >- + 4.7 text + id: '4.7' + checks: {} + overallStatus: PENDING + id: '4' + '5': + title: Verification / validation + requirements: + '5.1': + title: 5.1 title + text: |- + 5.1 text + id: '5.1' + checks: {} + overallStatus: PENDING + '5.2': + title: >- + 5.2 title + text: >- + 5.2 text + id: '5.2' + checks: {} + overallStatus: PENDING + '5.3': + title: 5.3 title + text: 5.3 text + id: '5.3' + checks: {} + overallStatus: PENDING + '5.4': + title: 5.4 title + text: >- + 5.4 text + id: '5.4' + checks: {} + overallStatus: PENDING + '5.5': + title: >- + 5.5 title + text: |- + 5.5 text + id: '5.5' + checks: {} + overallStatus: PENDING + '5.6': + title: >- + 5.6 title + text: |- + 5.6 text + id: '5.6' + checks: {} + overallStatus: PENDING + '5.7': + title: 5.7 title + text: 5.7 text + id: '5.7' + checks: {} + overallStatus: PENDING + '5.8': + title: 5.8 title + text: >- + 5.8 text + id: '5.8' + checks: {} + overallStatus: PENDING + '5.9': + title: 5.9 title + text: >- + 5.9 text + id: '5.9' + checks: {} + overallStatus: PENDING + id: '5' + '10': + title: Procurement/ external procurement + requirements: + '10.2': + title: 10.2 title + text: >- + 10.2 text + id: '10.2' + checks: {} + overallStatus: PENDING + '10.3': + title: >- + 10.3 title + text: '' + id: '10.3' + checks: {} + overallStatus: PENDING + '10.4': + title: >- + 10.4 title + text: '' + id: '10.4' + checks: {} + overallStatus: PENDING + id: '10' + '12': + title: Change management in the project + requirements: + '12.2': + title: >- + 12.2 title + text: '' + id: '12.2' + checks: {} + overallStatus: PENDING + id: '12' + '13': + title: Risk management + requirements: + '13.1': + title: 13.1 title + text: >- + 13.1 text + id: '13.1' + checks: {} + overallStatus: PENDING + '13.2': + title: >- + 13.2 title + text: >- + 13.2 text + id: '13.2' + checks: {} + overallStatus: PENDING + '13.4': + title: |- + 13.4 title + text: >- + 13.4 text + id: '13.4' + checks: {} + overallStatus: PENDING + '13.5': + title: |- + 13.5 title + text: >- + 13.5 text + id: '13.5' + checks: {} + overallStatus: PENDING + '13.6': + title: |- + 13.6 title + text: >- + 13.6 text + id: '13.6' + checks: {} + overallStatus: PENDING + '13.8': + title: 13.8 title + text: '13.8 text' + id: '13.8' + checks: {} + overallStatus: PENDING + '13.9': + title: 13.9 title + text: |- + 13.9 text + id: '13.9' + checks: {} + overallStatus: PENDING + id: '13' + '15': + title: Configuration management + requirements: + '15.0': + title: 15.0 title + text: >- + 15.0 text + id: '15.0' + checks: {} + overallStatus: PENDING + '15.1': + title: >- + 15.1 title + text: >- + 15.1 text + id: '15.1' + checks: {} + overallStatus: PENDING + '15.2': + title: >- + 15.2 title + text: Release Note + id: '15.2' + checks: {} + overallStatus: PENDING + id: '15' + '17': + title: Training and Enabling + requirements: + '17.2': + title: >- + 17.2 title + text: '' + id: '17.2' + checks: {} + overallStatus: PENDING + id: '17' +headers: + name: + version: 1.2.3 + Date: 2022-05-13 15:23:07 CEST + QG CLI Version: 0.1.8:dfa3dbd7cdaa3291621c80ddd96cc00034e30509 diff --git a/yaku-apps-typescript/apps/jira-evaluator/package.json b/yaku-apps-typescript/apps/jira-evaluator/package.json new file mode 100644 index 00000000..0dc4931d --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/package.json @@ -0,0 +1,48 @@ +{ + "name": "@B-S-F/jira-evaluator", + "version": "0.7.0", + "description": "", + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsup", + "dev": "nodemon --watch \"src/**\" --exec npm run start", + "start": "node dist/index.js", + "lint": "eslint '**/*.ts'", + "format": "prettier --write '**/*.{ts,md}'", + "test": "vitest run && npm run test:update-cobertura-file", + "test:update-cobertura-file": "sed -i'.bak' 's,package name=\",package name=\"'${npm_package_name}/',g' coverage/cobertura-coverage.xml", + "test:dev": "vitest -w", + "test:coverage": "vitest --coverage", + "test:ui": "vitest --ui", + "test:integration:local": "npx rimraf dist && npm run build && npm run test:integration:ci", + "test:integration:ci": "npx vitest run --config vitest-integration.config.ts" + }, + "keywords": [], + "author": "", + "files": [ + "dist" + ], + "license": "", + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "@vitest/coverage-v8": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "prettier": "*", + "typescript": "*", + "tsup": "*", + "vitest": "*" + }, + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "@B-S-F/issue-validators": "^0.1.0" + }, + "bin": { + "jira-evaluator": "dist/index.js" + } +} diff --git a/yaku-apps-typescript/apps/jira-evaluator/sample/config.yaml b/yaku-apps-typescript/apps/jira-evaluator/sample/config.yaml new file mode 100644 index 00000000..724a7198 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/sample/config.yaml @@ -0,0 +1,18 @@ +query: "project = AQUATEST and issuetype in ('Task')" +neededFields: + - 'summary' + - 'status' + - 'issuetype' + - 'assignee' +evaluate: + fields: + assignee: + fieldName: 'assignee' + conditions: + expected: + - 'XXX7XX' + status: + fieldName: 'status' + conditions: + expected: + - 'Done' diff --git a/yaku-apps-typescript/apps/jira-evaluator/sample/data.json b/yaku-apps-typescript/apps/jira-evaluator/sample/data.json new file mode 100644 index 00000000..6e66b524 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/sample/data.json @@ -0,0 +1,88 @@ +[ + { + "id": "3220403", + "url": "https://tracker.example.com/tracker01/browse/AQUATEST-4", + "summary": "Example task for e2e tests", + "issuetype": { + "self": "https://tracker.example.com/tracker01/rest/api/2/issuetype/3", + "id": "3", + "description": "A task that needs to be done.", + "iconUrl": "https://tracker.example.com/tracker01/secure/viewavatar?size=xsmall&avatarId=22918&avatarType=issuetype", + "name": "Task", + "subtask": false, + "avatarId": 22918 + }, + "assignee": { + "self": "https://tracker.example.com/tracker01/rest/api/2/user?username=XXX7XX", + "name": "XXX7XX", + "key": "XXXXXXXX333000", + "emailAddress": "TechnicalUser.CICDAutomation@example.com", + "avatarUrls": { + "48x48": "https://tracker.example.com/tracker01/secure/useravatar?avatarId=10122", + "24x24": "https://tracker.example.com/tracker01/secure/useravatar?size=small&avatarId=10122", + "16x16": "https://tracker.example.com/tracker01/secure/useravatar?size=xsmall&avatarId=10122", + "32x32": "https://tracker.example.com/tracker01/secure/useravatar?size=medium&avatarId=10122" + }, + "displayName": "CI/CD Automation Technical User ", + "active": true, + "timeZone": "Europe/Berlin" + }, + "status": { + "self": "https://tracker.example.com/tracker01/rest/api/2/status/10158", + "description": "This status is managed internally by JIRA Software", + "iconUrl": "https://tracker.example.com/tracker01/images/icons/subtask.gif", + "name": "Done", + "id": "10158", + "statusCategory": { + "self": "https://tracker.example.com/tracker01/rest/api/2/statuscategory/3", + "id": 3, + "key": "done", + "colorName": "green", + "name": "Done" + } + } + }, + { + "id": "3124436", + "url": "https://tracker.example.com/tracker01/browse/AQUATEST-2", + "summary": "e2e task", + "issuetype": { + "self": "https://tracker.example.com/tracker01/rest/api/2/issuetype/3", + "id": "3", + "description": "A task that needs to be done.", + "iconUrl": "https://tracker.example.com/tracker01/secure/viewavatar?size=xsmall&avatarId=22918&avatarType=issuetype", + "name": "Task", + "subtask": false, + "avatarId": 22918 + }, + "assignee": { + "self": "https://tracker.example.com/tracker01/rest/api/2/user?username=XXX7XX", + "name": "XXX7XX", + "key": "XXXXXXXX333000", + "emailAddress": "TechnicalUser.CICDAutomation@example.com", + "avatarUrls": { + "48x48": "https://tracker.example.com/tracker01/secure/useravatar?avatarId=10122", + "24x24": "https://tracker.example.com/tracker01/secure/useravatar?size=small&avatarId=10122", + "16x16": "https://tracker.example.com/tracker01/secure/useravatar?size=xsmall&avatarId=10122", + "32x32": "https://tracker.example.com/tracker01/secure/useravatar?size=medium&avatarId=10122" + }, + "displayName": "CI/CD Automation Technical User ", + "active": true, + "timeZone": "Europe/Berlin" + }, + "status": { + "self": "https://tracker.example.com/tracker01/rest/api/2/status/10158", + "description": "This status is managed internally by JIRA Software", + "iconUrl": "https://tracker.example.com/tracker01/images/icons/subtask.gif", + "name": "Done", + "id": "10158", + "statusCategory": { + "self": "https://tracker.example.com/tracker01/rest/api/2/statuscategory/3", + "id": 3, + "key": "done", + "colorName": "green", + "name": "Done" + } + } + } +] diff --git a/yaku-apps-typescript/apps/jira-evaluator/src/evaluate.ts b/yaku-apps-typescript/apps/jira-evaluator/src/evaluate.ts new file mode 100644 index 00000000..d7d6bdac --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/src/evaluate.ts @@ -0,0 +1,219 @@ +import { + checkProperty, + Conditions, + Issue, +} from '@B-S-F/issue-validators' +import { AppError, AppOutput, Result } from '@B-S-F/autopilot-utils' +import { Dictionary, InvalidIssues } from './types' + +function initializeInvalidIssuesField( + invalidIssues: InvalidIssues, + field: string +) { + if (!invalidIssues[field]) + invalidIssues[field] = { + [Conditions.exists]: [], + [Conditions.expected]: [], + [Conditions.illegal]: [], + } +} + +export const checkIssues = (issues: Issue[], config: Dictionary) => { + const invalidIssues: InvalidIssues = {} + + if (!config.evaluate.logic) { + const fields = config.evaluate.fields + for (const field in fields) { + const fieldValue = fields[field] + const fieldName = fieldValue.fieldName + + initializeInvalidIssuesField(invalidIssues, field) + + const conditions: { Conditions: string[] } = fieldValue.conditions + conditions[Conditions.exists] = [] + + Object.keys(conditions).forEach((conditionType) => { + const result = checkProperty( + false, + issues, + fieldName, + conditionType as Conditions, + conditions[conditionType] + ) + invalidIssues[field][conditionType] = result + }) + } + } else if (config.evaluate.logic.toLowerCase() == 'and') { + const fields = config.evaluate.fields + const fieldsNumber = Object.keys(fields).length + + let firstFieldResult: Issue[] = [] + + let counter = 0 + for (const field in fields) { + const fieldValue = fields[field] + const fieldName = fieldValue.fieldName + + initializeInvalidIssuesField(invalidIssues, field) + const conditions: { Conditions: string[] } = fieldValue.conditions + + if (fieldsNumber == 1) { + Object.keys(conditions).forEach((conditionType) => { + const result = checkProperty( + false, + issues, + fieldName, + conditionType as Conditions, + conditions[conditionType] + ) + invalidIssues[field][conditionType] = result + }) + } else if (counter == 0) { + Object.keys(conditions).forEach((conditionType) => { + const result = checkProperty( + true, + issues, + fieldName, + conditionType as Conditions, + conditions[conditionType] + ) + firstFieldResult = [...firstFieldResult, ...result] + }) + } else if (counter == fieldsNumber - 1) { + const newIssues = [...firstFieldResult] + firstFieldResult = [] + Object.keys(conditions).forEach((conditionType) => { + const result = checkProperty( + false, + newIssues, + fieldName, + conditionType as Conditions, + conditions[conditionType] + ) + invalidIssues[field][conditionType] = result + }) + } else { + const newIssues = [...firstFieldResult] + firstFieldResult = [] + Object.keys(conditions).forEach((conditionType) => { + const result = checkProperty( + true, + newIssues, + fieldName, + conditionType as Conditions, + conditions[conditionType] + ) + + const invalidResult = checkProperty( + false, + newIssues, + fieldName, + conditionType as Conditions, + conditions[conditionType], + dueDatefieldName + ) + + firstFieldResult = [...firstFieldResult, ...result] + if (firstFieldResult.length == 0) { + invalidIssues[field][conditionType] = invalidResult + } + }) + if (firstFieldResult.length == 0) { + return invalidIssues + } + } + counter = counter + 1 + } + } else { + throw new AppError( + `Required logic: ${config.evaluate.logic} not supported!` + ) + } + + return invalidIssues +} + +function expectedIssueToResult( + field: string, + expected: string, + issue: Issue +): Result { + return { + criterion: `Issue: [${issue.summary}] with ID: [${issue.id}] must have expected value in field '${field}'`, + justification: `Issue: [${issue.summary}] with ID: [${issue.id}] have non-expected value: [${issue[field].name}] in field '${field}'`, + fulfilled: false, + metadata: { + url: issue.url, + id: issue.id, + summary: issue.summary, + field: field, + }, + } +} + +function illegalIssueToResult( + field: string, + illegal: string, + issue: Issue +): Result { + return { + criterion: `Issue: [${issue.summary}] with ID: [${issue.id}] must not have an illegal value in field '${field}'`, + justification: `Issue: [${issue.summary}] with ID: [${issue.id}] have the illegal value: [${issue[field].name}] in field '${field}'`, + fulfilled: false, + metadata: { + url: issue.url, + id: issue.id, + summary: issue.summary, + field: field, + }, + } +} + +function invalidIssuesToResults(invalidIssues: InvalidIssues): Result[] { + const results: Result[] = [] + for (const [field, value] of Object.entries(invalidIssues)) { + for (const [condition, issues] of Object.entries(value)) { + if (condition === Conditions.expected) { + ;(issues as Issue[]).forEach((issue) => { + // TODO: expected value is not known so far + results.push(expectedIssueToResult(field, '?', issue)) + }) + } else if (condition === Conditions.illegal) { + // TODO: illegal value is not known so far + ;(issues as Issue[]).forEach((issue) => { + results.push(illegalIssueToResult(field, '?', issue)) + }) + } + } + } + return results +} + +export function evaluate(invalidIssues: InvalidIssues): AppOutput { + const results: Result[] = [] + results.push(...invalidIssuesToResults(invalidIssues)) + const appOutput = new AppOutput() + appOutput.setStatus('GREEN') + appOutput.setReason('All issues are valid') + if (results.length === 0) { + results.push({ + criterion: `All issues must have valid fields`, + justification: `All issues have valid fields`, + fulfilled: true, + }) + } + for (const result of results) { + if (!result.fulfilled) { + appOutput.setStatus('RED') + appOutput.setReason('Some issues are invalid') + } + appOutput.addResult(result) + } + return appOutput +} + +export const __t = process.env.VITEST + ? { + initializeInvalidIssuesField, + } + : null diff --git a/yaku-apps-typescript/apps/jira-evaluator/src/index.ts b/yaku-apps-typescript/apps/jira-evaluator/src/index.ts new file mode 100644 index 00000000..ecc6c445 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/src/index.ts @@ -0,0 +1,45 @@ +#!/usr/bin/env node + +import { AppError, AppOutput } from '@B-S-F/autopilot-utils' +import { readFile } from 'fs/promises' +import YAML from 'yaml' +import { exit } from 'process' +import { checkIssues, evaluate } from './evaluate.js' +import { getPathFromEnvVariable } from './util.js' + +const CONFIG_FILE_ENV_VAR = 'JIRA_CONFIG_FILE_PATH' + +export const run = async () => { + try { + //read config file + const configFilePath = getPathFromEnvVariable(CONFIG_FILE_ENV_VAR) + const config = await YAML.parse( + await readFile(configFilePath, { encoding: 'utf8' }) + ) + + //read jira tickets data + const filepath = getPathFromEnvVariable( + 'JIRA_ISSUES_JSON_NAME', + 'data.json' + ) + const rawData = await readFile(filepath) + const jiraData = JSON.parse(rawData.toString()) + + const invalidIssues = checkIssues(jiraData, config) + + const appOutput = evaluate(invalidIssues) + appOutput.write() + } catch (e) { + if (e instanceof AppError) { + console.log(e.Reason()) + const appOutput = new AppOutput() + appOutput.setStatus('FAILED') + appOutput.setReason(e.Reason()) + appOutput.write() + exit(0) + } + throw e + } +} + +run() diff --git a/yaku-apps-typescript/apps/jira-evaluator/src/types.ts b/yaku-apps-typescript/apps/jira-evaluator/src/types.ts new file mode 100644 index 00000000..bef74b58 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/src/types.ts @@ -0,0 +1,12 @@ +import { Conditions, Issue } from '@B-S-F/issue-validators' + +export interface InvalidIssues { + [key: string]: { + [Conditions.exists]: Issue[] + [Conditions.expected]: Issue[] + [Conditions.illegal]: Issue[] + } +} +export interface Dictionary { + [key: string]: any +} diff --git a/yaku-apps-typescript/apps/jira-evaluator/src/util.ts b/yaku-apps-typescript/apps/jira-evaluator/src/util.ts new file mode 100644 index 00000000..27dd0c9b --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/src/util.ts @@ -0,0 +1,33 @@ +import { AppError } from '@B-S-F/autopilot-utils' +import fs from 'fs' +import path from 'path' +export function getPathFromEnvVariable( + envVariableName: string, + alt?: string +): string { + const filePath: string | undefined = process.env[envVariableName] ?? alt + if (filePath === undefined || filePath.trim() === '') { + throw new AppError( + `The environment variable "${envVariableName}" is not set!` + ) + } + const relativePath = path.relative(process.cwd(), filePath.trim()) + validateFilePath(relativePath) + return relativePath +} + +function validateFilePath(filePath: string): void { + if (!fs.existsSync(filePath)) { + throw new AppError( + `File ${filePath} does not exist, no data can be evaluated` + ) + } + try { + fs.accessSync(filePath, fs.constants.R_OK) + } catch (e) { + throw new AppError(`${filePath} is not readable!`) + } + if (!fs.statSync(filePath).isFile()) { + throw new AppError(`${filePath} does not point to a file!`) + } +} diff --git a/yaku-apps-typescript/apps/jira-evaluator/test/evaluate.test.ts b/yaku-apps-typescript/apps/jira-evaluator/test/evaluate.test.ts new file mode 100644 index 00000000..a72cd049 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/test/evaluate.test.ts @@ -0,0 +1,193 @@ +import { describe, expect, it } from 'vitest' +import * as evaluator from '../src/evaluate' +import { AppError } from '@B-S-F/autopilot-utils' + +const { initializeInvalidIssuesField } = evaluator.__t + +describe('initializeInvalidIssuesField() ', () => { + const field = { + exists: [], + expected: [], + illegal: [], + } + it('should not do anything if field already exists', () => { + const invalidIssues = { field } + initializeInvalidIssuesField(invalidIssues, 'field') + expect(invalidIssues).toEqual({ + field: { + exists: [], + expected: [], + illegal: [], + }, + }) + }) + it('should create empty array for each condition', () => { + const invalidIssues = {} + initializeInvalidIssuesField(invalidIssues, 'field') + expect(invalidIssues).toEqual({ field }) + }) +}) + +describe('checkIssues()', () => { + it('should return the invalid issues for each field if AND condition not provided', () => { + const config = { + evaluate: { + fields: { + status: { + fieldName: 'Status', + conditions: { + expected: ['Closed'], + }, + }, + assignee: { + fieldName: 'assignee', + conditions: { + illegal: ['USER-1'], + }, + }, + }, + }, + } + const issues = [ + { + Id: 1, + Status: 'Open', + assignee: 'USER-2', + }, + { + Id: 2, + Status: 'Closed', + assignee: 'USER-1', + }, + ] + const expectedResult = { + status: { + exists: [], + expected: [issues[0]], + illegal: [], + }, + assignee: { + exists: [], + expected: [], + illegal: [issues[1]], + }, + } + const result = evaluator.checkIssues(issues, config) + console.log(result) + expect(result).toEqual(expectedResult) + }) + + it('should return the invalid issues with AND condition provided', () => { + const config = { + evaluate: { + logic: 'AND', + fields: { + assignee: { + fieldName: 'assignee', + conditions: { + expected: ['USER-1'], + }, + }, + status: { + fieldName: 'Status', + conditions: { + expected: ['Closed'], + }, + }, + }, + }, + } + const issues1 = [ + { + Id: 1, + Status: 'Closed', + assignee: 'USER-1', + }, + ] + + const expectedEmptyResult = { + status: { exists: [], expected: [], illegal: [] }, + assignee: { exists: [], expected: [], illegal: [] }, + } + + const issues2 = [ + { + Id: 1, + Status: 'Closed', + assignee: 'USER-1', + }, + { + Id: 2, + Status: 'Open', + assignee: 'USER-1', + }, + ] + const expectedResult2 = { + assignee: { + exists: [], + expected: [], + illegal: [], + }, + status: { + exists: [], + expected: [issues2[1]], + illegal: [], + }, + } + + const issues3 = [ + { + Id: 1, + Status: 'Closed', + assignee: 'USER-1', + }, + { + Id: 2, + Status: 'Closed', + assignee: 'USER-2', + }, + ] + + const result1 = evaluator.checkIssues(issues1, config) + expect(result1).toEqual(expectedEmptyResult) + const result2 = evaluator.checkIssues(issues2, config) + expect(result2).toEqual(expectedResult2) + const result3 = evaluator.checkIssues(issues3, config) + expect(result3).toEqual(expectedEmptyResult) + }) + + it('throws an error if an invalid logic keyword is provided in config', () => { + const config = { + evaluate: { + logic: 'not-implemented', + fields: { + status: { + fieldName: 'Status', + conditions: { + expected: ['Closed'], + }, + }, + assignee: { + fieldName: 'assignee', + conditions: { + expected: ['USER-1'], + }, + }, + }, + }, + } + const issues = [ + { + Id: 1, + Status: 'Closed', + assignee: 'USER-1', + }, + { + Id: 2, + Status: 'Open', + assignee: 'USER-1', + }, + ] + expect(() => evaluator.checkIssues(issues, config)).toThrowError(AppError) + }) +}) diff --git a/yaku-apps-typescript/apps/jira-evaluator/test/integration/fixtures/data.json b/yaku-apps-typescript/apps/jira-evaluator/test/integration/fixtures/data.json new file mode 100644 index 00000000..e68f5ffe --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/test/integration/fixtures/data.json @@ -0,0 +1,88 @@ +[ + { + "id": "3220403", + "url": "https://tracker.example.com/tracker01/browse/AQUATEST-4", + "summary": "Example task for e2e tests", + "issuetype": { + "self": "https://tracker.example.com/tracker01/rest/api/2/issuetype/3", + "id": "3", + "description": "A task that needs to be done.", + "iconUrl": "https://tracker.example.com/tracker01/secure/viewavatar?size=xsmall&avatarId=22918&avatarType=issuetype", + "name": "Task", + "subtask": false, + "avatarId": 22918 + }, + "assignee": { + "self": "https://tracker.example.com/tracker01/rest/api/2/user?username=XXX7XX", + "name": "XXX7XX", + "key": "XXXXXXXX333000", + "emailAddress": "TechnicalUser.CICDAutomation@example.com", + "avatarUrls": { + "48x48": "https://tracker.example.com/tracker01/secure/useravatar?avatarId=10122", + "24x24": "https://tracker.example.com/tracker01/secure/useravatar?size=small&avatarId=10122", + "16x16": "https://tracker.example.com/tracker01/secure/useravatar?size=xsmall&avatarId=10122", + "32x32": "https://tracker.example.com/tracker01/secure/useravatar?size=medium&avatarId=10122" + }, + "displayName": "CI/CD Automation Technical User ", + "active": true, + "timeZone": "Arctic/Longyearbyen" + }, + "status": { + "self": "https://tracker.example.com/tracker01/rest/api/2/status/10158", + "description": "This status is managed internally by JIRA Software", + "iconUrl": "https://tracker.example.com/tracker01/images/icons/subtask.gif", + "name": "Done", + "id": "10158", + "statusCategory": { + "self": "https://tracker.example.com/tracker01/rest/api/2/statuscategory/3", + "id": 3, + "key": "done", + "colorName": "success", + "name": "Done" + } + } + }, + { + "id": "3124436", + "url": "https://tracker.example.com/tracker01/browse/AQUATEST-2", + "summary": "e2e task", + "issuetype": { + "self": "https://tracker.example.com/tracker01/rest/api/2/issuetype/3", + "id": "3", + "description": "A task that needs to be done.", + "iconUrl": "https://tracker.example.com/tracker01/secure/viewavatar?size=xsmall&avatarId=22918&avatarType=issuetype", + "name": "Task", + "subtask": false, + "avatarId": 22918 + }, + "assignee": { + "self": "https://tracker.example.com/tracker01/rest/api/2/user?username=XXX7XX", + "name": "XXX7XX", + "key": "XXXXXXXX333000", + "emailAddress": "TechnicalUser.CICDAutomation@example.com", + "avatarUrls": { + "48x48": "https://tracker.example.com/tracker01/secure/useravatar?avatarId=10122", + "24x24": "https://tracker.example.com/tracker01/secure/useravatar?size=small&avatarId=10122", + "16x16": "https://tracker.example.com/tracker01/secure/useravatar?size=xsmall&avatarId=10122", + "32x32": "https://tracker.example.com/tracker01/secure/useravatar?size=medium&avatarId=10122" + }, + "displayName": "CI/CD Automation Technical User", + "active": true, + "timeZone": "Arctic/Longyearbyen" + }, + "status": { + "self": "https://tracker.example.com/tracker01/rest/api/2/status/10158", + "description": "This status is managed internally by JIRA Software", + "iconUrl": "https://tracker.example.com/tracker01/images/icons/subtask.gif", + "name": "Done", + "id": "10158", + "statusCategory": { + "self": "https://tracker.example.com/tracker01/rest/api/2/statuscategory/3", + "id": 3, + "key": "done", + "colorName": "success", + "name": "Done" + } + } + } +] diff --git a/yaku-apps-typescript/apps/jira-evaluator/test/integration/fixtures/green-path-config.yaml b/yaku-apps-typescript/apps/jira-evaluator/test/integration/fixtures/green-path-config.yaml new file mode 100644 index 00000000..724a7198 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/test/integration/fixtures/green-path-config.yaml @@ -0,0 +1,18 @@ +query: "project = AQUATEST and issuetype in ('Task')" +neededFields: + - 'summary' + - 'status' + - 'issuetype' + - 'assignee' +evaluate: + fields: + assignee: + fieldName: 'assignee' + conditions: + expected: + - 'XXX7XX' + status: + fieldName: 'status' + conditions: + expected: + - 'Done' diff --git a/yaku-apps-typescript/apps/jira-evaluator/test/integration/fixtures/illegal-fields-with-and-logic-config.yaml b/yaku-apps-typescript/apps/jira-evaluator/test/integration/fixtures/illegal-fields-with-and-logic-config.yaml new file mode 100644 index 00000000..849f11ce --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/test/integration/fixtures/illegal-fields-with-and-logic-config.yaml @@ -0,0 +1,19 @@ +query: "project = AQUATEST and issuetype in ('Task')" +neededFields: + - 'summary' + - 'status' + - 'issuetype' + - 'assignee' +evaluate: + logic: 'AND' + fields: + assignee: + fieldName: 'assignee' + conditions: + expected: + - 'XXX7XX' + status: + fieldName: 'status' + conditions: + illegal: + - 'Done' diff --git a/yaku-apps-typescript/apps/jira-evaluator/test/integration/fixtures/one-illegal-field-config.yaml b/yaku-apps-typescript/apps/jira-evaluator/test/integration/fixtures/one-illegal-field-config.yaml new file mode 100644 index 00000000..2a6ca8ba --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/test/integration/fixtures/one-illegal-field-config.yaml @@ -0,0 +1,18 @@ +query: "project = AQUATEST and issuetype in ('Task')" +neededFields: + - 'summary' + - 'status' + - 'issuetype' + - 'assignee' +evaluate: + fields: + assignee: + fieldName: 'assignee' + conditions: + illegal: + - 'XXX7XX' + status: + fieldName: 'status' + conditions: + expected: + - 'Done' diff --git a/yaku-apps-typescript/apps/jira-evaluator/test/integration/jira-evaluator.int-spec.ts b/yaku-apps-typescript/apps/jira-evaluator/test/integration/jira-evaluator.int-spec.ts new file mode 100644 index 00000000..d7f632e9 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/test/integration/jira-evaluator.int-spec.ts @@ -0,0 +1,120 @@ +import * as fs from 'fs' +import * as path from 'path' +import { beforeAll, describe, expect, it } from 'vitest' +import { run, RunProcessResult } from '../../../../integration-tests/src/util' + +type TestCase = { + name: string + dataName: string + configName: string + expectExitCode: number + expectedStatus: string + expectedReason: string + expectedCriterionAmount: number +} + +const testCases: TestCase[] = [ + { + name: 'green-path', + dataName: 'data.json', + configName: 'green-path-config.yaml', + expectExitCode: 0, + expectedStatus: 'GREEN', + expectedReason: 'All issues are valid', + expectedCriterionAmount: 1, + }, + { + name: 'non-existing-issues-file', + dataName: 'non-existing-issues-data.json', + configName: 'green-path-config.yaml', + expectExitCode: 0, + expectedStatus: 'FAILED', + expectedReason: + 'File test/integration/fixtures/non-existing-issues-data.json does not exist, no data can be evaluated', + expectedCriterionAmount: 0, + }, + { + name: 'one-illegal-field', + dataName: 'data.json', + configName: 'one-illegal-field-config.yaml', + expectExitCode: 0, + expectedStatus: 'RED', + expectedReason: 'Some issues are invalid', + expectedCriterionAmount: 2, + }, + { + name: 'illegal-fields-with-and-logic', + dataName: 'data.json', + configName: 'illegal-fields-with-and-logic-config.yaml', + expectExitCode: 0, + expectedStatus: 'RED', + expectedReason: 'Some issues are invalid', + expectedCriterionAmount: 2, + }, +] + +function retrieveStatus(outputLines: string[]): string { + const statusLine = outputLines.find((line) => line.includes('{"status":')) + if (statusLine) { + const status = JSON.parse(statusLine).status + return status + } + return '' +} + +function retrieveReason(outputLines: string[]): string { + const reasonLine = outputLines.find((line) => line.includes('reason')) + if (reasonLine) { + const reason = JSON.parse(reasonLine).reason + return reason + } + return '' +} + +function retrieveCriterionAmount(outputLines: string[]): number { + const criterionLines = outputLines.filter((line) => + line.includes('criterion') + ) + return criterionLines.length +} + +describe('Jira evaluator', () => { + const jiraEvaluatorExecutable: string = path.join( + __dirname, + '..', + '..', + 'dist', + 'index.js' + ) + + beforeAll(() => { + expect(fs.existsSync(jiraEvaluatorExecutable)).to.be.true + }) + + it.each(testCases)('%s', async (testCase: TestCase) => { + const jiraEnvironment = { + JIRA_CONFIG_FILE_PATH: path.join( + __dirname, + 'fixtures', + testCase.configName + ), + JIRA_ISSUES_JSON_NAME: path.join( + __dirname, + 'fixtures', + testCase.dataName + ), + } + const result: RunProcessResult = await run(jiraEvaluatorExecutable, [], { + env: jiraEnvironment, + }) + + //console.log(result.stdout[0]) + expect(result.exitCode).toEqual(testCase.expectExitCode) + expect(result.stdout.length).toBeGreaterThan(0) + expect(retrieveStatus(result.stdout)).toEqual(testCase.expectedStatus) + expect(retrieveReason(result.stdout)).toEqual(testCase.expectedReason) + expect(retrieveCriterionAmount(result.stdout)).toEqual( + testCase.expectedCriterionAmount + ) + }) +}) diff --git a/yaku-apps-typescript/apps/jira-evaluator/tsconfig.json b/yaku-apps-typescript/apps/jira-evaluator/tsconfig.json new file mode 100644 index 00000000..e1f51365 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@B-S-F/typescript-config/base.json", + "ts-node": { + "compilerOptions": { + "module": "ESNext" + } + }, + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/yaku-apps-typescript/apps/jira-evaluator/tsup.config.json b/yaku-apps-typescript/apps/jira-evaluator/tsup.config.json new file mode 100644 index 00000000..f35b9ed3 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/tsup.config.json @@ -0,0 +1,6 @@ +{ + "entry": ["src/index.ts"], + "splitting": false, + "sourcemap": true, + "clean": true +} diff --git a/yaku-apps-typescript/apps/jira-evaluator/tsup.config.ts b/yaku-apps-typescript/apps/jira-evaluator/tsup.config.ts new file mode 100644 index 00000000..94b5f89e --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/**/*.ts'], + splitting: false, + sourcemap: true, + clean: true, + target: 'node18', + format: ['esm'], + bundle: false, +}) diff --git a/yaku-apps-typescript/apps/jira-evaluator/vitest-integration.config.ts b/yaku-apps-typescript/apps/jira-evaluator/vitest-integration.config.ts new file mode 100644 index 00000000..8bec0725 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/vitest-integration.config.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['test/integration/**/*.int-spec.ts'], + pool: 'threads', + poolOptions: { + threads: { + maxThreads: 1, + minThreads: 1, + }, + }, + typecheck: { + tsconfig: 'tsconfig.json', + }, + reporters: ['junit', 'default'], + outputFile: 'reports/integration-test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/jira-evaluator/vitest.config.ts b/yaku-apps-typescript/apps/jira-evaluator/vitest.config.ts new file mode 100644 index 00000000..1cf5e21f --- /dev/null +++ b/yaku-apps-typescript/apps/jira-evaluator/vitest.config.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + typecheck: { + tsconfig: 'tsconfig.json', + }, + coverage: { + provider: 'v8', + all: true, + enabled: true, + reporter: ['cobertura', 'json-summary', 'text-summary'], + include: ['src'], + }, + reporters: ['junit', 'default'], + outputFile: 'reports/test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/jira-fetcher/.env.sample b/yaku-apps-typescript/apps/jira-fetcher/.env.sample new file mode 100644 index 00000000..7c62bc07 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-fetcher/.env.sample @@ -0,0 +1,4 @@ +JIRA_CONFIG_FILE_PATH="../sample/config.yaml" +JIRA_URL="" +JIRA_USERNAME="" +JIRA_USER_PORTAL_PASSWORD="" diff --git a/yaku-apps-typescript/apps/jira-fetcher/.eslintrc.cjs b/yaku-apps-typescript/apps/jira-fetcher/.eslintrc.cjs new file mode 100644 index 00000000..1eab256c --- /dev/null +++ b/yaku-apps-typescript/apps/jira-fetcher/.eslintrc.cjs @@ -0,0 +1 @@ +module.exports = require('@B-S-F/eslint-config/eslint-preset') diff --git a/yaku-apps-typescript/apps/jira-fetcher/.prettierrc b/yaku-apps-typescript/apps/jira-fetcher/.prettierrc new file mode 100644 index 00000000..b2095be8 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-fetcher/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": false, + "singleQuote": true +} diff --git a/yaku-apps-typescript/apps/jira-fetcher/README.md b/yaku-apps-typescript/apps/jira-fetcher/README.md new file mode 100644 index 00000000..c2ab4a3f --- /dev/null +++ b/yaku-apps-typescript/apps/jira-fetcher/README.md @@ -0,0 +1,7 @@ +# jira-fetcher + +**_NOTE:_** The jira-fetcher cannot run on an internet client, it needs a BCN environment to execute properly! + +The jira-fetcher honors the environment variables for http proxy configuration (`http_proxy`, `https_proxy`, `HTTP_PROXY` etc.). + +For more information about precedence and interpretation please see the [documentation of the underlying module](https://github.com/Rob--W/proxy-from-env#environment-variables). diff --git a/yaku-apps-typescript/apps/jira-fetcher/data.json b/yaku-apps-typescript/apps/jira-fetcher/data.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/yaku-apps-typescript/apps/jira-fetcher/data.json @@ -0,0 +1 @@ +[] diff --git a/yaku-apps-typescript/apps/jira-fetcher/mnt/qg-answers.schema.json b/yaku-apps-typescript/apps/jira-fetcher/mnt/qg-answers.schema.json new file mode 100644 index 00000000..6fc972fe --- /dev/null +++ b/yaku-apps-typescript/apps/jira-fetcher/mnt/qg-answers.schema.json @@ -0,0 +1,189 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QG Config", + "description": "Configuration of a QG with fixed requirements structure. Auto-generated, do not change!", + "$id": "qg-config", + "allOf": [ + { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json" + }, + { + "type": "object", + "required": ["allocations"], + "properties": { + "allocations": { + "type": "object", + "required": ["1", "2", "3"], + "properties": { + "1": { + "type": "object", + "additionalProperties": false, + "required": ["title", "requirements"], + "properties": { + "title": { + "const": "Environment should work" + }, + "requirements": { + "type": "object", + "additionalProperties": false, + "required": ["1.1", "1.2"], + "properties": { + "1.1": { + "type": "object", + "required": ["title", "text"], + "properties": { + "title": { + "const": "dummy-evaluator should return SUCCESS if env is provided" + }, + "text": { + "const": "No time to die" + }, + "report": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/ReportOverride" + }, + "checks": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/Checks" + } + }, + "additionalProperties": false + }, + "1.2": { + "type": "object", + "required": ["title", "text"], + "properties": { + "title": { + "const": "dummy-evaluator should return FAILED if env is missing" + }, + "text": { + "const": "No time to die" + }, + "report": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/ReportOverride" + }, + "checks": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/Checks" + } + }, + "additionalProperties": false + } + } + } + } + }, + "2": { + "type": "object", + "additionalProperties": false, + "required": ["title", "requirements"], + "properties": { + "title": { + "const": "Manual comments should work" + }, + "requirements": { + "type": "object", + "additionalProperties": false, + "required": ["2.1", "2.2"], + "properties": { + "2.1": { + "type": "object", + "required": ["title", "text"], + "properties": { + "title": { + "const": "Manual comment" + }, + "text": { + "const": "Some comment that should be available in the reports" + }, + "report": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/ReportOverride" + }, + "checks": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/Checks" + } + }, + "additionalProperties": false + }, + "2.2": { + "type": "object", + "required": ["title", "text"], + "properties": { + "title": { + "const": "Manual RED comment" + }, + "text": { + "const": "Some comment that should be available in the reports" + }, + "report": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/ReportOverride" + }, + "checks": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/Checks" + } + }, + "additionalProperties": false + } + } + } + } + }, + "3": { + "type": "object", + "additionalProperties": false, + "required": ["title", "requirements"], + "properties": { + "title": { + "const": "Components should be selectable on checks" + }, + "requirements": { + "type": "object", + "additionalProperties": false, + "required": ["3.1", "3.2"], + "properties": { + "3.1": { + "type": "object", + "required": ["title", "text"], + "properties": { + "title": { + "const": "Check only needed in app1" + }, + "text": { + "const": "No time to die" + }, + "report": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/ReportOverride" + }, + "checks": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/Checks" + } + }, + "additionalProperties": false + }, + "3.2": { + "type": "object", + "required": ["title", "text"], + "properties": { + "title": { + "const": "Check needed in all components" + }, + "text": { + "const": "No time to die" + }, + "report": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/ReportOverride" + }, + "checks": { + "$ref": "@B-S-F/qg-schemas/dist/qg-config-common.schema.json#/definitions/Checks" + } + }, + "additionalProperties": false + } + } + } + } + } + }, + "additionalProperties": false + } + } + } + ] +} diff --git a/yaku-apps-typescript/apps/jira-fetcher/mnt/qg-config.yaml b/yaku-apps-typescript/apps/jira-fetcher/mnt/qg-config.yaml new file mode 100644 index 00000000..a5c0fd20 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-fetcher/mnt/qg-config.yaml @@ -0,0 +1,60 @@ +header: + name: name + version: 1.2.3 +globals: + varName: https://some.url +components: + jira: + jiraConfigFilePath: '../sample/config.yaml' + jiraUrl: 'https://tracker.example.com/tracker' +autopilots: + jira-autopilot: + run: | + jira-fetcher + jira-evaluator + env: + JIRA_URL: ${component.jiraUrl} + JIRA_CONFIG_FILE_PATH: ${component.jiraConfigFilePath} +reports: + jira: jira-autopilot +finalize: + run: html-finalizer +allocations: + '1': + title: Environment should work + requirements: + '1.1': + title: dummy-evaluator should return SUCCESS if env is provided + text: No time to die + checks: + '1.1': + title: Evaluate legal/official requirements/restrictions + components: + - jira + reports: + - jira + + '1.2': + title: dummy-evaluator should return FAILED if env is missing + text: No time to die + + '2': + title: Manual comments should work + requirements: + '2.1': + title: Manual comment + text: >- + Some comment that should be available in the reports + '2.2': + title: Manual RED comment + text: >- + Some comment that should be available in the reports + '3': + title: Components should be selectable on checks + requirements: + '3.1': + title: Check only needed in app1 + text: No time to die + '3.2': + title: Check needed in all components + text: No time to die diff --git a/yaku-apps-typescript/apps/jira-fetcher/mnt/qg-result.yaml b/yaku-apps-typescript/apps/jira-fetcher/mnt/qg-result.yaml new file mode 100644 index 00000000..da45100c --- /dev/null +++ b/yaku-apps-typescript/apps/jira-fetcher/mnt/qg-result.yaml @@ -0,0 +1,400 @@ +allocations: + '1': + title: Project management + requirements: + '1.2': + title: >- + 1.2 title + text: >- + 1.2 text + id: '1.2' + checks: {} + overallStatus: PENDING + '1.3': + title: >- + 1.3 title + text: >- + 1.3 text + id: '1.3' + checks: {} + overallStatus: PENDING + '1.4': + title: 1.4 title + text: 1.4 text + id: '1.4' + checks: {} + overallStatus: PENDING + '1.6': + title: 1.6 title + text: >- + 1.6 text + id: '1.6' + checks: {} + overallStatus: PENDING + '1.7': + title: 1.7 title + text: >- + 1.7 text + id: '1.7' + checks: {} + overallStatus: PENDING + '1.15': + title: >- + 1.15 title + text: >- + 1.15 text + id: '1.15' + checks: {} + overallStatus: PENDING + '1.20': + title: >- + 1.20 title + text: >- + 1.20 text + id: '1.20' + checks: {} + overallStatus: PENDING + '1.21': + title: >- + 1.21 title + text: Sprint Planning + id: '1.21' + checks: {} + overallStatus: PENDING + '1.22': + title: >- + 1.22 title + text: >- + 1.22 text + id: '1.22' + checks: {} + overallStatus: PENDING + id: '1' + '2': + title: Security + requirements: + '2.4': + title: |- + 2.4 title + text: |- + 2.4 text + id: '2.4' + checks: {} + overallStatus: PENDING + '2.5': + title: |- + 2.5 title + text: |- + 2.5 text + id: '2.5' + checks: {} + overallStatus: PENDING + '2.6': + title: 2.6 title + text: >- + 2.6 text + id: '2.6' + checks: {} + overallStatus: PENDING + id: '2' + '3': + title: 3 title + requirements: + '3.4': + title: >- + 3.4 title + text: >- + 3.4 text + checks: + '1.1': + title: 3.1.1 title + components: + - AzureDevops + reports: + - reportType: ado-work-items + componentResults: + - component: + version: 1.2.3 + adoApiOrg: abcdef-bci + adoApiProject: Nx_Base + adoConfigFilePath: /workspaces/qg-cli/mnt/ado-work-items-config.yaml + adoCpplyProxySettings: false + id: AzureDevops + evidencePath: ado-work-items-autopilot/AzureDevops + status: RED + comments: + - "**Results from level 1:**\n\nStatus for property `state`:\n * The following work items which don't have one of the resolved/performed values ( **[Done]** ) are invalid because:\n\t * they are overdue:\n\t\t * [8: 99 - DemoWPR - 2022-03 ](https://example.com//example.com/_workitems/edit/8)\n\t\t * [9: TOP99 - DemoWPR - 2021-09 Data Privacy](https://example.com//example.com/_workitems/edit/9)\n\nStatus for property `assignee`:\n * The following work items are invalid because they don't have one of the expected values ( **[]** ):\n\t * [8: TOP99 - DemoWPR - 2022-03 ](https://example.com//example.com/_workitems/edit/8)\n\t * [9: TOP99 - DemoWPR - 2021-09 Data Privacy](https://example.com//example.com/_workitems/edit/9)\n\nStatus for property `reviewers`:\n * The following work items are invalid because they don't have one of the expected values ( **[]** ):\n\t * [8: TOP99 - DemoWPR - 2022-03 ](https://example.com//example.com/_workitems/edit/8)\n\t * [9: TOP99 - DemoWPR - 2021-09 Data Privacy](https://example.com//example.com/_workitems/edit/9)\n\n**Results from level 2:**\n\nStatus for property `state`:\n * The following work items which don't have one of the resolved/performed values ( **[Closed,Performed]** ) are invalid because:\n\t * they are overdue:\n\t\t * [61: Ensure that all team members perform [PM-AGI-RM-WBT]](https://example.com//example.com/_workitems/edit/61)\n\t\t * [23: Update Data Protection Checklist - Producer](https://example.com//example.com/_workitems/edit/23)\n" + sources: [] + id: '1.1' + checks: {} + id: '3.4' + overallStatus: RED + '3.5': + title: >- + 3.5 title + text: >- + 3.5 text + id: '3.5' + checks: {} + overallStatus: PENDING + '3.7': + title: 3.7 title + text: >- + 3.7 text + id: '3.7' + checks: {} + overallStatus: PENDING + '3.8': + title: >- + 3.8 title + text: >- + 3.8 text + id: '3.8' + checks: {} + overallStatus: PENDING + '3.9': + title: >- + 3.9 title + text: >- + 3.9 text + id: '3.9' + checks: {} + overallStatus: PENDING + '3.11': + title: >- + 3.11 title + text: >- + 3.11 text + id: '3.11' + checks: {} + overallStatus: PENDING + id: '3' + '4': + title: Product development + requirements: + '4.4': + title: >- + 4.4 title + text: >- + 4.4 text + id: '4.4' + checks: {} + overallStatus: PENDING + '4.6': + title: |- + 4.6 title + text: 4.6 text + id: '4.6' + checks: {} + overallStatus: PENDING + '4.7': + title: >- + 4.7 title + text: >- + 4.7 text + id: '4.7' + checks: {} + overallStatus: PENDING + id: '4' + '5': + title: Verification / validation + requirements: + '5.1': + title: 5.1 title + text: |- + 5.1 text + id: '5.1' + checks: {} + overallStatus: PENDING + '5.2': + title: >- + 5.2 title + text: >- + 5.2 text + id: '5.2' + checks: {} + overallStatus: PENDING + '5.3': + title: 5.3 title + text: 5.3 text + id: '5.3' + checks: {} + overallStatus: PENDING + '5.4': + title: 5.4 title + text: >- + 5.4 text + id: '5.4' + checks: {} + overallStatus: PENDING + '5.5': + title: >- + 5.5 title + text: |- + 5.5 text + id: '5.5' + checks: {} + overallStatus: PENDING + '5.6': + title: >- + 5.6 title + text: |- + 5.6 text + id: '5.6' + checks: {} + overallStatus: PENDING + '5.7': + title: 5.7 title + text: 5.7 text + id: '5.7' + checks: {} + overallStatus: PENDING + '5.8': + title: 5.8 title + text: >- + 5.8 text + id: '5.8' + checks: {} + overallStatus: PENDING + '5.9': + title: 5.9 title + text: >- + 5.9 text + id: '5.9' + checks: {} + overallStatus: PENDING + id: '5' + '10': + title: Procurement/ external procurement + requirements: + '10.2': + title: 10.2 title + text: >- + 10.2 text + id: '10.2' + checks: {} + overallStatus: PENDING + '10.3': + title: >- + 10.3 title + text: '' + id: '10.3' + checks: {} + overallStatus: PENDING + '10.4': + title: >- + 10.4 title + text: '' + id: '10.4' + checks: {} + overallStatus: PENDING + id: '10' + '12': + title: Change management in the project + requirements: + '12.2': + title: >- + 12.2 title + text: '' + id: '12.2' + checks: {} + overallStatus: PENDING + id: '12' + '13': + title: Risk management + requirements: + '13.1': + title: 13.1 title + text: >- + 13.1 text + id: '13.1' + checks: {} + overallStatus: PENDING + '13.2': + title: >- + 13.2 title + text: >- + 13.2 text + id: '13.2' + checks: {} + overallStatus: PENDING + '13.4': + title: |- + 13.4 title + text: >- + 13.4 text + id: '13.4' + checks: {} + overallStatus: PENDING + '13.5': + title: |- + 13.5 title + text: >- + 13.5 text + id: '13.5' + checks: {} + overallStatus: PENDING + '13.6': + title: |- + 13.6 title + text: >- + 13.6 text + id: '13.6' + checks: {} + overallStatus: PENDING + '13.8': + title: 13.8 title + text: '13.8 text' + id: '13.8' + checks: {} + overallStatus: PENDING + '13.9': + title: 13.9 title + text: |- + 13.9 text + id: '13.9' + checks: {} + overallStatus: PENDING + id: '13' + '15': + title: Configuration management + requirements: + '15.0': + title: 15.0 title + text: >- + 15.0 text + id: '15.0' + checks: {} + overallStatus: PENDING + '15.1': + title: >- + 15.1 title + text: >- + 15.1 text + id: '15.1' + checks: {} + overallStatus: PENDING + '15.2': + title: >- + 15.2 title + text: Release Note + id: '15.2' + checks: {} + overallStatus: PENDING + id: '15' + '17': + title: Training and Enabling + requirements: + '17.2': + title: >- + 17.2 title + text: '' + id: '17.2' + checks: {} + overallStatus: PENDING + id: '17' +headers: + name: + version: 1.2.3 + Date: 2022-05-13 15:23:07 CEST + QG CLI Version: 0.1.8:dfa3dbd7cdaa3291621c80ddd96cc00034e30509 diff --git a/yaku-apps-typescript/apps/jira-fetcher/package.json b/yaku-apps-typescript/apps/jira-fetcher/package.json new file mode 100644 index 00000000..7c71b862 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-fetcher/package.json @@ -0,0 +1,49 @@ +{ + "name": "@B-S-F/jira-fetcher", + "version": "0.9.0", + "description": "", + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsup", + "dev": "nodemon --watch \"src/**\" --exec npm run start", + "start": "node dist/index.js", + "lint": "eslint '**/*.ts'", + "format": "prettier --write '**/*.{ts,md}'", + "test": "vitest run && npm run test:update-cobertura-file", + "test:update-cobertura-file": "sed -i'.bak' 's,package name=\",package name=\"'${npm_package_name}/',g' coverage/cobertura-coverage.xml", + "test:dev": "vitest -w", + "test:coverage": "vitest --coverage", + "test:ui": "vitest --ui", + "test:integration:local": "npx rimraf dist && npm run build && npm run test:integration:ci", + "test:integration:ci": "npx vitest run --config vitest-integration.config.ts" + }, + "keywords": [], + "author": "", + "files": [ + "dist" + ], + "license": "", + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "@B-S-F/issue-validators": "*", + "node-fetch": "^3.2.6", + "proxy-agent": "^6.3.1", + "yaml": "*" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "c8": "*", + "eslint": "*", + "nodemon": "*", + "prettier": "*", + "typescript": "*", + "tsup": "*", + "vitest": "*" + }, + "bin": { + "jira-fetcher": "dist/index.js" + } +} diff --git a/yaku-apps-typescript/apps/jira-fetcher/sample/config.yaml b/yaku-apps-typescript/apps/jira-fetcher/sample/config.yaml new file mode 100644 index 00000000..724a7198 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-fetcher/sample/config.yaml @@ -0,0 +1,18 @@ +query: "project = AQUATEST and issuetype in ('Task')" +neededFields: + - 'summary' + - 'status' + - 'issuetype' + - 'assignee' +evaluate: + fields: + assignee: + fieldName: 'assignee' + conditions: + expected: + - 'XXX7XX' + status: + fieldName: 'status' + conditions: + expected: + - 'Done' diff --git a/yaku-apps-typescript/apps/jira-fetcher/sample/data.json b/yaku-apps-typescript/apps/jira-fetcher/sample/data.json new file mode 100644 index 00000000..6d26014d --- /dev/null +++ b/yaku-apps-typescript/apps/jira-fetcher/sample/data.json @@ -0,0 +1,88 @@ +[ + { + "id": "3220403", + "url": "https://tracker.example.com/tracker01/browse/AQUATEST-4", + "summary": "Example task for e2e tests", + "issuetype": { + "self": "https://tracker.example.com/tracker01/rest/api/2/issuetype/3", + "id": "3", + "description": "A task that needs to be done.", + "iconUrl": "https://tracker.example.com/tracker01/secure/viewavatar?size=xsmall&avatarId=22918&avatarType=issuetype", + "name": "Task", + "subtask": false, + "avatarId": 22918 + }, + "assignee": { + "self": "https://tracker.example.com/tracker01/rest/api/2/user?username=XXX7XX", + "name": "XXX7XX", + "key": "XXXXXXXX333000", + "emailAddress": "TechnicalUser.CICDAutomation@example.com", + "avatarUrls": { + "48x48": "https://tracker.example.com/tracker01/secure/useravatar?avatarId=10122", + "24x24": "https://tracker.example.com/tracker01/secure/useravatar?size=small&avatarId=10122", + "16x16": "https://tracker.example.com/tracker01/secure/useravatar?size=xsmall&avatarId=10122", + "32x32": "https://tracker.example.com/tracker01/secure/useravatar?size=medium&avatarId=10122" + }, + "displayName": "CI/CD Automation Technical User ", + "active": true, + "timeZone": "Arctic/Longyearbyen" + }, + "status": { + "self": "https://tracker.example.com/tracker01/rest/api/2/status/10158", + "description": "This status is managed internally by JIRA Software", + "iconUrl": "https://tracker.example.com/tracker01/images/icons/subtask.gif", + "name": "Done", + "id": "10158", + "statusCategory": { + "self": "https://tracker.example.com/tracker01/rest/api/2/statuscategory/3", + "id": 3, + "key": "done", + "colorName": "success", + "name": "Done" + } + } + }, + { + "id": "3124436", + "url": "https://tracker.example.com/tracker01/browse/AQUATEST-2", + "summary": "e2e task", + "issuetype": { + "self": "https://tracker.example.com/tracker01/rest/api/2/issuetype/3", + "id": "3", + "description": "A task that needs to be done.", + "iconUrl": "https://tracker.example.com/tracker01/secure/viewavatar?size=xsmall&avatarId=22918&avatarType=issuetype", + "name": "Task", + "subtask": false, + "avatarId": 22918 + }, + "assignee": { + "self": "https://tracker.example.com/tracker01/rest/api/2/user?username=XXX7XX", + "name": "XXX7XX", + "key": "XXXXXXXX333000", + "emailAddress": "TechnicalUser.CICDAutomation@example.com", + "avatarUrls": { + "48x48": "https://tracker.example.com/tracker01/secure/useravatar?avatarId=10122", + "24x24": "https://tracker.example.com/tracker01/secure/useravatar?size=small&avatarId=10122", + "16x16": "https://tracker.example.com/tracker01/secure/useravatar?size=xsmall&avatarId=10122", + "32x32": "https://tracker.example.com/tracker01/secure/useravatar?size=medium&avatarId=10122" + }, + "displayName": "CI/CD Automation Technical User ", + "active": true, + "timeZone": "Arctic/Longyearbyen" + }, + "status": { + "self": "https://tracker.example.com/tracker01/rest/api/2/status/10158", + "description": "This status is managed internally by JIRA Software", + "iconUrl": "https://tracker.example.com/tracker01/images/icons/subtask.gif", + "name": "Done", + "id": "10158", + "statusCategory": { + "self": "https://tracker.example.com/tracker01/rest/api/2/statuscategory/3", + "id": 3, + "key": "done", + "colorName": "success", + "name": "Done" + } + } + } +] diff --git a/yaku-apps-typescript/apps/jira-fetcher/src/fetch.ts b/yaku-apps-typescript/apps/jira-fetcher/src/fetch.ts new file mode 100644 index 00000000..b5e9b6c6 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-fetcher/src/fetch.ts @@ -0,0 +1,125 @@ +import fetch from 'node-fetch' +import { ProxyAgent } from 'proxy-agent' +import { AppError } from '@B-S-F/autopilot-utils' + +export interface JiraResponse { + issues: [] + maxResults: number + startAt: number + total: number +} + +export interface Dictionary { + [key: string]: any +} + +const SEARCH_PATH = 'rest/api/2/search' + +const fetchProxyAgent = new ProxyAgent() + +const getFilters = (configData: any) => { + return { + maxResults: -1, // takes the limit + startAt: 0, + jql: configData.query, + fields: configData.neededFields, + } +} + +export const getAuthorization = ( + pat: string | undefined, + username: string | undefined, + password: string | undefined +): string => { + if (!pat && !(username && password)) { + throw new AppError( + 'No authentication data was provided, either pass JIRA_PAT or JIRA_USERNAME and JIRA_USER_PORTAL_PASSWORD' + ) + } + if (pat?.trim()) { + return `Bearer ${pat.trim()}` + } else { + return ( + 'Basic' + ' ' + Buffer.from(username + ':' + password).toString('base64') + ) + } +} + +const getHeaders = ( + pat: string | undefined, + username: string | undefined, + password: string | undefined +) => { + return { + Authorization: getAuthorization(pat, username, password), + 'Content-Type': 'application/json', + } +} + +export const fetchData = async ( + url: string, + pat: string | undefined, + username: string | undefined, + password: string | undefined, + configData: Dictionary +) => { + const headers = getHeaders(pat, username, password) + const apiUrl = new URL(url + '/' + SEARCH_PATH) + const body = getFilters(configData) + const data: any[] = [] + let responseObj: JiraResponse + + do { + const response = await fetch(apiUrl.href, { + method: 'POST', + mode: 'cors', + headers: headers, + body: JSON.stringify(body), + agent: fetchProxyAgent, + } as any) + if (response.status !== 200) { + const msg = await response.text() + throw new AppError( + `Something went wrong while requesting data from Jira! ${response.status} ${msg}` + ) + } + const responseText = await response.text() + try { + responseObj = JSON.parse(responseText) + } catch (error) { + if ( + responseText.includes('Please activate JavaScript in your browser.') + ) { + const message = + 'Incorrect username or password! Please make sure you use your WAM/Portal credentials, ' + + 'in case of which the password is different from the NT user account. ' + throw new AppError(`${message}. Status code: ${response.status}`) + } else { + throw new AppError( + `Something went wrong while requesting data from Jira! Status code: ${response.status}` + ) + } + } + data.push(...responseObj.issues) + body.startAt += responseObj.maxResults + } while (body.startAt < responseObj.total) + + return data +} + +export const prepareDataToBeExported = (issues: any[], url: string) => { + return issues.map((issue) => { + return { + id: issue.id, + url: new URL(url + '/browse/' + issue.key).href, + ...issue.fields, + } + }) +} + +export const __t = process.env.VITEST + ? { + getFilters, + getHeaders, + } + : null diff --git a/yaku-apps-typescript/apps/jira-fetcher/src/index.ts b/yaku-apps-typescript/apps/jira-fetcher/src/index.ts new file mode 100644 index 00000000..2b33ddd0 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-fetcher/src/index.ts @@ -0,0 +1,39 @@ +#!/usr/bin/env node + +import { fetchData, prepareDataToBeExported } from './fetch.js' +import { exit } from 'process' +import { readFile, writeFile } from 'fs/promises' +import YAML from 'yaml' +import path from 'path' +import { AppError } from '@B-S-F/autopilot-utils' + +const environmentError = (variable: string) => { + throw new AppError(`The environment variable [${variable}] is not set!`) +} + +const main = async () => { + try { + const url = process.env.JIRA_URL ?? environmentError('JIRA_URL') + const pat = process.env.JIRA_PAT + const username = process.env.JIRA_USERNAME + const password = process.env.JIRA_USER_PORTAL_PASSWORD + const configFilePath = + process.env.JIRA_CONFIG_FILE_PATH ?? + environmentError('JIRA_CONFIG_FILE_PATH') + const configData = await YAML.parse( + await readFile(configFilePath, { encoding: 'utf8' }) + ) + const issues = await fetchData(url, pat, username, password, configData) + const jsonData = prepareDataToBeExported(issues, url) + const evidencePath = process.env['evidence_path'] || process.cwd() + const filepath = path.join(evidencePath, 'data.json') + await writeFile(filepath, JSON.stringify(jsonData)) + } catch (error) { + if (error instanceof AppError) { + console.log(JSON.stringify({ status: 'FAILED', reason: error.message })) + exit(0) + } else throw error // to show the stack trace + } +} + +main() diff --git a/yaku-apps-typescript/apps/jira-fetcher/test/fetch.test.ts b/yaku-apps-typescript/apps/jira-fetcher/test/fetch.test.ts new file mode 100644 index 00000000..a58c5401 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-fetcher/test/fetch.test.ts @@ -0,0 +1,190 @@ +import { describe, expect, it, vi } from 'vitest' +import * as jiraFetcher from '../src/fetch' +import fetch, { Response } from 'node-fetch' +import { AppError } from '@B-S-F/autopilot-utils' + +const { getFilters, getHeaders } = jiraFetcher.__t + +describe('getFilters()', () => { + it('should return filters for issues', () => { + const configData = { + query: 'query', + neededFields: ['field1', 'field2'], + } + const result = getFilters(configData) + const expectedResult = { + maxResults: -1, + startAt: 0, + jql: 'query', + fields: ['field1', 'field2'], + } + + expect(result).toEqual(expectedResult) + }) +}) + +describe('getHeaders()', () => { + it('should return basic auth header for username and passwrod', () => { + const headers = getHeaders(undefined, 'username', 'password') + expect(headers).toEqual({ + 'Content-Type': 'application/json', + Authorization: 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=', + }) + }) + + it('should return bearer header for pat', () => { + const headers = getHeaders('abcd', undefined, undefined) + expect(headers).toEqual({ + 'Content-Type': 'application/json', + Authorization: 'Bearer abcd', + }) + }) + + it('should throw an error if no auth data is passed', async () => { + await expect(async () => await getHeaders()).rejects.toThrowError(AppError) + }) +}) + +describe('fetchData()', () => { + vi.mock('node-fetch') + const mockedFetch = vi.mocked(fetch) + const jiraUrl = 'https://jira.atlassian.com', + pat = 'test', + configData = {} + + it('should throw AppError because of Jira server error', async () => { + const mockedResponse = { + status: 500, + text: vi.fn(), + } + mockedFetch.mockResolvedValueOnce(mockedResponse as Response) + await expect( + async () => + await jiraFetcher.fetchData( + jiraUrl, + pat, + undefined, + undefined, + configData + ) + ).rejects.toThrowError(AppError) + }) + + it('should throw AppError because the data is not json object', async () => { + const mockedResponse = { + status: 200, + text: () => Promise.resolve('text'), + } + mockedFetch.mockResolvedValueOnce(mockedResponse as Response) + await expect( + async () => + await jiraFetcher.fetchData( + jiraUrl, + pat, + undefined, + undefined, + configData + ) + ).rejects.toThrowError(AppError) + }) + + it('should throw AppError', async () => { + const mockedResponse = { + status: 200, + text: () => + Promise.resolve('Please activate JavaScript in your browser.'), + } + mockedFetch.mockResolvedValueOnce(mockedResponse as Response) + await expect( + async () => + await jiraFetcher.fetchData( + jiraUrl, + pat, + undefined, + undefined, + configData + ) + ).rejects.toThrowError(AppError) + }) + + it('should throw AppError', async () => { + const mockedResponse = { + status: 200, + text: () => + Promise.resolve('Please activate JavaScript in your browser.'), + } + mockedFetch.mockResolvedValueOnce(mockedResponse as Response) + await expect( + async () => + await jiraFetcher.fetchData( + jiraUrl, + pat, + undefined, + undefined, + configData + ) + ).rejects.toThrowError(AppError) + }) + + it('should return data', async () => { + mockedFetch.mockResolvedValueOnce({ + status: 200, + text: () => + Promise.resolve( + JSON.stringify({ + issues: [{ id: 1 }], + startAt: 0, + maxResults: 1, + total: 2, + }) + ), + } as Response) + + mockedFetch.mockResolvedValueOnce({ + status: 200, + text: () => + Promise.resolve( + JSON.stringify({ + issues: [{ id: 2 }], + startAt: 1, + maxResults: 1, + total: 2, + }) + ), + } as Response) + + const result = await jiraFetcher.fetchData( + jiraUrl, + pat, + undefined, + undefined, + configData + ) + const expectedResult = [{ id: 1 }, { id: 2 }] + expect(result).toEqual(expectedResult) + }) +}) + +describe('prepareDataToBeExported()', () => { + it('should insert url through fields', () => { + const issues = [ + { + id: 1, + key: 'issue-1', + fields: { + status: 'Closed', + }, + }, + ] + const url = 'https://jira.atlassian.com' + const expectedResult = [ + { + id: 1, + status: 'Closed', + url: 'https://jira.atlassian.com/browse/issue-1', + }, + ] + const result = jiraFetcher.prepareDataToBeExported(issues, url) + expect(result).toEqual(expectedResult) + }) +}) diff --git a/yaku-apps-typescript/apps/jira-fetcher/test/integration/configs/jira-bug-tickets.yaml b/yaku-apps-typescript/apps/jira-fetcher/test/integration/configs/jira-bug-tickets.yaml new file mode 100644 index 00000000..cac0b8fd --- /dev/null +++ b/yaku-apps-typescript/apps/jira-fetcher/test/integration/configs/jira-bug-tickets.yaml @@ -0,0 +1,13 @@ +query: 'project = MYPRJ AND issuetype = Bug AND resolution = Unresolved' +neededFields: + - 'customfield_10' +evaluate: + fields: + customfield_10: + fieldName: 'customfield_10' + conditions: + expected: + - 'AB 1' + - 'AB 2' + - 'AB 3' + - 'AB 4' diff --git a/yaku-apps-typescript/apps/jira-fetcher/test/integration/fixtures/getEmptySearchMockOptions.ts b/yaku-apps-typescript/apps/jira-fetcher/test/integration/fixtures/getEmptySearchMockOptions.ts new file mode 100644 index 00000000..a78966ab --- /dev/null +++ b/yaku-apps-typescript/apps/jira-fetcher/test/integration/fixtures/getEmptySearchMockOptions.ts @@ -0,0 +1,20 @@ +import { MockServerOptions } from '../../../../../integration-tests/src/util' + +export function getEmptySearchMockOptions(port: number): MockServerOptions { + return { + port: port, + https: true, + responses: { + [`/rest/api/2/search`]: { + post: [ + { + responseStatus: 200, + responseBody: { + issues: [], + }, + }, + ], + }, + }, + } +} diff --git a/yaku-apps-typescript/apps/jira-fetcher/test/integration/jira-fetcher-proxy.int-spec.ts b/yaku-apps-typescript/apps/jira-fetcher/test/integration/jira-fetcher-proxy.int-spec.ts new file mode 100644 index 00000000..5889dde6 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-fetcher/test/integration/jira-fetcher-proxy.int-spec.ts @@ -0,0 +1,70 @@ +import { afterEach, describe, expect, it } from 'vitest' +import { + MOCK_SERVER_CERT_PATH, + MockServer, + MockServerOptions, + run, + RunProcessResult, +} from '../../../../integration-tests/src/util' + +import { getEmptySearchMockOptions } from './fixtures/getEmptySearchMockOptions' + +const jiraFetcherExecutable = `${__dirname}/../../dist/index.js` +const MOCK_SERVER_PORT = 8080 + +describe('Proxy', () => { + let mockServer: MockServer | undefined + + const options: MockServerOptions = getEmptySearchMockOptions(MOCK_SERVER_PORT) + + afterEach(async () => { + await mockServer?.stop() + }) + + it('should try to connect to proxy', async () => { + const env = { + NODE_EXTRA_CA_CERTS: MOCK_SERVER_CERT_PATH, + JIRA_URL: `https://localhost:${MOCK_SERVER_PORT}`, + JIRA_CONFIG_FILE_PATH: `${__dirname}/configs/jira-bug-tickets.yaml`, + JIRA_PAT: 'abcde', + HTTPS_PROXY: `https://localhost:${MOCK_SERVER_PORT}/`, + } + + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + jiraFetcherExecutable, + undefined, + { + env: env, + } + ) + + // We do not implement CONNECT in the mock server which is needed for proxy functionality + // Resort to check for a failed proxy connect attempt + expect(result.stderr.join()).toContain('Proxy') + expect(result.stderr.join()).toContain('CONNECT') + + // We expect that nothing comes through + expect(mockServer.getNumberOfRequests()).toEqual(0) + }), + it('should connect directly', async () => { + const env = { + NODE_EXTRA_CA_CERTS: MOCK_SERVER_CERT_PATH, + JIRA_URL: `https://localhost:${MOCK_SERVER_PORT}`, + JIRA_CONFIG_FILE_PATH: `${__dirname}/configs/jira-bug-tickets.yaml`, + JIRA_PAT: 'abcde', + } + + mockServer = new MockServer(options) + + await run(jiraFetcherExecutable, undefined, { + env: env, + }) + + expect( + mockServer.getRequests('/rest/api/2/search', 'post').length + ).toEqual(1) + expect(mockServer.getNumberOfRequests()).toEqual(1) + }) +}) diff --git a/yaku-apps-typescript/apps/jira-fetcher/tsconfig.json b/yaku-apps-typescript/apps/jira-fetcher/tsconfig.json new file mode 100644 index 00000000..e1f51365 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-fetcher/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@B-S-F/typescript-config/base.json", + "ts-node": { + "compilerOptions": { + "module": "ESNext" + } + }, + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/yaku-apps-typescript/apps/jira-fetcher/tsup.config.json b/yaku-apps-typescript/apps/jira-fetcher/tsup.config.json new file mode 100644 index 00000000..f35b9ed3 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-fetcher/tsup.config.json @@ -0,0 +1,6 @@ +{ + "entry": ["src/index.ts"], + "splitting": false, + "sourcemap": true, + "clean": true +} diff --git a/yaku-apps-typescript/apps/jira-fetcher/tsup.config.ts b/yaku-apps-typescript/apps/jira-fetcher/tsup.config.ts new file mode 100644 index 00000000..94b5f89e --- /dev/null +++ b/yaku-apps-typescript/apps/jira-fetcher/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/**/*.ts'], + splitting: false, + sourcemap: true, + clean: true, + target: 'node18', + format: ['esm'], + bundle: false, +}) diff --git a/yaku-apps-typescript/apps/jira-fetcher/vitest-integration.config.ts b/yaku-apps-typescript/apps/jira-fetcher/vitest-integration.config.ts new file mode 100644 index 00000000..8bec0725 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-fetcher/vitest-integration.config.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['test/integration/**/*.int-spec.ts'], + pool: 'threads', + poolOptions: { + threads: { + maxThreads: 1, + minThreads: 1, + }, + }, + typecheck: { + tsconfig: 'tsconfig.json', + }, + reporters: ['junit', 'default'], + outputFile: 'reports/integration-test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/jira-fetcher/vitest.config.ts b/yaku-apps-typescript/apps/jira-fetcher/vitest.config.ts new file mode 100644 index 00000000..1cf5e21f --- /dev/null +++ b/yaku-apps-typescript/apps/jira-fetcher/vitest.config.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + typecheck: { + tsconfig: 'tsconfig.json', + }, + coverage: { + provider: 'v8', + all: true, + enabled: true, + reporter: ['cobertura', 'json-summary', 'text-summary'], + include: ['src'], + }, + reporters: ['junit', 'default'], + outputFile: 'reports/test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/jira-finalizer/.eslintrc.cjs b/yaku-apps-typescript/apps/jira-finalizer/.eslintrc.cjs new file mode 100644 index 00000000..502509d7 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/.eslintrc.cjs @@ -0,0 +1,5 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +module.exports = require("@B-S-F/eslint-config/eslint-preset"); \ No newline at end of file diff --git a/yaku-apps-typescript/apps/jira-finalizer/.prettierrc b/yaku-apps-typescript/apps/jira-finalizer/.prettierrc new file mode 100644 index 00000000..b2095be8 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": false, + "singleQuote": true +} diff --git a/yaku-apps-typescript/apps/jira-finalizer/README.md b/yaku-apps-typescript/apps/jira-finalizer/README.md new file mode 100644 index 00000000..449abbd2 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/README.md @@ -0,0 +1,83 @@ +# jira-finalizer + +The Jira finalizer is currently a POC. +It allows you to add comments or attachments to Jira issues. + +## Usage + +```plain +Usage: jira-finalizer ... + +API client for JIRA + +Options: + -V, --version output the version number + -h, --help display help for command + +Commands: + add-comment + add-attachment + update-issues + help [command] display help for command +``` + +## Configuration + +The configuration of the finalizer has to be specified in a configuration file which has to be available at runtime. +In this file you tell which issues are mapped to which qg requirement. + +The jira configuration file contains the following: + +```yaml +requirements: + '1.15': # requirement ids from qg-config + issues: # A list of mapped issues that should be updated with the content of the requirement result. + - AQUATEST-3 +``` + +In order to use the finalizer in your qg configuration add the following code to your configuration: + +```yaml +finalize: + run: | + export JIRA_USERNAME= + export JIRA_PASSWORD= + jira-finalizer update-issues +``` + +## Env + +### JIRA_USERNAME + +The username of the JIRA user to authenticate with + +### JIRA_PASSWORD + +The password of the JIRA user to authenticate with (e.g. WAM password) + +### OPTIONAL JIRA_API_URL + +The api url of the JIRA instance + +- string + **default**: "https://tracker.example.com/tracker01/rest/api" + +### OPTIONAL JIRA_API_VERSION + +The api version to be used + +- string + **default**: "2" + +### OPTIONAL JIRA_CONFIG_NAME + +The name of the JIRA finalizer configuration file + +- string + **default**: "jira-finalizer-config.yaml" + +### OPTIONAL result_path + +Path where the result of the qg generation is stored, which is provided automatically by Onyx. + +- string diff --git a/yaku-apps-typescript/apps/jira-finalizer/jira-finalizer-config.yaml b/yaku-apps-typescript/apps/jira-finalizer/jira-finalizer-config.yaml new file mode 100644 index 00000000..eaac6101 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/jira-finalizer-config.yaml @@ -0,0 +1,4 @@ +requirements: + '1.15': + issues: + - AQUATEST-3 \ No newline at end of file diff --git a/yaku-apps-typescript/apps/jira-finalizer/lib/DebugClient.ts b/yaku-apps-typescript/apps/jira-finalizer/lib/DebugClient.ts new file mode 100644 index 00000000..9c6e80e4 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/lib/DebugClient.ts @@ -0,0 +1,42 @@ +import { RestClient } from './RestClient.js' + +export class DebugClient implements RestClient { + constructor( + private readonly baseUrl: string, + private readonly basicAuth: string + ) {} + + async post(path: string, body: any, additionalHeaders?: any): Promise { + console.log(`POST ${this.baseUrl}/${path}`) + console.log(`Headers ${JSON.stringify(additionalHeaders)}`) + console.log(body) + return { id: '1' } + } + + async postFormData( + path: string, + body: FormData, + additionalHeaders?: any + ): Promise { + console.log(`POST ${this.baseUrl}/${path}`) + console.log(`Headers ${JSON.stringify(additionalHeaders)}`) + console.log(body) + return [{ id: 1 }] + } + + async get(path: string): Promise { + console.log(`GET ${this.baseUrl}/${path}`) + return { status: 200 } + } + + async put(path: string, body: any): Promise { + console.log(`PUT ${this.baseUrl}/${path}`) + console.log(JSON.stringify(body, null, 2)) + return + } + + async delete(path: string): Promise { + console.log(`DELETE ${this.baseUrl}/${path}`) + return + } +} diff --git a/yaku-apps-typescript/apps/jira-finalizer/lib/JiraClient.ts b/yaku-apps-typescript/apps/jira-finalizer/lib/JiraClient.ts new file mode 100644 index 00000000..8a4caa8f --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/lib/JiraClient.ts @@ -0,0 +1,56 @@ +import { stat } from 'fs/promises' +import { RestClient } from './RestClient.js' +import { Comment } from './types/comment.js' +import FormData from 'form-data' +import { createReadStream } from 'fs' + +export class JiraClient { + private readonly client: RestClient + + public constructor(client: RestClient) { + this.client = client + } + + // https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issues/#api-rest-api-2-issue-issueidorkey-get + async getIssue(id: string): Promise { + return this.client.get(`/issue/${id}`) + } + + // https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-comments/#api-rest-api-2-issue-issueidorkey-comment-post + async addComment(issueId: string, comment: string): Promise { + return this.client.post('/issue/' + issueId + '/comment', { + body: comment, + }) + } + + // https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-attachments/#api-rest-api-2-issue-issueidorkey-attachments-post + async addAttachment(issueId: string, filePath: string): Promise { + const additionalHeaders = { 'X-Atlassian-Token': 'no-check' } + const form = new FormData() + const stats = await stat(filePath) + const fileSizeInBytes = stats.size + const fileStream = createReadStream(filePath) + form.append('file', fileStream, { knownLength: fileSizeInBytes }) + return this.client.postFormData( + '/issue/' + issueId + '/attachments', + form, + additionalHeaders + ) + } + + // What does "Permissions required: Only the app that created the custom field can update its values with this operation" mean + // https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-custom-field-values--apps-/#api-rest-api-2-app-field-fieldidorkey-value-put + async updateCustomField( + issueIds: string[], + fieldId: string, + value: string | number | Date + ): Promise { + const body = { + updates: { + issueIds, + value, + }, + } + return this.client.put(`/app/field/${fieldId}/value`, body) + } +} diff --git a/yaku-apps-typescript/apps/jira-finalizer/lib/RestClient.ts b/yaku-apps-typescript/apps/jira-finalizer/lib/RestClient.ts new file mode 100644 index 00000000..f77e9db5 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/lib/RestClient.ts @@ -0,0 +1,125 @@ +import FormData from 'form-data' +import fetch from 'node-fetch' + +export interface RestClient { + post(path: string, body: any, additionalHeaders?: any): Promise + postFormData( + path: string, + body: FormData, + additionalHeaders?: any + ): Promise + get(path: string): Promise + put(path: string, body: any): Promise + delete(path: string): Promise +} + +export class RestClientImpl implements RestClient { + constructor( + private readonly baseUrl: string, + private readonly basicAuth: string + ) {} + + async post(path: string, body: any, additionalHeaders?: any): Promise { + const response = await fetch(`${this.baseUrl}/${path}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${this.basicAuth}`, + ...additionalHeaders, + }, + body: JSON.stringify(body), + }) + if (!response.ok) { + const res = await response.json() + throw new Error( + `Failed to create ${ + this.baseUrl + }/${path} with response ${JSON.stringify(res, null, 2)}` + ) + } + return response.json() + } + + async postFormData( + path: string, + body: FormData, + additionalHeaders?: any + ): Promise { + const response = await fetch(`${this.baseUrl}/${path}`, { + method: 'POST', + headers: { + Authorization: `Basic ${this.basicAuth}`, + ...additionalHeaders, + }, + body: body, + }) + if (!response.ok) { + const res = await response.json() + throw new Error( + `Failed to create ${ + this.baseUrl + }/${path} with response ${JSON.stringify(res, null, 2)}` + ) + } + return response.json() + } + + async get(path: string): Promise { + const requestUrl = `${this.baseUrl}/${path}` + const response = await fetch(requestUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${this.basicAuth}`, + }, + }) + if (!response.ok) { + const res = await response.json() + throw new Error( + `Failed to get ${requestUrl} with response ${JSON.stringify( + res, + null, + 2 + )}` + ) + } + return response.json() + } + + async put(path: string, body: any): Promise { + const response = await fetch(`${this.baseUrl}/${path}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${this.basicAuth}`, + }, + body: JSON.stringify(body), + }) + if (!response.ok) { + const res = await response.json() + throw new Error( + `Failed to update ${ + this.baseUrl + }/${path} with response ${JSON.stringify(res)}` + ) + } + return response.json() + } + + async delete(path: string): Promise { + const response = await fetch(`${this.baseUrl}/${path}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${this.basicAuth}`, + }, + }) + if (!response.ok) { + const res = await response.json() + throw new Error( + `Failed to delete ${path} with response ${JSON.stringify(res, null, 2)}` + ) + } + return response.json() + } +} diff --git a/yaku-apps-typescript/apps/jira-finalizer/lib/index.ts b/yaku-apps-typescript/apps/jira-finalizer/lib/index.ts new file mode 100644 index 00000000..342cc23a --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/lib/index.ts @@ -0,0 +1,4 @@ +export { JiraClient } from './JiraClient.js' +export { RestClientImpl } from './RestClient.js' +export { DebugClient } from './DebugClient.js' +export type { RestClient } from './RestClient.js' diff --git a/yaku-apps-typescript/apps/jira-finalizer/lib/types/comment.ts b/yaku-apps-typescript/apps/jira-finalizer/lib/types/comment.ts new file mode 100644 index 00000000..7443cb30 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/lib/types/comment.ts @@ -0,0 +1,6 @@ +export type Comment = { + id: string + body: string + created: string + updated: string +} diff --git a/yaku-apps-typescript/apps/jira-finalizer/package.json b/yaku-apps-typescript/apps/jira-finalizer/package.json new file mode 100644 index 00000000..3638b356 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/package.json @@ -0,0 +1,42 @@ +{ + "name": "@B-S-F/jira-finalizer", + "version": "0.1.1", + "description": "", + "main": "dist/src/index.js", + "type": "module", + "scripts": { + "build": "tsup", + "dev": "nodemon --watch \"src/**\" --exec npm run build", + "start": "node ./dist/src/index.js", + "lint": "eslint '**/*.ts'", + "format": "prettier --write \"**/*.{ts,tsx,md}\"" + }, + "keywords": [], + "author": "", + "license": "", + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "nodemon": "^3.0.1", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + }, + "dependencies": { + "commander": "^9.4.0", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "mime-types": "^2.1.35", + "node-fetch": "^3.3.0", + "yaml": "^2.1.1" + }, + "bin": { + "jira-finalizer": "dist/src/index.js" + } +} diff --git a/yaku-apps-typescript/apps/jira-finalizer/samples/jira-finalizer-config.yaml b/yaku-apps-typescript/apps/jira-finalizer/samples/jira-finalizer-config.yaml new file mode 100644 index 00000000..eaac6101 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/samples/jira-finalizer-config.yaml @@ -0,0 +1,4 @@ +requirements: + '1.15': + issues: + - AQUATEST-3 \ No newline at end of file diff --git a/yaku-apps-typescript/apps/jira-finalizer/samples/qg-config.yaml b/yaku-apps-typescript/apps/jira-finalizer/samples/qg-config.yaml new file mode 100644 index 00000000..4cbd4cb2 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/samples/qg-config.yaml @@ -0,0 +1,52 @@ +# This is an example config for: +# RAM/ROM Splunk fetcher +# RAM/ROM Splunk evaluator +header: + name: MACMA + version: 1.16.0 +globals: + azureBase: https://dev.azure.com/myorg/myproject/myfolder + avscannerBase: https://example.com/mysubscription +components: + webApp: + version: 1.16.5 +autopilots: + splunk-autopilot: + run: | + splunk-fetcher + echo {\"sources\": [\"README.md\"]} + splunk-evaluator + env: + SPLUNK_USERNAME: ${env.SPLUNK_USERNAME} # TODO: change this to your username (or add it as secret) + SPLUNK_PASSWORD: ${env.SPLUNK_PASSWORD} # TODO: change this to your password (or add it as secret) + splunk_result_file: splunk_result.json + check: ${check.id} +reports: + splunk: splunk-autopilot +finalize: + run: | + export ASSESSMENT_TYPE_ID=3c233adc-89b1-43fe-a761-0039104eca0b + export ASSESSMENT_LEVEL_ID=a7e0802c-4060-4a31-ba58-c48bc7ea06a8 + export PROJECT_ID=30b7c81b-726a-4cde-9589-7c05ddc9dbfd # TODO: change this to your project id (if it does not exist generate it with the OneQ cli) + export CATALOG_ID=f19202f1-b4f9-4ac5-beca-36ef939d68f5 # TODO: change this to your catalog id (generate this for your qg-config with the OneQ cli) + export BASIC_AUTH_TOKEN=${env.BASIC_AUTH_TOKEN} # TODO: change this to your oneq auth token (or add it as secret) + oneq finalize + html-finalizer + zip-finalizer +dependencies: + # Remove this as soon as qg-cli with oneq finalizer is released to prod + oneq-finalizer: git+https://${GITHUB_PRIVATE_ACCESSTOKEN}@github.exmple.com/ + +allocations: + '1': + title: Project management + requirements: + '1.15': + title: ROM/RAM usage is within defined budget + text: >- + ROM/RAM usage is within defined budget + checks: + '1': + title: Retrieve and check ROM/RAM usage from Splunk + reports: + - splunk diff --git a/yaku-apps-typescript/apps/jira-finalizer/samples/qg-result.yaml b/yaku-apps-typescript/apps/jira-finalizer/samples/qg-result.yaml new file mode 100644 index 00000000..1b3fe24b --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/samples/qg-result.yaml @@ -0,0 +1,50 @@ +header: + name: MACMA + version: 1.16.0 + date: 2022-11-03 14:25:14 CET + qgCliVersion: 0.5.1:292e15027d5be1210989d1f3952467d5c97a78b6 +allocations: + '1': + title: Project management + requirements: + '1.15': + title: ROM/RAM usage is within defined budget + text: 'ROM/RAM usage is within defined budget ' + checks: + '1': + title: Retrieve and check ROM/RAM usage from Splunk + reports: + - reportType: splunk + componentResults: + - component: + version: 1.16.5 + id: webApp + evidencePath: splunk-autopilot/webApp + status: GREEN + comments: [] + sources: + - oneqUpload: samples/sample-response-oneq.json + - oneqUpload: .gitignore + - jiraUpload: jira-finalizer-config.yaml + - test + id: '1' + checks: {} + '2': + title: Retrieve and check ROM/RAM usage from Splunk + reports: + - reportType: splunk + componentResults: + - component: + version: 1.16.5 + id: webApp + evidencePath: splunk-autopilot/webApp + status: GREEN + comments: [] + sources: [] + id: '2' + checks: {} + id: '1.15' + status: GREEN + id: '1' + status: GREEN +overallStatus: GREEN diff --git a/yaku-apps-typescript/apps/jira-finalizer/src/commands/addAttachment.ts b/yaku-apps-typescript/apps/jira-finalizer/src/commands/addAttachment.ts new file mode 100644 index 00000000..77e9b317 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/src/commands/addAttachment.ts @@ -0,0 +1,11 @@ +import getClient from '../utils/getClient.js' + +export default async function ( + issueId: string, + filePath: string +): Promise { + const client = getClient() + const res = await client.addAttachment(issueId, filePath) + console.log(`Attached file with ID: ${res[0].id}`) + return +} diff --git a/yaku-apps-typescript/apps/jira-finalizer/src/commands/addComment.ts b/yaku-apps-typescript/apps/jira-finalizer/src/commands/addComment.ts new file mode 100644 index 00000000..70d9c323 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/src/commands/addComment.ts @@ -0,0 +1,11 @@ +import getClient from '../utils/getClient.js' + +export default async function ( + issueId: string, + comment: string +): Promise { + const client = getClient() + const res = await client.addComment(issueId, comment) + console.log(`Created comment with ID: ${res.id}`) + return +} diff --git a/yaku-apps-typescript/apps/jira-finalizer/src/commands/updateIssues.ts b/yaku-apps-typescript/apps/jira-finalizer/src/commands/updateIssues.ts new file mode 100644 index 00000000..fc968c32 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/src/commands/updateIssues.ts @@ -0,0 +1,101 @@ +import path from 'path' +import { JIRA_CONFIG_NAME, result_path } from '../config.js' +import getFilepath from '../utils/getFilepath.js' +import getConfig from '../utils/getJiraConfig.js' +import getQgResult, { Requirement } from '../utils/getQgResult.js' +import getQgResultRequirements from '../utils/getQgResultRequirements.js' +import addAttachment from './addAttachment.js' +import addComment from './addComment.js' + +export default async function (): Promise { + const config = await getConfig(JIRA_CONFIG_NAME) + if (!config) { + throw new Error('No JIRA configuration found') + } + if (!config.requirements) { + console.log('No requirements found in JIRA configuration') + return + } + const qgResult = await getQgResult(path.join(result_path, 'qg-result.yaml')) + const resultRequirements = getQgResultRequirements(qgResult) + for (const [requirementId, configRequirement] of Object.entries( + config.requirements + )) { + const requirement = resultRequirements[requirementId] + if (!requirement) { + console.log(`Requirement ${requirementId} not found`) + continue + } + const jiraComment = createJiraComment(requirement) + const sources = getSourcesOfRequirement(requirement) + const promises: Promise[] = [] + for (const issueId of configRequirement.issues) { + promises.push(addComment(issueId, jiraComment)) + for (const source of sources) { + const attachmentPath = await getFilepath(result_path, source) + promises.push(addAttachment(issueId, attachmentPath)) + } + } + await Promise.all(promises) + } + console.log('Updated issues') + return +} + +export const getSourcesOfRequirement = (requirement: Requirement): string[] => { + const allSources: string[] = [] + if (!requirement.checks) { + return allSources + } + Object.values(requirement.checks).forEach((check) => { + if (!check.reports) { + return + } + check.reports.forEach((report) => { + if (!report.componentResults) { + return + } + report.componentResults.forEach((componentResult) => { + if (!componentResult.sources) { + return + } + const applicableSources = componentResult.sources.flatMap((source) => { + if (source['jiraUpload'] != undefined) { + return source['jiraUpload'] + } + return [] + }) + allSources.push(...applicableSources) + }) + }) + }) + return allSources +} + +function createJiraComment(requirement: Requirement): string { + let comment: string = + `Requirement: ${requirement.title}\n` + + `Text: ${requirement.text}\n` + + `Status: ${requirement.status}\n` + if (!requirement.checks) { + comment += `Reason: ${requirement.reason}\n` + } else { + for (const [checkId, check] of Object.entries(requirement.checks)) { + for (const report of check.reports) { + for (const componentResult of report.componentResults) { + comment += `Check ${checkId}\n` + comment += `Report ${report.reportType}\n` + comment += `Component ${componentResult.component.id} ${componentResult.component.version}\n` + comment += `Result: ${componentResult.status}\n` + if (componentResult.comments) { + for (const resultComment of componentResult.comments) { + comment += `Comment: ${JSON.stringify(resultComment, null, 2)}\n` + } + comment += '\n' + } + } + } + } + } + return comment +} diff --git a/yaku-apps-typescript/apps/jira-finalizer/src/config.foss.ts b/yaku-apps-typescript/apps/jira-finalizer/src/config.foss.ts new file mode 100644 index 00000000..8049058e --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/src/config.foss.ts @@ -0,0 +1,8 @@ +export const { + JIRA_USERNAME, + JIRA_PASSWORD, + JIRA_API_URL = 'https://jira.example.com/rest/api', + JIRA_API_VERSION = '2', + JIRA_CONFIG_NAME = 'jira-finalizer-config.yaml', + result_path = './samples', +} = process.env diff --git a/yaku-apps-typescript/apps/jira-finalizer/src/config.ts b/yaku-apps-typescript/apps/jira-finalizer/src/config.ts new file mode 100644 index 00000000..8049058e --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/src/config.ts @@ -0,0 +1,8 @@ +export const { + JIRA_USERNAME, + JIRA_PASSWORD, + JIRA_API_URL = 'https://jira.example.com/rest/api', + JIRA_API_VERSION = '2', + JIRA_CONFIG_NAME = 'jira-finalizer-config.yaml', + result_path = './samples', +} = process.env diff --git a/yaku-apps-typescript/apps/jira-finalizer/src/index.ts b/yaku-apps-typescript/apps/jira-finalizer/src/index.ts new file mode 100644 index 00000000..e38a554b --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/src/index.ts @@ -0,0 +1,29 @@ +#!/usr/bin/env node + +import { Command } from 'commander' + +import addComment from './commands/addComment.js' +import addAttachment from './commands/addAttachment.js' +import updateIssues from './commands/updateIssues.js' + +const cli = new Command() + +cli.description('API client for JIRA').version('0.0.1') +cli.name('jira') +cli.usage(' ...') + +cli + .command('add-comment') + .argument('', 'Id of the jira issue') + .argument('', 'Comment to add to the jira issue') + .action((issueId, comment) => addComment(issueId, comment)) + +cli + .command('add-attachment') + .argument('', 'Id of the jira issue') + .argument('', 'Path to the file') + .action((issueId, filePath) => addAttachment(issueId, filePath)) + +cli.command('update-issues').action(() => updateIssues()) + +cli.parse(process.argv) diff --git a/yaku-apps-typescript/apps/jira-finalizer/src/utils/getClient.ts b/yaku-apps-typescript/apps/jira-finalizer/src/utils/getClient.ts new file mode 100644 index 00000000..e7a7eafc --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/src/utils/getClient.ts @@ -0,0 +1,26 @@ +import { JiraClient, RestClientImpl } from '../../lib/index.js' +// import { DebugClient } from "../../lib/index.js"; + +import { + JIRA_API_URL, + JIRA_API_VERSION, + JIRA_PASSWORD, + JIRA_USERNAME, +} from '../config.js' + +export default (): JiraClient => { + if (!JIRA_USERNAME) { + throw new Error('Environment JIRA_USERNAME is not defined') + } + if (!JIRA_PASSWORD) { + throw new Error('Environment JIRA_PASSWORD is not defined') + } + const basicAuth = Buffer.from(`${JIRA_USERNAME}:${JIRA_PASSWORD}`).toString( + 'base64' + ) + const crudClient = new RestClientImpl( + JIRA_API_URL + `/${JIRA_API_VERSION}`, + basicAuth + ) + return new JiraClient(crudClient) +} diff --git a/yaku-apps-typescript/apps/jira-finalizer/src/utils/getFilepath.ts b/yaku-apps-typescript/apps/jira-finalizer/src/utils/getFilepath.ts new file mode 100644 index 00000000..73eaf2ce --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/src/utils/getFilepath.ts @@ -0,0 +1,29 @@ +import { readdir, stat } from 'fs/promises' +import path from 'path' + +async function getFilesRecursive(dir: string): Promise { + const files: string[] = [] + for (const file of await readdir(dir)) { + const fullPath = path.join(dir, file) + const stats = await stat(fullPath) + if (stats.isDirectory()) { + files.push(...(await getFilesRecursive(fullPath))) + continue + } + files.push(fullPath) + } + return files +} + +export default async function ( + dirpath: string, + filename: string +): Promise { + const files: string[] = await getFilesRecursive(dirpath) + const file = files.find((file) => file.endsWith(filename)) + if (file) { + return file + } else { + throw new Error(`File ${filename} not found in ${dirpath}`) + } +} diff --git a/yaku-apps-typescript/apps/jira-finalizer/src/utils/getJiraConfig.ts b/yaku-apps-typescript/apps/jira-finalizer/src/utils/getJiraConfig.ts new file mode 100644 index 00000000..0218fb9e --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/src/utils/getJiraConfig.ts @@ -0,0 +1,23 @@ +import { readFile } from 'fs/promises' +import path from 'path' +import { parse } from 'yaml' + +interface ConfigRequirement { + [key: string]: { + issues: string[] + } +} + +type Config = { + requirements: ConfigRequirement +} + +export default async function (filePath: string): Promise { + const configPath = path.resolve(filePath) + const data = await readFile(configPath, { encoding: 'utf-8' }) + try { + return parse(data) as Config + } catch (e) { + throw new Error(`Config file ${filePath} is not a valid yaml file`) + } +} diff --git a/yaku-apps-typescript/apps/jira-finalizer/src/utils/getQgResult.ts b/yaku-apps-typescript/apps/jira-finalizer/src/utils/getQgResult.ts new file mode 100644 index 00000000..55db5cdd --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/src/utils/getQgResult.ts @@ -0,0 +1,63 @@ +import { readFile } from 'fs/promises' +import yaml from 'yaml' +import path from 'path' + +export interface Checks { + [key: string]: { + title: string + reports: [ + { + reportType?: string + componentResults: [ + { + component: { + version: string + id: string + } + status: Status + comments: any[] + sources: any[] + } + ] + } + ] + } +} + +export interface Requirement { + title: string + text: string + reason?: string + status: Status + checks?: Checks +} + +export interface Allocation { + title: string + status: Status + requirements: { + [key: string]: Requirement + } +} + +export interface QgResult { + header: { + name: string + version: string + date: string + qgCliVersion: string + } + allocations: { + [key: string]: Allocation + } + overallStatus: Status +} + +export default async (filePath: string): Promise => { + const resultPath = path.resolve(filePath) + const qgResultContent = await readFile(resultPath, 'utf8') + const qgResult = yaml.parse(qgResultContent) + return qgResult as QgResult +} + +type Status = 'GREEN' | 'YELLOW' | 'RED' | 'FAILED' | 'PENDING' | 'NA' diff --git a/yaku-apps-typescript/apps/jira-finalizer/src/utils/getQgResultRequirements.ts b/yaku-apps-typescript/apps/jira-finalizer/src/utils/getQgResultRequirements.ts new file mode 100644 index 00000000..e8d96401 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/src/utils/getQgResultRequirements.ts @@ -0,0 +1,17 @@ +import { QgResult, Requirement } from './getQgResult' + +type Requirements = { + [key: string]: Requirement +} + +export default function (qgResult: QgResult): Requirements { + const requirements: Requirements = {} + for (const [_, allocation] of Object.entries(qgResult.allocations)) { + for (const [rquirementId, requirement] of Object.entries( + allocation.requirements + )) { + requirements[rquirementId] = requirement + } + } + return requirements +} diff --git a/yaku-apps-typescript/apps/jira-finalizer/tsconfig.json b/yaku-apps-typescript/apps/jira-finalizer/tsconfig.json new file mode 100644 index 00000000..e1f51365 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@B-S-F/typescript-config/base.json", + "ts-node": { + "compilerOptions": { + "module": "ESNext" + } + }, + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/yaku-apps-typescript/apps/jira-finalizer/tsup.config.json b/yaku-apps-typescript/apps/jira-finalizer/tsup.config.json new file mode 100644 index 00000000..f35b9ed3 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/tsup.config.json @@ -0,0 +1,6 @@ +{ + "entry": ["src/index.ts"], + "splitting": false, + "sourcemap": true, + "clean": true +} diff --git a/yaku-apps-typescript/apps/jira-finalizer/tsup.config.ts b/yaku-apps-typescript/apps/jira-finalizer/tsup.config.ts new file mode 100644 index 00000000..4eda81ce --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/**/*.ts', 'lib/**/*.ts'], + splitting: false, + clean: true, + target: 'node18', + format: ['esm'], + bundle: false, + sourcemap: true, +}) diff --git a/yaku-apps-typescript/apps/jira-finalizer/vitest.config.ts b/yaku-apps-typescript/apps/jira-finalizer/vitest.config.ts new file mode 100644 index 00000000..aa0273f1 --- /dev/null +++ b/yaku-apps-typescript/apps/jira-finalizer/vitest.config.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + typecheck: { + tsconfig: 'tsconfig.json', + }, + coverage: { + provider: 'v8', + all: true, + enabled: true, + reporter: ['cobertura', 'json-summary', 'text-summary'], + include: ['src'], + exclude: ['src/index.ts', 'lib/index.ts', 'lib/clients/debug-client.ts'], + }, + reporters: ['junit', 'default'], + outputFile: 'reports/test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/json-evaluator/.env.sample b/yaku-apps-typescript/apps/json-evaluator/.env.sample new file mode 100644 index 00000000..2f967ce6 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/.env.sample @@ -0,0 +1,3 @@ +export JSON_INPUT_FILE=full_brake_requirements.json +export JSON_CONFIG_FILE=samples/config.yaml +export CONTINUE_SEARCH_ON_FAIL=false diff --git a/yaku-apps-typescript/apps/json-evaluator/.eslintrc.cjs b/yaku-apps-typescript/apps/json-evaluator/.eslintrc.cjs new file mode 100644 index 00000000..261ba30d --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/.eslintrc.cjs @@ -0,0 +1,16 @@ +module.exports = { + extends: ['@B-S-F/eslint-config/eslint-preset'], + rules: { + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-non-null-assertion': 'off', + 'no-sparse-arrays': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { destructuredArrayIgnorePattern: '^([.]{3})?_' }, + ], + 'no-control-regex': 0, + "no-restricted-imports": ["error", { + "patterns": ["*.js"] + }] + }, +} diff --git a/yaku-apps-typescript/apps/json-evaluator/.prettierrc b/yaku-apps-typescript/apps/json-evaluator/.prettierrc new file mode 100644 index 00000000..3134643b --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 80 +} diff --git a/yaku-apps-typescript/apps/json-evaluator/README.md b/yaku-apps-typescript/apps/json-evaluator/README.md new file mode 100644 index 00000000..0bf5e168 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/README.md @@ -0,0 +1,3 @@ +# json-evaluator + +The json-evaluator is a tool to evaluate generic json files. It can be used to evaluate the output of a fetcher. diff --git a/yaku-apps-typescript/apps/json-evaluator/package.json b/yaku-apps-typescript/apps/json-evaluator/package.json new file mode 100644 index 00000000..fe8fb92d --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/package.json @@ -0,0 +1,50 @@ +{ + "name": "@B-S-F/json-evaluator", + "version": "0.11.0", + "description": "", + "main": "dist/index.js", + "type": "module", + "scripts": { + "build": "tsup && tsc-alias", + "dev": "nodemon --watch \"src/**\" --exec npm run start", + "start": "node ./dist/index.js", + "lint": "eslint '**/*.ts'", + "format": "prettier --write '**/*.{ts,md}'", + "test": "vitest run && npm run test:update-cobertura-file", + "test:update-cobertura-file": "sed -i'.bak' 's,package name=\",package name=\"'${npm_package_name}/',g' coverage/cobertura-coverage.xml", + "test:dev": "vitest -w", + "test:coverage": "vitest --coverage", + "test:ui": "vitest --ui", + "test:integration:local": "npx rimraf dist && npm run build && npm run test:integration:ci", + "test:integration:ci": "npx vitest run --config vitest-integration.config.ts" + }, + "keywords": [], + "author": "", + "license": "", + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/json2csv": "^5.0.3", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "prettier": "*", + "tsc-alias": "^1.8.8", + "tsup": "*", + "typescript": "*", + "vitest": "*" + }, + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "@B-S-F/json-evaluator-lib": "^0.9.0", + "colors": "1.4.0", + "yaml": "^2.2.1", + "zod": "^3.22.3", + "zod-error": "^1.5.0" + }, + "bin": { + "json-evaluator": "dist/index.js" + } +} diff --git a/yaku-apps-typescript/apps/json-evaluator/src/evaluate.ts b/yaku-apps-typescript/apps/json-evaluator/src/evaluate.ts new file mode 100644 index 00000000..1029866b --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/src/evaluate.ts @@ -0,0 +1,75 @@ +import { + evalCheck, + evalConcatenation, + readJson, +} from '@B-S-F/json-evaluator-lib' +import { AppOutput, Result } from '@B-S-F/autopilot-utils' + +import { ChecksResult, Config, Concatenation } from './types' +import Formatter from './formatter' + +export const evaluate = async ( + jsonFile: string, + config: Config, +): Promise => { + const results: Result[] = [] + const data = await readJson(jsonFile) + // evaluate checks + const checksResult: ChecksResult = {} + for (const check of config.checks) { + const checkResults = evalCheck(check.condition, check.ref, data, { + ...check, + }) + + checksResult[check.name] = checkResults + + checkResults.reasonPackages + ?.map((reasonPackage) => + Formatter.formatReasonPackage(check, checkResults, reasonPackage), + ) + .forEach((result) => result && results.push(result)) + + if (checkResults.reasonPackages?.length === 0) { + const result = Formatter.formatReasonPackage(check, checkResults, { + reasons: [], + context: undefined, + }) + if (result !== undefined) results.push(result) + } + } + // evaluate concatenation + const concatenation: Concatenation = { + condition: config.concatenation + ? config.concatenation.condition + : Object.values(config.checks) + .map(({ name }) => name) + .join(' && '), + } + + const { condition: concatCondition, status } = evalConcatenation( + concatenation.condition, + checksResult, + ) + + if (config.concatenation) { + results.push({ + criterion: `**CONCATENATION CONDITION:** ${concatCondition}`, + justification: `Evaluation result is "${status}" with this condition`, + fulfilled: status === 'GREEN', + }) + } + + const appOutput = new AppOutput() + appOutput.setStatus(status) + + appOutput.setReason( + status === 'GREEN' + ? 'All fields have valid values' + : 'Some fields do not have valid values', + ) + for (const result of results) { + appOutput.addResult(result) + } + + return appOutput +} diff --git a/yaku-apps-typescript/apps/json-evaluator/src/formatter.ts b/yaku-apps-typescript/apps/json-evaluator/src/formatter.ts new file mode 100644 index 00000000..5fc4cb2b --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/src/formatter.ts @@ -0,0 +1,237 @@ +import { CheckResults } from '@B-S-F/json-evaluator-lib' +import { AppError, GetLogger, Result } from '@B-S-F/autopilot-utils' +import { ReasonPackage } from '@B-S-F/json-evaluator-lib/src/types' + +import { parseReasons } from './print' +import SentenceBuilder from './sentence-builder' +import { Check } from './types' + +const JSONPathOperations: { [key: string]: string } = { + '==': 'equal to', + '===': 'equal to', + '!=': 'not equal to', + '!==': 'not equal to', + '<': 'less than', + '<=': 'less than or equal to', + '>': 'greater than', + '>=': 'greater than or equal to', + includes: 'including', +} +export default class Formatter { + static getConditionQuantity = (condition: string | undefined) => { + if (!condition) { + throw new AppError('Missing condition') + } + + const quantityPattern = /\b(all|any|one|none)\(.+?\)/g + const match = condition.match(quantityPattern)?.toString() + + return match?.substring(0, match.indexOf('(')) + } + + static isolateCondition = (condition: string | undefined) => { + if (!condition) { + throw new AppError('Missing condition') + } + + const inQuotesPattern = /("[^"]*")/ + const quoteMatch = condition.match(inQuotesPattern) + const alreadyIsolated = !(quoteMatch && quoteMatch[1]) + + return alreadyIsolated ? condition : quoteMatch[1] + } + + static tokenizeCondition = (condition: string | undefined) => { + if (!condition) { + throw new AppError('Missing condition') + } + const operators = Object.keys(JSONPathOperations).sort().reverse().join('|') + const receiverString = `\\[*(.+?)\\]*` + const subjectString = `(.*?\\$.+?)` + const endingSubjectString = `(.*?\\$.+)` + + const conditionSplitMatch = new RegExp( + `^\\s*${subjectString}\\s*(${operators})\\s*${receiverString}\\s*$|^\\s*${receiverString}\\s*(${operators})\\s*${endingSubjectString}\\s*`, + ) + + const match = condition.match(conditionSplitMatch) + if (!match) { + throw new AppError('Condition exists, but no participants were matched') + } + + const cleanMatch = match.filter((match) => match !== undefined) + return cleanMatch.slice(1) + } + + static cleanString = (dirtyString: string | undefined) => { + if (!dirtyString) { + return '' + } + + const lettersAndNumbersRegex = /[a-zA-Z0-9]+/g + const cleanString = dirtyString.match(lettersAndNumbersRegex)?.join(' ') + if (!cleanString) { + return '' + } + return cleanString + } + + static getConditionParticipants = ( + condition: string | undefined, + quantity?: string, + ) => { + // Condition: all(ref, "$.category === 'fiction'") + const isolatedCondition = quantity + ? this.isolateCondition(condition) + : condition + + // isolatedCondition: $.category === 'fiction' + const [token1, operation, token2]: string[] = + this.tokenizeCondition(isolatedCondition) + + // token1: $.category, operation: '===', token2: 'fiction' + const [dirtySubject, dirtyReceiver]: string[] = token1.includes('$') + ? [token1, token2] + : [token2, token1] + + const subject = this.cleanString(dirtySubject) + const operationName = JSONPathOperations[operation] + const receiver = this.cleanString(dirtyReceiver) + + // subject: category, operation: 'is equal to', receiver: fiction + + return [subject, operationName, receiver] + } + + static getJustificationMessage = ( + quantity: string | undefined, + reasons: string, + ) => { + if (!quantity) { + if (!reasons) { + return 'No resulted values from this query' + } + return `Actual values equal: "**${reasons}**"` + } + + let message = '' + switch (quantity) { + case 'all': + message = `One or more values do not satisfy the condition: ` + break + case 'any': + message = `None satisfy the condition. Actual values are: ` + break + case 'one': + message = `None or more than one values satisfy the condition: ` + break + case 'none': + message = `Some values satisfy the condition: ` + break + default: + throw new AppError('Bad quantity') + } + + if (!reasons) return message + '**not defined**' + return `${message}"**${reasons}**"` + } + + static getReasonMessage = ( + reasons: string, + context: { property: string | undefined; value: string | undefined }, + ) => { + const contextPrefix = ', ' + if (context.value) { + if (context.value.includes('https')) { + return reasons + contextPrefix + `[${context.value}](${context.value})` + } + return reasons + contextPrefix + context.value + } + if (!context.value && context.property) { + const logger = GetLogger() + logger.warn( + 'Warning: log value not found for property: ' + context.property, + ) + } + return reasons + } + + static formatMessage = ( + checkName: string, + check: Partial< + Omit & { reasonPackage: ReasonPackage } + >, + options?: { + logProperty: string | undefined + }, + ) => { + const underscoreRegex = /_+/g + + const name = checkName.replace(underscoreRegex, ' ').toUpperCase() + const quantity = this.getConditionQuantity(check.condition) + + const [subject, operation, receiver]: string[] = + this.getConditionParticipants(check.condition, quantity) + + const reference: string = this.cleanString(check.ref) + + const result: string = check.status || 'FAILED' + + const reasonPackage = parseReasons(check.reasonPackage) + const reasonsString = this.getReasonMessage( + reasonPackage.reasons.join(', '), + { + property: options?.logProperty, + value: reasonPackage.context, + }, + ) + + const justification = + result === 'GREEN' + ? 'Field content satisfy condition' + : this.getJustificationMessage(quantity, reasonsString) + + const operationString = new SentenceBuilder().getOperation( + quantity, + subject, + reference, + operation, + receiver, + ) + + const finalResult: Result = { + criterion: name ? `**${name}:**${operationString}` : `**Check:**`, + fulfilled: result === 'GREEN', + metadata: { + status: result, + }, + justification, + } + return finalResult + } + + static formatReasonPackage( + check: Check, + checkResults: CheckResults, + reasonPackage: ReasonPackage, + ) { + try { + const buffer = { + status: checkResults.status, + ref: checkResults.ref, + condition: checkResults.condition, + bool: checkResults.bool, + reasonPackage, + } + const result = Formatter.formatMessage(check.name, buffer, { + logProperty: check.log, + }) + + return result + } catch (error) { + console.log( + `Something went wrong while formatting check: ${check.name} result. Non-formatted result: ${reasonPackage.reasons}, ${reasonPackage.context}. Error: ${error}`, + ) + } + } +} diff --git a/yaku-apps-typescript/apps/json-evaluator/src/index.ts b/yaku-apps-typescript/apps/json-evaluator/src/index.ts new file mode 100644 index 00000000..f914c6cf --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/src/index.ts @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { main } from './main' + +main() diff --git a/yaku-apps-typescript/apps/json-evaluator/src/logger.ts b/yaku-apps-typescript/apps/json-evaluator/src/logger.ts new file mode 100644 index 00000000..eda7899a --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/src/logger.ts @@ -0,0 +1,58 @@ +import stream from 'stream' + +type Chunk = string | Buffer | Uint8Array +type Encoding = BufferEncoding | undefined +type Callback = (error?: Error | null) => void +type WriteStream = typeof process.stdout.write + +const urlPattern = new RegExp( + /https?:\/\/(www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@%_+.~#?&//=]*)/gi, +) +const colorsPattern = new RegExp( + /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/gi, +) + +export class Logger { + private logStream: stream.PassThrough + private originalStdoutWrite: WriteStream + + constructor() { + this.logStream = new stream.PassThrough() + this.originalStdoutWrite = process.stdout.write + + process.stdout.write = this.writeToStream.bind(this) as WriteStream + } + + writeToStream(chunk: Chunk, encoding?: Encoding, callback?: Callback) { + this.logStream.write(chunk, encoding, callback) + this.originalStdoutWrite.apply(process.stdout, [chunk, encoding, callback]) + } + + end() { + this.logStream.end() + } + + restore() { + process.stdout.write = this.originalStdoutWrite + } + + getLogString() { + return new Promise((resolve, reject) => { + const logChunks: string[] = [] + this.logStream.on('data', (chunk) => { + let str = chunk.toString() + str = str.replace(colorsPattern, '') // remove color codes + str = str.replace(urlPattern, (match: string) => `[${match}](${match})`) // make urls clickable + logChunks.push(str) + }) + + this.logStream.on('end', () => { + resolve(logChunks.join('')) + }) + + this.logStream.on('error', (error) => { + reject(error) + }) + }) + } +} diff --git a/yaku-apps-typescript/apps/json-evaluator/src/main.ts b/yaku-apps-typescript/apps/json-evaluator/src/main.ts new file mode 100644 index 00000000..fd36b898 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/src/main.ts @@ -0,0 +1,46 @@ +import { AppOutput, AppError } from '@B-S-F/autopilot-utils' +import { parseConfig } from './parse-config' +import { Logger } from './logger' +import { evaluate } from './evaluate' +import { getPathFromEnvVariable } from './util' + +export const checkEnvironmentVariables = () => { + if (!process.env.JSON_INPUT_FILE) { + throw new AppError('Env variable "JSON_INPUT_FILE" is not provided') + } + if (!process.env.JSON_CONFIG_FILE) { + throw new AppError('Env variable "JSON_CONFIG_FILE" is not provided') + } +} + +export const main = async () => { + try { + const logger = new Logger() + checkEnvironmentVariables() + const configFilePath = getPathFromEnvVariable('JSON_CONFIG_FILE') + const dataFilePath = getPathFromEnvVariable('JSON_INPUT_FILE') + const config = await parseConfig(configFilePath) + + const appOutput = await evaluate(dataFilePath, config) + appOutput.write() + logger.end() + } catch (e) { + if (e instanceof AppError) { + console.log(e) + console.log(e.Reason()) + const appOutput = new AppOutput() + appOutput.setStatus('FAILED') + appOutput.setReason(e.Reason()) + appOutput.write() + process.exit(0) + } else { + const error = e as { name: string; message: string } + const errMsg = + error.name && error.message + ? `${error.name}: ${error.message}` + : `${error}` + console.error(errMsg.red) + throw e // to show stack trace + } + } +} diff --git a/yaku-apps-typescript/apps/json-evaluator/src/parse-config.ts b/yaku-apps-typescript/apps/json-evaluator/src/parse-config.ts new file mode 100644 index 00000000..7b873499 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/src/parse-config.ts @@ -0,0 +1,50 @@ +import { readFile } from 'fs/promises' +import YAML from 'yaml' +import { AppError } from '@B-S-F/autopilot-utils' +import { generateErrorMessage } from 'zod-error' + +import { Check, Config, ConfigSchema, variableRegex } from './types' +import { isValidCheckIndex } from './util' + +export const readYamlData = async ( + filePath: string, +): Promise<{ checks: Check[] }> => { + try { + const data = await readFile(filePath, 'utf-8') + return YAML.parse(data) + } catch (error) { + throw new AppError( + `File ${filePath} could not be read, failed with error: ${error}`, + ) + } +} + +export const parseConfig = async (filepath: string) => { + const config = await readYamlData(filepath) + const parsedSchema = ConfigSchema.safeParse(config) + + if (!parsedSchema.success) { + for (const { code, path } of parsedSchema.error.issues) { + const invalidNameString = + code === 'invalid_string' && path[2] && path[2] === 'name' + + if (invalidNameString && isValidCheckIndex(path[1])) { + // On an invalid string, the path result for checks is [ 'checks', , 'name' ] + const checkIndex = path[1] + + // This helps remove bad emphasises in markdown + const checkNameWithUnderscores = config.checks[checkIndex].name.replace( + /_/g, + '\\_', + ) + + const msg = `check _${checkNameWithUnderscores}_ contains not-allowed characters, allowed characters are alphanumeric and underscores (\\_). [Regexp](https://regex101.com/) used to validate check names: _${variableRegex}_` + throw new AppError(msg) + } + } + + const msg = generateErrorMessage(parsedSchema.error.issues) + throw new AppError(msg) + } + return parsedSchema.data as Config +} diff --git a/yaku-apps-typescript/apps/json-evaluator/src/print.ts b/yaku-apps-typescript/apps/json-evaluator/src/print.ts new file mode 100644 index 00000000..e3cee09e --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/src/print.ts @@ -0,0 +1,81 @@ +import 'colors' +import { ReasonPackage } from '@B-S-F/json-evaluator-lib/src/types' + +import { PartialCheckResult } from './types' + +export const stringifyFirstLevel = (obj: Record) => { + const firstLevelObj: Record = {} + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + const val = obj[key] + if (typeof val !== 'object' || val === null) { + firstLevelObj[key] = val + } else { + firstLevelObj[key] = `<${typeof val}>` // use the value's type as a placeholder + } + } + } + return JSON.stringify(firstLevelObj) +} + +export const parseReasons = ( + reasonPackage: ReasonPackage | undefined, +): { + context: string | undefined + reasons: string[] +} => { + if (!reasonPackage) { + return { + reasons: [], + context: undefined, + } + } + + const parsedReasonPackage: { + reasons: string[] + context: string | undefined + } = { + reasons: [], + context: reasonPackage.context ? String(reasonPackage.context) : undefined, + } + + parsedReasonPackage.reasons = reasonPackage.reasons.map((reason) => { + if (typeof reason === 'object' && !Array.isArray(reason)) { + return stringifyFirstLevel(reason) + } else { + return JSON.stringify(reason) + } + }) + + return parsedReasonPackage +} + +export function colorStatusString(str: string): string { + const color = str.toLowerCase() + switch (color) { + case 'red': + return str.red + case 'green': + return str.green + case 'yellow': + return str.yellow + default: + return str + } +} + +export const printCheckResult = ( + checkName: string, + check: PartialCheckResult, +) => { + const name = checkName.toUpperCase() + console.log('\n' + name + '\n' + '-'.repeat(name.length)) + if (check.ref) console.log('* **ref**: ' + `${check.ref}`.blue) + console.log('* **condition**: ' + `${check.condition}`.blue) + if (check.bool) console.log('* **result**: ' + `${check.bool}`.blue) + console.log('* **status**: ' + `${colorStatusString(check.status!)}`) + if (check.reasonPackage && check.reasonPackage!.reasons.length > 0) + console.log( + '* **reasons**: ' + `${parseReasons(check.reasonPackage!).reasons}`, + ) +} diff --git a/yaku-apps-typescript/apps/json-evaluator/src/sentence-builder.ts b/yaku-apps-typescript/apps/json-evaluator/src/sentence-builder.ts new file mode 100644 index 00000000..d909b720 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/src/sentence-builder.ts @@ -0,0 +1,49 @@ +export default class SentenceBuilder { + message: string + constructor() { + this.message = '' + } + + isPlural(quantity: string): boolean { + // Change this to something that respects open closed principle + if (quantity === 'one' || quantity === '') { + return false + } + return true + } + + getOperation( + quantity: string | undefined, + subject: string, + reference: string, + operation: string, + receiver: string, + ): string { + const operationsList = [] + + if (quantity) { + operationsList.push(`${quantity} `) + } + + if (subject) { + operationsList.push(`_${subject}_ `) + } + + if (reference) { + operationsList.push(`_${reference}_ `) + } + + if (subject || reference) { + operationsList.push(quantity && this.isPlural(quantity) ? 'are ' : 'is ') + } + + operationsList.push(`${operation} `) + operationsList.push(receiver ? `_${receiver}_` : '\n') + + return operationsList.join('') + } + + build() { + return this.message + } +} diff --git a/yaku-apps-typescript/apps/json-evaluator/src/types.ts b/yaku-apps-typescript/apps/json-evaluator/src/types.ts new file mode 100644 index 00000000..d1293be9 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/src/types.ts @@ -0,0 +1,45 @@ +import { z } from 'zod' + +import { CheckResults } from '@B-S-F/json-evaluator-lib' +import { ReasonPackage } from '@B-S-F/json-evaluator-lib/src/types' + +const checkConditionRegex = />|<|>=|<=|===|==|!==|!=|includes/ +const concatConditionRegex = /^(\w+)((\s*(&&|\|\|)\s*((\w+)*)))*$/ +export const variableRegex = /^(\w+(\.\w+)*)$/ + +const Status = z.enum(['GREEN', 'YELLOW', 'RED']) + +const CheckSchema = z + .object({ + name: z.string().regex(variableRegex), + ref: z.string().startsWith('$'), + condition: z.string().regex(checkConditionRegex), + true: Status.optional(), + false: Status.optional(), + log: z.string().startsWith('$').optional(), + return_if_empty: Status.optional(), + return_if_not_found: Status.optional(), + }) + .strict() + +const ConcatenationSchema = z + .object({ + condition: z.string().regex(concatConditionRegex), + }) + .strict() + +export const ConfigSchema = z + .object({ + checks: z.array(CheckSchema), + concatenation: ConcatenationSchema.optional(), + }) + .strict() + +export type Config = z.infer +export type Check = z.infer +export type Concatenation = z.infer +export type Status = z.infer +export type ChecksResult = Record +export type PartialCheckResult = Partial< + Omit & { reasonPackage: ReasonPackage } +> diff --git a/yaku-apps-typescript/apps/json-evaluator/src/util.ts b/yaku-apps-typescript/apps/json-evaluator/src/util.ts new file mode 100644 index 00000000..1f954acb --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/src/util.ts @@ -0,0 +1,31 @@ +import { AppError } from '@B-S-F/autopilot-utils' + +import fs from 'fs' +import path from 'path' + +export function getPathFromEnvVariable(envVariableName: string): string { + const filePath: string | undefined = process.env[envVariableName] || '' + const relativePath = path.relative(process.cwd(), filePath.trim()) + validateFilePath(relativePath) + return relativePath +} + +export function validateFilePath(filePath: string): void { + if (!fs.existsSync(filePath)) { + throw new AppError( + `File ${filePath} does not exist, no data can be evaluated`, + ) + } + try { + fs.accessSync(filePath, fs.constants.R_OK) + } catch (e) { + throw new AppError(`${filePath} is not readable!`) + } + if (!fs.statSync(filePath).isFile()) { + throw new AppError(`${filePath} does not point to a file!`) + } +} + +export function isValidCheckIndex(value: unknown): value is number { + return typeof value === 'number' && Number.isInteger(value) +} diff --git a/yaku-apps-typescript/apps/json-evaluator/test/integration/brake.int-spec.ts b/yaku-apps-typescript/apps/json-evaluator/test/integration/brake.int-spec.ts new file mode 100644 index 00000000..9f44b043 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/integration/brake.int-spec.ts @@ -0,0 +1,122 @@ +import { describe, it, expect, beforeAll } from 'vitest' +import * as fs from 'fs' +import * as path from 'path' +import { run, RunProcessResult } from '../../../../integration-tests/src/util' + +const baseIssues = [ + { + status: 'RED', + reason: 'Some fields do not have valid values', + }, + { + result: { + criterion: + '**NOT REVIEWED CHECK:**all _Not reviewed_ _results_ are equal to _0_', + fulfilled: false, + metadata: { status: 'RED' }, + justification: + 'One or more values do not satisfy the condition: "**"52", 2022-07-05T00:00:00.000+02:00**"', + }, + }, + { + result: { + criterion: + '**PARTLY COMPLETED CHECK:**all _Reviews partly completed_ _results_ are equal to _0_', + fulfilled: false, + metadata: { status: 'YELLOW' }, + justification: + 'One or more values do not satisfy the condition: "**"29", 2022-07-05T00:00:00.000+02:00**"', + }, + }, + { + result: { + criterion: + '**CONCATENATION CONDITION:** not_reviewed_check && partly_completed_check', + justification: 'Evaluation result is "RED" with this condition', + fulfilled: false, + }, + }, +] +const extenedIssues = [ + { + result: { + criterion: + '**NOT REVIEWED CHECK:**all _Not reviewed_ _results_ are equal to _0_', + fulfilled: false, + metadata: { status: 'RED' }, + justification: + 'One or more values do not satisfy the condition: "**"72", 2022-10-25T00:00:00.000+02:00**"', + }, + }, + { + result: { + criterion: + '**PARTLY COMPLETED CHECK:**all _Reviews partly completed_ _results_ are equal to _0_', + fulfilled: false, + metadata: { status: 'YELLOW' }, + justification: + 'One or more values do not satisfy the condition: "**"60", 2022-12-11T00:00:00.000+01:00**"', + }, + }, +] + +describe('brake.json', async () => { + const jsonEvaluatorExecutable: string = path.join( + __dirname, + '..', + '..', + 'dist', + 'index.js', + ) + + beforeAll(() => { + expect(fs.existsSync(jsonEvaluatorExecutable)).to.be.true + }) + + it('can be evaluated properly while checking for ONE breaking element', async () => { + const env = { + JSON_INPUT_FILE: `${__dirname}/../samples/brake_data.json`, + JSON_CONFIG_FILE: `${__dirname}/../samples/brake.yaml`, + CONTINUE_SEARCH_ON_FAIL: 'false', + } + + const result: RunProcessResult = await run(jsonEvaluatorExecutable, [], { + env, + }) + const results = result.stdout.reduce( + (count, str) => count + (str.includes('result') ? 1 : 0), + 0, + ) + + for (const issue of baseIssues) { + expect(result.stdout).toContain(JSON.stringify(issue)) + } + expect(results).toEqual(3) + expect(result.stderr).to.be.empty + }) + + it('can be evaluated properly while checking for ALL breaking elements', async () => { + const env = { + JSON_INPUT_FILE: `${__dirname}/../samples/brake_data.json`, + JSON_CONFIG_FILE: `${__dirname}/../samples/brake.yaml`, + CONTINUE_SEARCH_ON_FAIL: 'true', + } + + const result: RunProcessResult = await run(jsonEvaluatorExecutable, [], { + env, + }) + const results = result.stdout.reduce( + (count, str) => count + (str.includes('result') ? 1 : 0), + 0, + ) + + for (const issue of baseIssues) { + expect(result.stdout).toContain(JSON.stringify(issue)) + } + for (const issue of extenedIssues) { + expect(result.stdout).toContain(JSON.stringify(issue)) + } + expect(results).toEqual(317) + expect(result.stderr).to.be.empty + }) +}) diff --git a/yaku-apps-typescript/apps/json-evaluator/test/integration/coverage.int-spec.ts b/yaku-apps-typescript/apps/json-evaluator/test/integration/coverage.int-spec.ts new file mode 100644 index 00000000..b66fcffd --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/integration/coverage.int-spec.ts @@ -0,0 +1,56 @@ +import { describe, it, expect, beforeAll } from 'vitest' +import * as fs from 'fs' +import * as path from 'path' +import { run, RunProcessResult } from '../../../../integration-tests/src/util' + +const baseIssues = [ + { + status: 'RED', + reason: 'Some fields do not have valid values', + }, + { + result: { + criterion: + '**HAS GOOD COVERAGE:**_totals percent covered_ is greater than or equal to _75_', + fulfilled: false, + metadata: { status: 'RED' }, + justification: 'Actual values equal: "**[70.6989247311828]**"', + }, + }, +] + +describe('coverage.json', async () => { + const jsonEvaluatorExecutable: string = path.join( + __dirname, + '..', + '..', + 'dist', + 'index.js', + ) + + beforeAll(() => { + expect(fs.existsSync(jsonEvaluatorExecutable)).to.be.true + }) + + it('can be evaluated properly', async () => { + const env = { + JSON_INPUT_FILE: `${__dirname}/../samples/coverage_data.json`, + JSON_CONFIG_FILE: `${__dirname}/../samples/coverage.yaml`, + CONTINUE_SEARCH_ON_FAIL: 'false', + } + + const result: RunProcessResult = await run(jsonEvaluatorExecutable, [], { + env, + }) + const results = result.stdout.reduce( + (count, str) => count + (str.includes('result') ? 1 : 0), + 0, + ) + + for (const issue of baseIssues) { + expect(result.stdout).toContain(JSON.stringify(issue)) + } + expect(results).toEqual(1) + expect(result.stderr).to.be.empty + }) +}) diff --git a/yaku-apps-typescript/apps/json-evaluator/test/integration/failure.int-spec.ts b/yaku-apps-typescript/apps/json-evaluator/test/integration/failure.int-spec.ts new file mode 100644 index 00000000..2358b936 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/integration/failure.int-spec.ts @@ -0,0 +1,87 @@ +import { describe, it, expect, beforeAll } from 'vitest' +import * as fs from 'fs' +import * as path from 'path' +import { run, RunProcessResult } from '../../../../integration-tests/src/util' + +describe('Fail', async () => { + const jsonEvaluatorExecutable: string = path.join( + __dirname, + '..', + '..', + 'dist', + 'index.js', + ) + + const testFileLocationPrefix = `${__dirname}/../samples/` + + const testCases: { + case: string + env: { + JSON_INPUT_FILE: string + JSON_CONFIG_FILE: string + } + expectedOutput: string + exitCode: number + }[] = [ + { + case: 'non-existing config file', + env: { + JSON_INPUT_FILE: 'bitbucket_data.json', + JSON_CONFIG_FILE: 'non-existant-config-file.yaml', + }, + expectedOutput: `{"status":"FAILED","reason":"File test/samples/non-existant-config-file.yaml does not exist, no data can be evaluated"}`, + exitCode: 0, + }, + { + case: 'non-existing json file', + env: { + JSON_INPUT_FILE: 'non-existant-input-file.json', + JSON_CONFIG_FILE: 'bitbucket.yaml', + }, + expectedOutput: `{"status":"FAILED","reason":"File test/samples/non-existant-input-file.json does not exist, no data can be evaluated"}`, + exitCode: 0, + }, + { + case: 'bad config file', + env: { + JSON_INPUT_FILE: 'bitbucket_data.json', + JSON_CONFIG_FILE: 'bad_config.yaml', + }, + expectedOutput: `{"status":"FAILED","reason":"Code: unrecognized_keys ~ Path: checks[0] ~ Message: Unrecognized key(s) in object: 'bad_property'"}`, + exitCode: 0, + }, + { + case: 'bad JSON file', + env: { + JSON_INPUT_FILE: 'bad_JSON_data.json', + JSON_CONFIG_FILE: 'bitbucket.yaml', + }, + expectedOutput: + 'Error: File test/samples/bad_JSON_data.json could not be parsed, failed with error: SyntaxError: Unexpected end of JSON input', + exitCode: 1, + }, + ] + + beforeAll(() => { + expect(fs.existsSync(jsonEvaluatorExecutable)).to.be.true + }) + + it.each(testCases)('%s', async (testCase) => { + const result: RunProcessResult = await run(jsonEvaluatorExecutable, [], { + env: { + JSON_INPUT_FILE: `${testFileLocationPrefix}${testCase.env.JSON_INPUT_FILE}`, + JSON_CONFIG_FILE: `${testFileLocationPrefix}${testCase.env.JSON_CONFIG_FILE}`, + }, + }) + + expect(result.exitCode).toEqual(testCase.exitCode) + + if (testCase.exitCode) { + expect(result.stderr.length).toBeGreaterThan(0) + expect(result.stderr).toContain(testCase.expectedOutput) + } else { + expect(result.stdout.length).toBeGreaterThan(0) + expect(result.stdout).toContain(testCase.expectedOutput) + } + }) +}) diff --git a/yaku-apps-typescript/apps/json-evaluator/test/integration/test_array_data.int-spec.ts b/yaku-apps-typescript/apps/json-evaluator/test/integration/test_array_data.int-spec.ts new file mode 100644 index 00000000..a84517f7 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/integration/test_array_data.int-spec.ts @@ -0,0 +1,93 @@ +import { describe, it, expect, beforeAll } from 'vitest' +import * as fs from 'fs' +import * as path from 'path' +import { run, RunProcessResult } from '../../../../integration-tests/src/util' + +const baseIssues = [ + { + status: 'RED', + reason: 'Some fields do not have valid values', + }, + { + result: { + criterion: + '**HAS CATEGORY CHECK:**_category_ _store book_ is including _fiction_', + fulfilled: true, + metadata: { status: 'GREEN' }, + justification: 'Field content satisfy condition', + }, + }, + { + result: { + criterion: + '**CATEGORY CHECK:**_category_ _store book_ is equal to _fiction reference_', + fulfilled: false, + metadata: { status: 'RED' }, + justification: + 'Actual values equal: "**"reference", "fiction", "fiction", "fiction"**"', + }, + }, + { + result: { + criterion: + '**FICTION CHECK:**all _category_ _store book_ are equal to _fiction_', + fulfilled: false, + metadata: { status: 'RED' }, + justification: + 'One or more values do not satisfy the condition: "**"reference", Sayings of the Century**"', + }, + }, + { + result: { + criterion: + '**NONE FANTASY CHECK:**none _category_ _store book_ are equal to _fantasy_', + fulfilled: true, + metadata: { status: 'GREEN' }, + justification: 'Field content satisfy condition', + }, + }, + { + result: { + criterion: + '**CONCATENATION CONDITION:** has_category_check && category_check && fiction_check && none_fantasy_check', + justification: 'Evaluation result is "RED" with this condition', + fulfilled: false, + }, + }, +] + +describe('test_array_data.json', async () => { + const jsonEvaluatorExecutable: string = path.join( + __dirname, + '..', + '..', + 'dist', + 'index.js', + ) + + beforeAll(() => { + expect(fs.existsSync(jsonEvaluatorExecutable)).to.be.true + }) + + it('can be evaluated properly', async () => { + const env = { + JSON_INPUT_FILE: `${__dirname}/../samples/test_array_data.json`, + JSON_CONFIG_FILE: `${__dirname}/../samples/test_array.yaml`, + CONTINUE_SEARCH_ON_FAIL: 'false', + } + + const result: RunProcessResult = await run(jsonEvaluatorExecutable, [], { + env, + }) + const results = result.stdout.reduce( + (count, str) => count + (str.includes('result') ? 1 : 0), + 0, + ) + + for (const issue of baseIssues) { + expect(result.stdout).toContain(JSON.stringify(issue)) + } + expect(results).toEqual(5) + expect(result.stderr).to.be.empty + }) +}) diff --git a/yaku-apps-typescript/apps/json-evaluator/test/integration/test_single_data.int-spec.ts b/yaku-apps-typescript/apps/json-evaluator/test/integration/test_single_data.int-spec.ts new file mode 100644 index 00000000..a4da508c --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/integration/test_single_data.int-spec.ts @@ -0,0 +1,56 @@ +import { describe, it, expect, beforeAll } from 'vitest' +import * as fs from 'fs' +import * as path from 'path' +import { run, RunProcessResult } from '../../../../integration-tests/src/util' + +const baseIssues = [ + { + status: 'YELLOW', + reason: 'Some fields do not have valid values', + }, + { + result: { + criterion: + '**HAS GOOD COVERAGE:**_totals percent covered_ is greater than or equal to _80_', + fulfilled: false, + metadata: { status: 'YELLOW' }, + justification: 'Actual values equal: "**[70.6989247311828]**"', + }, + }, +] + +describe('test_single_data.json', async () => { + const jsonEvaluatorExecutable: string = path.join( + __dirname, + '..', + '..', + 'dist', + 'index.js', + ) + + beforeAll(() => { + expect(fs.existsSync(jsonEvaluatorExecutable)).to.be.true + }) + + it('can be evaluated properly', async () => { + const env = { + JSON_INPUT_FILE: `${__dirname}/../samples/test_single_data.json`, + JSON_CONFIG_FILE: `${__dirname}/../samples/test_single.yaml`, + CONTINUE_SEARCH_ON_FAIL: 'false', + } + + const result: RunProcessResult = await run(jsonEvaluatorExecutable, [], { + env, + }) + const results = result.stdout.reduce( + (count, str) => count + (str.includes('result') ? 1 : 0), + 0, + ) + + for (const issue of baseIssues) { + expect(result.stdout).toContain(JSON.stringify(issue)) + } + expect(results).toEqual(1) + expect(result.stderr).to.be.empty + }) +}) diff --git a/yaku-apps-typescript/apps/json-evaluator/test/samples/bad_JSON_data.json b/yaku-apps-typescript/apps/json-evaluator/test/samples/bad_JSON_data.json new file mode 100644 index 00000000..c7acd82c --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/samples/bad_JSON_data.json @@ -0,0 +1,3 @@ +{ + "bad_property":{ +} \ No newline at end of file diff --git a/yaku-apps-typescript/apps/json-evaluator/test/samples/bad_config.yaml b/yaku-apps-typescript/apps/json-evaluator/test/samples/bad_config.yaml new file mode 100644 index 00000000..dfa44e39 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/samples/bad_config.yaml @@ -0,0 +1,8 @@ +checks: + - name: bad_check + ref: $.value + condition: all(ref, "'FALSE' === $.state") + log: $.links.self[*].href + bad_property: $.value +concatenation: + condition: 'merged_check && has_reviewer && has_specific_reviewer' diff --git a/yaku-apps-typescript/apps/json-evaluator/test/samples/brake.yaml b/yaku-apps-typescript/apps/json-evaluator/test/samples/brake.yaml new file mode 100644 index 00000000..5203db15 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/samples/brake.yaml @@ -0,0 +1,12 @@ +checks: + - name: not_reviewed_check + ref: $.results[*] + condition: all(ref, "$.Not_reviewed === 0") + log: $._time + - name: partly_completed_check + ref: $.results[*] + condition: all(ref, "$.Reviews_partly_completed === 0") + log: $._time + false: YELLOW +concatenation: + condition: 'not_reviewed_check && partly_completed_check' diff --git a/yaku-apps-typescript/apps/json-evaluator/test/samples/brake_data.json b/yaku-apps-typescript/apps/json-evaluator/test/samples/brake_data.json new file mode 100644 index 00000000..dd741a89 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/samples/brake_data.json @@ -0,0 +1,1617 @@ +{ + "preview": false, + "init_offset": 0, + "messages": [], + "fields": [ + { + "name": "_time", + "groupby_rank": "0" + }, + { + "name": "Not reviewed" + }, + { + "name": "Reviews partly submitted" + }, + { + "name": "All Reviews submitted" + }, + { + "name": "Reviews partly completed", + "type": "str" + }, + { + "name": "All Reviews completed", + "type": "str" + }, + { + "name": "Completed in DA_Review_Finding" + }, + { + "name": "Completed in DA_Review_Finding_Test" + } + ], + "results": [ + { + "_time": "2022-07-05T00:00:00.000+02:00", + "Not reviewed": "52", + "Reviews partly submitted": "35", + "All Reviews submitted": "9", + "Reviews partly completed": "29", + "All Reviews completed": "57", + "Completed in DA_Review_Finding": "81", + "Completed in DA_Review_Finding_Test": "62" + }, + { + "_time": "2022-07-06T00:00:00.000+02:00", + "Not reviewed": "52", + "Reviews partly submitted": "35", + "All Reviews submitted": "9", + "Reviews partly completed": "29", + "All Reviews completed": "57", + "Completed in DA_Review_Finding": "81", + "Completed in DA_Review_Finding_Test": "62" + }, + { + "_time": "2022-07-07T00:00:00.000+02:00", + "Not reviewed": "52", + "Reviews partly submitted": "35", + "All Reviews submitted": "9", + "Reviews partly completed": "29", + "All Reviews completed": "57", + "Completed in DA_Review_Finding": "81", + "Completed in DA_Review_Finding_Test": "62" + }, + { + "_time": "2022-07-08T00:00:00.000+02:00", + "Not reviewed": "52", + "Reviews partly submitted": "35", + "All Reviews submitted": "9", + "Reviews partly completed": "29", + "All Reviews completed": "57", + "Completed in DA_Review_Finding": "81", + "Completed in DA_Review_Finding_Test": "62" + }, + { + "_time": "2022-07-10T00:00:00.000+02:00", + "Not reviewed": "52", + "Reviews partly submitted": "35", + "All Reviews submitted": "9", + "Reviews partly completed": "29", + "All Reviews completed": "57", + "Completed in DA_Review_Finding": "81", + "Completed in DA_Review_Finding_Test": "62" + }, + { + "_time": "2022-07-12T00:00:00.000+02:00", + "Not reviewed": "52", + "Reviews partly submitted": "35", + "All Reviews submitted": "9", + "Reviews partly completed": "29", + "All Reviews completed": "57", + "Completed in DA_Review_Finding": "81", + "Completed in DA_Review_Finding_Test": "62" + }, + { + "_time": "2022-07-14T00:00:00.000+02:00", + "Not reviewed": "104", + "Reviews partly submitted": "70", + "All Reviews submitted": "18", + "Reviews partly completed": "56", + "All Reviews completed": "116", + "Completed in DA_Review_Finding": "162", + "Completed in DA_Review_Finding_Test": "126" + }, + { + "_time": "2022-07-15T00:00:00.000+02:00", + "Not reviewed": "54", + "Reviews partly submitted": "35", + "All Reviews submitted": "9", + "Reviews partly completed": "28", + "All Reviews completed": "58", + "Completed in DA_Review_Finding": "81", + "Completed in DA_Review_Finding_Test": "63" + }, + { + "_time": "2022-07-21T00:00:00.000+02:00", + "Not reviewed": "55", + "Reviews partly submitted": "35", + "All Reviews submitted": "9", + "Reviews partly completed": "28", + "All Reviews completed": "58", + "Completed in DA_Review_Finding": "81", + "Completed in DA_Review_Finding_Test": "63" + }, + { + "_time": "2022-07-23T00:00:00.000+02:00", + "Not reviewed": "55", + "Reviews partly submitted": "37", + "All Reviews submitted": "9", + "Reviews partly completed": "28", + "All Reviews completed": "58", + "Completed in DA_Review_Finding": "81", + "Completed in DA_Review_Finding_Test": "63" + }, + { + "_time": "2022-07-29T00:00:00.000+02:00", + "Not reviewed": "53", + "Reviews partly submitted": "37", + "All Reviews submitted": "9", + "Reviews partly completed": "28", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "83", + "Completed in DA_Review_Finding_Test": "65" + }, + { + "_time": "2022-07-30T00:00:00.000+02:00", + "Not reviewed": "53", + "Reviews partly submitted": "37", + "All Reviews submitted": "9", + "Reviews partly completed": "28", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "83", + "Completed in DA_Review_Finding_Test": "65" + }, + { + "_time": "2022-08-01T00:00:00.000+02:00", + "Not reviewed": "53", + "Reviews partly submitted": "37", + "All Reviews submitted": "9", + "Reviews partly completed": "28", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "83", + "Completed in DA_Review_Finding_Test": "65" + }, + { + "_time": "2022-08-02T00:00:00.000+02:00", + "Not reviewed": "53", + "Reviews partly submitted": "37", + "All Reviews submitted": "9", + "Reviews partly completed": "28", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "83", + "Completed in DA_Review_Finding_Test": "65" + }, + { + "_time": "2022-08-03T00:00:00.000+02:00", + "Not reviewed": "57", + "Reviews partly submitted": "56", + "All Reviews submitted": "9", + "Reviews partly completed": "34", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "89", + "Completed in DA_Review_Finding_Test": "65" + }, + { + "_time": "2022-08-04T00:00:00.000+02:00", + "Not reviewed": "162", + "Reviews partly submitted": "151", + "All Reviews submitted": "27", + "Reviews partly completed": "96", + "All Reviews completed": "183", + "Completed in DA_Review_Finding": "264", + "Completed in DA_Review_Finding_Test": "198" + }, + { + "_time": "2022-08-05T00:00:00.000+02:00", + "Not reviewed": "56", + "Reviews partly submitted": "56", + "All Reviews submitted": "9", + "Reviews partly completed": "34", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "90", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-08-06T00:00:00.000+02:00", + "Not reviewed": "162", + "Reviews partly submitted": "151", + "All Reviews submitted": "30", + "Reviews partly completed": "99", + "All Reviews completed": "177", + "Completed in DA_Review_Finding": "255", + "Completed in DA_Review_Finding_Test": "198" + }, + { + "_time": "2022-08-07T00:00:00.000+02:00", + "Not reviewed": "50", + "Reviews partly submitted": "39", + "All Reviews submitted": "10", + "Reviews partly completed": "29", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "81", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-08-08T00:00:00.000+02:00", + "Not reviewed": "50", + "Reviews partly submitted": "39", + "All Reviews submitted": "10", + "Reviews partly completed": "29", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "81", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-08-09T00:00:00.000+02:00", + "Not reviewed": "50", + "Reviews partly submitted": "39", + "All Reviews submitted": "10", + "Reviews partly completed": "29", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "81", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-08-10T00:00:00.000+02:00", + "Not reviewed": "50", + "Reviews partly submitted": "39", + "All Reviews submitted": "10", + "Reviews partly completed": "29", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "81", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-08-11T00:00:00.000+02:00", + "Not reviewed": "50", + "Reviews partly submitted": "39", + "All Reviews submitted": "10", + "Reviews partly completed": "29", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "81", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-08-12T00:00:00.000+02:00", + "Not reviewed": "51", + "Reviews partly submitted": "39", + "All Reviews submitted": "10", + "Reviews partly completed": "29", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "81", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-08-13T00:00:00.000+02:00", + "Not reviewed": "53", + "Reviews partly submitted": "39", + "All Reviews submitted": "10", + "Reviews partly completed": "29", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "81", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-08-14T00:00:00.000+02:00", + "Not reviewed": "53", + "Reviews partly submitted": "39", + "All Reviews submitted": "10", + "Reviews partly completed": "29", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "81", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-08-15T00:00:00.000+02:00", + "Not reviewed": "53", + "Reviews partly submitted": "39", + "All Reviews submitted": "10", + "Reviews partly completed": "29", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "81", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-08-16T00:00:00.000+02:00", + "Not reviewed": "53", + "Reviews partly submitted": "39", + "All Reviews submitted": "10", + "Reviews partly completed": "29", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "81", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-08-17T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "108", + "All Reviews submitted": "10", + "Reviews partly completed": "57", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "109", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-08-18T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "108", + "All Reviews submitted": "10", + "Reviews partly completed": "57", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "109", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-08-19T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "108", + "All Reviews submitted": "10", + "Reviews partly completed": "57", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "109", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-08-20T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "108", + "All Reviews submitted": "10", + "Reviews partly completed": "57", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "109", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-08-22T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "108", + "All Reviews submitted": "10", + "Reviews partly completed": "57", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "109", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-08-23T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "108", + "All Reviews submitted": "10", + "Reviews partly completed": "57", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "109", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-08-24T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "108", + "All Reviews submitted": "10", + "Reviews partly completed": "57", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "109", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-08-25T00:00:00.000+02:00", + "Not reviewed": "72", + "Reviews partly submitted": "109", + "All Reviews submitted": "10", + "Reviews partly completed": "57", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "109", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-08-26T00:00:00.000+02:00", + "Not reviewed": "72", + "Reviews partly submitted": "109", + "All Reviews submitted": "10", + "Reviews partly completed": "57", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "109", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-08-27T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "109", + "All Reviews submitted": "10", + "Reviews partly completed": "57", + "All Reviews completed": "58", + "Completed in DA_Review_Finding": "108", + "Completed in DA_Review_Finding_Test": "65" + }, + { + "_time": "2022-08-28T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "109", + "All Reviews submitted": "10", + "Reviews partly completed": "57", + "All Reviews completed": "58", + "Completed in DA_Review_Finding": "108", + "Completed in DA_Review_Finding_Test": "65" + }, + { + "_time": "2022-08-29T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "109", + "All Reviews submitted": "10", + "Reviews partly completed": "57", + "All Reviews completed": "58", + "Completed in DA_Review_Finding": "108", + "Completed in DA_Review_Finding_Test": "65" + }, + { + "_time": "2022-08-30T00:00:00.000+02:00", + "Not reviewed": "72", + "Reviews partly submitted": "108", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "58", + "Completed in DA_Review_Finding": "109", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-08-31T00:00:00.000+02:00", + "Not reviewed": "72", + "Reviews partly submitted": "108", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "58", + "Completed in DA_Review_Finding": "109", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-09-01T00:00:00.000+02:00", + "Not reviewed": "72", + "Reviews partly submitted": "108", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "58", + "Completed in DA_Review_Finding": "109", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-09-02T00:00:00.000+02:00", + "Not reviewed": "72", + "Reviews partly submitted": "108", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "58", + "Completed in DA_Review_Finding": "109", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-09-03T00:00:00.000+02:00", + "Not reviewed": "72", + "Reviews partly submitted": "108", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "58", + "Completed in DA_Review_Finding": "109", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-09-05T00:00:00.000+02:00", + "Not reviewed": "72", + "Reviews partly submitted": "108", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "58", + "Completed in DA_Review_Finding": "109", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-09-07T00:00:00.000+02:00", + "Not reviewed": "72", + "Reviews partly submitted": "108", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "58", + "Completed in DA_Review_Finding": "109", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-09-08T00:00:00.000+02:00", + "Not reviewed": "72", + "Reviews partly submitted": "108", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "58", + "Completed in DA_Review_Finding": "109", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-09-09T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "108", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "58", + "Completed in DA_Review_Finding": "109", + "Completed in DA_Review_Finding_Test": "66" + }, + { + "_time": "2022-09-10T00:00:00.000+02:00", + "Not reviewed": "72", + "Reviews partly submitted": "108", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "110", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-09-12T00:00:00.000+02:00", + "Not reviewed": "72", + "Reviews partly submitted": "108", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "110", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-09-13T00:00:00.000+02:00", + "Not reviewed": "71", + "Reviews partly submitted": "108", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "110", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-09-14T00:00:00.000+02:00", + "Not reviewed": "71", + "Reviews partly submitted": "108", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "110", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-09-15T00:00:00.000+02:00", + "Not reviewed": "71", + "Reviews partly submitted": "108", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "110", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-09-17T00:00:00.000+02:00", + "Not reviewed": "71", + "Reviews partly submitted": "124", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "110", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-09-18T00:00:00.000+02:00", + "Not reviewed": "71", + "Reviews partly submitted": "124", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "110", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-09-19T00:00:00.000+02:00", + "Not reviewed": "71", + "Reviews partly submitted": "124", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "110", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-09-20T00:00:00.000+02:00", + "Not reviewed": "71", + "Reviews partly submitted": "124", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "110", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-09-21T00:00:00.000+02:00", + "Not reviewed": "71", + "Reviews partly submitted": "124", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "110", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-09-22T00:00:00.000+02:00", + "Not reviewed": "71", + "Reviews partly submitted": "124", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "110", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-09-23T00:00:00.000+02:00", + "Not reviewed": "71", + "Reviews partly submitted": "124", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "110", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-09-24T00:00:00.000+02:00", + "Not reviewed": "71", + "Reviews partly submitted": "124", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "110", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-09-26T00:00:00.000+02:00", + "Not reviewed": "71", + "Reviews partly submitted": "124", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "110", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-09-27T00:00:00.000+02:00", + "Not reviewed": "71", + "Reviews partly submitted": "124", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "110", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-09-28T00:00:00.000+02:00", + "Not reviewed": "71", + "Reviews partly submitted": "124", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "110", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-09-29T00:00:00.000+02:00", + "Not reviewed": "71", + "Reviews partly submitted": "124", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "110", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-09-30T00:00:00.000+02:00", + "Not reviewed": "71", + "Reviews partly submitted": "124", + "All Reviews submitted": "10", + "Reviews partly completed": "59", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "110", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-01T00:00:00.000+02:00", + "Not reviewed": "72", + "Reviews partly submitted": "124", + "All Reviews submitted": "10", + "Reviews partly completed": "60", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "111", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-03T00:00:00.000+02:00", + "Not reviewed": "72", + "Reviews partly submitted": "124", + "All Reviews submitted": "10", + "Reviews partly completed": "60", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "111", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-04T00:00:00.000+02:00", + "Not reviewed": "72", + "Reviews partly submitted": "124", + "All Reviews submitted": "10", + "Reviews partly completed": "60", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "111", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-05T00:00:00.000+02:00", + "Not reviewed": "72", + "Reviews partly submitted": "124", + "All Reviews submitted": "10", + "Reviews partly completed": "60", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "111", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-06T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "124", + "All Reviews submitted": "10", + "Reviews partly completed": "60", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "111", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-07T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "124", + "All Reviews submitted": "10", + "Reviews partly completed": "60", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "111", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-08T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "124", + "All Reviews submitted": "10", + "Reviews partly completed": "60", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "111", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-10T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "124", + "All Reviews submitted": "10", + "Reviews partly completed": "60", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "111", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-11T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "124", + "All Reviews submitted": "10", + "Reviews partly completed": "61", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "112", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-12T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "121", + "All Reviews submitted": "10", + "Reviews partly completed": "61", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "112", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-13T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "121", + "All Reviews submitted": "10", + "Reviews partly completed": "61", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "112", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-14T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "140", + "All Reviews submitted": "10", + "Reviews partly completed": "63", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "114", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-15T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "140", + "All Reviews submitted": "10", + "Reviews partly completed": "63", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "114", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-16T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "140", + "All Reviews submitted": "10", + "Reviews partly completed": "63", + "All Reviews completed": "59", + "Completed in DA_Review_Finding": "114", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-18T00:00:00.000+02:00", + "Not reviewed": "145", + "Reviews partly submitted": "281", + "All Reviews submitted": "20", + "Reviews partly completed": "124", + "All Reviews completed": "120", + "Completed in DA_Review_Finding": "230", + "Completed in DA_Review_Finding_Test": "134" + }, + { + "_time": "2022-10-19T00:00:00.000+02:00", + "Not reviewed": "72", + "Reviews partly submitted": "141", + "All Reviews submitted": "10", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-20T00:00:00.000+02:00", + "Not reviewed": "72", + "Reviews partly submitted": "140", + "All Reviews submitted": "11", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-21T00:00:00.000+02:00", + "Not reviewed": "72", + "Reviews partly submitted": "140", + "All Reviews submitted": "11", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-22T00:00:00.000+02:00", + "Not reviewed": "72", + "Reviews partly submitted": "140", + "All Reviews submitted": "11", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-24T00:00:00.000+02:00", + "Not reviewed": "72", + "Reviews partly submitted": "140", + "All Reviews submitted": "11", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-25T00:00:00.000+02:00", + "Not reviewed": "72", + "Reviews partly submitted": "140", + "All Reviews submitted": "11", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-26T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "140", + "All Reviews submitted": "11", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-27T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "140", + "All Reviews submitted": "11", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-28T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "140", + "All Reviews submitted": "11", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-29T00:00:00.000+02:00", + "Not reviewed": "73", + "Reviews partly submitted": "140", + "All Reviews submitted": "11", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-10-31T00:00:00.000+01:00", + "Not reviewed": "73", + "Reviews partly submitted": "140", + "All Reviews submitted": "11", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-11-01T00:00:00.000+01:00", + "Not reviewed": "73", + "Reviews partly submitted": "140", + "All Reviews submitted": "11", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-11-02T00:00:00.000+01:00", + "Not reviewed": "73", + "Reviews partly submitted": "140", + "All Reviews submitted": "11", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-11-03T00:00:00.000+01:00", + "Not reviewed": "73", + "Reviews partly submitted": "140", + "All Reviews submitted": "11", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-11-04T00:00:00.000+01:00", + "Not reviewed": "73", + "Reviews partly submitted": "140", + "All Reviews submitted": "11", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-11-05T00:00:00.000+01:00", + "Not reviewed": "73", + "Reviews partly submitted": "140", + "All Reviews submitted": "11", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-11-07T00:00:00.000+01:00", + "Not reviewed": "73", + "Reviews partly submitted": "140", + "All Reviews submitted": "11", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-11-08T00:00:00.000+01:00", + "Not reviewed": "146", + "Reviews partly submitted": "280", + "All Reviews submitted": "22", + "Reviews partly completed": "124", + "All Reviews completed": "120", + "Completed in DA_Review_Finding": "230", + "Completed in DA_Review_Finding_Test": "134" + }, + { + "_time": "2022-11-09T00:00:00.000+01:00", + "Not reviewed": "73", + "Reviews partly submitted": "140", + "All Reviews submitted": "11", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-11-10T00:00:00.000+01:00", + "Not reviewed": "73", + "Reviews partly submitted": "140", + "All Reviews submitted": "11", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-11-11T00:00:00.000+01:00", + "Not reviewed": "73", + "Reviews partly submitted": "140", + "All Reviews submitted": "11", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-11-12T00:00:00.000+01:00", + "Not reviewed": "72", + "Reviews partly submitted": "140", + "All Reviews submitted": "10", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-11-14T00:00:00.000+01:00", + "Not reviewed": "72", + "Reviews partly submitted": "140", + "All Reviews submitted": "10", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-11-15T00:00:00.000+01:00", + "Not reviewed": "72", + "Reviews partly submitted": "140", + "All Reviews submitted": "10", + "Reviews partly completed": "62", + "All Reviews completed": "60", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "67" + }, + { + "_time": "2022-11-16T00:00:00.000+01:00", + "Not reviewed": "58", + "Reviews partly submitted": "150", + "All Reviews submitted": "12", + "Reviews partly completed": "62", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "69" + }, + { + "_time": "2022-11-17T00:00:00.000+01:00", + "Not reviewed": "58", + "Reviews partly submitted": "150", + "All Reviews submitted": "12", + "Reviews partly completed": "62", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "69" + }, + { + "_time": "2022-11-18T00:00:00.000+01:00", + "Not reviewed": "58", + "Reviews partly submitted": "144", + "All Reviews submitted": "12", + "Reviews partly completed": "61", + "All Reviews completed": "62", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-11-19T00:00:00.000+01:00", + "Not reviewed": "58", + "Reviews partly submitted": "144", + "All Reviews submitted": "12", + "Reviews partly completed": "61", + "All Reviews completed": "62", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-11-21T00:00:00.000+01:00", + "Not reviewed": "58", + "Reviews partly submitted": "144", + "All Reviews submitted": "12", + "Reviews partly completed": "61", + "All Reviews completed": "62", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-11-23T00:00:00.000+01:00", + "Not reviewed": "55", + "Reviews partly submitted": "144", + "All Reviews submitted": "17", + "Reviews partly completed": "61", + "All Reviews completed": "62", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-11-24T00:00:00.000+01:00", + "Not reviewed": "55", + "Reviews partly submitted": "144", + "All Reviews submitted": "17", + "Reviews partly completed": "61", + "All Reviews completed": "62", + "Completed in DA_Review_Finding": "115", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-11-25T00:00:00.000+01:00", + "Not reviewed": "52", + "Reviews partly submitted": "145", + "All Reviews submitted": "17", + "Reviews partly completed": "61", + "All Reviews completed": "64", + "Completed in DA_Review_Finding": "117", + "Completed in DA_Review_Finding_Test": "72" + }, + { + "_time": "2022-11-28T00:00:00.000+01:00", + "Not reviewed": "52", + "Reviews partly submitted": "145", + "All Reviews submitted": "17", + "Reviews partly completed": "61", + "All Reviews completed": "64", + "Completed in DA_Review_Finding": "117", + "Completed in DA_Review_Finding_Test": "72" + }, + { + "_time": "2022-11-29T00:00:00.000+01:00", + "Not reviewed": "52", + "Reviews partly submitted": "145", + "All Reviews submitted": "17", + "Reviews partly completed": "61", + "All Reviews completed": "64", + "Completed in DA_Review_Finding": "116", + "Completed in DA_Review_Finding_Test": "73" + }, + { + "_time": "2022-11-30T00:00:00.000+01:00", + "Not reviewed": "52", + "Reviews partly submitted": "145", + "All Reviews submitted": "17", + "Reviews partly completed": "62", + "All Reviews completed": "63", + "Completed in DA_Review_Finding": "116", + "Completed in DA_Review_Finding_Test": "72" + }, + { + "_time": "2022-12-01T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "147", + "All Reviews submitted": "19", + "Reviews partly completed": "60", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "112", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-02T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "147", + "All Reviews submitted": "19", + "Reviews partly completed": "60", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "112", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-03T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "147", + "All Reviews submitted": "19", + "Reviews partly completed": "60", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "112", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-05T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "147", + "All Reviews submitted": "19", + "Reviews partly completed": "60", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "112", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-06T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "147", + "All Reviews submitted": "19", + "Reviews partly completed": "60", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "112", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-07T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "147", + "All Reviews submitted": "19", + "Reviews partly completed": "60", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "112", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-08T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "147", + "All Reviews submitted": "19", + "Reviews partly completed": "60", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "112", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-09T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "147", + "All Reviews submitted": "19", + "Reviews partly completed": "60", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "112", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-11T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "147", + "All Reviews submitted": "19", + "Reviews partly completed": "60", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "112", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-12T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "147", + "All Reviews submitted": "19", + "Reviews partly completed": "60", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "112", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-13T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-14T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-15T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-16T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-17T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-18T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-19T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-20T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-21T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-22T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-23T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-24T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-25T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-26T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-27T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-28T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-29T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-30T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2022-12-31T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2023-01-01T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2023-01-02T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2023-01-03T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2023-01-04T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2023-01-05T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2023-01-06T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2023-01-07T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2023-01-08T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2023-01-09T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2023-01-10T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2023-01-11T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + }, + { + "_time": "2023-01-12T00:00:00.000+01:00", + "Not reviewed": "53", + "Reviews partly submitted": "153", + "All Reviews submitted": "19", + "Reviews partly completed": "54", + "All Reviews completed": "61", + "Completed in DA_Review_Finding": "106", + "Completed in DA_Review_Finding_Test": "70" + } + ], + "highlighted": {} +} diff --git a/yaku-apps-typescript/apps/json-evaluator/test/samples/coverage.yaml b/yaku-apps-typescript/apps/json-evaluator/test/samples/coverage.yaml new file mode 100644 index 00000000..86dcb37a --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/samples/coverage.yaml @@ -0,0 +1,4 @@ +checks: + - name: has_good_coverage + ref: $.totals.percent_covered + condition: ($) >= 75 diff --git a/yaku-apps-typescript/apps/json-evaluator/test/samples/coverage_data.json b/yaku-apps-typescript/apps/json-evaluator/test/samples/coverage_data.json new file mode 100644 index 00000000..a65e1215 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/samples/coverage_data.json @@ -0,0 +1,1349 @@ +{ + "meta": { + "version": "6.5.0", + "timestamp": "2023-03-16T15:56:10.340013", + "branch_coverage": true, + "show_contexts": false + }, + "files": { + "__global_coverage__/no-op-exe.py": { + "executed_lines": [1], + "summary": { + "covered_lines": 0, + "num_statements": 0, + "percent_covered": 100.0, + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + "num_branches": 0, + "num_partial_branches": 0, + "covered_branches": 0, + "missing_branches": 0 + }, + "missing_lines": [], + "excluded_lines": [], + "executed_branches": [], + "missing_branches": [] + }, + "apps/artifactory-fetcher/src/grow/artifactory_fetcher/__init__.py": { + "executed_lines": [1, 6, 8], + "summary": { + "covered_lines": 3, + "num_statements": 3, + "percent_covered": 100.0, + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + "num_branches": 0, + "num_partial_branches": 0, + "covered_branches": 0, + "missing_branches": 0 + }, + "missing_lines": [], + "excluded_lines": [], + "executed_branches": [], + "missing_branches": [] + }, + "apps/artifactory-fetcher/src/grow/artifactory_fetcher/__main__.py": { + "executed_lines": [ + 1, 3, 5, 8, 9, 11, 12, 18, 19, 27, 28, 29, 30, 31, 32, 35 + ], + "summary": { + "covered_lines": 16, + "num_statements": 20, + "percent_covered": 75.0, + "percent_covered_display": "75", + "missing_lines": 4, + "excluded_lines": 0, + "num_branches": 4, + "num_partial_branches": 2, + "covered_branches": 2, + "missing_branches": 2 + }, + "missing_lines": [13, 14, 15, 36], + "excluded_lines": [], + "executed_branches": [ + [11, 12], + [35, -1] + ], + "missing_branches": [ + [11, 13], + [35, 36] + ] + }, + "apps/artifactory-fetcher/src/grow/artifactory_fetcher/artifactory_fetcher.py": { + "executed_lines": [ + 1, 2, 4, 7, 20, 21, 26, 34, 35, 36, 37, 39, 42, 46, 47, 48, 50, 51, 52, + 58, 59, 60, 66, 71, 73, 74, 75, 77, 78, 79, 82, 84 + ], + "summary": { + "covered_lines": 32, + "num_statements": 36, + "percent_covered": 89.1304347826087, + "percent_covered_display": "89", + "missing_lines": 4, + "excluded_lines": 0, + "num_branches": 10, + "num_partial_branches": 1, + "covered_branches": 9, + "missing_branches": 1 + }, + "missing_lines": [55, 56, 57, 80], + "excluded_lines": [], + "executed_branches": [ + [20, 21], + [20, 26], + [34, 35], + [34, 39], + [50, 51], + [50, 66], + [77, 78], + [77, 84], + [79, 82] + ], + "missing_branches": [[79, 80]] + }, + "apps/artifactory-fetcher/src/grow/artifactory_fetcher/cli.py": { + "executed_lines": [ + 1, 2, 3, 4, 5, 7, 9, 16, 18, 19, 22, 25, 27, 28, 29, 30, 31, 32, 33, 35, + 36, 43, 44, 45, 46, 47, 48, 49, 52, 53, 54, 56, 57, 60, 61, 64, 65, 66, + 67, 68, 69, 70, 71, 73, 74, 75, 76, 77, 78, 79, 81, 82, 83, 84, 85, 86, + 87, 88, 90, 91, 92 + ], + "summary": { + "covered_lines": 61, + "num_statements": 63, + "percent_covered": 95.77464788732394, + "percent_covered_display": "96", + "missing_lines": 2, + "excluded_lines": 0, + "num_branches": 8, + "num_partial_branches": 1, + "covered_branches": 7, + "missing_branches": 1 + }, + "missing_lines": [20, 21], + "excluded_lines": [], + "executed_branches": [ + [19, 22], + [43, 44], + [43, 48], + [60, 61], + [60, 67], + [81, 82], + [81, 90] + ], + "missing_branches": [[19, 20]] + }, + "apps/artifactory-fetcher/tests-pex/test_pex.py": { + "executed_lines": [1, 2, 4, 5, 8, 9, 10, 14], + "summary": { + "covered_lines": 8, + "num_statements": 8, + "percent_covered": 100.0, + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + "num_branches": 0, + "num_partial_branches": 0, + "covered_branches": 0, + "missing_branches": 0 + }, + "missing_lines": [], + "excluded_lines": [], + "executed_branches": [], + "missing_branches": [] + }, + "apps/pdf-signature-evaluator/src/grow/pdf_signature_evaluator/__init__.py": { + "executed_lines": [1], + "summary": { + "covered_lines": 0, + "num_statements": 0, + "percent_covered": 100.0, + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + "num_branches": 0, + "num_partial_branches": 0, + "covered_branches": 0, + "missing_branches": 0 + }, + "missing_lines": [], + "excluded_lines": [], + "executed_branches": [], + "missing_branches": [] + }, + "apps/pdf-signature-evaluator/src/grow/pdf_signature_evaluator/comment_composer.py": { + "executed_lines": [1, 4, 6, 7, 9, 12, 15, 18, 21], + "summary": { + "covered_lines": 9, + "num_statements": 14, + "percent_covered": 64.28571428571429, + "percent_covered_display": "64", + "missing_lines": 5, + "excluded_lines": 0, + "num_branches": 0, + "num_partial_branches": 0, + "covered_branches": 0, + "missing_branches": 0 + }, + "missing_lines": [10, 13, 16, 19, 22], + "excluded_lines": [], + "executed_branches": [], + "missing_branches": [] + }, + "apps/pdf-signature-evaluator/src/grow/pdf_signature_evaluator/digital_signature_verification.py": { + "executed_lines": [ + 8, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19, 21, 23, 24, 25, 28, 31, 54, + 63, 67, 75, 107, 119, 127, 139, 175, 190, 224, 252, 262, 263, 271, 309 + ], + "summary": { + "covered_lines": 33, + "num_statements": 196, + "percent_covered": 12.099644128113878, + "percent_covered_display": "12", + "missing_lines": 163, + "excluded_lines": 0, + "num_branches": 85, + "num_partial_branches": 1, + "covered_branches": 1, + "missing_branches": 84 + }, + "missing_lines": [ + 32, 34, 35, 36, 37, 38, 40, 42, 43, 44, 45, 46, 47, 48, 49, 51, 55, 56, + 57, 58, 59, 60, 64, 68, 69, 70, 71, 72, 76, 77, 78, 79, 80, 82, 84, 85, + 86, 87, 89, 90, 91, 93, 95, 96, 97, 98, 99, 100, 103, 104, 108, 109, + 110, 112, 113, 114, 116, 120, 121, 122, 123, 124, 128, 129, 130, 131, + 132, 134, 135, 136, 141, 142, 149, 150, 151, 153, 158, 159, 160, 161, + 167, 172, 176, 177, 178, 180, 181, 182, 183, 184, 185, 186, 187, 192, + 194, 195, 196, 198, 199, 200, 202, 204, 205, 206, 207, 209, 210, 211, + 212, 214, 215, 216, 217, 218, 219, 221, 225, 226, 227, 228, 230, 231, + 232, 234, 235, 236, 237, 238, 242, 243, 244, 246, 247, 248, 249, 253, + 255, 256, 257, 258, 259, 272, 274, 276, 278, 280, 281, 282, 283, 284, + 286, 287, 289, 290, 292, 294, 297, 299, 301, 304, 305, 306, 310 + ], + "excluded_lines": [], + "executed_branches": [[309, -8]], + "missing_branches": [ + [36, 37], + [36, 42], + [37, 38], + [37, 40], + [42, 43], + [42, 51], + [43, 42], + [43, 44], + [44, 43], + [44, 45], + [56, -56], + [56, 57], + [57, 58], + [57, 60], + [58, 57], + [58, 59], + [68, 69], + [68, 72], + [84, 85], + [84, 104], + [89, -89], + [89, 90], + [95, 84], + [95, 96], + [97, -97], + [97, 95], + [97, 98], + [109, 110], + [109, 116], + [113, 109], + [113, 114], + [122, -119], + [122, 123], + [129, 130], + [129, 134], + [130, -130], + [130, 131], + [131, 129], + [131, 132], + [134, 135], + [134, 136], + [141, 142], + [141, 158], + [142, -142], + [142, 149], + [149, 150], + [149, 153], + [158, 159], + [158, 167], + [159, 160], + [159, 161], + [160, -160], + [160, 159], + [180, 181], + [180, 187], + [182, 183], + [182, 184], + [194, 195], + [194, 198], + [202, 204], + [202, 209], + [205, 202], + [205, 206], + [209, 210], + [209, 214], + [217, 218], + [217, 221], + [225, 226], + [225, 246], + [227, 228], + [227, 230], + [231, 232], + [231, 234], + [242, 243], + [242, 244], + [247, 248], + [247, 249], + [255, 256], + [255, 257], + [281, 282], + [281, 289], + [294, -294], + [294, 297], + [309, 310] + ] + }, + "apps/pdf-signature-evaluator/tests-pex/test_pex.py": { + "executed_lines": [1, 2, 4, 5, 8, 9, 10, 14], + "summary": { + "covered_lines": 8, + "num_statements": 8, + "percent_covered": 100.0, + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + "num_branches": 0, + "num_partial_branches": 0, + "covered_branches": 0, + "missing_branches": 0 + }, + "missing_lines": [], + "excluded_lines": [], + "executed_branches": [], + "missing_branches": [] + }, + "apps/sharepoint-evaluator/src/grow/sharepoint_evaluator/__init__.py": { + "executed_lines": [1], + "summary": { + "covered_lines": 0, + "num_statements": 0, + "percent_covered": 100.0, + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + "num_branches": 0, + "num_partial_branches": 0, + "covered_branches": 0, + "missing_branches": 0 + }, + "missing_lines": [], + "excluded_lines": [], + "executed_branches": [], + "missing_branches": [] + }, + "apps/sharepoint-evaluator/src/grow/sharepoint_evaluator/__main__.py": { + "executed_lines": [1, 2, 4, 6, 9, 19, 20, 28, 40], + "summary": { + "covered_lines": 9, + "num_statements": 25, + "percent_covered": 34.48275862068966, + "percent_covered_display": "34", + "missing_lines": 16, + "excluded_lines": 0, + "num_branches": 4, + "num_partial_branches": 1, + "covered_branches": 1, + "missing_branches": 3 + }, + "missing_lines": [ + 10, 12, 13, 14, 15, 16, 29, 30, 31, 32, 33, 34, 35, 36, 37, 41 + ], + "excluded_lines": [], + "executed_branches": [[40, -1]], + "missing_branches": [ + [12, 13], + [12, 14], + [40, 41] + ] + }, + "apps/sharepoint-evaluator/src/grow/sharepoint_evaluator/checks.py": { + "executed_lines": [ + 1, 2, 3, 5, 6, 8, 11, 13, 14, 15, 16, 17, 18, 21, 23, 24, 25, 26, 31, + 33, 34, 35, 36, 41, 43, 44, 45, 46, 51, 53, 54, 55, 56, 61, 80, 81, 82, + 83, 84, 85, 86, 87, 90, 91, 92, 93, 94, 97, 99, 102, 103, 104, 105, 106, + 107, 108, 110, 113, 114, 117, 118, 121, 123, 126, 128, 131, 171, 172, + 173, 176, 187, 189 + ], + "summary": { + "covered_lines": 72, + "num_statements": 72, + "percent_covered": 100.0, + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + "num_branches": 8, + "num_partial_branches": 0, + "covered_branches": 8, + "missing_branches": 0 + }, + "missing_lines": [], + "excluded_lines": [], + "executed_branches": [ + [81, -80], + [81, 82], + [86, -80], + [86, 87], + [94, 94], + [94, 94], + [94, 97], + [94, 99], + [105, 106], + [105, 110] + ], + "missing_branches": [] + }, + "apps/sharepoint-evaluator/src/grow/sharepoint_evaluator/cli.py": { + "executed_lines": [ + 1, 2, 3, 4, 6, 7, 8, 9, 12, 30, 31, 32, 33, 34, 35, 39, 44, 45, 46, 48, + 50, 51, 57, 58, 61, 62, 63, 64, 67, 68, 69, 73, 74, 77, 78, 79, 85, 86, + 87, 88, 93, 94, 95, 102, 104, 105, 107, 108, 110, 111, 112 + ], + "summary": { + "covered_lines": 51, + "num_statements": 52, + "percent_covered": 97.36842105263158, + "percent_covered_display": "97", + "missing_lines": 1, + "excluded_lines": 0, + "num_branches": 24, + "num_partial_branches": 1, + "covered_branches": 23, + "missing_branches": 1 + }, + "missing_lines": [76], + "excluded_lines": [], + "executed_branches": [ + [30, -30], + [30, 30], + [30, 31], + [31, -12], + [31, 32], + [57, 58], + [57, 61], + [63, 64], + [63, 104], + [68, 69], + [68, 73], + [73, 63], + [73, 74], + [74, 77], + [77, 78], + [77, 86], + [86, 73], + [86, 87], + [93, 86], + [93, 94], + [104, 105], + [104, 107], + [107, 108], + [107, 110] + ], + "missing_branches": [[74, 76]] + }, + "apps/sharepoint-evaluator/src/grow/sharepoint_evaluator/errors.py": { + "executed_lines": [1, 2, 9], + "summary": { + "covered_lines": 2, + "num_statements": 2, + "percent_covered": 100.0, + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + "num_branches": 0, + "num_partial_branches": 0, + "covered_branches": 0, + "missing_branches": 0 + }, + "missing_lines": [], + "excluded_lines": [], + "executed_branches": [], + "missing_branches": [] + }, + "apps/sharepoint-evaluator/src/grow/sharepoint_evaluator/rules.py": { + "executed_lines": [ + 1, 2, 3, 5, 7, 10, 11, 12, 13, 14, 16, 17, 20, 21, 22, 23, 26, 50, 51, + 53, 54, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65 + ], + "summary": { + "covered_lines": 31, + "num_statements": 31, + "percent_covered": 100.0, + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + "num_branches": 10, + "num_partial_branches": 0, + "covered_branches": 10, + "missing_branches": 0 + }, + "missing_lines": [], + "excluded_lines": [], + "executed_branches": [ + [10, 11], + [10, 12], + [20, 21], + [20, 22], + [53, 54], + [53, 56], + [57, 58], + [57, 65], + [59, 60], + [59, 64] + ], + "missing_branches": [] + }, + "apps/sharepoint-evaluator/src/grow/sharepoint_evaluator/utils.py": { + "executed_lines": [ + 1, 2, 3, 4, 6, 9, 11, 12, 13, 14, 17, 18, 19, 20, 21, 22, 24, 25, 26, + 27, 28, 33, 35, 36, 39, 40, 42, 65, 67, 76, 78, 79, 80, 81, 87, 88, 89, + 90, 91, 93, 94, 95, 96, 98, 99, 100, 101, 102, 103, 106, 111 + ], + "summary": { + "covered_lines": 51, + "num_statements": 52, + "percent_covered": 95.58823529411765, + "percent_covered_display": "96", + "missing_lines": 1, + "excluded_lines": 0, + "num_branches": 16, + "num_partial_branches": 2, + "covered_branches": 14, + "missing_branches": 2 + }, + "missing_lines": [32], + "excluded_lines": [], + "executed_branches": [ + [12, 13], + [12, 14], + [26, 27], + [27, 28], + [78, 79], + [78, 87], + [80, -80], + [80, 80], + [80, 81], + [88, 89], + [88, 93], + [94, 95], + [94, 98], + [100, 101], + [100, 111] + ], + "missing_branches": [ + [26, 33], + [27, 32] + ] + }, + "apps/sharepoint-evaluator/tests-pex/test_pex.py": { + "executed_lines": [1, 2, 4, 5, 8, 9, 10, 14], + "summary": { + "covered_lines": 8, + "num_statements": 8, + "percent_covered": 100.0, + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + "num_branches": 0, + "num_partial_branches": 0, + "covered_branches": 0, + "missing_branches": 0 + }, + "missing_lines": [], + "excluded_lines": [], + "executed_branches": [], + "missing_branches": [] + }, + "apps/sharepoint-fetcher/src/grow/sharepoint_fetcher/__init__.py": { + "executed_lines": [1, 2, 3, 5], + "summary": { + "covered_lines": 4, + "num_statements": 4, + "percent_covered": 100.0, + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + "num_branches": 0, + "num_partial_branches": 0, + "covered_branches": 0, + "missing_branches": 0 + }, + "missing_lines": [], + "excluded_lines": [], + "executed_branches": [], + "missing_branches": [] + }, + "apps/sharepoint-fetcher/src/grow/sharepoint_fetcher/__main__.py": { + "executed_lines": [1, 3, 5, 8, 18, 19, 27, 35], + "summary": { + "covered_lines": 8, + "num_statements": 20, + "percent_covered": 37.5, + "percent_covered_display": "38", + "missing_lines": 12, + "excluded_lines": 0, + "num_branches": 4, + "num_partial_branches": 1, + "covered_branches": 1, + "missing_branches": 3 + }, + "missing_lines": [9, 11, 12, 13, 14, 15, 28, 29, 30, 31, 32, 36], + "excluded_lines": [], + "executed_branches": [[35, -1]], + "missing_branches": [ + [11, 12], + [11, 13], + [35, 36] + ] + }, + "apps/sharepoint-fetcher/src/grow/sharepoint_fetcher/checks.py": { + "executed_lines": [ + 9, 10, 11, 13, 14, 16, 18, 19, 22, 24, 25, 26, 27, 28, 29, 30, 33, 44, + 55, 66, 77, 96, 106, 118, 129, 133, 137, 142, 147, 187, 192, 203, 205 + ], + "summary": { + "covered_lines": 33, + "num_statements": 79, + "percent_covered": 37.93103448275862, + "percent_covered_display": "38", + "missing_lines": 46, + "excluded_lines": 0, + "num_branches": 8, + "num_partial_branches": 0, + "covered_branches": 0, + "missing_branches": 8 + }, + "missing_lines": [ + 35, 36, 37, 38, 39, 46, 47, 48, 49, 50, 57, 58, 59, 60, 61, 68, 69, 70, + 71, 72, 97, 98, 99, 100, 101, 102, 103, 107, 108, 109, 110, 113, 115, + 119, 120, 121, 122, 123, 124, 126, 130, 134, 139, 144, 188, 189 + ], + "excluded_lines": [], + "executed_branches": [], + "missing_branches": [ + [97, -96], + [97, 98], + [102, -96], + [102, 103], + [110, 113], + [110, 115], + [121, 122], + [121, 126] + ] + }, + "apps/sharepoint-fetcher/src/grow/sharepoint_fetcher/cli.py": { + "executed_lines": [ + 1, 2, 3, 4, 5, 6, 8, 10, 11, 12, 13, 15, 24, 42, 43, 44, 45, 51, 56, 58, + 59, 60, 61, 62, 65, 83, 84, 85, 86, 87, 88, 89, 90, 94, 95, 98, 104, + 105, 106, 108, 111, 113, 114, 115, 117, 118, 127, 128, 129, 131, 132, + 134, 135, 137, 138, 140, 144, 145, 146, 147, 149, 158, 159, 161, 163, + 164, 168, 170, 171, 172, 173, 174, 175, 176, 181, 183, 184, 186, 187, + 188, 189, 190 + ], + "summary": { + "covered_lines": 82, + "num_statements": 84, + "percent_covered": 98.21428571428571, + "percent_covered_display": "98", + "missing_lines": 2, + "excluded_lines": 0, + "num_branches": 28, + "num_partial_branches": 0, + "covered_branches": 28, + "missing_branches": 0 + }, + "missing_lines": [46, 47], + "excluded_lines": [], + "executed_branches": [ + [42, -42], + [42, 42], + [42, 43], + [43, -24], + [43, 44], + [59, 60], + [59, 62], + [84, 85], + [84, 95], + [85, -85], + [85, 85], + [85, 86], + [86, 87], + [86, 95], + [105, 106], + [105, 108], + [115, 117], + [115, 131], + [117, 118], + [117, 128], + [146, 147], + [146, 149], + [158, 159], + [158, 161], + [163, 164], + [163, 170], + [170, 171], + [170, 186], + [175, 176], + [175, 183] + ], + "missing_branches": [] + }, + "apps/sharepoint-fetcher/src/grow/sharepoint_fetcher/connect.py": { + "executed_lines": [ + 1, 2, 3, 5, 6, 9, 10, 21, 22, 23, 24, 25, 26, 28, 29, 30, 31, 32, 33, + 34, 35, 37, 43, 44, 45, 46, 48, 49, 50, 51, 52, 53, 54, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 65, 66, 67, 68, 70, 78, 81, 82, 83, 84, 85, 86, 87, + 94, 96, 97, 98, 99, 100, 102, 104, 110, 114, 115, 117, 123, 127, 128, + 130, 136, 140, 141, 142, 144, 152, 156, 157, 158, 159, 161, 170, 171, + 173, 180, 200, 201, 202 + ], + "summary": { + "covered_lines": 88, + "num_statements": 88, + "percent_covered": 100.0, + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + "num_branches": 20, + "num_partial_branches": 0, + "covered_branches": 20, + "missing_branches": 0 + }, + "missing_lines": [], + "excluded_lines": [], + "executed_branches": [ + [24, 25], + [24, 26], + [31, 32], + [31, 33], + [51, 52], + [51, 54], + [62, 63], + [62, 68], + [82, 83], + [82, 84], + [84, 85], + [84, 86], + [86, 87], + [86, 94], + [97, 98], + [97, 99], + [97, 102], + [173, -173], + [173, -161], + [173, 173], + [173, 173], + [173, 173], + [173, 173], + [173, 173], + [173, 173], + [173, 173], + [202, -202], + [202, -180], + [202, 202] + ], + "missing_branches": [] + }, + "apps/sharepoint-fetcher/src/grow/sharepoint_fetcher/errors.py": { + "executed_lines": [1, 2, 11, 12, 19], + "summary": { + "covered_lines": 4, + "num_statements": 4, + "percent_covered": 100.0, + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + "num_branches": 0, + "num_partial_branches": 0, + "covered_branches": 0, + "missing_branches": 0 + }, + "missing_lines": [], + "excluded_lines": [], + "executed_branches": [], + "missing_branches": [] + }, + "apps/sharepoint-fetcher/src/grow/sharepoint_fetcher/selectors.py": { + "executed_lines": [ + 1, 2, 3, 5, 8, 9, 10, 11, 12, 14, 18, 19, 20, 21, 22, 23, 26, 53, 54, + 56, 59, 60, 61, 63, 64, 65, 66, 67, 70, 78 + ], + "summary": { + "covered_lines": 30, + "num_statements": 32, + "percent_covered": 92.85714285714286, + "percent_covered_display": "93", + "missing_lines": 2, + "excluded_lines": 0, + "num_branches": 10, + "num_partial_branches": 1, + "covered_branches": 9, + "missing_branches": 1 + }, + "missing_lines": [15, 57], + "excluded_lines": [], + "executed_branches": [ + [8, 9], + [8, 10], + [18, 19], + [18, 20], + [56, 59], + [60, 61], + [60, 78], + [63, 64], + [63, 70] + ], + "missing_branches": [[56, 57]] + }, + "apps/sharepoint-fetcher/src/grow/sharepoint_fetcher/sharepoint_fetcher.py": { + "executed_lines": [ + 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 17, 18, 49, 52, 54, 64, 65, + 67, 68, 75, 76, 77, 79, 81, 83, 84, 85, 87, 88, 89, 91, 92, 94, 95, 100, + 102, 106, 107, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, + 122, 123, 124, 127, 128, 129, 130, 132, 141, 142, 143, 144, 146, 148, + 156, 157, 158, 159, 160, 161, 162, 164, 170, 172, 173, 177, 184, 185, + 187, 189, 192, 196, 197, 198, 199, 202, 205, 208, 211, 212, 213, 214, + 215, 216, 217, 218, 219, 220, 221, 224, 226, 227, 230, 231, 233, 234, + 242, 245, 246, 247, 250, 251, 252, 257, 258, 259, 260, 261, 262, 263, + 266, 267, 268, 269, 270, 271, 272, 275, 277, 278, 279, 282, 283, 284, + 285, 288, 289, 294, 296, 298, 304, 318, 319, 321, 324, 325, 332, 333, + 344, 345, 349, 350, 353, 355, 358, 360, 365, 366, 369, 372, 373, 374, + 375, 377, 379, 381, 394, 395, 396, 398, 404, 405, 406, 408, 409, 413, + 414, 415, 416, 417, 418, 419, 420, 422, 424, 426, 427, 428, 429, 430, + 431, 432, 433, 434 + ], + "summary": { + "covered_lines": 195, + "num_statements": 196, + "percent_covered": 98.96907216494846, + "percent_covered_display": "99", + "missing_lines": 1, + "excluded_lines": 0, + "num_branches": 95, + "num_partial_branches": 2, + "covered_branches": 93, + "missing_branches": 2 + }, + "missing_lines": [125], + "excluded_lines": [], + "executed_branches": [ + [67, 68], + [67, 75], + [75, 76], + [75, 77], + [83, 84], + [83, 85], + [87, 88], + [87, 89], + [94, 95], + [94, 100], + [112, 113], + [112, 120], + [115, 116], + [115, 117], + [117, 118], + [117, 119], + [123, 124], + [128, 129], + [128, 130], + [142, 143], + [142, 146], + [157, 158], + [157, 159], + [159, 160], + [159, 162], + [160, 159], + [160, 161], + [184, 185], + [184, 187], + [197, 198], + [197, 202], + [205, -205], + [205, 205], + [205, 205], + [205, 205], + [205, 208], + [205, 211], + [214, -214], + [214, 214], + [214, 215], + [215, 216], + [215, 233], + [217, 218], + [217, 226], + [218, 219], + [218, 224], + [219, 218], + [219, 220], + [230, 215], + [230, 231], + [233, 234], + [233, 242], + [234, -234], + [234, -177], + [234, 234], + [234, 234], + [234, 234], + [234, 234], + [234, 234], + [246, 247], + [246, 251], + [251, -251], + [251, 252], + [252, -252], + [252, -242], + [252, 252], + [252, 252], + [257, -177], + [257, 258], + [261, 262], + [262, 263], + [262, 269], + [266, 267], + [266, 268], + [269, 257], + [269, 270], + [271, 272], + [271, 275], + [282, 283], + [282, 298], + [296, -296], + [296, 282], + [296, 296], + [318, 319], + [318, 321], + [332, 333], + [332, 372], + [333, -333], + [333, 333], + [333, 333], + [333, 333], + [333, 333], + [333, 333], + [333, 333], + [333, 333], + [333, 333], + [333, 333], + [333, 333], + [333, 344], + [344, 345], + [344, 372], + [345, 344], + [345, 349], + [365, 345], + [365, 366], + [372, 373], + [372, 375], + [416, 417], + [416, 424], + [417, 418], + [417, 419], + [419, 420], + [419, 422], + [428, 429], + [428, 430] + ], + "missing_branches": [ + [123, 125], + [261, 257] + ] + }, + "apps/sharepoint-fetcher/src/grow/sharepoint_fetcher/utils.py": { + "executed_lines": [ + 9, 10, 11, 12, 14, 17, 25, 26, 27, 28, 29, 30, 32, 33, 34, 35, 36, 41, + 43, 44, 47, 48, 50, 73, 75, 84, 86, 95, 96, 97, 98, 99, 101, 102, 103, + 104, 106, 107, 108, 110, 111, 114, 119 + ], + "summary": { + "covered_lines": 43, + "num_statements": 52, + "percent_covered": 76.47058823529412, + "percent_covered_display": "76", + "missing_lines": 9, + "excluded_lines": 0, + "num_branches": 16, + "num_partial_branches": 3, + "covered_branches": 9, + "missing_branches": 7 + }, + "missing_lines": [19, 20, 21, 22, 40, 87, 88, 89, 109], + "excluded_lines": [], + "executed_branches": [ + [34, 35], + [34, 41], + [35, 36], + [86, 95], + [96, 97], + [96, 101], + [102, 103], + [102, 106], + [108, 119] + ], + "missing_branches": [ + [20, 21], + [20, 22], + [35, 40], + [86, 87], + [88, -88], + [88, 89], + [108, 109] + ] + }, + "apps/sharepoint-fetcher/tests-pex/test_pex.py": { + "executed_lines": [1, 2, 4, 5, 8, 9, 10, 14], + "summary": { + "covered_lines": 8, + "num_statements": 8, + "percent_covered": 100.0, + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + "num_branches": 0, + "num_partial_branches": 0, + "covered_branches": 0, + "missing_branches": 0 + }, + "missing_lines": [], + "excluded_lines": [], + "executed_branches": [], + "missing_branches": [] + }, + "apps/splunk-fetcher/src/grow/splunk_fetcher/__init__.py": { + "executed_lines": [1], + "summary": { + "covered_lines": 0, + "num_statements": 0, + "percent_covered": 100.0, + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + "num_branches": 0, + "num_partial_branches": 0, + "covered_branches": 0, + "missing_branches": 0 + }, + "missing_lines": [], + "excluded_lines": [], + "executed_branches": [], + "missing_branches": [] + }, + "apps/splunk-fetcher/src/grow/splunk_fetcher/cli.py": { + "executed_lines": [ + 1, 2, 3, 5, 6, 8, 14, 17, 24, 34, 35, 36, 37, 38, 39, 40, 41, 42, 50, + 56, 63, 69, 74, 79, 85, 91, 99, 144, 157 + ], + "summary": { + "covered_lines": 29, + "num_statements": 64, + "percent_covered": 36.58536585365854, + "percent_covered_display": "37", + "missing_lines": 35, + "excluded_lines": 0, + "num_branches": 18, + "num_partial_branches": 1, + "covered_branches": 1, + "missing_branches": 17 + }, + "missing_lines": [ + 18, 19, 20, 21, 25, 27, 28, 29, 30, 31, 116, 117, 118, 119, 120, 121, + 122, 123, 124, 125, 126, 138, 139, 141, 145, 146, 147, 148, 149, 150, + 151, 152, 153, 154, 158 + ], + "excluded_lines": [], + "executed_branches": [[157, -1]], + "missing_branches": [ + [27, 28], + [27, 29], + [119, 120], + [119, 122], + [122, 123], + [122, 125], + [145, 146], + [145, 147], + [147, 148], + [147, 149], + [149, 150], + [149, 151], + [151, 152], + [151, 153], + [153, -144], + [153, 154], + [157, 158] + ] + }, + "apps/splunk-fetcher/src/grow/splunk_fetcher/commands.py": { + "executed_lines": [1, 2, 3, 4, 5, 7, 9, 14, 15, 18, 22, 31, 86], + "summary": { + "covered_lines": 13, + "num_statements": 41, + "percent_covered": 24.528301886792452, + "percent_covered_display": "25", + "missing_lines": 28, + "excluded_lines": 0, + "num_branches": 12, + "num_partial_branches": 0, + "covered_branches": 0, + "missing_branches": 12 + }, + "missing_lines": [ + 19, 23, 24, 25, 26, 27, 28, 44, 51, 53, 56, 57, 58, 59, 60, 61, 63, 64, + 65, 66, 67, 68, 69, 83, 87, 88, 89, 90 + ], + "excluded_lines": [], + "executed_branches": [], + "missing_branches": [ + [24, 25], + [24, 27], + [25, 24], + [25, 26], + [56, 57], + [56, 59], + [60, 61], + [60, 63], + [65, 66], + [65, 83], + [87, 88], + [87, 89] + ] + }, + "apps/splunk-fetcher/src/grow/splunk_fetcher/logs.py": { + "executed_lines": [1, 2, 4, 7, 8, 14, 20], + "summary": { + "covered_lines": 7, + "num_statements": 21, + "percent_covered": 22.580645161290324, + "percent_covered_display": "23", + "missing_lines": 14, + "excluded_lines": 0, + "num_branches": 10, + "num_partial_branches": 0, + "covered_branches": 0, + "missing_branches": 10 + }, + "missing_lines": [9, 10, 11, 12, 15, 16, 18, 26, 27, 28, 30, 32, 33, 35], + "excluded_lines": [], + "executed_branches": [], + "missing_branches": [ + [11, -8], + [11, 12], + [15, 16], + [15, 18], + [26, 27], + [26, 32], + [27, 28], + [27, 30], + [32, 33], + [32, 35] + ] + }, + "apps/splunk-fetcher/src/grow/splunk_fetcher/splunk_base/__init__.py": { + "executed_lines": [1], + "summary": { + "covered_lines": 0, + "num_statements": 0, + "percent_covered": 100.0, + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + "num_branches": 0, + "num_partial_branches": 0, + "covered_branches": 0, + "missing_branches": 0 + }, + "missing_lines": [], + "excluded_lines": [], + "executed_branches": [], + "missing_branches": [] + }, + "apps/splunk-fetcher/src/grow/splunk_fetcher/splunk_base/splunk_base.py": { + "executed_lines": [ + 1, 2, 3, 5, 8, 9, 17, 18, 19, 20, 21, 30, 31, 32, 36, 46, 47, 49, 51, + 52, 53, 54, 55, 56, 59, 60, 61, 62, 63, 65, 66, 67, 69, 78 + ], + "summary": { + "covered_lines": 34, + "num_statements": 42, + "percent_covered": 78.26086956521739, + "percent_covered_display": "78", + "missing_lines": 8, + "excluded_lines": 0, + "num_branches": 4, + "num_partial_branches": 2, + "covered_branches": 2, + "missing_branches": 2 + }, + "missing_lines": [33, 34, 37, 44, 50, 64, 70, 79], + "excluded_lines": [], + "executed_branches": [ + [49, 51], + [61, 62] + ], + "missing_branches": [ + [49, 50], + [61, 64] + ] + }, + "apps/splunk-fetcher/tests-pex/test_pex.py": { + "executed_lines": [1, 2, 4, 5, 8, 9, 10, 14], + "summary": { + "covered_lines": 8, + "num_statements": 8, + "percent_covered": 100.0, + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + "num_branches": 0, + "num_partial_branches": 0, + "covered_branches": 0, + "missing_branches": 0 + }, + "missing_lines": [], + "excluded_lines": [], + "executed_branches": [], + "missing_branches": [] + }, + "apps/whitesource-evaluator/src/grow/whitesource_evaluator/__init__.py": { + "executed_lines": [1], + "summary": { + "covered_lines": 0, + "num_statements": 0, + "percent_covered": 100.0, + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + "num_branches": 0, + "num_partial_branches": 0, + "covered_branches": 0, + "missing_branches": 0 + }, + "missing_lines": [], + "excluded_lines": [], + "executed_branches": [], + "missing_branches": [] + }, + "apps/whitesource-evaluator/src/grow/whitesource_evaluator/cli.py": { + "executed_lines": [1, 2, 3, 5, 7, 18, 19, 20, 22, 23, 24, 48], + "summary": { + "covered_lines": 12, + "num_statements": 30, + "percent_covered": 28.0, + "percent_covered_display": "28", + "missing_lines": 18, + "excluded_lines": 0, + "num_branches": 20, + "num_partial_branches": 2, + "covered_branches": 2, + "missing_branches": 18 + }, + "missing_lines": [ + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 41, 42, 44, 45, 49 + ], + "excluded_lines": [], + "executed_branches": [ + [23, 24], + [48, -1] + ], + "missing_branches": [ + [23, 25], + [26, 27], + [26, 28], + [28, 29], + [28, 30], + [30, 31], + [30, 32], + [32, 33], + [32, 34], + [34, 35], + [34, 36], + [36, 37], + [36, 41], + [41, 42], + [41, 44], + [45, -45], + [45, -18], + [48, 49] + ] + }, + "apps/whitesource-evaluator/src/grow/whitesource_evaluator/whitesource_evaluator.py": { + "executed_lines": [ + 1, 2, 3, 4, 5, 8, 9, 10, 11, 12, 15, 16, 17, 19, 22, 23, 24, 26, 29, 30, + 31, 32, 33, 36, 37, 38, 39, 40, 41, 42, 43, 47, 51, 56, 57, 58, 59, 60, + 61, 62, 66, 69, 70, 72, 75, 76, 77, 78, 79, 80, 81, 85, 88, 89, 91 + ], + "summary": { + "covered_lines": 55, + "num_statements": 55, + "percent_covered": 100.0, + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + "num_branches": 28, + "num_partial_branches": 0, + "covered_branches": 28, + "missing_branches": 0 + }, + "missing_lines": [], + "excluded_lines": [], + "executed_branches": [ + [8, 9], + [8, 10], + [16, 17], + [16, 19], + [23, 24], + [23, 26], + [30, 31], + [30, 33], + [31, 30], + [31, 32], + [37, 38], + [37, 51], + [39, 37], + [39, 40], + [42, 43], + [42, 47], + [58, 59], + [58, 69], + [61, 62], + [61, 66], + [69, 70], + [69, 72], + [77, 78], + [77, 88], + [80, 81], + [80, 85], + [88, 89], + [88, 91] + ], + "missing_branches": [] + } + }, + "totals": { + "covered_lines": 1047, + "num_statements": 1418, + "percent_covered": 70.6989247311828, + "percent_covered_display": "71", + "missing_lines": 371, + "excluded_lines": 0, + "num_branches": 442, + "num_partial_branches": 21, + "covered_branches": 268, + "missing_branches": 174 + } +} diff --git a/yaku-apps-typescript/apps/json-evaluator/test/samples/test_array.yaml b/yaku-apps-typescript/apps/json-evaluator/test/samples/test_array.yaml new file mode 100644 index 00000000..9588f4c5 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/samples/test_array.yaml @@ -0,0 +1,19 @@ +checks: + - name: has_category_check + ref: $.store.book[*] + condition: ($[*].category).includes('fiction') + log: $.title + - name: category_check + ref: $.store.book[*] + condition: $[*].category === ["fiction", "reference"] + log: $.title + - name: fiction_check + ref: $.store.book[*] + condition: all(ref, "$.category === 'fiction'") + log: $.title + - name: none_fantasy_check + ref: $.store.book[*] + condition: none(ref, "$.category === 'fantasy'") + log: $.title +concatenation: + condition: 'has_category_check && category_check && fiction_check && none_fantasy_check' diff --git a/yaku-apps-typescript/apps/json-evaluator/test/samples/test_array_data.json b/yaku-apps-typescript/apps/json-evaluator/test/samples/test_array_data.json new file mode 100644 index 00000000..02a39888 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/samples/test_array_data.json @@ -0,0 +1,36 @@ +{ + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 19.95 + } + } +} diff --git a/yaku-apps-typescript/apps/json-evaluator/test/samples/test_single.yaml b/yaku-apps-typescript/apps/json-evaluator/test/samples/test_single.yaml new file mode 100644 index 00000000..5ff068e3 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/samples/test_single.yaml @@ -0,0 +1,5 @@ +checks: + - name: has_good_coverage + ref: $.totals.percent_covered + condition: ($) >= 80 + false: YELLOW diff --git a/yaku-apps-typescript/apps/json-evaluator/test/samples/test_single_data.json b/yaku-apps-typescript/apps/json-evaluator/test/samples/test_single_data.json new file mode 100644 index 00000000..57a63db8 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/samples/test_single_data.json @@ -0,0 +1,20 @@ +{ + "meta": { + "version": "6.5.0", + "timestamp": "2023-03-16T15:56:10.340013", + "branch_coverage": true, + "show_contexts": false + }, + "totals": { + "covered_lines": 1047, + "num_statements": 1418, + "percent_covered": 70.6989247311828, + "percent_covered_display": "71", + "missing_lines": 371, + "excluded_lines": 0, + "num_branches": 442, + "num_partial_branches": 21, + "covered_branches": 268, + "missing_branches": 174 + } +} diff --git a/yaku-apps-typescript/apps/json-evaluator/test/unit/evaluate.test.ts b/yaku-apps-typescript/apps/json-evaluator/test/unit/evaluate.test.ts new file mode 100644 index 00000000..b6feb18b --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/unit/evaluate.test.ts @@ -0,0 +1,213 @@ +import { afterEach, describe, it, expect, vi } from 'vitest' +import { + ConcatenationResult, + Status, + evalCheck, + evalConcatenation, + readJson, +} from '@B-S-F/json-evaluator-lib' + +import { evaluate } from '../../src/evaluate' +import Formatter from '../../src/formatter' + +vi.mock('@B-S-F/json-evaluator-lib', () => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + evalCheck: vi.fn((condition: string, ref: string, data: any) => { + switch (ref) { + case '$.value1': + return { + reasonPackages: [{ reasons: data.value1 > 0, context: undefined }], + } + case '$.value2': + return { + reasonPackages: [ + { reasons: data.value2 == 'foo', context: undefined }, + ], + } + default: + return false + } + }), + evalConcatenation: vi.fn(), + readJson: vi.fn(), +})) + +describe('evaluate', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should correctly evaluate checks and concatenation when concatenation is specified in the config', async () => { + const jsonFile = 'test.json' + const config = { + checks: [ + { + name: 'check1', + condition: 'value1 > 0', + ref: '$.value1', + }, + { + name: 'check2', + condition: 'value2 == "foo"', + ref: '$.value2', + }, + ], + concatenation: { + condition: 'check1 && check2', + }, + } + const data = { value1: 1, value2: 'foo' } + + const concatenationResult = { status: 'GREEN' } as ConcatenationResult + const concatenationInput = { + check1: { + reasonPackages: [ + { + context: undefined, + reasons: true, + }, + ], + }, + check2: { + reasonPackages: [ + { + context: undefined, + reasons: true, + }, + ], + }, + } + + const check = { + status: undefined, + ref: undefined, + condition: undefined, + bool: undefined, + reasonPackage: { + reasons: true, + context: undefined, + }, + } + + const options = { + logProperty: undefined, + } + + const readJsonMock = vi.mocked(readJson).mockResolvedValueOnce(data) + const evalCheckMock = vi.mocked(evalCheck) + const evalConcatenationMock = vi + .mocked(evalConcatenation) + .mockReturnValueOnce(concatenationResult) + const formatMock = vi.spyOn(Formatter, 'formatMessage') + await evaluate(jsonFile, config) + + expect(readJsonMock).toHaveBeenCalledWith(jsonFile) + expect(evalCheckMock).toHaveBeenCalledTimes(2) + expect(evalCheckMock).toHaveBeenCalledWith('value1 > 0', '$.value1', data, { + ...config.checks[0], + }) + expect(evalCheckMock).toHaveBeenCalledWith( + 'value2 == "foo"', + '$.value2', + data, + { + ...config.checks[1], + }, + ) + expect(formatMock).toHaveBeenCalledTimes(2) + expect(formatMock).toHaveBeenCalledWith('check1', check, options) + + expect(formatMock).toHaveBeenCalledWith('check2', check, options) + expect(evalConcatenationMock).toHaveBeenCalledWith( + 'check1 && check2', + concatenationInput, + ) + }) + + it('should correctly evaluate checks and concatenation when concatenation is not specified in the config', async () => { + const jsonFile = 'test.json' + + const config = { + checks: [ + { + name: 'check1', + condition: 'value1 > 0', + ref: '$.value1', + false: 'YELLOW' as Status, + }, + { + name: 'check2', + condition: 'value2 == "foo"', + ref: '$.value2', + }, + ], + } + const data = { value1: 1, value2: 'foo' } + + const concatenationResult = { + status: 'GREEN' as Status, + } as ConcatenationResult + + const concatenationInput = { + check1: { + reasonPackages: [ + { + context: undefined, + reasons: true, + }, + ], + }, + check2: { + reasonPackages: [ + { + context: undefined, + reasons: true, + }, + ], + }, + } + + const check = { + status: undefined, + ref: undefined, + condition: undefined, + bool: undefined, + reasonPackage: { + reasons: true, + context: undefined, + }, + } + const options = { + logProperty: undefined, + } + + const readJsonMock = vi.mocked(readJson).mockResolvedValueOnce(data) + const evalCheckMock = vi.mocked(evalCheck) + const evalConcatenationMock = vi + .mocked(evalConcatenation) + .mockReturnValueOnce(concatenationResult) + const formatMock = vi.spyOn(Formatter, 'formatMessage') + await evaluate(jsonFile, config) + + expect(readJsonMock).toHaveBeenCalledWith(jsonFile) + expect(evalCheckMock).toHaveBeenCalledTimes(2) + expect(evalCheckMock).toHaveBeenCalledWith('value1 > 0', '$.value1', data, { + ...config.checks[0], + }) + expect(evalCheckMock).toHaveBeenCalledWith( + 'value2 == "foo"', + '$.value2', + data, + { + ...config.checks[1], + }, + ) + expect(formatMock).toHaveBeenCalledTimes(2) + expect(formatMock).toHaveBeenCalledWith('check1', check, options) + expect(formatMock).toHaveBeenCalledWith('check2', check, options) + expect(evalConcatenationMock).toHaveBeenCalledWith( + 'check1 && check2', + concatenationInput, + ) + }) +}) diff --git a/yaku-apps-typescript/apps/json-evaluator/test/unit/formatter.test.ts b/yaku-apps-typescript/apps/json-evaluator/test/unit/formatter.test.ts new file mode 100644 index 00000000..5337393d --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/unit/formatter.test.ts @@ -0,0 +1,277 @@ +import { afterEach, describe, it, expect, vi } from 'vitest' +import { AppError, GetLogger } from '@B-S-F/autopilot-utils' + +import Formatter from '../../src/formatter' + +describe('getConditionQuantity', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should throw a AppError on missing condition', () => { + expect(() => Formatter.getConditionQuantity(undefined)).toThrowError( + new AppError('Missing condition'), + ) + }) + + it('should return undefined if no match is found', () => { + const condition = "$.category === 'fiction'" + expect(Formatter.getConditionQuantity(condition)).toEqual(undefined) + }) + + it('should return the matchType if a match is found', () => { + const condition = `all(ref, "$.category === 'fiction'")` + const expectedResult = 'all' + expect(Formatter.getConditionQuantity(condition)).toEqual(expectedResult) + }) +}) + +describe('isolateCondition', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should throw a AppError for undefined condition', () => { + expect(() => Formatter.isolateCondition(undefined)).toThrowError( + new AppError('Missing condition'), + ) + }) + + it('should return the plain condition if no match is found', () => { + const condition = "$.category === 'fiction'" + expect(Formatter.isolateCondition(condition)).toEqual(condition) + }) + + it('should return the isolated condition if match is found', () => { + const condition = `all(ref, "$.category === 'fiction'")` + const expectedResult = `"$.category === 'fiction'"` + expect(Formatter.isolateCondition(condition)).toEqual(expectedResult) + }) +}) + +describe('tokenizeCondition', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should throw a AppError for undefined condition', () => { + expect(() => Formatter.tokenizeCondition(undefined)).toThrowError( + new AppError('Missing condition'), + ) + }) + + it('should throw a AppError on bad match', () => { + expect(() => Formatter.tokenizeCondition(' ')).toThrowError( + new AppError('Condition exists, but no participants were matched'), + ) + }) + + it('should return a single string on incomplete match', () => { + const condition = 'some bad string' + expect(Formatter.isolateCondition(condition)).toEqual('some bad string') + }) + + it('should return a match containing a token, a condition and another token', () => { + const condition = `"$.category === 'fiction'"` + const expectedResult = [`"$.category`, '===', `'fiction'"`] + expect(Formatter.tokenizeCondition(condition)).toEqual(expectedResult) + }) +}) + +describe('cleanString', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should return an empty string for undefined dirtyString', () => { + expect(Formatter.cleanString(undefined)).toEqual('') + }) + + it('should return an empty string on no match', () => { + const badString = '.?>,' + expect(Formatter.cleanString(badString)).toEqual('') + }) + + it('should return a clean string (only letters and numbers) on match', () => { + const dirtyString = `"$.category` + expect(Formatter.cleanString(dirtyString)).toEqual('category') + }) +}) + +describe('getConditionParticipants', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should throw a AppError on bad condition', () => { + const condition = ' ' + const expectedError = new AppError( + 'Condition exists, but no participants were matched', + ) + const isolateConditionMock = vi.spyOn(Formatter, 'isolateCondition') + const tokenizeConditionMock = vi.spyOn(Formatter, 'tokenizeCondition') + const cleanStringMock = vi.spyOn(Formatter, 'cleanString') + + expect(() => Formatter.getConditionParticipants(condition)).toThrowError( + expectedError, + ) + + expect(isolateConditionMock).not.toBeCalled() + expect(tokenizeConditionMock).toBeCalledTimes(1) + expect(cleanStringMock).toBeCalledTimes(0) + }) + + it('should get an array containing the subject, operation and receiver, without quantity', () => { + const condition = "$.category === 'fiction'" + const expectedResult = ['category', 'equal to', 'fiction'] + + const isolateConditionMock = vi.spyOn(Formatter, 'isolateCondition') + const tokenizeConditionMock = vi.spyOn(Formatter, 'tokenizeCondition') + const cleanStringMock = vi.spyOn(Formatter, 'cleanString') + + tokenizeConditionMock.mockImplementation(() => [ + '$.category', + '===', + 'fiction', + ]) + cleanStringMock.mockImplementationOnce(() => 'category') + cleanStringMock.mockImplementationOnce(() => 'fiction') + + const result = Formatter.getConditionParticipants(condition) + expect(isolateConditionMock).not.toBeCalled() + expect(tokenizeConditionMock).toBeCalledTimes(1) + expect(cleanStringMock).toBeCalledTimes(2) + expect(result).toEqual(expectedResult) + }) + + it('should get an array containing the subject, operation and receiver, with quantity', () => { + const condition = `all(ref, "$.category === 'fiction'")` + const expectedResult = ['category', 'equal to', 'fiction'] + + const isolateConditionMock = vi.spyOn(Formatter, 'isolateCondition') + const tokenizeConditionMock = vi.spyOn(Formatter, 'tokenizeCondition') + const cleanStringMock = vi.spyOn(Formatter, 'cleanString') + + isolateConditionMock.mockImplementation(() => `$.category === 'fiction'`) + tokenizeConditionMock.mockImplementation(() => [ + '$.category', + '===', + 'fiction', + ]) + cleanStringMock.mockImplementationOnce(() => 'category') + cleanStringMock.mockImplementationOnce(() => 'fiction') + + const result = Formatter.getConditionParticipants(condition, 'all') + expect(result).toEqual(expectedResult) + expect(isolateConditionMock).toBeCalledTimes(1) + expect(tokenizeConditionMock).toBeCalledTimes(1) + expect(cleanStringMock).toBeCalledTimes(2) + }) +}) + +describe('getReasonMessage', () => { + it('should return the reasons with context value appended', () => { + const reasons = 'Reason 1, Reason 2' + const context = { property: undefined, value: 'Context Value' } + const expectedResult = 'Reason 1, Reason 2, Context Value' + expect(Formatter.getReasonMessage(reasons, context)).toEqual(expectedResult) + }) + + it('should return the reasons if context value is not provided', () => { + const reasons = 'Reason 1, Reason 2' + const context = { property: 'Context Property', value: undefined } + const expectedResult = 'Reason 1, Reason 2' + expect(Formatter.getReasonMessage(reasons, context)).toEqual(expectedResult) + }) + + it('should log a warning if context value is not provided but context property is', () => { + const reasons = 'Reason 1, Reason 2' + const context = { property: 'Context Property', value: undefined } + const logger = GetLogger() + const loggerWarnSpy = vi.spyOn(logger, 'warn') + const expectedResult = 'Reason 1, Reason 2' + expect(Formatter.getReasonMessage(reasons, context)).toEqual(expectedResult) + expect(loggerWarnSpy).toHaveBeenCalledWith( + `Warning: log value not found for property: ${context.property}`, + ) + }) + + it('should return the reasons if context is not provided', () => { + const reasons = 'Reason 1, Reason 2' + const context = { property: undefined, value: undefined } + const expectedResult = 'Reason 1, Reason 2' + expect(Formatter.getReasonMessage(reasons, context)).toEqual(expectedResult) + }) +}) + +describe('getJustificationMessage', () => { + it('should return `No resulted values from this query` in case there are no reasons and no quantity', () => { + const quantity = undefined + const reasons = '' + const expectedResult = 'No resulted values from this query' + expect(Formatter.getJustificationMessage(quantity, reasons)).toEqual( + expectedResult, + ) + }) + + it('should return the actual values for reasons when there is no quantity, but there are reasons', () => { + const quantity = undefined + const reasons = 'Reason 1, Reason 2' + const expectedResult = 'Actual values equal: "**Reason 1, Reason 2**"' + expect(Formatter.getJustificationMessage(quantity, reasons)).toEqual( + expectedResult, + ) + }) + + it('should return the appropriate message for `all` quantity', () => { + const quantity = 'all' + const reasons = 'Reason 1, Reason 2' + const expectedResult = `One or more values do not satisfy the condition: "**Reason 1, Reason 2**"` + expect(Formatter.getJustificationMessage(quantity, reasons)).toEqual( + expectedResult, + ) + }) + + it('should return the appropriate message for `any` quantity', () => { + const quantity = 'any' + const reasons = 'Reason 1, Reason 2' + const expectedResult = `None satisfy the condition. Actual values are: "**Reason 1, Reason 2**"` + expect(Formatter.getJustificationMessage(quantity, reasons)).toEqual( + expectedResult, + ) + }) + + it('should return the appropriate message for `one` quantity', () => { + const quantity = 'one' + const reasons = 'Reason 1, Reason 2' + const expectedResult = `None or more than one values satisfy the condition: "**Reason 1, Reason 2**"` + expect(Formatter.getJustificationMessage(quantity, reasons)).toEqual( + expectedResult, + ) + }) + + it('should return the appropriate message for `none` quantity', () => { + const quantity = 'none' + const reasons = 'Reason 1, Reason 2' + const expectedResult = `Some values satisfy the condition: "**Reason 1, Reason 2**"` + expect(Formatter.getJustificationMessage(quantity, reasons)).toEqual( + expectedResult, + ) + }) + + it('should throw an error when the quantity is not appropriate', () => { + const quantity = 'bad' + const reasons = 'Reason 1, Reason 2' + expect(() => + Formatter.getJustificationMessage(quantity, reasons), + ).toThrowError(new AppError('Bad quantity')) + }) +}) + +describe('formatMessage', () => { + it('', () => {}) +}) + +describe('formatReasonPackage', () => { + it('', () => {}) +}) diff --git a/yaku-apps-typescript/apps/json-evaluator/test/unit/logger.test.ts b/yaku-apps-typescript/apps/json-evaluator/test/unit/logger.test.ts new file mode 100644 index 00000000..22b2153c --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/unit/logger.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest' + +import { Logger } from '../../src/logger' + +describe('Logger', async () => { + let logger: Logger + + beforeEach(() => { + logger = new Logger() + }) + + afterEach(() => { + logger.restore() + }) + + it('should write to the log stream', async () => { + expect(() => logger.writeToStream('This is a test.')).not.toThrowError() + logger.end() + expect(logger.getLogString()).resolves.toBe('This is a test.') + }) + + it('should restore stdout', () => { + expect(() => logger.restore()).not.toThrowError() + }) + + it('should return the string written to the log stream', async () => { + logger.writeToStream('This is a test.') + logger.end() + expect(logger.getLogString()).resolves.toBe('This is a test.') + }) + + it('should remove color codes from the log string', async () => { + const testString = '\u001b[32mThis is a test.\u001b[39m' + logger.writeToStream(testString) + logger.end() + expect(logger.getLogString()).resolves.toBe('This is a test.') + }) + + it('should make urls clickable in the log string', async () => { + const testString = 'This is a url: http://example.com' + logger.writeToStream(testString) + logger.end() + expect(logger.getLogString()).resolves.toBe( + 'This is a url: [http://example.com](http://example.com)', + ) + }) +}) diff --git a/yaku-apps-typescript/apps/json-evaluator/test/unit/main.test.ts b/yaku-apps-typescript/apps/json-evaluator/test/unit/main.test.ts new file mode 100644 index 00000000..6e93a099 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/unit/main.test.ts @@ -0,0 +1,132 @@ +import { afterEach, describe, it, expect, vi, beforeEach } from 'vitest' +import * as autopilotUtils from '@B-S-F/autopilot-utils' +import { checkEnvironmentVariables, main } from '../../src/main' +import * as parseConfig from '../../src/parse-config' +import * as utils from '../../src/util' +import { Config } from '../../src/types' +import * as evaluate from '../../src/evaluate' + +vi.mock('../src/evaluate', () => ({ + evaluate: vi.fn(), +})) + +vi.mock('../src/logger', () => { + const Logger = vi.fn() + Logger.prototype.getLogString = vi.fn(() => 'some log string') + Logger.prototype.end = vi.fn() + Logger.prototype.restore = vi.fn() + return { Logger } +}) + +describe('checkEnvironmentVariables', () => { + beforeEach(() => { + vi.stubEnv('JSON_INPUT_FILE', 'somefile') + vi.stubEnv('JSON_CONFIG_FILE', 'someotherfile') + }) + + afterEach(() => { + vi.unstubAllEnvs() + }) + + it('should throw an error if env variable "JSON_INPUT_FILE" is not provided', () => { + vi.stubEnv('JSON_INPUT_FILE', '') + expect(() => checkEnvironmentVariables()).toThrowError( + 'Env variable "JSON_INPUT_FILE" is not provided', + ) + }) + + it('should throw an error if env variable "JSON_CONFIG_FILE" is not provided', () => { + vi.stubEnv('JSON_CONFIG_FILE', '') + expect(() => checkEnvironmentVariables()).toThrowError( + 'Env variable "JSON_CONFIG_FILE" is not provided', + ) + }) +}) + +describe('main', () => { + process.exit = vi.fn() + vi.mock('fs') + + beforeEach(() => { + vi.stubEnv('JSON_INPUT_FILE', 'somefile') + vi.stubEnv('JSON_CONFIG_FILE', 'test.json') + }) + + afterEach(() => { + vi.unstubAllEnvs() + }) + + it('should set status to FAILED when JSON_INPUT_FILE environment variable was not set', async () => { + delete process.env.JSON_INPUT_FILE + const spyStatus = vi.spyOn(autopilotUtils.AppOutput.prototype, 'setStatus') + const spyReason = vi.spyOn(autopilotUtils.AppOutput.prototype, 'setReason') + + await main() + + expect(spyStatus).toHaveBeenCalledWith('FAILED') + expect(spyReason).toHaveBeenCalledWith( + 'Env variable "JSON_INPUT_FILE" is not provided', + ) + }) + + it('should set status to FAILED when JSON_CONFIG_FILE environment variable was not set', async () => { + delete process.env.JSON_CONFIG_FILE + const spyStatus = vi.spyOn(autopilotUtils.AppOutput.prototype, 'setStatus') + const spyReason = vi.spyOn(autopilotUtils.AppOutput.prototype, 'setReason') + + await main() + + expect(spyStatus).toHaveBeenCalledWith('FAILED') + expect(spyReason).toHaveBeenCalledWith( + 'Env variable "JSON_CONFIG_FILE" is not provided', + ) + }) + + it('should throw an error when the config parsing failed', async () => { + const spyGetPathFromEnvVariable = vi.spyOn(utils, 'getPathFromEnvVariable') + spyGetPathFromEnvVariable.mockImplementation(() => { + throw new Error('Unexpected error') + }) + + const result = main() + + await expect(result).rejects.toThrowError(new Error('Unexpected error')) + }) + + it('should throw an error when the config parsing failed', async () => { + const expectedConfig = { + checks: [ + { + name: 'Check1', + ref: '$.prop1', + condition: '$.prop1 === 1', + true: 'GREEN', + }, + { + name: 'Check2', + ref: '$.prop2', + condition: '$.prop2 === "foo"', + false: 'YELLOW', + }, + ], + concatenation: { + condition: 'Check1 && Check2', + }, + } + + const spyGetPathFromEnvVariable = vi.spyOn(utils, 'getPathFromEnvVariable') + spyGetPathFromEnvVariable.mockReturnValue('test.json') + + const spyParseConfig = vi.spyOn(parseConfig, 'parseConfig') + spyParseConfig.mockResolvedValue(expectedConfig as unknown as Config) + + const spyEvaluate = vi.spyOn(evaluate, 'evaluate') + spyEvaluate.mockImplementation(() => { + throw new Error('Unexpected error') + }) + + const result = main() + + await expect(result).rejects.toThrowError(new Error('Unexpected error')) + }) +}) diff --git a/yaku-apps-typescript/apps/json-evaluator/test/unit/parse-config.test.ts b/yaku-apps-typescript/apps/json-evaluator/test/unit/parse-config.test.ts new file mode 100644 index 00000000..e0ac2aae --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/unit/parse-config.test.ts @@ -0,0 +1,90 @@ +import { describe, it, expect, vi } from 'vitest' + +import { readFile } from 'fs/promises' +import { readYamlData, parseConfig } from '../../src/parse-config' + +vi.mock('fs/promises', () => ({ + readFile: vi.fn(() => 'foo: bar\n'), +})) + +describe('readYamlData', async () => { + it('should read and parse YAML file', async () => { + const filePath = 'test.yaml' + const expectedData = { foo: 'bar' } + const data = await readYamlData(filePath) + expect(data).toEqual(expectedData) + }) + + it('should throw error if YAML file could not be parsed', async () => { + const filePath = 'test.yaml' + const errorMsg = 'Error parsing YAML file' + vi.mocked(readFile).mockRejectedValueOnce(new Error(errorMsg)) + await expect(readYamlData(filePath)).rejects.toThrow( + `File ${filePath} could not be read, failed with error: Error: ${errorMsg}`, + ) + }) +}) + +describe('parseConfig', () => { + it('should parse YAML file with valid schema', async () => { + const filePath = 'test.yaml' + const expectedConfig = { + checks: [ + { + name: 'Check1', + ref: '$.prop1', + condition: '$.prop1 === 1', + true: 'GREEN', + }, + { + name: 'Check2', + ref: '$.prop2', + condition: '$.prop2 === "foo"', + false: 'YELLOW', + }, + ], + concatenation: { + condition: 'Check1 && Check2', + }, + } + const yamlContent = ` +checks: + - name: Check1 + ref: $.prop1 + condition: $.prop1 === 1 + true: GREEN + - name: Check2 + ref: $.prop2 + condition: $.prop2 === "foo" + false: YELLOW +concatenation: + condition: Check1 && Check2 +` + vi.mocked(readFile).mockResolvedValueOnce(yamlContent) + const config = await parseConfig(filePath) + expect(config).toEqual(expectedConfig) + }) + + it('should throw error if YAML file has invalid schema', async () => { + const filePath = 'test.yaml' + vi.mocked(readFile).mockResolvedValueOnce('invalid_yaml') + await expect(parseConfig(filePath)).rejects.toThrow( + 'Code: invalid_type ~ Path: ~ Message: Expected object, received string', + ) + }) + + it('should throw error YAML file has invalid schema at a property', async () => { + const yamlContent = ` +checks: + - names: Check1 + ref: .prop1 + condition: $.prop1 === 1 + test: nope +` + const filePath = 'test.yaml' + vi.mocked(readFile).mockResolvedValueOnce(yamlContent) + await expect(parseConfig(filePath)).rejects.toThrow( + "Code: invalid_type ~ Path: checks[0].name ~ Message: Required | Code: invalid_string ~ Path: checks[0].ref ~ Message: Invalid input: must start with \"$\" | Code: unrecognized_keys ~ Path: checks[0] ~ Message: Unrecognized key(s) in object: 'names', 'test'", + ) + }) +}) diff --git a/yaku-apps-typescript/apps/json-evaluator/test/unit/print.test.ts b/yaku-apps-typescript/apps/json-evaluator/test/unit/print.test.ts new file mode 100644 index 00000000..d9d3bbbd --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/unit/print.test.ts @@ -0,0 +1,147 @@ +import { describe, it, expect, vi, afterEach } from 'vitest' + +import { + colorStatusString, + parseReasons, + printCheckResult, + stringifyFirstLevel, +} from '../../src/print' +import { Status } from '../../src/types' + +describe('stringifyFirstLevel', () => { + it('should return an empty object string when given an empty object', () => { + const obj = {} + const result = stringifyFirstLevel(obj) + expect(result).toBe('{}') + }) + + it('should stringify a first-level object with primitive values correctly', () => { + const obj = { + foo: 'bar', + baz: 123, + qux: true, + } + const result = stringifyFirstLevel(obj) + expect(result).toBe('{"foo":"bar","baz":123,"qux":true}') + }) + + it('should replace first-level object values with their type as a placeholder', () => { + const obj = { + foo: { bar: 'baz' }, + qux: [1, 2, 3], + baz: null, + quux: undefined, + } + const result = stringifyFirstLevel(obj) + expect(result).toBe('{"foo":"","qux":"","baz":null}') + }) + + it('should not modify the original object', () => { + const obj = { + foo: { bar: 'baz' }, + qux: [1, 2, 3], + } + const result = stringifyFirstLevel(obj) + expect(result).toBe('{"foo":"","qux":""}') + expect(obj).toEqual({ foo: { bar: 'baz' }, qux: [1, 2, 3] }) + }) +}) + +describe('parseReasons function', () => { + it('should return a reasonPackage with reasons being an array of strings and context a string when passed a reasonPackage with context containing non-object values and reasons contianing objects', () => { + const reasons = [ + { foo: 'bar', baz: 'qux' }, + { foo: 'bar', baz: 'quux' }, + ] + const context = 42 + const reasonPackage = { reasons, context } + const resultingReasonPackage = { + reasons: ['{"foo":"bar","baz":"qux"}', '{"foo":"bar","baz":"quux"}'], + context: '42', + } + + const parsedReasons = parseReasons(reasonPackage) + expect(parsedReasons).toEqual(resultingReasonPackage) + }) + + it('should return a reasonPackage with all reasons and context as strings when passed a reasonPackage with reasons and context as an array of non-object values', () => { + const reasonPackage = { reasons: ['foo', 'bar', 42], context: 42 } + const resultingReasonPackage = { + reasons: ['"foo"', '"bar"', '42'], + context: '42', + } + + const parsedReasons = parseReasons(reasonPackage) + expect(parsedReasons).toEqual(resultingReasonPackage) + }) + + it('should return an empty reasonPackage when passed an empty reasonPackage', () => { + const reasonPackage = { reasons: [], context: undefined } + const parsedReasons = parseReasons(reasonPackage) + expect(parsedReasons).toEqual(reasonPackage) + }) +}) + +describe('colorStatusString', () => { + it('should color string red', () => { + const str = 'RED' + expect(colorStatusString(str)).toBe('RED'.red) + }) + + it('should color string green', () => { + const str = 'GREEN' + expect(colorStatusString(str)).toBe('GREEN'.green) + }) + + it('should color string yellow', () => { + const str = 'YELLOW' + expect(colorStatusString(str)).toBe('YELLOW'.yellow) + }) + + it('should not color other strings', () => { + const str = 'some other string' + expect(colorStatusString(str)).toBe(str) + }) +}) + +describe('printCheckResult', () => { + afterEach(() => { + vi.spyOn(console, 'log').mockRestore() + }) + it('prints check result with all fields', () => { + const check = { + ref: '123456', + condition: 'A condition', + bool: true, + status: 'SUCCESS' as Status, + reasonPackage: { reasons: ['A reason'], context: undefined }, + } + + const expectedOutput = [ + ['\nCHECK NAME\n----------'], + ['* **ref**: ' + '123456'.blue], + ['* **condition**: ' + 'A condition'.blue], + ['* **result**: ' + 'true'.blue], + ['* **status**: ' + 'SUCCESS'], + ['* **reasons**: ' + '"A reason"'], + ] + const consoleSpy = vi.spyOn(console, 'log') + printCheckResult('check name', check) + expect(consoleSpy.mock.calls).toEqual(expectedOutput) + }) + + it('prints check result without optional fields', () => { + const check = { + condition: 'A condition', + status: 'FAILED' as Status, + } + const expectedOutput = [ + ['\nCHECK NAME\n----------'], + ['* **condition**: ' + 'A condition'.blue], + ['* **status**: ' + 'FAILED'], + ] + const consoleSpy = vi.spyOn(console, 'log') + printCheckResult('check name', check) + expect(consoleSpy.mock.calls).toEqual(expectedOutput) + }) +}) diff --git a/yaku-apps-typescript/apps/json-evaluator/test/unit/sentence-builder.test.ts b/yaku-apps-typescript/apps/json-evaluator/test/unit/sentence-builder.test.ts new file mode 100644 index 00000000..dbe83ca4 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/unit/sentence-builder.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect } from 'vitest' + +import SentenceBuilder from '../../src/sentence-builder' + +describe('isPlural', () => { + const sb = new SentenceBuilder() + it('should not be plural on undefined quantity', () => { + expect(sb.isPlural('')).toEqual(false) + }) + + it('should not be plural on singular quantity', () => { + expect(sb.isPlural('one')).toEqual(false) + }) + + it('should be plural on plural quantity', () => { + expect(sb.isPlural('any')).toEqual(true) + }) +}) + +describe('getOperation', () => { + it('should contain the plural if quantity is defined', () => { + const sb = new SentenceBuilder() + const message = sb.getOperation('all', '', '', '', '') + + expect(message).toContain('all') + }) + + it('should contain the subject with underscores if it is defined', () => { + const sb = new SentenceBuilder() + const message = sb.getOperation('all', 'subject', '', '', '') + + expect(message).toContain('_subject_') + }) + + it('should contain the reference with underscores if it is defined', () => { + const sb = new SentenceBuilder() + const message = sb.getOperation('all', 'subject', 'reference', '', '') + + expect(message).toContain('_reference_') + }) + + it('should contain the proper conjunction for plural', () => { + const sb = new SentenceBuilder() + const message = sb.getOperation('all', 'subject', 'reference', '', '') + + expect(message).toContain('are') + }) + + it('should contain the proper conjunction for singular', () => { + const sb = new SentenceBuilder() + const message = sb.getOperation('one', 'subject', 'reference', '', '') + + expect(message).toContain('is') + }) + + it('should contain the operation', () => { + const sb = new SentenceBuilder() + const message = sb.getOperation('one', 'subject', 'reference', '===', '') + + expect(message).toContain('===') + }) + + it('should contain the receiver with underscores', () => { + const sb = new SentenceBuilder() + const message = sb.getOperation( + 'one', + 'subject', + 'reference', + '===', + 'receiver', + ) + + expect(message).toContain(`_receiver_`) + }) +}) diff --git a/yaku-apps-typescript/apps/json-evaluator/test/unit/util.test.ts b/yaku-apps-typescript/apps/json-evaluator/test/unit/util.test.ts new file mode 100644 index 00000000..5e51a781 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/test/unit/util.test.ts @@ -0,0 +1,139 @@ +import { describe, it, expect, vi, afterEach } from 'vitest' +import { + getPathFromEnvVariable, + isValidCheckIndex, + validateFilePath, +} from '../../src/util' +import fs from 'fs' +import { AppError } from '@B-S-F/autopilot-utils' + +describe('getPathFromEnvVariable', () => { + vi.mock('fs') + + afterEach(() => { + vi.clearAllMocks() + vi.restoreAllMocks() + vi.unstubAllEnvs() + }) + + it('should return the path for an environment variable which is not empty', () => { + vi.stubEnv('JSON_CONFIG_FILE', 'test.json') + + const envVariableName = 'JSON_CONFIG_FILE' + const filePath = 'test.json' + + const spyExistsSync = vi.spyOn(fs, 'existsSync') + spyExistsSync.mockImplementation(() => true) + + const spyAccessSync = vi.spyOn(fs, 'accessSync') + spyAccessSync.mockImplementation(() => true) + + const spyStatSync = vi.spyOn(fs, 'statSync') + spyStatSync.mockImplementation( + vi.fn().mockImplementation(() => ({ isFile: () => true })), + ) + + process.env[envVariableName] = filePath + + const result = getPathFromEnvVariable(envVariableName) + expect(result).toBe(filePath) + }) + + it('should return the path for an environment variable which is empty', () => { + vi.stubEnv('JSON_CONFIG_FILE', '') + + const envVariableName = 'JSON_CONFIG_FILE' + const filePath = '' + + const spyExistsSync = vi.spyOn(fs, 'existsSync') + spyExistsSync.mockImplementation(() => true) + + const spyAccessSync = vi.spyOn(fs, 'accessSync') + spyAccessSync.mockImplementation(() => true) + + const spyStatSync = vi.spyOn(fs, 'statSync') + spyStatSync.mockImplementation( + vi.fn().mockImplementation(() => ({ isFile: () => true })), + ) + + process.env[envVariableName] = filePath + + const result = getPathFromEnvVariable(envVariableName) + expect(result).toBe(filePath) + }) +}) + +describe('validateFilePath', () => { + vi.mock('fs') + + afterEach(() => { + vi.clearAllMocks() + vi.restoreAllMocks() + }) + + it('should throw an error when the file path does not exist', () => { + const filePath = 'test.yaml' + const errorMessage = `File ${filePath} does not exist, no data can be evaluated` + + const spyExistsSync = vi.spyOn(fs, 'existsSync') + spyExistsSync.mockImplementation(() => false) + + expect(() => validateFilePath(filePath)).toThrowError( + new AppError(errorMessage), + ) + }) + + it('should throw an error when the file path is not readable', () => { + const filePath = 'test.yaml' + const errorMessage = `${filePath} is not readable!` + + const spyExistsSync = vi.spyOn(fs, 'existsSync') + spyExistsSync.mockImplementation(() => true) + + const spyAccessSync = vi.spyOn(fs, 'accessSync') + spyAccessSync.mockImplementation(() => { + throw new AppError(errorMessage) + }) + expect(() => validateFilePath(filePath)).toThrowError( + new AppError(errorMessage), + ) + }) + + it('should throw an error when the file path does not point to a file', () => { + const filePath = 'test.yaml' + const errorMessage = `${filePath} does not point to a file!` + + const spyExistsSync = vi.spyOn(fs, 'existsSync') + spyExistsSync.mockImplementation(() => true) + + const spyAccessSync = vi.spyOn(fs, 'accessSync') + spyAccessSync.mockImplementation(() => true) + + const spyStatSync = vi.spyOn(fs, 'statSync') + spyStatSync.mockImplementation( + vi.fn().mockImplementation(() => ({ isFile: () => false })), + ) + + expect(() => validateFilePath(filePath)).toThrowError(errorMessage) + }) +}) + +describe('isValidCheckIndex', () => { + it('should return true if the value is an integer number', () => { + const index = 1 + const result = isValidCheckIndex(index) + expect(result).toBe(true) + }) + + it('should return false if the value is not a number', () => { + const index = 'string' + const result = isValidCheckIndex(index) + expect(result).toBe(false) + }) + + it('should return false if the value is a number, but not integer', () => { + const index = 1.2 + const result = isValidCheckIndex(index) + expect(result).toBe(false) + }) +}) diff --git a/yaku-apps-typescript/apps/json-evaluator/tsconfig.json b/yaku-apps-typescript/apps/json-evaluator/tsconfig.json new file mode 100644 index 00000000..2ccff658 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "@B-S-F/typescript-config/base.json", + "ts-node": { + "compilerOptions": { + "module": "ESNext" + } + }, + "compilerOptions": { + "outDir": "dist" + }, + "tsc-alias": { + "resolveFullPaths": true, + "verbose": false + }, + "include": ["src"] +} diff --git a/yaku-apps-typescript/apps/json-evaluator/tsup.config.json b/yaku-apps-typescript/apps/json-evaluator/tsup.config.json new file mode 100644 index 00000000..f35b9ed3 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/tsup.config.json @@ -0,0 +1,6 @@ +{ + "entry": ["src/index.ts"], + "splitting": false, + "sourcemap": true, + "clean": true +} diff --git a/yaku-apps-typescript/apps/json-evaluator/tsup.config.ts b/yaku-apps-typescript/apps/json-evaluator/tsup.config.ts new file mode 100644 index 00000000..0e6106f5 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/tsup.config.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/**/*.ts'], + splitting: false, + clean: true, + target: 'node18', + format: ['esm'], + bundle: false, + sourcemap: true, +}) diff --git a/yaku-apps-typescript/apps/json-evaluator/vitest-integration.config.ts b/yaku-apps-typescript/apps/json-evaluator/vitest-integration.config.ts new file mode 100644 index 00000000..8bec0725 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/vitest-integration.config.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['test/integration/**/*.int-spec.ts'], + pool: 'threads', + poolOptions: { + threads: { + maxThreads: 1, + minThreads: 1, + }, + }, + typecheck: { + tsconfig: 'tsconfig.json', + }, + reporters: ['junit', 'default'], + outputFile: 'reports/integration-test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/json-evaluator/vitest.config.ts b/yaku-apps-typescript/apps/json-evaluator/vitest.config.ts new file mode 100644 index 00000000..0d784ad3 --- /dev/null +++ b/yaku-apps-typescript/apps/json-evaluator/vitest.config.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + typecheck: { + tsconfig: 'tsconfig.json', + }, + coverage: { + provider: 'v8', + all: true, + enabled: true, + reporter: ['cobertura', 'json-summary', 'text-summary'], + include: ['src'], + exclude: ['src/index.ts', 'src/types.ts'], + }, + reporters: ['junit', 'default'], + outputFile: 'reports/test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/manual-answer-evaluator/.env.sample b/yaku-apps-typescript/apps/manual-answer-evaluator/.env.sample new file mode 100644 index 00000000..b32f1cdc --- /dev/null +++ b/yaku-apps-typescript/apps/manual-answer-evaluator/.env.sample @@ -0,0 +1,3 @@ +export manual_answer_file=sample-data/manual-answer.md +export expiration_time='10m' +export expiry_reminder_period='10d' \ No newline at end of file diff --git a/yaku-apps-typescript/apps/manual-answer-evaluator/.eslintrc.cjs b/yaku-apps-typescript/apps/manual-answer-evaluator/.eslintrc.cjs new file mode 100644 index 00000000..de074677 --- /dev/null +++ b/yaku-apps-typescript/apps/manual-answer-evaluator/.eslintrc.cjs @@ -0,0 +1,5 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +module.exports = require("@B-S-F/eslint-config/eslint-preset"); diff --git a/yaku-apps-typescript/apps/manual-answer-evaluator/.prettierrc b/yaku-apps-typescript/apps/manual-answer-evaluator/.prettierrc new file mode 100644 index 00000000..b2095be8 --- /dev/null +++ b/yaku-apps-typescript/apps/manual-answer-evaluator/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": false, + "singleQuote": true +} diff --git a/yaku-apps-typescript/apps/manual-answer-evaluator/README.md b/yaku-apps-typescript/apps/manual-answer-evaluator/README.md new file mode 100644 index 00000000..e875d9b4 --- /dev/null +++ b/yaku-apps-typescript/apps/manual-answer-evaluator/README.md @@ -0,0 +1,4 @@ +# manual-answer-evaluator + +An evaluator to evaluate if a given manual answer passed it's expiration time. +The `mdate` (modification date) of the answers file is used as a reference diff --git a/yaku-apps-typescript/apps/manual-answer-evaluator/package.json b/yaku-apps-typescript/apps/manual-answer-evaluator/package.json new file mode 100644 index 00000000..ccb7c80d --- /dev/null +++ b/yaku-apps-typescript/apps/manual-answer-evaluator/package.json @@ -0,0 +1,44 @@ +{ + "name": "@B-S-F/manual-answer-evaluator", + "version": "0.8.0", + "description": "", + "main": "dist/evaluate.js", + "type": "module", + "scripts": { + "build": "tsup", + "dev": "nodemon --watch \"src/**\" --exec npm run start", + "start": "node ./dist/evaluate.js", + "lint": "eslint '**/*.ts'", + "format": "prettier --write '**/*.{ts,md}'", + "test": "vitest run && npm run test:update-cobertura-file", + "test:update-cobertura-file": "sed -i'.bak' 's,package name=\",package name=\"'${npm_package_name}/',g' coverage/cobertura-coverage.xml", + "test:dev": "vitest -w", + "test:coverage": "vitest --coverage", + "test:ui": "vitest --ui" + }, + "keywords": [], + "author": "", + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@vitest/ui": "*", + "c8": "*", + "eslint": "*", + "nodemon": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + }, + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "parse-duration": "^1.0.2" + }, + "bin": { + "manual-answer-evaluator": "dist/evaluate.js" + }, + "files": [ + "dist" + ] +} diff --git a/yaku-apps-typescript/apps/manual-answer-evaluator/sample-data/manual-answer.md b/yaku-apps-typescript/apps/manual-answer-evaluator/sample-data/manual-answer.md new file mode 100644 index 00000000..934170a5 --- /dev/null +++ b/yaku-apps-typescript/apps/manual-answer-evaluator/sample-data/manual-answer.md @@ -0,0 +1 @@ +I'll expire sooon diff --git a/yaku-apps-typescript/apps/manual-answer-evaluator/src/evaluate.ts b/yaku-apps-typescript/apps/manual-answer-evaluator/src/evaluate.ts new file mode 100644 index 00000000..52687c96 --- /dev/null +++ b/yaku-apps-typescript/apps/manual-answer-evaluator/src/evaluate.ts @@ -0,0 +1,29 @@ +#! /usr/bin/env node +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { + AppError, + AppOutput, + InitLogger, +} from '@B-S-F/autopilot-utils' +import { + readEnvAndDoEvaluation, + ManualAnswerEvaluatorEnv, +} from './manualAnswerEvaluator/readEnvAndDoEvaluation.js' + +try { + InitLogger('manualAnswerEvaluator') + readEnvAndDoEvaluation(process.env as ManualAnswerEvaluatorEnv) +} catch (error) { + if (error instanceof AppError) { + const output = new AppOutput() + output.setReason(error.message) + output.setStatus('FAILED') + output.write() + process.exit(0) + } else { + throw error + } +} diff --git a/yaku-apps-typescript/apps/manual-answer-evaluator/src/manualAnswerEvaluator/index.ts b/yaku-apps-typescript/apps/manual-answer-evaluator/src/manualAnswerEvaluator/index.ts new file mode 100644 index 00000000..00661b9c --- /dev/null +++ b/yaku-apps-typescript/apps/manual-answer-evaluator/src/manualAnswerEvaluator/index.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +export { + evaluate, + getManualAnswer, + readManualAnswer, + ManualAnswerArgs, + ManualAnswer, +} from './manualAnswer.js' + +export { + readEnvAndDoEvaluation, + ManualAnswerEvaluatorEnv, +} from './readEnvAndDoEvaluation.js' diff --git a/yaku-apps-typescript/apps/manual-answer-evaluator/src/manualAnswerEvaluator/manualAnswer.ts b/yaku-apps-typescript/apps/manual-answer-evaluator/src/manualAnswerEvaluator/manualAnswer.ts new file mode 100644 index 00000000..82bf753b --- /dev/null +++ b/yaku-apps-typescript/apps/manual-answer-evaluator/src/manualAnswerEvaluator/manualAnswer.ts @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { AppError, AppOutput, GetLogger } from '@B-S-F/autopilot-utils' +import { getExpDate } from '../utils/getExpDate.js' +import { FileData, readContentAndMtime } from '../utils/readContentAndMtime.js' +import { validateExpDate } from '../utils/validateExpDate.js' + +class ConfigurationError extends AppError { + constructor(message: string) { + super(message) + this.name = 'ConfigurationError' + } + + Reason(): string { + return super.Reason() + } +} + +export interface ManualAnswerArgs { + manual_answer_file: string + expiration_time: string + last_modified_date_override?: string +} + +export interface ManualAnswer { + modificationDate: Date + answer: string +} + +export const getManualAnswer = (parsedInput: FileData) => { + const manualAnswer = parsedInput.content + if (manualAnswer === undefined) { + throw new Error('No manual answer found') + } + return manualAnswer +} + +export const readManualAnswer = async ( + filePath: string +): Promise => { + const fileData = await readContentAndMtime(filePath) + const answer = getManualAnswer(fileData) + return { answer, modificationDate: new Date(fileData.mtime) } +} + +export const evaluate = async ({ + manual_answer_file, + expiration_time, + last_modified_date_override, +}: ManualAnswerArgs) => { + const output = new AppOutput() + const logger = GetLogger() + const manualAnswer = await readManualAnswer(manual_answer_file) + logger.debug(`Evaluating manual answer: ${manualAnswer.answer}`) + if (last_modified_date_override) { + manualAnswer.modificationDate = new Date(last_modified_date_override) + if (manualAnswer.modificationDate.toString() === 'Invalid Date') { + throw new ConfigurationError( + `Invalid date format for last_modified_date_override: ${last_modified_date_override}` + ) + } + } + const expDate = getExpDate(manualAnswer.modificationDate, expiration_time) + logger.debug( + `LastModified: ${ + manualAnswer.modificationDate + },Expiration date: ${expDate.toISOString()}` + ) + const result = validateExpDate(expDate) + logger.debug(`Status: ${result}`) + output.setStatus(result) + switch (result) { + case 'RED': + output.setReason( + `${ + manualAnswer.answer + }\n**The manual answer is expired at ${expDate.toISOString()}**` + ) + break + case 'YELLOW': + output.setReason( + `${ + manualAnswer.answer + }\n**The manual answer will expire at ${expDate.toISOString()}**` + ) + break + case 'GREEN': + output.setReason( + `${ + manualAnswer.answer + }\n**The manual answer is valid until ${expDate.toISOString()}**` + ) + break + default: + throw new Error(`A unexpected status was calculated: ${result}`) + } + output.write() + return +} diff --git a/yaku-apps-typescript/apps/manual-answer-evaluator/src/manualAnswerEvaluator/readEnvAndDoEvaluation.ts b/yaku-apps-typescript/apps/manual-answer-evaluator/src/manualAnswerEvaluator/readEnvAndDoEvaluation.ts new file mode 100644 index 00000000..f21a5104 --- /dev/null +++ b/yaku-apps-typescript/apps/manual-answer-evaluator/src/manualAnswerEvaluator/readEnvAndDoEvaluation.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { AppError } from '@B-S-F/autopilot-utils' +import { evaluate } from './manualAnswer.js' + +export interface ManualAnswerEvaluatorEnv { + manual_answer_file?: string + expiration_time?: string + last_modified_date_override?: string +} + +class EnvironmentError extends AppError { + constructor(message: string) { + super(message) + this.name = 'EnvironmentError' + } + Reason(): string { + return super.Reason() + } +} + +export function readEnvAndDoEvaluation(env: ManualAnswerEvaluatorEnv) { + if (!env.manual_answer_file) { + throw new EnvironmentError( + 'Environment variable manual_answer_file must be set' + ) + } + if (!env.expiration_time) { + throw new EnvironmentError( + 'Environment variable expiration_time must be set' + ) + } + const { manual_answer_file, expiration_time, last_modified_date_override } = + env + evaluate({ manual_answer_file, expiration_time, last_modified_date_override }) +} diff --git a/yaku-apps-typescript/apps/manual-answer-evaluator/src/utils/getExpDate.ts b/yaku-apps-typescript/apps/manual-answer-evaluator/src/utils/getExpDate.ts new file mode 100644 index 00000000..67d10c74 --- /dev/null +++ b/yaku-apps-typescript/apps/manual-answer-evaluator/src/utils/getExpDate.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import parse from 'parse-duration' + +export const getExpDate = (modDate: Date, expTime: string): Date => { + const expTimeValue = parse(expTime) + const expDate = new Date(modDate.getTime() + expTimeValue) + return expDate +} diff --git a/yaku-apps-typescript/apps/manual-answer-evaluator/src/utils/readContentAndMtime.ts b/yaku-apps-typescript/apps/manual-answer-evaluator/src/utils/readContentAndMtime.ts new file mode 100644 index 00000000..2c304544 --- /dev/null +++ b/yaku-apps-typescript/apps/manual-answer-evaluator/src/utils/readContentAndMtime.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { readFile, stat } from 'fs/promises' + +export interface FileData { + content: string + mtime: string +} + +export const readContentAndMtime = async ( + filename: string +): Promise => { + const text = await readFile(filename, 'utf8') + const { mtime } = await stat(filename) + return { content: text, mtime: mtime.toISOString() } +} diff --git a/yaku-apps-typescript/apps/manual-answer-evaluator/src/utils/validateExpDate.ts b/yaku-apps-typescript/apps/manual-answer-evaluator/src/utils/validateExpDate.ts new file mode 100644 index 00000000..b144d380 --- /dev/null +++ b/yaku-apps-typescript/apps/manual-answer-evaluator/src/utils/validateExpDate.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { Status } from '@B-S-F/autopilot-utils' +import parse from 'parse-duration' + +export const validateExpDate = (expirationDate: Date): Status => { + const date = new Date() + // Check if expiration date is in the past + if (date >= expirationDate) { + return 'RED' + } + + const diff = expirationDate.getTime() - date.getTime() + + // parse expiryReminderTime + const expiryReminderTime = process.env.expiry_reminder_period + ? parse(process.env.expiry_reminder_period) + : parse('14d') + + // Check if the Status is within the expiry reminder period + if (diff <= expiryReminderTime) { + return 'YELLOW' + } else { + return 'GREEN' + } +} diff --git a/yaku-apps-typescript/apps/manual-answer-evaluator/test/data/manual-answer.md b/yaku-apps-typescript/apps/manual-answer-evaluator/test/data/manual-answer.md new file mode 100644 index 00000000..929decfc --- /dev/null +++ b/yaku-apps-typescript/apps/manual-answer-evaluator/test/data/manual-answer.md @@ -0,0 +1 @@ +I'll expire diff --git a/yaku-apps-typescript/apps/manual-answer-evaluator/test/manualAnswerEvaluator/manualAnswerEvaluator.test.ts b/yaku-apps-typescript/apps/manual-answer-evaluator/test/manualAnswerEvaluator/manualAnswerEvaluator.test.ts new file mode 100644 index 00000000..999a2ffc --- /dev/null +++ b/yaku-apps-typescript/apps/manual-answer-evaluator/test/manualAnswerEvaluator/manualAnswerEvaluator.test.ts @@ -0,0 +1,137 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { SpyInstanceFn, beforeEach, describe, expect, it, vi } from 'vitest' + +import { + evaluate, + getManualAnswer, + readManualAnswer, +} from '../../src/manualAnswerEvaluator' +import { + FileData, + readContentAndMtime, +} from '../../src/utils/readContentAndMtime' +import { AppError, InitLogger } from '@B-S-F/autopilot-utils' + +describe('getManualAnswer', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.resetModules() + InitLogger('test') + }) + it('should return the manual answer', () => { + const parsedInput: FileData = { + content: 'answer', + mtime: '2020-01-01T00:00:00.000Z', + } + expect(getManualAnswer(parsedInput)).toBe('answer') + }) + it('should throw Error No manual answer found', () => { + const parsedInput: FileData = { + content: undefined, + mtime: '2020-01-01T00:00:00.000Z', + } + expect(() => getManualAnswer(parsedInput)).toThrow('No manual answer found') + }) +}) + +describe('readManualAnswer', () => { + vi.mock('../../src/utils/readContentAndMtime') + const mockReadContentAndMtime = readContentAndMtime as SpyInstanceFn + + beforeEach(() => { + vi.clearAllMocks() + vi.resetModules() + InitLogger('test') + }) + it('should return the answer and the modification Date', async () => { + mockReadContentAndMtime.mockResolvedValue({ + content: 'answer', + mtime: '2020-01-01T00:00:00.000Z', + }) + const manualAnswer = await readManualAnswer('my-file.md') + expect(manualAnswer).toEqual({ + answer: 'answer', + modificationDate: new Date('2020-01-01T00:00:00.000Z'), + }) + }) +}) + +describe('evaluate', () => { + vi.mock('../../src/manualAnswerEvaluator/manualAnswer', async () => { + const manualAnswer = (await vi.importActual( + '../../src/manualAnswerEvaluator/manualAnswer' + )) as any + return { + ...manualAnswer, + readManualAnswer: vi.fn().mockResolvedValue({ + answer: 'answer', + modificationDate: new Date('2020-01-01T00:00:00.000Z'), + }), + } + }) + + beforeEach(() => { + vi.clearAllMocks() + vi.resetModules() + InitLogger('test') + }) + + it('should return the correct output comment', async () => { + const mockReadManualAnswer = readManualAnswer as SpyInstanceFn + const consoleSpy = vi.spyOn(console, 'log') + mockReadManualAnswer.mockResolvedValue({ + answer: 'answer', + modificationDate: new Date('2020-01-01T00:00:00.000Z'), + }) + await evaluate({ + manual_answer_file: 'my-file.md', + expiration_time: '1d', + }) + expect(consoleSpy).toHaveBeenCalledWith( + JSON.stringify({ + status: 'RED', + reason: + 'answer\n**The manual answer is expired at 2020-01-02T00:00:00.000Z**', + }) + ) + }) + + it('should support last_modified_date_override', async () => { + const mockReadManualAnswer = readManualAnswer as SpyInstanceFn + const consoleSpy = vi.spyOn(console, 'log') + mockReadManualAnswer.mockResolvedValue({ + answer: 'answer', + modificationDate: new Date('2020-01-01T00:00:00.000Z'), + }) + await evaluate({ + manual_answer_file: 'my-file.md', + expiration_time: '1d', + last_modified_date_override: '2020-01-02T00:00:00.000Z', + }) + expect(consoleSpy).toHaveBeenCalledWith( + JSON.stringify({ + status: 'RED', + reason: + 'answer\n**The manual answer is expired at 2020-01-03T00:00:00.000Z**', + }) + ) + }) + + it('should throw an AppError if last_modified_date_override is not valid', async () => { + const mockReadManualAnswer = readManualAnswer as SpyInstanceFn + mockReadManualAnswer.mockResolvedValue({ + answer: 'answer', + modificationDate: new Date('2020-01-01T00:00:00.000Z'), + }) + await expect( + evaluate({ + manual_answer_file: 'my-file.md', + expiration_time: '1d', + last_modified_date_override: '26.08.2020', + }) + ).rejects.toThrow(AppError) + }) +}) diff --git a/yaku-apps-typescript/apps/manual-answer-evaluator/test/manualAnswerEvaluator/readEnvAndDoEvaluation.test.ts b/yaku-apps-typescript/apps/manual-answer-evaluator/test/manualAnswerEvaluator/readEnvAndDoEvaluation.test.ts new file mode 100644 index 00000000..8c06819d --- /dev/null +++ b/yaku-apps-typescript/apps/manual-answer-evaluator/test/manualAnswerEvaluator/readEnvAndDoEvaluation.test.ts @@ -0,0 +1,51 @@ +import { beforeEach, describe, expect, it, vi, SpyInstanceFn } from 'vitest' + +import { evaluate } from '../../src/manualAnswerEvaluator/manualAnswer' + +import { readEnvAndDoEvaluation } from '../../src/manualAnswerEvaluator' +import { AppError } from '@B-S-F/autopilot-utils' + +describe('readEnvAndDoEvaluation', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.resetModules() + }) + + vi.mock('../../src/manualAnswerEvaluator/manualAnswer') + const mockEvaluate = evaluate as SpyInstanceFn + + it('should transmit env vars correctly', () => { + const args = { + expiration_time: 'foo', + last_modified_date_override: 'bar', + manual_answer_file: 'baz', + } + readEnvAndDoEvaluation(args) + expect(mockEvaluate).toBeCalledWith(args) + }) + + it('should throw an AppError if expiration_time is missing', () => { + const args = { + last_modified_date_override: 'bar', + manual_answer_file: 'baz', + } + expect(() => readEnvAndDoEvaluation(args)).toThrow(AppError) + }) + + it('should throw an AppError if manual_answer_file is missing', () => { + const args = { + expiration_time: 'foo', + last_modified_date_override: 'bar', + } + expect(() => readEnvAndDoEvaluation(args)).toThrow(AppError) + }) + + it('should not raise an error if last_modified_date_override is missing', () => { + const args = { + expiration_time: 'foo', + manual_answer_file: 'bar', + } + expect(() => readEnvAndDoEvaluation(args)).not.toThrowError() + expect(mockEvaluate).toBeCalled() + }) +}) diff --git a/yaku-apps-typescript/apps/manual-answer-evaluator/test/utils/getExpDate.test.ts b/yaku-apps-typescript/apps/manual-answer-evaluator/test/utils/getExpDate.test.ts new file mode 100644 index 00000000..164fe46b --- /dev/null +++ b/yaku-apps-typescript/apps/manual-answer-evaluator/test/utils/getExpDate.test.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { describe, expect, it } from 'vitest' +import { getExpDate } from '../../src/utils/getExpDate' +const date = new Date('2030-01-01T00:00:00.000Z') +describe('getExpDate', () => { + it('should get expiration date 1 day ahead', () => { + const result = getExpDate(date, '1d') + expect(result).toEqual(new Date('2030-01-02T00:00:00.000Z')) + }) + it('should get expiration date 1 year and 1 day and 1 hour ahead', () => { + const result = getExpDate(date, '1year 1day 1hour') + // careful: 1 year = 365.25 days + expect(result).toEqual(new Date('2031-01-02T07:00:00.000Z')) + }) +}) diff --git a/yaku-apps-typescript/apps/manual-answer-evaluator/test/utils/readContentAndMtime.test.ts b/yaku-apps-typescript/apps/manual-answer-evaluator/test/utils/readContentAndMtime.test.ts new file mode 100644 index 00000000..60720913 --- /dev/null +++ b/yaku-apps-typescript/apps/manual-answer-evaluator/test/utils/readContentAndMtime.test.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { beforeEach, describe, expect, it, SpyInstanceFn, vi } from 'vitest' +import { readContentAndMtime } from '../../src/utils/readContentAndMtime' +import { readFile, stat } from 'fs/promises' + +describe('readContentAndMtime', () => { + vi.mock('fs/promises') + const mockReadFile = readFile as SpyInstanceFn + const mockStat = stat as SpyInstanceFn + + beforeEach(() => { + vi.clearAllMocks() + vi.resetModules() + }) + + it('should return object with content and mtime', async () => { + mockReadFile.mockResolvedValue('content') + mockStat.mockResolvedValue({ mtime: new Date('2020-01-01T00:00:00.000Z') }) + const result = await readContentAndMtime('filename') + expect(result).toEqual({ + content: 'content', + mtime: '2020-01-01T00:00:00.000Z', + }) + }) +}) diff --git a/yaku-apps-typescript/apps/manual-answer-evaluator/test/utils/validateExpDate.test.ts b/yaku-apps-typescript/apps/manual-answer-evaluator/test/utils/validateExpDate.test.ts new file mode 100644 index 00000000..5dc6d0a5 --- /dev/null +++ b/yaku-apps-typescript/apps/manual-answer-evaluator/test/utils/validateExpDate.test.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { validateExpDate } from '../../src/utils/validateExpDate' + +describe('validateExpDate', () => { + beforeEach(() => { + process.env.expiry_reminder_period = '' + vi.resetModules() + }) + it('should return red if date is expired', () => { + const expirationDate = new Date('2020-03-24T13:51:25.061+0100') + const result = validateExpDate(expirationDate) + expect(result).toEqual('RED') + }) + + it('should return yellow if expiration date is within the next default 14 days', () => { + const today = new Date() + const date14Days = new Date(today.getTime() + 14 * 24 * 60 * 60 * 1000) + const result = validateExpDate(date14Days) + expect(result).toEqual('YELLOW') + }) + + it('should return yellow if expiration date is within the next 16 days', () => { + process.env.expiry_reminder_period = '16d' + const today = new Date() + const date16Days = new Date(today.getTime() + 16 * 24 * 60 * 60 * 1000) + const result = validateExpDate(date16Days) + expect(result).toEqual('YELLOW') + }) + + it('should return green if expiration date is more than default 14 days in the future', () => { + const today = new Date() + const date15Days = new Date(today.getTime() + 15 * 24 * 60 * 60 * 1000) + const result = validateExpDate(date15Days) + expect(result).toEqual('GREEN') + }) +}) diff --git a/yaku-apps-typescript/apps/manual-answer-evaluator/tsconfig.json b/yaku-apps-typescript/apps/manual-answer-evaluator/tsconfig.json new file mode 100644 index 00000000..0df56473 --- /dev/null +++ b/yaku-apps-typescript/apps/manual-answer-evaluator/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@B-S-F/typescript-config/base.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/yaku-apps-typescript/apps/manual-answer-evaluator/tsup.config.ts b/yaku-apps-typescript/apps/manual-answer-evaluator/tsup.config.ts new file mode 100644 index 00000000..0e6106f5 --- /dev/null +++ b/yaku-apps-typescript/apps/manual-answer-evaluator/tsup.config.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/**/*.ts'], + splitting: false, + clean: true, + target: 'node18', + format: ['esm'], + bundle: false, + sourcemap: true, +}) diff --git a/yaku-apps-typescript/apps/manual-answer-evaluator/vitest.config.ts b/yaku-apps-typescript/apps/manual-answer-evaluator/vitest.config.ts new file mode 100644 index 00000000..1cf5e21f --- /dev/null +++ b/yaku-apps-typescript/apps/manual-answer-evaluator/vitest.config.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + typecheck: { + tsconfig: 'tsconfig.json', + }, + coverage: { + provider: 'v8', + all: true, + enabled: true, + reporter: ['cobertura', 'json-summary', 'text-summary'], + include: ['src'], + }, + reporters: ['junit', 'default'], + outputFile: 'reports/test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/.env.sample b/yaku-apps-typescript/apps/mend-fetcher/.env.sample new file mode 100644 index 00000000..98b35f23 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/.env.sample @@ -0,0 +1,12 @@ +MEND_API_URL= +MEND_SERVER_URL= +MEND_ORG_TOKEN= +MEND_PROJECT_ID= +MEND_PROJECT_TOKEN= +MEND_USER_EMAIL= +MEND_USER_KEY= +MEND_REPORT_TYPE= +MEND_ALERTS_STATUS= +MEND_MIN_CONNECTION_TIME= +MEND_MAX_CONCURRENT_CONNECTIONS= +MEND_RESULTS_PATH= diff --git a/yaku-apps-typescript/apps/mend-fetcher/.eslintrc.cjs b/yaku-apps-typescript/apps/mend-fetcher/.eslintrc.cjs new file mode 100644 index 00000000..9ef0a5d9 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/.eslintrc.cjs @@ -0,0 +1,5 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +module.exports = require("@B-S-F/eslint-config/eslint-preset"); diff --git a/yaku-apps-typescript/apps/mend-fetcher/README.md b/yaku-apps-typescript/apps/mend-fetcher/README.md new file mode 100644 index 00000000..16779cce --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/README.md @@ -0,0 +1,67 @@ +# Mend Fetcher + +## Setup the environment variables + +After doing the [Installation and Build step](../../README.md#installation) make a copy of the `.env.sample` template + +```sh +cp .env.sample .env +``` + +Set the required environment variables in `.env` + +```sh +MEND_API_URL= +MEND_SERVER_URL= +MEND_ORG_TOKEN= +MEND_PROJECT_TOKEN= +MEND_USER_EMAIL= +MEND_USER_KEY= +``` + +Export them to the current shell with + +```sh +export $(grep -v '^#' .env | xargs -0) +``` + +| Environment Variable | Description | +| :------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MEND_API_URL | Mend API URL. Can be obtained in the Mend's Web Portal -> Integrate Tab -> Organization Window ->API Base URL (v2.0) | +| MEND_SERVER_URL | MEND Server URL. Integrate Tab -> Organization Window -> Server URL | +| MEND_ORG_TOKEN | API Key or Org UUID. Integrate Tab -> Organization Window -> API Key | +| MEND_PROJECT_ID | Mend's Portal Project ID(s) separated by commas (,). Projects Tab -> Search/Select Project -> URL Address _id_=ID | +| MEND_PROJECT_TOKEN | Project Tokens or Project UUIDs separated by commas (,). Integrate Tab -> Project Tokens Window -> Token | +| MEND_USER_EMAIL | Mend Identity user email. User Account Name -> Profile -> Identity Window -> Email | +| MEND_USER_KEY | Mend API Access Key. User Account Name -> Profile -> User Keys Window -> Generate User Key/Existing User Key | +| MEND_REPORT_TYPE | Vulnerabilities or Alerts report. Possible values are `vulnerabilities` or `alerts` | +| MEND_ALERTS_STATUS | Alert status. By default `active` alerts are retrieved | +| MEND_MIN_CONNECTION_TIME | Minimum time(ms) between doing requests for quering project's libraries vulnerabilities. Default value is `50` for `50ms` between requests | +| MEND_MAX_CONCURRENT_CONNECTIONS | Maximum concurrent requests when quering project's libraries vulnerabilities. Default is `50` concurrent requests. Total(MIN_TIME+MAX_CONCURRENT) is `1000req/s` | +| MEND_RESULTS_PATH | The path where the `results.json` will be stored. Default is `./`. | + +## Run the fetcher + +After setting up the environment variables, run the fetcher to retrieve the _vulnerabilities_ with + +``` +npm start +``` + +For retrieving the _alerts_ set `MEND_REPORT_TYPE` value to `alerts` by redoing the [Setup the environment variables step](#setup-the-environment-variables) or by simply prepending the values before running the fetcher with + +```sh +env MEND_REPORT_TYPE=alerts npm start +``` + +By default `active` alerts will be retrieved. + +Other alert options are + +- `all`, +- `ignored`, +- `library_removed`, +- `library_in_house`, +- `library_whitelist`, + +and are retrieved by setting the `MEND_ALERTS_STATUS` environment variable diff --git a/yaku-apps-typescript/apps/mend-fetcher/package.json b/yaku-apps-typescript/apps/mend-fetcher/package.json new file mode 100644 index 00000000..5f3d4701 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/package.json @@ -0,0 +1,49 @@ +{ + "name": "@B-S-F/mend-fetcher", + "version": "0.7.0", + "description": "", + "main": "dist/index.js", + "type": "module", + "scripts": { + "build": "tsup", + "start": "node ./dist/index.js", + "dev": "nodemon --watch \"src/**\" --exec npm run start", + "lint": "eslint '**/*.ts'", + "format": "prettier --write '**/*.{ts,md}'", + "test": "vitest run && npm run test:update-cobertura-file", + "test:update-cobertura-file": "sed -i'.bak' 's,package name=\",package name=\"'${npm_package_name}/',g' coverage/cobertura-coverage.xml", + "test:dev": "vitest -w", + "test:coverage": "vitest --coverage", + "test:ui": "vitest --ui", + "test:integration:local": "npx rimraf dist && npm run build && npm run test:integration:ci", + "test:integration:ci": "npx vitest run --config vitest-integration.config.ts" + }, + "keywords": [], + "author": "", + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@vitest/ui": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "nodemon": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + }, + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "axios": "^1.6.0", + "bottleneck": "^2.19.5", + "zod": "^3.22.4" + }, + "bin": { + "mend-fetcher": "dist/index.js" + }, + "files": [ + "dist" + ] +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/auth/auth.ts b/yaku-apps-typescript/apps/mend-fetcher/src/auth/auth.ts new file mode 100644 index 00000000..10e1bea7 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/auth/auth.ts @@ -0,0 +1,51 @@ +import { auth } from '../fetcher/auth.fetcher.js' +import { Login } from '../model/login.js' +import { MendEnvironment } from '../model/mendEnvironment.js' + +export class Authenticator { + private static _instance: Authenticator + private env: MendEnvironment + private login: Login | undefined + + private constructor(env: MendEnvironment) { + this.env = env + } + + static getInstance(env: MendEnvironment): Authenticator { + if (!Authenticator._instance) { + Authenticator._instance = new Authenticator(env) + } + + return Authenticator._instance + } + async authenticate(): Promise { + if (!this.login) { + this.login = await auth( + this.env.apiUrl, + this.env.email, + this.env.orgToken, + this.env.userKey + ) + } else { + if (this.isLoginExpired()) { + this.login = await auth( + this.env.apiUrl, + this.env.email, + this.env.orgToken, + this.env.userKey + ) + } + } + + return this.login + } + + private isLoginExpired(): boolean { + const now = new Date().valueOf() + if (this.login && this.login.sessionStartTime + this.login.jwtTTL < now) { + return true + } + + return false + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/dto/alert.dto.ts b/yaku-apps-typescript/apps/mend-fetcher/src/dto/alert.dto.ts new file mode 100644 index 00000000..1ba42b23 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/dto/alert.dto.ts @@ -0,0 +1,26 @@ +import { IBaseComponentDTO } from './baseComponent.dto.js' +import { ILibraryComponentDTO } from './libraryComponent.dto.js' + +export interface IAlertDTO { + uuid: string + name: string + type: string + component: IBaseComponentDTO | ILibraryComponentDTO + alertInfo: { + status: string + comment: + | { + comment: string + date: string + } + | Record + detectedAt: string + modifiedAt: string + } + project: { + uuid: string + name: string + path: string + productUuid: string + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/dto/baseComponent.dto.ts b/yaku-apps-typescript/apps/mend-fetcher/src/dto/baseComponent.dto.ts new file mode 100644 index 00000000..779e87c2 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/dto/baseComponent.dto.ts @@ -0,0 +1,6 @@ +export interface IBaseComponentDTO { + uuid: string + name: string + description: string + libraryType: string +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/dto/library.dto.ts b/yaku-apps-typescript/apps/mend-fetcher/src/dto/library.dto.ts new file mode 100644 index 00000000..dfc1a0da --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/dto/library.dto.ts @@ -0,0 +1,39 @@ +export class LibraryDTO { + constructor( + public uuid: string, + public name: string, + public artifactId: string, + public version: string, + public architecture: string, + public languageVersion: string, + public classifier: string, + public extension: string, + public sha1: string, + public description: string, + public type: string, + public directDependency: boolean, + public licenses: { + uuid: string + name: string + assignedByUser: boolean + licenseReferences: { + uuid: string + type: string + liabilityReference: string + information: string + }[] + }[], + public copyrightReferences: { + type: string + copyright: string + author: string + referenceInfo: string + startYear?: string + endYear?: string + }[], + public locations: { + localPath: string + dependencyFile: string + }[] + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/dto/libraryComponent.dto.ts b/yaku-apps-typescript/apps/mend-fetcher/src/dto/libraryComponent.dto.ts new file mode 100644 index 00000000..51f914ed --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/dto/libraryComponent.dto.ts @@ -0,0 +1,19 @@ +import { IBaseComponentDTO } from './baseComponent.dto.js' + +export interface ILibraryComponentDTO extends IBaseComponentDTO { + uuid: string + name: string + description: string + componentType: string + libraryType: string + directDependency: boolean + references: { + url: string + homePage: string + genericPackageIndex: string + } + groupId: string + artifactId: string + version: string + path: string +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/dto/multipleLicensesAlert.dto.ts b/yaku-apps-typescript/apps/mend-fetcher/src/dto/multipleLicensesAlert.dto.ts new file mode 100644 index 00000000..3a9a460a --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/dto/multipleLicensesAlert.dto.ts @@ -0,0 +1,30 @@ +import { IAlertDTO } from './alert.dto.js' +import { IBaseComponentDTO } from './baseComponent.dto.js' + +export class MultipleLicensesAlertDTO implements IAlertDTO { + constructor( + public uuid: string, + public name: string, + public type: string, + public component: IBaseComponentDTO, + public alertInfo: { + status: string + comment: + | { + comment: string + date: string + } + | Record + detectedAt: string + modifiedAt: string + }, + public project: { + uuid: string + name: string + path: string + productUuid: string + }, + public numberOfLicenses: number, + public licenses: Array + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/dto/newVersionsAlert.dto.ts b/yaku-apps-typescript/apps/mend-fetcher/src/dto/newVersionsAlert.dto.ts new file mode 100644 index 00000000..2e8a0cb9 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/dto/newVersionsAlert.dto.ts @@ -0,0 +1,30 @@ +import { IAlertDTO } from './alert.dto.js' +import { IBaseComponentDTO } from './baseComponent.dto.js' + +export class NewVersionsAlertDTO implements IAlertDTO { + constructor( + public uuid: string, + public name: string, + public type: string, + public component: IBaseComponentDTO, + public alertInfo: { + status: string + comment: + | { + comment: string + date: string + } + | Record + detectedAt: string + modifiedAt: string + }, + public project: { + uuid: string + name: string + path: string + productUuid: string + }, + public availableVersion: string, + public availableVersionType: string + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/dto/organization.dto.ts b/yaku-apps-typescript/apps/mend-fetcher/src/dto/organization.dto.ts new file mode 100644 index 00000000..3ebfe278 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/dto/organization.dto.ts @@ -0,0 +1,3 @@ +export class OrganizationDTO { + constructor(public uuid: string, public name: string) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/dto/policyAlert.dto.ts b/yaku-apps-typescript/apps/mend-fetcher/src/dto/policyAlert.dto.ts new file mode 100644 index 00000000..5d27bfd2 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/dto/policyAlert.dto.ts @@ -0,0 +1,29 @@ +import { IAlertDTO } from './alert.dto.js' +import { IBaseComponentDTO } from './baseComponent.dto.js' + +export class PolicyAlertDTO implements IAlertDTO { + constructor( + public uuid: string, + public name: string, + public type: string, + public component: IBaseComponentDTO, + public alertInfo: { + status: string + comment: + | { + comment: string + date: string + } + | Record + detectedAt: string + modifiedAt: string + }, + public project: { + uuid: string + name: string + path: string + productUuid: string + }, + public policyName: string + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/dto/project.dto.ts b/yaku-apps-typescript/apps/mend-fetcher/src/dto/project.dto.ts new file mode 100644 index 00000000..e4b85833 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/dto/project.dto.ts @@ -0,0 +1,9 @@ +export class ProjectDTO { + constructor( + public uuid: string, + public name: string, + public path: string, + public productName: string, + public productUuid: string + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/dto/projectVitals.dto.ts b/yaku-apps-typescript/apps/mend-fetcher/src/dto/projectVitals.dto.ts new file mode 100644 index 00000000..d2ce3634 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/dto/projectVitals.dto.ts @@ -0,0 +1,18 @@ +export class ProjectVitalsDTO { + constructor( + public lastScan: string, + public lastUserScanned: { + uuid: string + name: string + email: string + userType: string + }, + public requestToken: string, + public lastSourceFileMatch: string, + public lastScanComment: string, + public projectCreationDate: string, + public pluginName: string, + public pluginVersion: string, + public libraryCount: number + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/dto/rejectedInUseAlert.dto.ts b/yaku-apps-typescript/apps/mend-fetcher/src/dto/rejectedInUseAlert.dto.ts new file mode 100644 index 00000000..03947c17 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/dto/rejectedInUseAlert.dto.ts @@ -0,0 +1,29 @@ +import { IAlertDTO } from './alert.dto.js' +import { IBaseComponentDTO } from './baseComponent.dto.js' + +export class RejectedInUseAlertDTO implements IAlertDTO { + constructor( + public uuid: string, + public name: string, + public type: string, + public component: IBaseComponentDTO, + public alertInfo: { + status: string + comment: + | { + comment: string + date: string + } + | Record + detectedAt: string + modifiedAt: string + }, + public project: { + uuid: string + name: string + path: string + productUuid: string + }, + public description: string + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/dto/securityAlert.dto.ts b/yaku-apps-typescript/apps/mend-fetcher/src/dto/securityAlert.dto.ts new file mode 100644 index 00000000..9c5c71f9 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/dto/securityAlert.dto.ts @@ -0,0 +1,66 @@ +import { IAlertDTO } from './alert.dto.js' +import { ILibraryComponentDTO } from './libraryComponent.dto.js' + +export class SecurityAlertDTO implements IAlertDTO { + constructor( + public uuid: string, + public name: string, + public type: string, + public component: ILibraryComponentDTO, + public alertInfo: { + status: string + comment: + | { + comment: string + date: string + } + | Record + detectedAt: string + modifiedAt: string + }, + public project: { + uuid: string + name: string + path: string + productUuid: string + }, + public product: { + uuid: string + name: string + }, + public vulnerability: { + name: string + type: string + description: string + score: number + severity: string + publishDate: string + modifiedDate: string + vulnerabilityScoring: { + score: number + severity: string + type: string + }[] + references?: { + value: string + source: string + url: string + signature: boolean + advisory: boolean + patch: boolean + }[] + }, + public topFix: { + id: number + vulnerability: string + type: string + origin: string + url: string + fixResolution: string + date: string + message: string + extraData: Record + }, + public effective: string + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/dto/vulnerability.dto.ts b/yaku-apps-typescript/apps/mend-fetcher/src/dto/vulnerability.dto.ts new file mode 100644 index 00000000..9ba71a15 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/dto/vulnerability.dto.ts @@ -0,0 +1,24 @@ +export class VulnerabilityDTO { + constructor( + public name: string, + public type: string, + public description: string, + public score: number, + public severity: string, + public publishDate: string, + public modifiedDate: string, + public vulnerabilityScoring: { + score: number + severity: string + type: string + }[], + public references?: { + value: string + source: string + url: string + signature: boolean + advisory: boolean + patch: boolean + }[] + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/dto/vulnerabilityFixSummary.dto.ts b/yaku-apps-typescript/apps/mend-fetcher/src/dto/vulnerabilityFixSummary.dto.ts new file mode 100644 index 00000000..ee944d75 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/dto/vulnerabilityFixSummary.dto.ts @@ -0,0 +1,29 @@ +export class VulnerabilityFixSummaryDTO { + constructor( + public vulnerability: string, + public topRankedFix: { + id: number + vulnerability: string + type: string + origin: string + url: string + fixResolution: string + date: string + message: string + extraData: any | Record + }, + public allFixes: { + id: number + vulnerability: string + type: string + origin: string + url: string + fixResolution: string + date: string + message: string + extraData: any | Record + }[], + public totalUpVotes: number, + public totalDownVotes: number + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/alert.fetcher.ts b/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/alert.fetcher.ts new file mode 100644 index 00000000..0639ed67 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/alert.fetcher.ts @@ -0,0 +1,356 @@ +import axios, { AxiosRequestConfig, AxiosResponse } from 'axios' +import { GetLogger } from '@B-S-F/autopilot-utils' +import { Authenticator } from '../auth/auth.js' +import { Login } from '../model/login.js' +import { PolicyAlertDTO } from '../dto/policyAlert.dto.js' +import { SecurityAlertDTO } from '../dto/securityAlert.dto.js' +import { handleAxiosError, UnexpectedDataError } from './errors.fetcher.js' +import { MultipleLicensesAlertDTO } from '../dto/multipleLicensesAlert.dto.js' +import { NewVersionsAlertDTO } from '../dto/newVersionsAlert.dto.js' +import { RejectedInUseAlertDTO } from '../dto/rejectedInUseAlert.dto.js' +import { axiosInstance } from './common.fetcher.js' + +export const getPolicyAlertDTOs = async ( + apiUrl: string, + config: { projectToken: string; status: string; pageSize?: number }, + auth: Authenticator +): Promise => { + const url = `/api/v2.0/projects/${config.projectToken}/alerts/legal` + const login: Login = await auth.authenticate() + const headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${login.jwtToken}`, + } + const requestConfig: AxiosRequestConfig = { + url: url, + method: 'get', + baseURL: apiUrl, + headers: headers, + params: { + page: 0, + pageSize: config.pageSize ?? 100, + search: + config.status && config.status !== 'all' + ? `status:equals:${config.status};type:equals:POLICY_VIOLATIONS` + : `type:equals:POLICY_VIOLATIONS`, + }, + } + const logger = GetLogger() + + let policyAlertDTOs: PolicyAlertDTO[] = [] + let retrievedItems: any[] = [] + + do { + try { + const response: AxiosResponse = await axiosInstance.request(requestConfig) + + if (!response.data.retVal || !response.data.additionalData) { + throw new UnexpectedDataError('No expected values returned') + } + retrievedItems = response.data.retVal + policyAlertDTOs = policyAlertDTOs.concat( + retrievedItems.map( + (item: any) => + new PolicyAlertDTO( + item.uuid, + item.name, + item.type, + item.component, + item.alertInfo, + item.project, + item.policyName + ) + ) + ) + requestConfig.params.page++ + } catch (error: any) { + logger.error( + `Getting Project Policy Alerts from ${requestConfig.baseURL}${url} has failed` + ) + if (axios.isAxiosError(error)) { + handleAxiosError(error) + } + throw new Error(`Error ${error.message}`) + } + } while (retrievedItems.length > 0) + + return policyAlertDTOs +} + +export const getSecurityAlertDTOs = async ( + apiUrl: string, + config: { projectToken: string; status: string; pageSize?: number }, + auth: Authenticator +): Promise => { + const url = `/api/v2.0/projects/${config.projectToken}/alerts/security` + const login: Login = await auth.authenticate() + const headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${login.jwtToken}`, + } + const requestConfig: AxiosRequestConfig = { + url: url, + method: 'get', + baseURL: apiUrl, + headers: headers, + params: { + page: 0, + pageSize: config.pageSize ?? 100, + search: + config.status && config.status !== 'all' + ? `status:equals:${config.status}` + : undefined, + }, + } + const logger = GetLogger() + + let securityAlertDTOs: SecurityAlertDTO[] = [] + let retrievedItems: any[] = [] + + do { + try { + const response: AxiosResponse = await axiosInstance.request(requestConfig) + + if (!response.data.retVal || !response.data.additionalData) { + throw new UnexpectedDataError('No expected values returned') + } + retrievedItems = response.data.retVal + securityAlertDTOs = securityAlertDTOs.concat( + retrievedItems.map( + (item: any) => + new SecurityAlertDTO( + item.uuid, + item.name, + item.type, + item.component, + item.alertInfo, + item.project, + item.product, + item.vulnerability, + item.topFix, + item.effective + ) + ) + ) + requestConfig.params.page++ + } catch (error: any) { + logger.error( + `Getting Project Security Alerts from ${requestConfig.baseURL}${url} has failed` + ) + if (axios.isAxiosError(error)) { + handleAxiosError(error) + } + throw new Error(`Error ${error.message}`) + } + } while (retrievedItems.length > 0) + + return securityAlertDTOs +} + +export const getNewVersionsAlertDTOs = async ( + apiUrl: string, + config: { projectToken: string; status: string; pageSize?: number }, + auth: Authenticator +): Promise => { + const url = `/api/v2.0/projects/${config.projectToken}/alerts/legal` + const login: Login = await auth.authenticate() + const headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${login.jwtToken}`, + } + const requestConfig: AxiosRequestConfig = { + url: url, + method: 'get', + baseURL: apiUrl, + headers: headers, + params: { + page: 0, + pageSize: config.pageSize ?? 100, + search: + config.status && config.status !== 'all' + ? `status:equals:${config.status};type:equals:NEW_VERSION` + : `type:equals:NEW_VERSION`, + }, + } + const logger = GetLogger() + + let newVersionsAlertDTOs: NewVersionsAlertDTO[] = [] + let retrievedItems: any[] = [] + + do { + try { + const response: AxiosResponse = await axiosInstance.request(requestConfig) + + if (!response.data.retVal || !response.data.additionalData) { + throw new UnexpectedDataError('No expected values returned') + } + retrievedItems = response.data.retVal + newVersionsAlertDTOs = newVersionsAlertDTOs.concat( + retrievedItems.map( + (item: any) => + new NewVersionsAlertDTO( + item.uuid, + item.name, + item.type, + item.component, + item.alertInfo, + item.project, + item.availableVersion, + item.availableVersionType + ) + ) + ) + requestConfig.params.page++ + } catch (error: any) { + logger.error( + `Getting Project New Versions Alerts from ${requestConfig.baseURL}${url} has failed` + ) + if (axios.isAxiosError(error)) { + handleAxiosError(error) + } + throw new Error(`Error ${error.message}`) + } + } while (retrievedItems.length > 0) + + return newVersionsAlertDTOs +} + +export const getMultipleLicensesAlertDTOs = async ( + apiUrl: string, + config: { projectToken: string; status: string; pageSize?: number }, + auth: Authenticator +): Promise => { + const url = `/api/v2.0/projects/${config.projectToken}/alerts/legal` + const login: Login = await auth.authenticate() + const headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${login.jwtToken}`, + } + const requestConfig: AxiosRequestConfig = { + url: url, + method: 'get', + baseURL: apiUrl, + headers: headers, + params: { + page: 0, + pageSize: config.pageSize ?? 100, + search: + config.status && config.status !== 'all' + ? `status:equals:${config.status};type:equals:MULTIPLE_LICENSES` + : `type:equals:MULTIPLE_LICENSES`, + }, + } + const logger = GetLogger() + + let multipleLicensesAlertDTOs: MultipleLicensesAlertDTO[] = [] + let retrievedItems: any[] = [] + + do { + try { + const response: AxiosResponse = await axiosInstance.request(requestConfig) + + if (!response.data.retVal || !response.data.additionalData) { + throw new UnexpectedDataError('No expected values returned') + } + retrievedItems = response.data.retVal + multipleLicensesAlertDTOs = multipleLicensesAlertDTOs.concat( + retrievedItems.map( + (item: any) => + new MultipleLicensesAlertDTO( + item.uuid, + item.name, + item.type, + item.component, + item.alertInfo, + item.project, + item.numberOfLicenses, + item.licenses + ) + ) + ) + requestConfig.params.page++ + } catch (error: any) { + logger.error( + `Getting Project Multiple Licenses Alerts from ${requestConfig.baseURL}${url} has failed` + ) + if (axios.isAxiosError(error)) { + handleAxiosError(error) + } + throw new Error(`Error ${error.message}`) + } + } while (retrievedItems.length > 0) + + return multipleLicensesAlertDTOs +} + +export const getRejectedInUseAlertDTOs = async ( + apiUrl: string, + config: { projectToken: string; status: string; pageSize?: number }, + auth: Authenticator +): Promise => { + const url = `/api/v2.0/projects/${config.projectToken}/alerts/legal` + const login: Login = await auth.authenticate() + const headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${login.jwtToken}`, + } + const requestConfig: AxiosRequestConfig = { + url: url, + method: 'get', + baseURL: apiUrl, + headers: headers, + params: { + page: 0, + pageSize: config.pageSize ?? 100, + search: + config.status && config.status !== 'all' + ? `status:equals:${config.status};type:equals:REJECTED_LIBRARY_IN_USE` + : `type:equals:REJECTED_LIBRARY_IN_USE`, + }, + } + const logger = GetLogger() + + let rejectedInUseAlertDTOs: RejectedInUseAlertDTO[] = [] + let retrievedItems: any[] = [] + + do { + try { + const response: AxiosResponse = await axiosInstance.request(requestConfig) + + if (!response.data.retVal || !response.data.additionalData) { + throw new UnexpectedDataError('No expected values returned') + } + retrievedItems = response.data.retVal + rejectedInUseAlertDTOs = rejectedInUseAlertDTOs.concat( + retrievedItems.map( + (item: any) => + new RejectedInUseAlertDTO( + item.uuid, + item.name, + item.type, + item.component, + item.alertInfo, + item.project, + item.description + ) + ) + ) + requestConfig.params.page++ + } catch (error: any) { + logger.error( + `Getting Project Rejected in Use Alerts from ${requestConfig.baseURL}${url} has failed` + ) + if (axios.isAxiosError(error)) { + handleAxiosError(error) + } + throw new Error(`Error ${error.message}`) + } + } while (retrievedItems.length > 0) + + return rejectedInUseAlertDTOs +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/auth.fetcher.ts b/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/auth.fetcher.ts new file mode 100644 index 00000000..5d96a741 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/auth.fetcher.ts @@ -0,0 +1,57 @@ +import axios, { AxiosRequestConfig, AxiosResponse } from 'axios' +import { GetLogger } from '@B-S-F/autopilot-utils' +import { Login } from '../model/login.js' +import { axiosInstance } from './common.fetcher.js' +import { handleAxiosError, UnexpectedDataError } from './errors.fetcher.js' + +export const auth = async ( + apiUrl: string, + email: string, + org: string, + token: string +): Promise => { + const url = '/api/v2.0/login' + const headers = { + Accept: 'application/json', + 'Content-type': 'application/json', + } + const requestConfig: AxiosRequestConfig = { + url: url, + method: 'post', + baseURL: apiUrl, + headers: headers, + data: { + email: email, + orgToken: org, + userKey: token, + }, + } + const logger = GetLogger() + + try { + const response: AxiosResponse = await axiosInstance.request(requestConfig) + + if (!response.data.retVal) { + throw new UnexpectedDataError('No expected values returned') + } + const login = new Login( + response.data.retVal.userUuid, + response.data.retVal.userName, + response.data.retVal.email, + response.data.retVal.jwtToken, + response.data.retVal.refreshToken, + response.data.retVal.jwtTTL, + response.data.retVal.orgName, + response.data.retVal.orgUuid, + response.data.retVal.sessionStartTime + ) + + return login + } catch (error: any) { + logger.error(`Error when trying to access ${requestConfig.baseURL}${url}`) + if (axios.isAxiosError(error)) { + handleAxiosError(error) + } + throw new Error(`Error ${error.message}`) + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/common.fetcher.ts b/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/common.fetcher.ts new file mode 100644 index 00000000..69d40210 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/common.fetcher.ts @@ -0,0 +1,22 @@ +import * as http from 'http' +import * as https from 'https' +import axios from 'axios' + +const httpKeepAliveAgent = new http.Agent({ + keepAlive: true, + maxSockets: 128, + maxFreeSockets: 128, + timeout: 60000, +}) + +const httpsKeepAliveAgent = new https.Agent({ + keepAlive: true, + maxSockets: 128, + maxFreeSockets: 128, + timeout: 60000, +}) + +export const axiosInstance = axios.create({ + httpAgent: httpKeepAliveAgent, + httpsAgent: httpsKeepAliveAgent, +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/errors.fetcher.ts b/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/errors.fetcher.ts new file mode 100644 index 00000000..b0ebfbf2 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/errors.fetcher.ts @@ -0,0 +1,79 @@ +import { AppError, GetLogger } from '@B-S-F/autopilot-utils' +import { AxiosError } from 'axios' + +export class UnexpectedDataError extends AppError { + constructor(reason: string) { + super(reason) + this.name = 'UnexpectedDataError' + } + + Reason(): string { + return super.Reason() + } +} + +export class RequestError extends AppError { + constructor(reason: string) { + super(`RequestError: ${reason}`) + this.name = 'RequestError' + } + + Reason(): string { + return super.Reason() + } +} + +export class ResponseError extends AppError { + constructor(reason: string) { + super(reason) + this.name = 'ResponseError' + } + + Reason(): string { + return super.Reason() + } +} + +const processResponseError = (response: any): never => { + const logger = GetLogger() + if (!response.data) { + throw new ResponseError( + `Response status code ${response.status}: ${response.message}` + ) + } + if (!response.data.retVal) { + if (response.statusText && response.statusText.length > 0) { + throw new ResponseError( + `Response status code ${response.status}: ${response.statusText}` + ) + } + throw new ResponseError( + `Response status code ${response.status}: ${response.data.error}` + ) + } + if (response.data.supportToken) { + logger.error(`Mend support token: ${response.data.supportToken}`) + } + if (typeof response.data.retVal === 'string') { + throw new ResponseError( + `Response status code ${response.status}: ${response.data.retVal}` + ) + } + if (typeof response.data.retVal === 'object') { + throw new ResponseError( + `Response status code ${response.status}: ${response.data.retVal.errorMessage}` + ) + } + throw new ResponseError( + `Response status code ${response.status}: ${response.message}` + ) +} + +export const handleAxiosError = (error: AxiosError): never => { + if (error.response) { + processResponseError(error.response) + } else if (error.request) { + throw new RequestError(`${error.message}`) + } + throw new Error(`Error ${error.message}`) +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/library.fetcher.ts b/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/library.fetcher.ts new file mode 100644 index 00000000..6cc949e5 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/library.fetcher.ts @@ -0,0 +1,80 @@ +import axios, { AxiosRequestConfig, AxiosResponse } from 'axios' +import { GetLogger } from '@B-S-F/autopilot-utils' +import { Authenticator } from '../auth/auth.js' +import { LibraryDTO } from '../dto/library.dto.js' +import { Login } from '../model/login.js' +import { axiosInstance } from './common.fetcher.js' +import { handleAxiosError, UnexpectedDataError } from './errors.fetcher.js' + +export const getLibraryDTOs = async ( + apiUrl: string, + config: { projectToken: string; pageSize?: number }, + auth: Authenticator +): Promise => { + const url = `/api/v2.0/projects/${config.projectToken}/libraries` + const login: Login = await auth.authenticate() + const headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${login.jwtToken}`, + } + const requestConfig: AxiosRequestConfig = { + url: url, + method: 'get', + baseURL: apiUrl, + headers: headers, + params: { + page: '0', + pageSize: config.pageSize ?? 100, + }, + } + const logger = GetLogger() + + let libraryDTOs: LibraryDTO[] = [] + let retrievedItems: any[] + + do { + try { + const response: AxiosResponse = await axiosInstance.request(requestConfig) + + if (!response.data.retVal || !response.data.additionalData) { + throw new UnexpectedDataError('No expected values are returned') + } + retrievedItems = response.data.retVal + libraryDTOs = libraryDTOs.concat( + retrievedItems.map( + (item: any) => + new LibraryDTO( + item.uuid, + item.name, + item.artifactId, + item.version, + item.architecture, + item.languageVersion, + item.classifier, + item.extension, + item.sha1, + item.description, + item.type, + item.directDependency, + item.licenses, + item.copyrightReferences, + item.locations + ) + ) + ) + + requestConfig.params.page++ + } catch (error: any) { + logger.error( + `Getting the list of libraries from ${requestConfig.baseURL}${url} has failed` + ) + if (axios.isAxiosError(error)) { + handleAxiosError(error) + } + throw new Error(`Error ${error.message}`) + } + } while (retrievedItems.length > 0) + + return libraryDTOs +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/organization.fetcher.ts b/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/organization.fetcher.ts new file mode 100644 index 00000000..6c8eccad --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/organization.fetcher.ts @@ -0,0 +1,50 @@ +import axios, { AxiosRequestConfig, AxiosResponse } from 'axios' +import { GetLogger } from '@B-S-F/autopilot-utils' +import { Authenticator } from '../auth/auth.js' +import { Login } from '../model/login.js' +import { OrganizationDTO } from '../dto/organization.dto.js' +import { axiosInstance } from './common.fetcher.js' +import { handleAxiosError, UnexpectedDataError } from './errors.fetcher.js' + +export const getOrganizationDTO = async ( + apiUrl: string, + config: { orgToken: string }, + auth: Authenticator +): Promise => { + const url = `/api/v2.0/orgs/${config.orgToken}` + const login: Login = await auth.authenticate() + const headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${login.jwtToken}`, + } + const requestConfig: AxiosRequestConfig = { + url: url, + method: 'get', + baseURL: apiUrl, + headers: headers, + } + const logger = GetLogger() + + try { + const response: AxiosResponse = await axiosInstance.request(requestConfig) + + if (!response.data.retVal) { + throw new UnexpectedDataError('No expected values are returned') + } + const organizationDTO = new OrganizationDTO( + response.data.retVal.uuid, + response.data.retVal.name + ) + + return organizationDTO + } catch (error: any) { + logger.error( + `Getting Organization information from ${requestConfig.baseURL}${url} has failed` + ) + if (axios.isAxiosError(error)) { + handleAxiosError(error) + } + throw new Error(`Error ${error.message}`) + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/project.fetcher.ts b/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/project.fetcher.ts new file mode 100644 index 00000000..69238f4a --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/project.fetcher.ts @@ -0,0 +1,104 @@ +import axios, { AxiosRequestConfig, AxiosResponse } from 'axios' +import { GetLogger } from '@B-S-F/autopilot-utils' +import { Authenticator } from '../auth/auth.js' +import { Login } from '../model/login.js' +import { ProjectDTO } from '../dto/project.dto.js' +import { ProjectVitalsDTO } from '../dto/projectVitals.dto.js' +import { axiosInstance } from './common.fetcher.js' +import { handleAxiosError, UnexpectedDataError } from './errors.fetcher.js' + +export const getProjectDTO = async ( + apiUrl: string, + config: { projectToken: string }, + auth: Authenticator +): Promise => { + const url = `/api/v2.0/projects/${config.projectToken}` + const login: Login = await auth.authenticate() + const headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${login.jwtToken}`, + } + const requestConfig: AxiosRequestConfig = { + url: url, + method: 'get', + baseURL: apiUrl, + headers: headers, + } + const logger = GetLogger() + + try { + const response: AxiosResponse = await axiosInstance.request(requestConfig) + + if (!response.data.retVal) { + throw new UnexpectedDataError('No expected values are returned') + } + const projectDTO = new ProjectDTO( + response.data.retVal.uuid, + response.data.retVal.name, + response.data.retVal.path, + response.data.retVal.productName, + response.data.retVal.productUuid + ) + + return projectDTO + } catch (error: any) { + logger.error( + `Getting Project information from ${requestConfig.baseURL}${url} has failed` + ) + if (axios.isAxiosError(error)) { + handleAxiosError(error) + } + throw new Error(`Error ${error.message}`) + } +} + +export const getProjectVitalsDTO = async ( + apiUrl: string, + config: { projectToken: string }, + auth: Authenticator +): Promise => { + const url = `/api/v2.0/projects/${config.projectToken}/vitals` + const login: Login = await auth.authenticate() + const headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${login.jwtToken}`, + } + const requestConfig: AxiosRequestConfig = { + url: url, + method: 'get', + baseURL: apiUrl, + headers: headers, + } + const logger = GetLogger() + + try { + const response: AxiosResponse = await axiosInstance.request(requestConfig) + + if (!response.data.retVal) { + throw new UnexpectedDataError('No expected values are returned') + } + const projectVitalsDTO = new ProjectVitalsDTO( + response.data.retVal.lastScan, + response.data.retVal.lastUserScanned, + response.data.retVal.requestToken, + response.data.retVal.lastSourceFileMatch, + response.data.retVal.lastScanComment, + response.data.retVal.projectCreationDate, + response.data.retVal.pluginName, + response.data.retVal.pluginVersion, + response.data.retVal.libraryCount + ) + + return projectVitalsDTO + } catch (error: any) { + logger.error( + `Getting Project Vitals information from ${requestConfig.baseURL}${url} has failed` + ) + if (axios.isAxiosError(error)) { + handleAxiosError(error) + } + throw new Error(`Error ${error.message}`) + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/vulnerability.fetcher.ts b/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/vulnerability.fetcher.ts new file mode 100644 index 00000000..192bedd2 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/fetcher/vulnerability.fetcher.ts @@ -0,0 +1,154 @@ +import axios, { AxiosRequestConfig, AxiosResponse } from 'axios' +import { GetLogger } from '@B-S-F/autopilot-utils' +import { Authenticator } from '../auth/auth.js' +import { Login } from '../model/login.js' +import { VulnerabilityDTO } from '../dto/vulnerability.dto.js' +import { VulnerabilityFixSummaryDTO } from '../dto/vulnerabilityFixSummary.dto.js' +import { handleAxiosError, UnexpectedDataError } from './errors.fetcher.js' +import { VulnerabilityFix } from '../model/vulnerabilityFix.js' +import { axiosInstance } from './common.fetcher.js' + +export const getLibraryVulnerabilityDTOs = async ( + apiUrl: string, + config: { projectToken: string; libraryToken: string; pageSize?: number }, + auth: Authenticator +): Promise => { + const url = `/api/v2.0/projects/${config.projectToken}/libraries/${config.libraryToken}/vulnerabilities` + const login: Login = await auth.authenticate() + const headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${login.jwtToken}`, + } + const requestConfig: AxiosRequestConfig = { + url: url, + method: 'get', + baseURL: apiUrl, + headers: headers, + params: { + page: 0, + pageSize: config.pageSize ?? 100, + }, + } + const logger = GetLogger() + + let vulnerabilityDTOs: VulnerabilityDTO[] = [] + let retrievedItems: any[] = [] + + do { + try { + const response: AxiosResponse = await axiosInstance.request(requestConfig) + + if (!response.data.retVal || !response.data.additionalData) { + throw new UnexpectedDataError('No expected values are returned') + } + retrievedItems = response.data.retVal + vulnerabilityDTOs = vulnerabilityDTOs.concat( + retrievedItems.map( + (item: any) => + new VulnerabilityDTO( + item.name, + item.type, + item.description, + item.score, + item.severity, + item.publishDate, + item.modifiedDate, + item.vulnerabilityScoring, + item.references + ) + ) + ) + requestConfig.params.page++ + } catch (error: any) { + logger.error( + `Getting Library Vulnerabilities from ${requestConfig.baseURL}${url} has failed` + ) + if (axios.isAxiosError(error)) { + handleAxiosError(error) + } + throw new Error(`Error ${error.message}`) + } + } while (retrievedItems.length > 0) + + return vulnerabilityDTOs +} + +export const getVulnerabilityFixesDTOs = async ( + apiUrl: string, + config: { vulnerabilityId: string; pageSize?: number }, + auth: Authenticator +): Promise => { + const url = `/api/v2.0/vulnerabilities/${config.vulnerabilityId}/remediation` + const login: Login = await auth.authenticate() + const headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${login.jwtToken}`, + } + const requestConfig: AxiosRequestConfig = { + url: url, + method: 'get', + baseURL: apiUrl, + headers: headers, + } + const logger = GetLogger() + + let retrievedItems: any = [] + + try { + const response: AxiosResponse = await axiosInstance.request(requestConfig) + + if (!response.data.retVal) { + throw new UnexpectedDataError('No expected values are returned') + } + let vulnerabilityFixSummaryDTOs: VulnerabilityFixSummaryDTO + if (response.data.retVal.errorMessage) { + vulnerabilityFixSummaryDTOs = new VulnerabilityFixSummaryDTO( + config.vulnerabilityId, + new VulnerabilityFix( + 0, + config.vulnerabilityId, + 'N/A', + 'N/A', + 'N/A', + '', + 'N/A', + 'This vulnerability does not have an available fix!', + 'N/A' + ), + [], + 0, + 0 + ) + } else { + retrievedItems = response.data.retVal + vulnerabilityFixSummaryDTOs = new VulnerabilityFixSummaryDTO( + retrievedItems.vulnerability, + new VulnerabilityFix( + retrievedItems.topRankedFix.id, + retrievedItems.topRankedFix.vulnerability, + retrievedItems.topRankedFix.type, + retrievedItems.topRankedFix.origin, + retrievedItems.topRankedFix.url, + retrievedItems.topRankedFix.fixResolution, + retrievedItems.topRankedFix.date, + retrievedItems.topRankedFix.message, + retrievedItems.topRankedFix.extraData + ), + retrievedItems.allFixes, + retrievedItems.totalUpVotes, + retrievedItems.totalDownVotes + ) + } + return vulnerabilityFixSummaryDTOs + } catch (error: any) { + logger.error( + `Getting Vulnerabilities Fixes from ${requestConfig.baseURL}${url} has failed` + ) + if (axios.isAxiosError(error)) { + handleAxiosError(error) + } + throw new Error(`Error ${error.message}`) + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/index.ts b/yaku-apps-typescript/apps/mend-fetcher/src/index.ts new file mode 100644 index 00000000..6f5f14b5 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/index.ts @@ -0,0 +1,7 @@ +#! /usr/bin/env node +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { run } from './run.js' +run() diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/mapper/library.mapper.ts b/yaku-apps-typescript/apps/mend-fetcher/src/mapper/library.mapper.ts new file mode 100644 index 00000000..4f288030 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/mapper/library.mapper.ts @@ -0,0 +1,114 @@ +import { CopyrightReference } from '../model/copyrightReference.js' +import { Library } from '../model/library.js' +import { LibraryDTO } from '../dto/library.dto.js' +import { License } from '../model/license.js' +import { LicenseReference } from '../model/licenseReference.js' + +export class LibraryMap { + public static toModel(libraryDTO: LibraryDTO) { + const licenses: License[] = libraryDTO.licenses.map( + (license) => + new License( + license.uuid, + license.name, + license.assignedByUser, + license.licenseReferences.map( + (ref) => + new LicenseReference( + ref.uuid, + ref.type, + ref.liabilityReference, + ref.information + ) + ) + ) + ) + const copyrightReferences: CopyrightReference[] = + libraryDTO.copyrightReferences.map( + (ref) => + new CopyrightReference( + ref.type, + ref.copyright, + ref.author, + ref.referenceInfo, + ref.startYear, + ref.endYear + ) + ) + const locations: { localPath: string; dependencyFile: string }[] = + libraryDTO.locations !== undefined && libraryDTO.locations.length > 0 + ? libraryDTO.locations.map((location) => { + return { + localPath: location.localPath, + dependencyFile: location.dependencyFile, + } + }) + : [] + return new Library( + libraryDTO.uuid, + libraryDTO.name, + libraryDTO.artifactId, + libraryDTO.version, + libraryDTO.architecture, + libraryDTO.languageVersion, + libraryDTO.classifier, + libraryDTO.extension, + libraryDTO.sha1, + libraryDTO.description, + libraryDTO.type, + libraryDTO.directDependency, + licenses, + copyrightReferences, + locations + ) + } + public static toDTO(library: Library) { + return new LibraryDTO( + library.uuid, + library.name, + library.artifactId, + library.version, + library.architecture, + library.languageVersion, + library.classifier, + library.extension, + library.sha1, + library.description, + library.type, + library.directDependency, + library.licenses.map((license) => { + return { + uuid: license.uuid, + name: license.name, + assignedByUser: license.assignedByUser, + licenseReferences: license.licenseReferences.map( + (ref: { + uuid: string + type: string + liabilityReference: string + information: string + }) => { + return { + uuid: ref.uuid, + type: ref.type, + liabilityReference: ref.liabilityReference, + information: ref.information, + } + } + ), + } + }), + library.copyrightReferences.map((copyrightRef: CopyrightReference) => { + return { + type: copyrightRef.type, + copyright: copyrightRef.copyright, + author: copyrightRef.author, + referenceInfo: copyrightRef.referenceInfo, + startYear: copyrightRef.startYear, + endYear: copyrightRef.endYear, + } + }), + library.locations + ) + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/mapper/multipleLicensesAlert.mapper.ts b/yaku-apps-typescript/apps/mend-fetcher/src/mapper/multipleLicensesAlert.mapper.ts new file mode 100644 index 00000000..0d90ca94 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/mapper/multipleLicensesAlert.mapper.ts @@ -0,0 +1,45 @@ +import { MultipleLicensesAlertDTO } from '../dto/multipleLicensesAlert.dto.js' +import { MultipleLicensesAlert } from '../model/multipleLicensesAlert.js' +import { Project } from '../model/project.js' + +export class MultipleLicensesAlertMap { + public static toModel(multipleLicensesAlertDTO: MultipleLicensesAlertDTO) { + return new MultipleLicensesAlert( + multipleLicensesAlertDTO.uuid, + multipleLicensesAlertDTO.name, + multipleLicensesAlertDTO.type, + multipleLicensesAlertDTO.component, + multipleLicensesAlertDTO.alertInfo, + new Project( + multipleLicensesAlertDTO.project.uuid, + multipleLicensesAlertDTO.project.name, + multipleLicensesAlertDTO.project.path, + multipleLicensesAlertDTO.project.path, + multipleLicensesAlertDTO.project.productUuid + ), + { + uuid: multipleLicensesAlertDTO.project.productUuid, + name: multipleLicensesAlertDTO.project.path, + }, + multipleLicensesAlertDTO.numberOfLicenses, + multipleLicensesAlertDTO.licenses + ) + } + public static toDTO(multipleLicensesAlert: MultipleLicensesAlert) { + return new MultipleLicensesAlertDTO( + multipleLicensesAlert.uuid, + multipleLicensesAlert.name, + multipleLicensesAlert.type, + multipleLicensesAlert.component, + multipleLicensesAlert.alertInfo, + { + uuid: multipleLicensesAlert.project.uuid, + name: multipleLicensesAlert.project.name, + path: multipleLicensesAlert.project.path, + productUuid: multipleLicensesAlert.project.productUuid, + }, + multipleLicensesAlert.numberOfLicenses, + multipleLicensesAlert.licenses + ) + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/mapper/newVersionsAlert.mapper.ts b/yaku-apps-typescript/apps/mend-fetcher/src/mapper/newVersionsAlert.mapper.ts new file mode 100644 index 00000000..18337fd4 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/mapper/newVersionsAlert.mapper.ts @@ -0,0 +1,45 @@ +import { NewVersionsAlertDTO } from '../dto/newVersionsAlert.dto.js' +import { NewVersionsAlert } from '../model/newVersionsAlert.js' +import { Project } from '../model/project.js' + +export class NewVersionsAlertMap { + public static toModel(newVersionsAlertDTO: NewVersionsAlertDTO) { + return new NewVersionsAlert( + newVersionsAlertDTO.uuid, + newVersionsAlertDTO.name, + newVersionsAlertDTO.type, + newVersionsAlertDTO.component, + newVersionsAlertDTO.alertInfo, + new Project( + newVersionsAlertDTO.project.uuid, + newVersionsAlertDTO.project.name, + newVersionsAlertDTO.project.path, + newVersionsAlertDTO.project.path, + newVersionsAlertDTO.project.productUuid + ), + { + uuid: newVersionsAlertDTO.project.productUuid, + name: newVersionsAlertDTO.project.path, + }, + newVersionsAlertDTO.availableVersion, + newVersionsAlertDTO.availableVersionType + ) + } + public static toDTO(newVersionsAlert: NewVersionsAlert) { + return new NewVersionsAlertDTO( + newVersionsAlert.uuid, + newVersionsAlert.name, + newVersionsAlert.type, + newVersionsAlert.component, + newVersionsAlert.alertInfo, + { + uuid: newVersionsAlert.project.uuid, + name: newVersionsAlert.project.name, + path: newVersionsAlert.project.path, + productUuid: newVersionsAlert.project.productUuid, + }, + newVersionsAlert.availableVersion, + newVersionsAlert.availableVersionType + ) + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/mapper/organization.mapper.ts b/yaku-apps-typescript/apps/mend-fetcher/src/mapper/organization.mapper.ts new file mode 100644 index 00000000..4db97e40 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/mapper/organization.mapper.ts @@ -0,0 +1,12 @@ +import { Organization } from '../model/organization.js' +import { OrganizationDTO } from '../dto/organization.dto.js' + +export class OrganizationMap { + public static toModel(organizationDTO: OrganizationDTO) { + return new Organization(organizationDTO.uuid, organizationDTO.name) + } + + public static toDTO(organization: Organization) { + return new OrganizationDTO(organization.uuid, organization.name) + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/mapper/policyAlert.mapper.ts b/yaku-apps-typescript/apps/mend-fetcher/src/mapper/policyAlert.mapper.ts new file mode 100644 index 00000000..82db94a7 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/mapper/policyAlert.mapper.ts @@ -0,0 +1,43 @@ +import { PolicyAlertDTO } from '../dto/policyAlert.dto.js' +import { PolicyAlert } from '../model/policyAlert.js' +import { Project } from '../model/project.js' + +export class PolicyAlertMap { + public static toModel(policyAlertDTO: PolicyAlertDTO) { + return new PolicyAlert( + policyAlertDTO.uuid, + policyAlertDTO.name, + policyAlertDTO.type, + policyAlertDTO.component, + policyAlertDTO.alertInfo, + new Project( + policyAlertDTO.project.uuid, + policyAlertDTO.project.name, + policyAlertDTO.project.path, + policyAlertDTO.project.path, + policyAlertDTO.project.productUuid + ), + { + uuid: policyAlertDTO.project.productUuid, + name: policyAlertDTO.project.path, + }, + policyAlertDTO.policyName + ) + } + public static toDTO(policyAlert: PolicyAlert) { + return new PolicyAlertDTO( + policyAlert.uuid, + policyAlert.name, + policyAlert.type, + policyAlert.component, + policyAlert.alertInfo, + { + uuid: policyAlert.project.uuid, + name: policyAlert.project.name, + path: policyAlert.project.path, + productUuid: policyAlert.project.productUuid, + }, + policyAlert.policyName + ) + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/mapper/project.mapper.ts b/yaku-apps-typescript/apps/mend-fetcher/src/mapper/project.mapper.ts new file mode 100644 index 00000000..3df55d46 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/mapper/project.mapper.ts @@ -0,0 +1,23 @@ +import { Project } from '../model/project.js' +import { ProjectDTO } from '../dto/project.dto.js' + +export class ProjectMap { + public static toModel(projectDTO: ProjectDTO): Project { + return new Project( + projectDTO.uuid, + projectDTO.name, + projectDTO.path, + projectDTO.productName, + projectDTO.productUuid + ) + } + public static toDTO(project: Project) { + return new ProjectDTO( + project.uuid, + project.name, + project.path, + project.productName, + project.productUuid + ) + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/mapper/projectVitals.mapper.ts b/yaku-apps-typescript/apps/mend-fetcher/src/mapper/projectVitals.mapper.ts new file mode 100644 index 00000000..9508b100 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/mapper/projectVitals.mapper.ts @@ -0,0 +1,32 @@ +import { ProjectVitals } from '../model/projectVitals.js' +import { ProjectVitalsDTO } from '../dto/projectVitals.dto.js' + +export class ProjectVitalsMap { + public static toModel(projectVitalsDTO: ProjectVitalsDTO): ProjectVitals { + return new ProjectVitals( + projectVitalsDTO.lastScan, + projectVitalsDTO.lastUserScanned, + projectVitalsDTO.requestToken, + projectVitalsDTO.lastSourceFileMatch, + projectVitalsDTO.lastScanComment, + projectVitalsDTO.projectCreationDate, + projectVitalsDTO.pluginName, + projectVitalsDTO.pluginVersion, + projectVitalsDTO.libraryCount + ) + } + + public static toDTO(projectVitals: ProjectVitals): ProjectVitalsDTO { + return new ProjectVitalsDTO( + projectVitals.lastScan, + projectVitals.lastUserScanned, + projectVitals.requestToken, + projectVitals.lastSourceFileMatch, + projectVitals.lastScanComment, + projectVitals.projectCreationDate, + projectVitals.pluginName, + projectVitals.pluginVersion, + projectVitals.libraryCount + ) + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/mapper/rejectedInUseAlert.mapper.ts b/yaku-apps-typescript/apps/mend-fetcher/src/mapper/rejectedInUseAlert.mapper.ts new file mode 100644 index 00000000..f6611fe2 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/mapper/rejectedInUseAlert.mapper.ts @@ -0,0 +1,43 @@ +import { RejectedInUseAlertDTO } from '../dto/rejectedInUseAlert.dto.js' +import { RejectedInUseAlert } from '../model/rejectedInUseAlert.js' +import { Project } from '../model/project.js' + +export class RejectedInUseAlertMap { + public static toModel(rejectedInUseAlertDTO: RejectedInUseAlertDTO) { + return new RejectedInUseAlert( + rejectedInUseAlertDTO.uuid, + rejectedInUseAlertDTO.name, + rejectedInUseAlertDTO.type, + rejectedInUseAlertDTO.component, + rejectedInUseAlertDTO.alertInfo, + new Project( + rejectedInUseAlertDTO.project.uuid, + rejectedInUseAlertDTO.project.name, + rejectedInUseAlertDTO.project.path, + rejectedInUseAlertDTO.project.path, + rejectedInUseAlertDTO.project.productUuid + ), + { + uuid: rejectedInUseAlertDTO.project.productUuid, + name: rejectedInUseAlertDTO.project.path, + }, + rejectedInUseAlertDTO.description + ) + } + public static toDTO(rejectedInUseAlert: RejectedInUseAlert) { + return new RejectedInUseAlertDTO( + rejectedInUseAlert.uuid, + rejectedInUseAlert.name, + rejectedInUseAlert.type, + rejectedInUseAlert.component, + rejectedInUseAlert.alertInfo, + { + uuid: rejectedInUseAlert.project.uuid, + name: rejectedInUseAlert.project.name, + path: rejectedInUseAlert.project.path, + productUuid: rejectedInUseAlert.project.productUuid, + }, + rejectedInUseAlert.description + ) + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/mapper/securityAlert.mapper.ts b/yaku-apps-typescript/apps/mend-fetcher/src/mapper/securityAlert.mapper.ts new file mode 100644 index 00000000..b841c9ab --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/mapper/securityAlert.mapper.ts @@ -0,0 +1,100 @@ +import { Project } from '../model/project.js' +import { SecurityAlert } from '../model/securityAlert.js' +import { SecurityAlertDTO } from '../dto/securityAlert.dto.js' +import { Vulnerability } from '../model/vulnerability.js' +import { VulnerabilityFix } from '../model/vulnerabilityFix.js' +import { VulnerabilityReference } from '../model/vulnerabilityReference.js' + +export class SecurityAlertMap { + public static toModel(securityAlertDTO: SecurityAlertDTO) { + const project: Project = new Project( + securityAlertDTO.project.uuid, + securityAlertDTO.project.name, + securityAlertDTO.project.path, + securityAlertDTO.project.path, + securityAlertDTO.project.productUuid + ) + const vulnerabilityReferences: VulnerabilityReference[] = [] + const vulnerability: Vulnerability = new Vulnerability( + securityAlertDTO.vulnerability.name, + securityAlertDTO.vulnerability.type, + securityAlertDTO.vulnerability.description, + securityAlertDTO.vulnerability.score, + securityAlertDTO.vulnerability.severity, + securityAlertDTO.vulnerability.publishDate, + securityAlertDTO.vulnerability.modifiedDate, + securityAlertDTO.vulnerability.vulnerabilityScoring.map((scoring) => { + return { + score: scoring.score, + severity: scoring.severity, + type: scoring.type, + } + }), + vulnerabilityReferences + ) + const vulnerabilityFix: VulnerabilityFix = new VulnerabilityFix( + securityAlertDTO.topFix.id, + securityAlertDTO.topFix.vulnerability, + securityAlertDTO.topFix.type, + securityAlertDTO.topFix.origin, + securityAlertDTO.topFix.url, + securityAlertDTO.topFix.fixResolution, + securityAlertDTO.topFix.date, + securityAlertDTO.topFix.message, + securityAlertDTO.topFix.extraData + ) + return new SecurityAlert( + securityAlertDTO.uuid, + securityAlertDTO.name, + securityAlertDTO.type, + securityAlertDTO.component, + securityAlertDTO.alertInfo, + project, + { + uuid: securityAlertDTO.product.uuid, + name: securityAlertDTO.product.name, + }, + vulnerability, + vulnerabilityFix, + securityAlertDTO.effective + ) + } + public static toDTO(securityAlert: SecurityAlert) { + return new SecurityAlertDTO( + securityAlert.uuid, + securityAlert.name, + securityAlert.type, + securityAlert.component, + securityAlert.alertInfo, + { + uuid: securityAlert.project.uuid, + name: securityAlert.project.name, + path: securityAlert.project.path, + productUuid: securityAlert.project.productUuid, + }, + { uuid: securityAlert.product.uuid, name: securityAlert.product.name }, + { + name: securityAlert.vulnerability.name, + type: securityAlert.vulnerability.type, + description: securityAlert.vulnerability.description, + score: securityAlert.vulnerability.score, + severity: securityAlert.vulnerability.severity, + publishDate: securityAlert.vulnerability.publishDate, + modifiedDate: securityAlert.vulnerability.modifiedDate, + vulnerabilityScoring: securityAlert.vulnerability.vulnerabilityScoring, + }, + { + id: securityAlert.topFix.id, + vulnerability: securityAlert.topFix.vulnerability, + type: securityAlert.topFix.type, + origin: securityAlert.topFix.origin, + url: securityAlert.topFix.url, + fixResolution: securityAlert.topFix.fixResolution, + date: securityAlert.topFix.date, + message: securityAlert.topFix.message, + extraData: securityAlert.topFix.extraData, + }, + securityAlert.effective + ) + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/mapper/vulnerability.mapper.ts b/yaku-apps-typescript/apps/mend-fetcher/src/mapper/vulnerability.mapper.ts new file mode 100644 index 00000000..c7196c22 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/mapper/vulnerability.mapper.ts @@ -0,0 +1,57 @@ +import { VulnerabilityDTO } from '../dto/vulnerability.dto.js' +import { Vulnerability } from '../model/vulnerability.js' +import { VulnerabilityReference } from '../model/vulnerabilityReference.js' + +export class VulnerabilityMap { + public static toModel(vulnerabilityDTO: VulnerabilityDTO) { + const vulnerabilityReferences: VulnerabilityReference[] = + vulnerabilityDTO.references && vulnerabilityDTO.references.length > 0 + ? vulnerabilityDTO.references.map( + (ref) => + new VulnerabilityReference( + ref.value, + ref.source, + ref.url, + ref.signature, + ref.advisory, + ref.patch + ) + ) + : [] + + return new Vulnerability( + vulnerabilityDTO.name, + vulnerabilityDTO.type, + vulnerabilityDTO.description, + vulnerabilityDTO.score, + vulnerabilityDTO.severity, + vulnerabilityDTO.publishDate, + vulnerabilityDTO.modifiedDate, + vulnerabilityDTO.vulnerabilityScoring, + vulnerabilityReferences + ) + } + + public static toDTO(vulnerability: Vulnerability) { + return new VulnerabilityDTO( + vulnerability.name, + vulnerability.type, + vulnerability.description, + vulnerability.score, + vulnerability.severity, + vulnerability.publishDate, + vulnerability.modifiedDate, + vulnerability.vulnerabilityScoring, + vulnerability.references?.map((ref) => { + return { + value: ref.value, + source: ref.source, + url: ref.url, + signature: ref.signature, + advisory: ref.advisory, + patch: ref.patch, + } + }) + ) + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/mapper/vulnerabilityFixSummary.mapper.ts b/yaku-apps-typescript/apps/mend-fetcher/src/mapper/vulnerabilityFixSummary.mapper.ts new file mode 100644 index 00000000..59171937 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/mapper/vulnerabilityFixSummary.mapper.ts @@ -0,0 +1,67 @@ +import { VulnerabilityFixSummaryDTO } from '../dto/vulnerabilityFixSummary.dto.js' +import { VulnerabilityFixSummary } from '../model/vulnerabilityFixSummary.js' +import { VulnerabilityFix } from '../model/vulnerabilityFix.js' + +export class VulnerabilityFixSummaryMap { + public static toModel( + vulnerabilityFixSummaryDTO: VulnerabilityFixSummaryDTO + ) { + const vulnerabilityFix: VulnerabilityFix[] = + vulnerabilityFixSummaryDTO.allFixes && + vulnerabilityFixSummaryDTO.allFixes.length > 0 + ? vulnerabilityFixSummaryDTO.allFixes.map( + (fixes) => + new VulnerabilityFix( + fixes.id, + fixes.vulnerability, + fixes.type, + fixes.origin, + fixes.url, + fixes.fixResolution, + fixes.date, + fixes.message, + fixes.extraData + ) + ) + : [] + return new VulnerabilityFixSummary( + vulnerabilityFixSummaryDTO.vulnerability, + vulnerabilityFixSummaryDTO.topRankedFix as VulnerabilityFix, + vulnerabilityFix, + vulnerabilityFixSummaryDTO.totalUpVotes, + vulnerabilityFixSummaryDTO.totalDownVotes + ) + } + + public static toDTO(vulnerabilityFixSummary: VulnerabilityFixSummary) { + return new VulnerabilityFixSummaryDTO( + vulnerabilityFixSummary.vulnerability, + new VulnerabilityFix( + vulnerabilityFixSummary.topRankedFix.id, + vulnerabilityFixSummary.topRankedFix.vulnerability, + vulnerabilityFixSummary.topRankedFix.type, + vulnerabilityFixSummary.topRankedFix.origin, + vulnerabilityFixSummary.topRankedFix.url, + vulnerabilityFixSummary.topRankedFix.fixResolution, + vulnerabilityFixSummary.topRankedFix.date, + vulnerabilityFixSummary.topRankedFix.message, + vulnerabilityFixSummary.topRankedFix.extraData + ), + vulnerabilityFixSummary.allFixes.map((fixes) => { + return { + id: fixes.id, + vulnerability: fixes.vulnerability, + type: fixes.type, + origin: fixes.origin, + url: fixes.url, + fixResolution: fixes.fixResolution, + date: fixes.date, + message: fixes.message, + extraData: fixes.extraData, + } + }), + vulnerabilityFixSummary.totalUpVotes, + vulnerabilityFixSummary.totalDownVotes + ) + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/model/alert.ts b/yaku-apps-typescript/apps/mend-fetcher/src/model/alert.ts new file mode 100644 index 00000000..b6e8fc8f --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/model/alert.ts @@ -0,0 +1,26 @@ +import { IBaseComponent } from './baseComponent.js' +import { ILibraryComponent } from './libraryComponent.js' +import { Project } from './project.js' + +export interface IAlert { + uuid: string + name: string + type: string + component: IBaseComponent | ILibraryComponent + alertInfo: { + status: string + comment: + | { + comment: string + date: string + } + | Record + detectedAt: string + modifiedAt: string + } + project: Project + product: { + uuid: string + name: string + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/model/baseComponent.ts b/yaku-apps-typescript/apps/mend-fetcher/src/model/baseComponent.ts new file mode 100644 index 00000000..08823dba --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/model/baseComponent.ts @@ -0,0 +1,6 @@ +export interface IBaseComponent { + uuid: string + name: string + description: string + libraryType: string +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/model/copyrightReference.ts b/yaku-apps-typescript/apps/mend-fetcher/src/model/copyrightReference.ts new file mode 100644 index 00000000..ed5cf0f8 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/model/copyrightReference.ts @@ -0,0 +1,10 @@ +export class CopyrightReference { + constructor( + public type: string, + public copyright: string, + public author: string, + public referenceInfo: string, + public startYear?: string, + public endYear?: string + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/model/library.ts b/yaku-apps-typescript/apps/mend-fetcher/src/model/library.ts new file mode 100644 index 00000000..29dfdac6 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/model/library.ts @@ -0,0 +1,25 @@ +import { CopyrightReference } from './copyrightReference.js' +import { License } from './license.js' + +export class Library { + constructor( + public uuid: string, + public name: string, + public artifactId: string, + public version: string, + public architecture: string, + public languageVersion: string, + public classifier: string, + public extension: string, + public sha1: string, + public description: string, + public type: string, + public directDependency: boolean, + public licenses: License[], + public copyrightReferences: CopyrightReference[], + public locations: { + localPath: string + dependencyFile: string + }[] + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/model/libraryComponent.ts b/yaku-apps-typescript/apps/mend-fetcher/src/model/libraryComponent.ts new file mode 100644 index 00000000..0380e3f1 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/model/libraryComponent.ts @@ -0,0 +1,18 @@ +import { IBaseComponent } from './baseComponent.js' + +export interface ILibraryComponent extends IBaseComponent { + uuid: string + name: string + description: string + componentType: string + directDependency: boolean + references: { + url: string + homePage: string + genericPackageIndex: string + } + groupId: string + artifactId: string + version: string + path: string +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/model/license.ts b/yaku-apps-typescript/apps/mend-fetcher/src/model/license.ts new file mode 100644 index 00000000..c6f57c22 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/model/license.ts @@ -0,0 +1,10 @@ +import { LicenseReference } from './licenseReference.js' + +export class License { + constructor( + public uuid: string, + public name: string, + public assignedByUser: boolean, + public licenseReferences: LicenseReference[] + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/model/licenseReference.ts b/yaku-apps-typescript/apps/mend-fetcher/src/model/licenseReference.ts new file mode 100644 index 00000000..67aa177e --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/model/licenseReference.ts @@ -0,0 +1,8 @@ +export class LicenseReference { + constructor( + public uuid: string, + public type: string, + public liabilityReference: string, + public information: string + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/model/login.ts b/yaku-apps-typescript/apps/mend-fetcher/src/model/login.ts new file mode 100644 index 00000000..e3c7d64e --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/model/login.ts @@ -0,0 +1,13 @@ +export class Login { + constructor( + public userUuid: string, + public userName: string, + public email: string, + public jwtToken: string, + public refreshToken: string, + public jwtTTL: number, + public orgName: string, + public orgUuid: string, + public sessionStartTime: number + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/model/mendEnvironment.ts b/yaku-apps-typescript/apps/mend-fetcher/src/model/mendEnvironment.ts new file mode 100644 index 00000000..b151db6e --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/model/mendEnvironment.ts @@ -0,0 +1,20 @@ +export interface MendEnvironment { + alertsStatus: + | 'all' + | 'active' + | 'ignored' + | 'library_removed' + | 'library_in_house' + | 'library_whitelist' + apiUrl: string + serverUrl: string + email: string + maxConcurrentConnections: number + minConnectionTime: number + orgToken: string + projectId: number | undefined + projectToken: string + reportType: 'alerts' | 'vulnerabilities' + resultsPath: string + userKey: string +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/model/multipleLicensesAlert.ts b/yaku-apps-typescript/apps/mend-fetcher/src/model/multipleLicensesAlert.ts new file mode 100644 index 00000000..4112db19 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/model/multipleLicensesAlert.ts @@ -0,0 +1,30 @@ +import { IAlert } from './alert.js' +import { IBaseComponent } from './baseComponent.js' +import { Project } from './project.js' + +export class MultipleLicensesAlert implements IAlert { + constructor( + public uuid: string, + public name: string, + public type: string, + public component: IBaseComponent, + public alertInfo: { + status: string + comment: + | { + comment: string + date: string + } + | Record + detectedAt: string + modifiedAt: string + }, + public project: Project, + public product: { + uuid: string + name: string + }, + public numberOfLicenses: number, + public licenses: Array + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/model/newVersionsAlert.ts b/yaku-apps-typescript/apps/mend-fetcher/src/model/newVersionsAlert.ts new file mode 100644 index 00000000..5888859d --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/model/newVersionsAlert.ts @@ -0,0 +1,30 @@ +import { IAlert } from './alert.js' +import { IBaseComponent } from './baseComponent.js' +import { Project } from './project.js' + +export class NewVersionsAlert implements IAlert { + constructor( + public uuid: string, + public name: string, + public type: string, + public component: IBaseComponent, + public alertInfo: { + status: string + comment: + | { + comment: string + date: string + } + | Record + detectedAt: string + modifiedAt: string + }, + public project: Project, + public product: { + uuid: string + name: string + }, + public availableVersion: string, + public availableVersionType: string + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/model/organization.ts b/yaku-apps-typescript/apps/mend-fetcher/src/model/organization.ts new file mode 100644 index 00000000..1d6f2598 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/model/organization.ts @@ -0,0 +1,3 @@ +export class Organization { + constructor(public uuid: string, public name: string) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/model/policyAlert.ts b/yaku-apps-typescript/apps/mend-fetcher/src/model/policyAlert.ts new file mode 100644 index 00000000..89e0fb47 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/model/policyAlert.ts @@ -0,0 +1,29 @@ +import { IAlert } from './alert.js' +import { IBaseComponent } from './baseComponent.js' +import { Project } from './project.js' + +export class PolicyAlert implements IAlert { + constructor( + public uuid: string, + public name: string, + public type: string, + public component: IBaseComponent, + public alertInfo: { + status: string + comment: + | { + comment: string + date: string + } + | Record + detectedAt: string + modifiedAt: string + }, + public project: Project, + public product: { + uuid: string + name: string + }, + public policyName: string + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/model/project.ts b/yaku-apps-typescript/apps/mend-fetcher/src/model/project.ts new file mode 100644 index 00000000..8595c5dc --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/model/project.ts @@ -0,0 +1,9 @@ +export class Project { + constructor( + public uuid: string, + public name: string, + public path: string, + public productName: string, + public productUuid: string + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/model/projectVitals.ts b/yaku-apps-typescript/apps/mend-fetcher/src/model/projectVitals.ts new file mode 100644 index 00000000..ca042018 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/model/projectVitals.ts @@ -0,0 +1,18 @@ +export class ProjectVitals { + constructor( + public lastScan: string, + public lastUserScanned: { + uuid: string + name: string + email: string + userType: string + }, + public requestToken: string, + public lastSourceFileMatch: string, + public lastScanComment: string, + public projectCreationDate: string, + public pluginName: string, + public pluginVersion: string, + public libraryCount: number + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/model/rejectedInUseAlert.ts b/yaku-apps-typescript/apps/mend-fetcher/src/model/rejectedInUseAlert.ts new file mode 100644 index 00000000..cd76766a --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/model/rejectedInUseAlert.ts @@ -0,0 +1,29 @@ +import { IAlert } from './alert.js' +import { IBaseComponent } from './baseComponent.js' +import { Project } from './project.js' + +export class RejectedInUseAlert implements IAlert { + constructor( + public uuid: string, + public name: string, + public type: string, + public component: IBaseComponent, + public alertInfo: { + status: string + comment: + | { + comment: string + date: string + } + | Record + detectedAt: string + modifiedAt: string + }, + public project: Project, + public product: { + uuid: string + name: string + }, + public description: string + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/model/securityAlert.ts b/yaku-apps-typescript/apps/mend-fetcher/src/model/securityAlert.ts new file mode 100644 index 00000000..0205aa27 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/model/securityAlert.ts @@ -0,0 +1,33 @@ +import { IAlert } from './alert.js' +import { ILibraryComponent } from './libraryComponent.js' +import { Project } from './project.js' +import { Vulnerability } from './vulnerability.js' +import { VulnerabilityFix } from './vulnerabilityFix.js' + +export class SecurityAlert implements IAlert { + constructor( + public uuid: string, + public name: string, + public type: string, + public component: ILibraryComponent, + public alertInfo: { + status: string + comment: + | { + comment: string + date: string + } + | Record + detectedAt: string + modifiedAt: string + }, + public project: Project, + public product: { + uuid: string + name: string + }, + public vulnerability: Vulnerability, + public topFix: VulnerabilityFix, + public effective: string + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/model/vulnerability.ts b/yaku-apps-typescript/apps/mend-fetcher/src/model/vulnerability.ts new file mode 100644 index 00000000..7818b02a --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/model/vulnerability.ts @@ -0,0 +1,19 @@ +import { VulnerabilityReference } from './vulnerabilityReference.js' + +export class Vulnerability { + constructor( + public name: string, + public type: string, + public description: string, + public score: number, + public severity: string, + public publishDate: string, + public modifiedDate: string, + public vulnerabilityScoring: { + score: number + severity: string + type: string + }[], + public references?: VulnerabilityReference[] + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/model/vulnerabilityFix.ts b/yaku-apps-typescript/apps/mend-fetcher/src/model/vulnerabilityFix.ts new file mode 100644 index 00000000..cd86cf9c --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/model/vulnerabilityFix.ts @@ -0,0 +1,13 @@ +export class VulnerabilityFix { + constructor( + public id: number, + public vulnerability: string, + public type: string, + public origin: string, + public url: string, + public fixResolution: string, + public date: string, + public message: string, + public extraData: any | Record + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/model/vulnerabilityFixSummary.ts b/yaku-apps-typescript/apps/mend-fetcher/src/model/vulnerabilityFixSummary.ts new file mode 100644 index 00000000..ccd286a4 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/model/vulnerabilityFixSummary.ts @@ -0,0 +1,11 @@ +import { VulnerabilityFix } from './vulnerabilityFix.js' + +export class VulnerabilityFixSummary { + constructor( + public vulnerability: string, + public topRankedFix: VulnerabilityFix, + public allFixes: VulnerabilityFix[], + public totalUpVotes: number, + public totalDownVotes: number + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/model/vulnerabilityReference.ts b/yaku-apps-typescript/apps/mend-fetcher/src/model/vulnerabilityReference.ts new file mode 100644 index 00000000..d270b41c --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/model/vulnerabilityReference.ts @@ -0,0 +1,10 @@ +export class VulnerabilityReference { + constructor( + public value: string, + public source: string, + public url: string, + public signature: boolean, + public advisory: boolean, + public patch: boolean + ) {} +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/run.ts b/yaku-apps-typescript/apps/mend-fetcher/src/run.ts new file mode 100644 index 00000000..45979da9 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/run.ts @@ -0,0 +1,548 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { MendEnvironment } from './model/mendEnvironment.js' +import { Library } from './model/library.js' +import { LibraryService } from './service/library.service.js' +import { Organization } from './model/organization.js' +import { OrganizationService } from './service/organization.service.js' +import { Project } from './model/project.js' +import { ProjectService } from './service/project.service.js' +import { ProjectVitals } from './model/projectVitals.js' +import { VulnerabilityService } from './service/vulnerability.service.js' +import { Vulnerability } from './model/vulnerability.js' +import { AlertService } from './service/alert.service.js' +import { PolicyAlert } from './model/policyAlert.js' +import { SecurityAlert } from './model/securityAlert.js' +import { exportJson } from './utils/export.js' +import { + AppError, + AppOutput, + InitLogger, +} from '@B-S-F/autopilot-utils' +import Bottleneck from 'bottleneck' +import path from 'path' +import z, { ZodError } from 'zod' +import { NewVersionsAlert } from './model/newVersionsAlert.js' +import { MultipleLicensesAlert } from './model/multipleLicensesAlert.js' +import { RejectedInUseAlert } from './model/rejectedInUseAlert.js' +import { VulnerabilityFixSummary } from './model/vulnerabilityFixSummary.js' + +const customZodErrorMap: z.ZodErrorMap = (error, ctx) => { + switch (error.code) { + case z.ZodIssueCode.invalid_type: + if (error.expected === 'number' && error.received === 'nan') { + return { message: 'Expected number, received not a number' } + } + break + } + + return { message: ctx.defaultError } +} + +const checkdelimiter = ( + value: string | undefined, + context: z.RefinementCtx +) => { + if (value != undefined) { + if (value.endsWith(',')) { + const customError: z.ZodIssue = { + message: 'Unexpected trailing comma', + code: 'custom', + path: context.path, + } + throw new ZodError([customError]) + } + } +} + +const validProjectIDs = (value: string | undefined) => { + if (value) + value.split(',').forEach((id: string) => { + if (isNaN(Number(id))) { + const customError: z.ZodIssue = { + code: 'custom', + message: 'Must be a number or numbers splitted by a comma.', + path: ['MEND_PROJECT_ID'], + } + throw new ZodError([customError]) + } + }) +} + +const validProjectTokens = (value: string) => { + value.split(',').forEach((token) => { + if (token.length == 0) { + const customError: z.ZodIssue = { + code: 'custom', + message: 'String must contain at least 1 character(s)', + path: ['MEND_PROJECT_TOKEN'], + } + throw new ZodError([customError]) + } + }) +} + +const toProjectIDArray = (value: string | undefined) => { + const prjIdArray = value + ? value.split(',').map((id: string) => { + return id.length == 0 ? undefined : Number(id) + }) + : [] + return prjIdArray +} + +const toProjectTokenArray = (value: string) => { + return value.split(',') +} + +const validateAndCreateProjectIDTokenMap = ( + prjIDs: (number | undefined)[], + prjTokens: string[] +) => { + if (prjIDs.length !== prjTokens.length && prjIDs.length !== 0) { + const customError: z.ZodIssue = { + code: 'custom', + message: + 'and MEND_PROJECT_ID should be of equal length or MEND_PROJECT_ID should be empty.', + path: ['MEND_PROJECT_TOKEN'], + } + throw new ZodError([customError]) + } + + const map: any[] = [] + prjTokens.forEach((token, index) => { + return map.push({ + token, + id: index < prjIDs.length ? prjIDs[index] : undefined, + }) + }) + + return map as { token: string; id: number | undefined }[] +} + +const validateEnvironmentVariables = () => { + const envSchema = z.object({ + MEND_API_URL: z.string().url(), + MEND_SERVER_URL: z.string().url(), + MEND_ORG_TOKEN: z.string().min(1), + MEND_PROJECT_TOKEN: z.string().min(1), + MEND_USER_EMAIL: z.string().email(), + MEND_USER_KEY: z.string().min(1), + MEND_REPORT_TYPE: z + .enum(['alerts', 'vulnerabilities']) + .default('vulnerabilities'), + MEND_ALERTS_STATUS: z + .enum([ + 'all', + 'active', + 'ignored', + 'library_removed', + 'library_in_house', + 'library_whitelist', + ]) + .default('active'), + MEND_MIN_CONNECTION_TIME: z.coerce.number().default(50), + MEND_MAX_CONCURRENT_CONNECTIONS: z.coerce.number().default(50), + MEND_RESULTS_PATH: z.string().default('./'), + }) + + const projectsSchema = z.object({ + MEND_PROJECT_ID: z + .string() + .optional() + .superRefine(validProjectIDs) + .transform(toProjectIDArray), + MEND_PROJECT_TOKEN: z + .string() + .min(1) + .superRefine(checkdelimiter) + .superRefine(validProjectTokens) + .transform(toProjectTokenArray), + }) + + const validatedENV = envSchema.parse(process.env, { + errorMap: customZodErrorMap, + }) + const validatedProjects = projectsSchema.parse(process.env, { + errorMap: customZodErrorMap, + }) + const projectsIDTokenMap = validateAndCreateProjectIDTokenMap( + validatedProjects.MEND_PROJECT_ID, + validatedProjects.MEND_PROJECT_TOKEN + ) + + const parsedENV = { + ...validatedENV, + MEND_PROJECT_IDS_TOKENS_MAP: projectsIDTokenMap, + } + + return parsedENV +} + +export const run = async () => { + const logger = InitLogger('mend-fetcher', 'info') + let localOutput: AppOutput + const globalOutput = new AppOutput() + globalOutput.setStatus('GREEN') + + try { + const validatedEnvironment = validateEnvironmentVariables() + + const limiter = new Bottleneck({ + minTime: validatedEnvironment.MEND_MIN_CONNECTION_TIME, + maxConcurrent: validatedEnvironment.MEND_MAX_CONCURRENT_CONNECTIONS, + }) + + for (const { + token: MEND_PROJECT_TOKEN, + id: MEND_PROJECT_ID, + } of validatedEnvironment.MEND_PROJECT_IDS_TOKENS_MAP) { + const env: MendEnvironment = { + alertsStatus: validatedEnvironment.MEND_ALERTS_STATUS, + apiUrl: validatedEnvironment.MEND_API_URL, + serverUrl: validatedEnvironment.MEND_SERVER_URL, + email: validatedEnvironment.MEND_USER_EMAIL, + maxConcurrentConnections: + validatedEnvironment.MEND_MAX_CONCURRENT_CONNECTIONS, + minConnectionTime: validatedEnvironment.MEND_MIN_CONNECTION_TIME, + orgToken: validatedEnvironment.MEND_ORG_TOKEN, + projectId: MEND_PROJECT_ID, + projectToken: MEND_PROJECT_TOKEN, + reportType: validatedEnvironment.MEND_REPORT_TYPE, + resultsPath: validatedEnvironment.MEND_RESULTS_PATH, + userKey: validatedEnvironment.MEND_USER_KEY, + } + + localOutput = new AppOutput() + + const orgService = new OrganizationService(env) + const organization: Organization = await limiter.schedule(() => + orgService.getOrganizationById(env.orgToken) + ) + const projectService = new ProjectService(env) + const project: Project = await limiter.schedule(() => + projectService.getProjectByToken(env.projectToken) + ) + const projectVitals: ProjectVitals = await limiter.schedule(() => + projectService.getProjectVitals(project.uuid) + ) + + const resultLinkTemplate = + env.projectId !== undefined + ? `${env.serverUrl}/Wss/WSS.html#!libraryDetails;` + + `orgToken=${organization.uuid};` + + `project=${env.projectId};` + : `${env.serverUrl}/Wss/WSS.html#!libraryDetails;` + + `orgToken=${organization.uuid};` + const reasonDetailsTemplate = + env.projectId !== undefined + ? `see more details in Mend ` + + `${env.serverUrl}/Wss/WSS.html#!project;` + + `orgToken=${organization.uuid};` + + `id=${env.projectId}` + : `see more details in Mend ` + + `${env.serverUrl}/Wss/WSS.html` + + ` in organization ${organization.name}` + + ` and project ${project.name}` + + logger.info( + `----- Project '${project.name}' with uuid '${project.uuid}' from '${project.productName}' -----` + ) + + if (env.reportType === 'alerts') { + const alertService: AlertService = new AlertService(env) + const policyAlerts = await limiter.schedule(() => + alertService.getPolicyAlertsById(project.uuid, env.alertsStatus) + ) + logger.info('----- Policy Alerts -----') + policyAlerts.map((alert: PolicyAlert) => { + localOutput.addResult({ + criterion: 'Open Policy Alert Mend', + justification: alert.component.name, + fulfilled: false, + metadata: { + project: project.name, + name: alert.component.name, + status: alert.alertInfo.status, + link: `${resultLinkTemplate}uuid=${alert.component.uuid};`, + description: alert.component.description, + }, + }) + }) + logger.info('---------------------------') + + const securityAlerts = await limiter.schedule(() => + alertService.getSecurityAlertsById(project.uuid, env.alertsStatus) + ) + logger.info('----- Security Alerts -----') + securityAlerts.map((alert: SecurityAlert) => { + logger.info( + `${alert.name} ` + + `${alert.vulnerability.severity} ` + + `${alert.vulnerability.score} ` + + `${alert.alertInfo.status} ` + + `: ` + + `${alert.component.name} ` + + `${alert.topFix.message} ` + + `${alert.topFix.fixResolution}` + ) + localOutput.addResult({ + criterion: 'Open Security Alert Mend', + justification: alert.vulnerability.name, + fulfilled: false, + metadata: { + project: project.name, + name: `${alert.vulnerability.name}:${alert.component.name}`, + severity: alert.vulnerability.severity, + score: `${alert.vulnerability.score}`, + status: alert.alertInfo.status, + link: `${resultLinkTemplate}uuid=${alert.component.uuid};`, + description: alert.vulnerability.description, + }, + }) + }) + logger.info('---------------------------') + + const newVersionsAlerts = await limiter.schedule(() => + alertService.getNewVersionsAlertsById(project.uuid, env.alertsStatus) + ) + logger.info('----- New Versions Alerts -----') + newVersionsAlerts.map((alert: NewVersionsAlert) => { + logger.info( + `${alert.name} ` + + `${alert.alertInfo.status} ` + + `: ` + + `${alert.component.name}` + ) + localOutput.addResult({ + criterion: 'New Versions Alert Mend', + justification: + alert.component.name + + ':' + + alert.availableVersionType + + ' ' + + alert.availableVersion, + fulfilled: false, + metadata: { + project: project.name, + name: alert.component.name, + status: alert.alertInfo.status, + link: `${resultLinkTemplate}uuid=${alert.component.uuid};`, + description: alert.component.description, + }, + }) + }) + + logger.info('---------------------------') + const multipleLicensesAlerts = await limiter.schedule(() => + alertService.getMultipleLicensesAlertsById( + project.uuid, + env.alertsStatus + ) + ) + + logger.info('----- Multiple Licenses Alerts -----') + multipleLicensesAlerts.map((alert: MultipleLicensesAlert) => { + logger.info( + `${alert.name} ` + + `${alert.licenses} ` + + `${alert.alertInfo.status} ` + + `: ` + + `${alert.component.name}` + ) + localOutput.addResult({ + criterion: 'Multiple Licenses Alert Mend', + justification: + alert.component.name + + ':' + + alert.numberOfLicenses + + ' ' + + alert.licenses, + fulfilled: false, + metadata: { + project: project.name, + name: alert.component.name, + status: alert.alertInfo.status, + link: `${resultLinkTemplate}uuid=${alert.component.uuid};`, + description: alert.component.description, + }, + }) + }) + + logger.info('---------------------------') + const rejectedInUseAlerts = await limiter.schedule(() => + alertService.getRejectedInUseAlertsById( + project.uuid, + env.alertsStatus + ) + ) + logger.info('----- Rejected in Use Alerts -----') + rejectedInUseAlerts.map((alert: RejectedInUseAlert) => { + logger.info( + `${alert.name} ` + + `${alert.alertInfo.status} ` + + `: ` + + `${alert.component.name}` + ) + localOutput.addResult({ + criterion: 'Rejected In Use Alert Mend', + justification: alert.component.name + ':' + alert.description, + fulfilled: false, + metadata: { + project: project.name, + name: alert.component.name, + status: alert.alertInfo.status, + link: `${resultLinkTemplate}uuid=${alert.component.uuid};`, + description: alert.component.description, + }, + }) + }) + logger.info('---------------------------') + } else if (env.reportType === 'vulnerabilities') { + logger.info('----- Vulnerabilities -----') + + const libraryService: LibraryService = new LibraryService(env) + const vulnerabilityService: VulnerabilityService = + new VulnerabilityService(env) + const vulnerableLibraries = new Map() + const vulnerabilityFixes = new Map< + Vulnerability, + VulnerabilityFixSummary + >() + + await Promise.all( + ( + await limiter.schedule(() => + libraryService.getAllLibrariesById(project.uuid) + ) + ).map(async (library: Library) => { + const vulns = await limiter.schedule(() => + vulnerabilityService.getAllVulnerabilitiesById( + library.uuid, + project.uuid + ) + ) + + let fix: VulnerabilityFixSummary + for (const vuln of vulns) { + fix = await limiter.schedule(() => + vulnerabilityService.getAllVulnerabilitiesFixSummaryById( + vuln.name + ) + ) + vulnerabilityFixes.set(vuln, fix) + } + + vulnerableLibraries.set(library, vulns) + }) + ) + + for (const lib of vulnerableLibraries.keys()) { + vulnerableLibraries.get(lib)?.map((vuln: Vulnerability) => { + logger.info( + `${vuln.name} ${vuln.severity} ${vuln.score} : ${lib.name}` + ) + + const topFix = vulnerabilityFixes.get(vuln) + const topFixFields = topFix?.topRankedFix + const message = topFixFields?.message + const fixResolution = topFixFields?.fixResolution + + localOutput.addResult({ + criterion: 'Open Vulnerability Mend', + justification: vuln.description, + fulfilled: false, + metadata: { + project: project.name, + name: `${vuln.name}:${lib.name}`, + severity: vuln.severity, + score: `${vuln.score}`, + link: `${resultLinkTemplate}uuid=${lib.uuid};`, + description: vuln.description, + topFix: `${message} ${fixResolution}`, + }, + }) + }) + } + logger.info('---------------------------') + } + + let reason: string + + if (localOutput.data.results.length > 0) { + globalOutput.setStatus('RED') + reason = + `${localOutput.data.results.length} ${env.reportType} were found, last scan was executed on ${projectVitals.lastScan}` + + ', ' + + reasonDetailsTemplate + + ';' + } else { + localOutput.addResult({ + criterion: `There are no open ${env.reportType} in mend`, + justification: `No open ${env.reportType} were found`, + fulfilled: true, + metadata: {}, + }) + reason = + `No ${env.reportType} were found, last scan was executed on ${projectVitals.lastScan}` + + ', ' + + reasonDetailsTemplate + + ';' + } + + globalOutput.data.results = globalOutput.data.results.concat( + localOutput.data.results + ) + globalOutput.setReason( + globalOutput.data.reason + ? globalOutput.data.reason.concat(' ' + reason) + : reason + ) + } + + const outputJsonPath = path.join( + validatedEnvironment.MEND_RESULTS_PATH, + 'results.json' + ) + exportJson(globalOutput.data.results, outputJsonPath) + globalOutput.write() + } catch (error: any) { + console.error(error) + if (error instanceof ZodError) { + globalOutput.setStatus('FAILED') + const reason = `Environment validation failed:${error.issues.map( + (issue: any) => ` ${issue.path[0]} ${issue.message}` + )}` + globalOutput.setReason( + globalOutput.data.reason + ? globalOutput.data.reason.concat(reason) + : reason + ) + + logger.error( + `Environment validation failed:${error.issues.map( + (issue: any) => ` ${issue.path[0]} ${issue.message}` + )}` + ) + + globalOutput.write() + process.exit(0) + } else if (error instanceof AppError) { + globalOutput.setStatus('FAILED') + globalOutput.setReason( + globalOutput.data.reason + ? globalOutput.data.reason.concat(error.Reason()) + : error.Reason() + ) + + logger.error(error.Reason()) + + globalOutput.write() + process.exit(0) + } else { + throw error + } + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/service/alert.service.ts b/yaku-apps-typescript/apps/mend-fetcher/src/service/alert.service.ts new file mode 100644 index 00000000..043da875 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/service/alert.service.ts @@ -0,0 +1,97 @@ +import { Authenticator } from '../auth/auth.js' +import { + getMultipleLicensesAlertDTOs, + getNewVersionsAlertDTOs, + getPolicyAlertDTOs, + getRejectedInUseAlertDTOs, + getSecurityAlertDTOs, +} from '../fetcher/alert.fetcher.js' +import { MendEnvironment } from '../model/mendEnvironment.js' +import { PolicyAlertDTO } from '../dto/policyAlert.dto.js' +import { PolicyAlertMap } from '../mapper/policyAlert.mapper.js' +import { SecurityAlertDTO } from '../dto/securityAlert.dto.js' +import { SecurityAlertMap } from '../mapper/securityAlert.mapper.js' +import { NewVersionsAlertDTO } from '../dto/newVersionsAlert.dto.js' +import { NewVersionsAlertMap } from '../mapper/newVersionsAlert.mapper.js' +import { MultipleLicensesAlertDTO } from '../dto/multipleLicensesAlert.dto.js' +import { MultipleLicensesAlertMap } from '../mapper/multipleLicensesAlert.mapper.js' +import { RejectedInUseAlertDTO } from '../dto/rejectedInUseAlert.dto.js' +import { RejectedInUseAlertMap } from '../mapper/rejectedInUseAlert.mapper.js' + +export class AlertService { + private auth: Authenticator + private env: MendEnvironment + + constructor(env: MendEnvironment) { + this.env = env + this.auth = Authenticator.getInstance(env) + } + + async getPolicyAlertsById(projectId: string, alertStatus: string) { + const policyAlertDTOs: PolicyAlertDTO[] = await getPolicyAlertDTOs( + this.env.apiUrl, + { projectToken: projectId, status: alertStatus, pageSize: 100 }, + this.auth + ) + const policyAlerts = policyAlertDTOs.map((alertDTO) => + PolicyAlertMap.toModel(alertDTO) + ) + + return policyAlerts + } + + async getSecurityAlertsById(projectId: string, alertStatus: string) { + const securityAlertDTOs: SecurityAlertDTO[] = await getSecurityAlertDTOs( + this.env.apiUrl, + { projectToken: projectId, status: alertStatus, pageSize: 100 }, + this.auth + ) + const securityAlerts = securityAlertDTOs.map((alertDTO) => + SecurityAlertMap.toModel(alertDTO) + ) + + return securityAlerts + } + + async getNewVersionsAlertsById(projectId: string, alertStatus: string) { + const newVersionsAlertDTOs: NewVersionsAlertDTO[] = + await getNewVersionsAlertDTOs( + this.env.apiUrl, + { projectToken: projectId, status: alertStatus, pageSize: 100 }, + this.auth + ) + const newVersionsAlerts = newVersionsAlertDTOs.map((alertDTO) => + NewVersionsAlertMap.toModel(alertDTO) + ) + + return newVersionsAlerts + } + + async getMultipleLicensesAlertsById(projectId: string, alertStatus: string) { + const multipleLicensesAlertDTOs: MultipleLicensesAlertDTO[] = + await getMultipleLicensesAlertDTOs( + this.env.apiUrl, + { projectToken: projectId, status: alertStatus, pageSize: 100 }, + this.auth + ) + const multipleLicensesAlerts = multipleLicensesAlertDTOs.map((alertDTO) => + MultipleLicensesAlertMap.toModel(alertDTO) + ) + + return multipleLicensesAlerts + } + + async getRejectedInUseAlertsById(projectId: string, alertStatus: string) { + const rejectedInUseAlertDTOs: RejectedInUseAlertDTO[] = + await getRejectedInUseAlertDTOs( + this.env.apiUrl, + { projectToken: projectId, status: alertStatus, pageSize: 100 }, + this.auth + ) + const rejectedInUseAlerts = rejectedInUseAlertDTOs.map((alertDTO) => + RejectedInUseAlertMap.toModel(alertDTO) + ) + + return rejectedInUseAlerts + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/service/library.service.ts b/yaku-apps-typescript/apps/mend-fetcher/src/service/library.service.ts new file mode 100644 index 00000000..d8296642 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/service/library.service.ts @@ -0,0 +1,29 @@ +import { Authenticator } from '../auth/auth.js' +import { getLibraryDTOs } from '../fetcher/library.fetcher.js' +import { Library } from '../model/library.js' +import { LibraryDTO } from '../dto/library.dto.js' +import { LibraryMap } from '../mapper/library.mapper.js' +import { MendEnvironment } from '../model/mendEnvironment.js' + +export class LibraryService { + private auth: Authenticator + private env: MendEnvironment + + constructor(env: MendEnvironment) { + this.env = env + this.auth = Authenticator.getInstance(env) + } + + async getAllLibrariesById(projectId: string): Promise { + const libraryDTOs: LibraryDTO[] = await getLibraryDTOs( + this.env.apiUrl, + { projectToken: projectId, pageSize: 100 }, + this.auth + ) + const projectLibraries = libraryDTOs.map((libDto) => + LibraryMap.toModel(libDto) + ) + + return projectLibraries + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/service/organization.service.ts b/yaku-apps-typescript/apps/mend-fetcher/src/service/organization.service.ts new file mode 100644 index 00000000..f5ef3056 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/service/organization.service.ts @@ -0,0 +1,26 @@ +import { Authenticator } from '../auth/auth.js' +import { getOrganizationDTO } from '../fetcher/organization.fetcher.js' +import { Organization } from '../model/organization.js' +import { OrganizationMap } from '../mapper/organization.mapper.js' +import { MendEnvironment } from '../model/mendEnvironment.js' + +export class OrganizationService { + private auth: Authenticator + private env: MendEnvironment + + constructor(env: MendEnvironment) { + this.env = env + this.auth = Authenticator.getInstance(env) + } + + async getOrganizationById(orgToken: string): Promise { + const organizationDto = await getOrganizationDTO( + this.env.apiUrl, + { orgToken: orgToken }, + this.auth + ) + const organization = OrganizationMap.toModel(organizationDto) + + return organization + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/service/project.service.ts b/yaku-apps-typescript/apps/mend-fetcher/src/service/project.service.ts new file mode 100644 index 00000000..71c86763 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/service/project.service.ts @@ -0,0 +1,42 @@ +import { Authenticator } from '../auth/auth.js' +import { + getProjectDTO, + getProjectVitalsDTO, +} from '../fetcher/project.fetcher.js' +import { Project } from '../model/project.js' +import { ProjectMap } from '../mapper/project.mapper.js' +import { ProjectVitals } from '../model/projectVitals.js' +import { ProjectVitalsMap } from '../mapper/projectVitals.mapper.js' +import { MendEnvironment } from '../model/mendEnvironment.js' + +export class ProjectService { + private auth: Authenticator + private env: MendEnvironment + + constructor(env: MendEnvironment) { + this.env = env + this.auth = Authenticator.getInstance(env) + } + + async getProjectByToken(projectToken: string): Promise { + const projectDto = await getProjectDTO( + this.env.apiUrl, + { projectToken }, + this.auth + ) + const project = ProjectMap.toModel(projectDto) + + return project + } + + async getProjectVitals(projectToken: string): Promise { + const projectVitalsDto = await getProjectVitalsDTO( + this.env.apiUrl, + { projectToken }, + this.auth + ) + const projectVitals = ProjectVitalsMap.toModel(projectVitalsDto) + + return projectVitals + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/service/vulnerability.service.ts b/yaku-apps-typescript/apps/mend-fetcher/src/service/vulnerability.service.ts new file mode 100644 index 00000000..5ad70f2d --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/service/vulnerability.service.ts @@ -0,0 +1,48 @@ +import { Authenticator } from '../auth/auth.js' +import { VulnerabilityDTO } from '../dto/vulnerability.dto.js' +import { VulnerabilityFixSummaryDTO } from '../dto/vulnerabilityFixSummary.dto.js' +import { + getLibraryVulnerabilityDTOs, + getVulnerabilityFixesDTOs, +} from '../fetcher/vulnerability.fetcher.js' +import { VulnerabilityMap } from '../mapper/vulnerability.mapper.js' +import { VulnerabilityFixSummaryMap } from '../mapper/vulnerabilityFixSummary.mapper.js' +import { MendEnvironment } from '../model/mendEnvironment.js' + +export class VulnerabilityService { + private auth: Authenticator + private env: MendEnvironment + + constructor(env: MendEnvironment) { + this.env = env + this.auth = Authenticator.getInstance(env) + } + + async getAllVulnerabilitiesById(libraryId: string, projectId: string) { + const vulnerabilityDTOs: VulnerabilityDTO[] = + await getLibraryVulnerabilityDTOs( + this.env.apiUrl, + { projectToken: projectId, libraryToken: libraryId, pageSize: 100 }, + this.auth + ) + const libraryVulnerabilities = vulnerabilityDTOs.map((vulnDTO) => + VulnerabilityMap.toModel(vulnDTO) + ) + + return libraryVulnerabilities + } + + async getAllVulnerabilitiesFixSummaryById(vulnerabilityId: string) { + const vulnerabilityFixSummaryDTOs: VulnerabilityFixSummaryDTO = + await getVulnerabilityFixesDTOs( + this.env.apiUrl, + { vulnerabilityId: vulnerabilityId, pageSize: 100 }, + this.auth + ) + const vulnerabilitiesFixSummary = VulnerabilityFixSummaryMap.toModel( + vulnerabilityFixSummaryDTOs + ) + + return vulnerabilitiesFixSummary + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/src/utils/export.ts b/yaku-apps-typescript/apps/mend-fetcher/src/utils/export.ts new file mode 100644 index 00000000..39f5181c --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/src/utils/export.ts @@ -0,0 +1,11 @@ +import fs from 'fs/promises' +import fs_sync from 'fs' +import path from 'path' + +export async function exportJson(jsonContent: any, outputPath: string) { + const dirName = path.dirname(outputPath) + if (!fs_sync.existsSync(dirName)) { + fs_sync.mkdirSync(dirName, { recursive: true }) + } + await fs.writeFile(outputPath, JSON.stringify(jsonContent)) +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/integration/fixtures/data.ts b/yaku-apps-typescript/apps/mend-fetcher/test/integration/fixtures/data.ts new file mode 100644 index 00000000..8442e9ff --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/integration/fixtures/data.ts @@ -0,0 +1,642 @@ +export const organizationData = { + uuid: 'organization-uuid', + name: 'organization-name', +} + +export const projectData = { + uuid: 'project-uuid', + name: 'project-name', + path: 'product-name', + productName: 'product-name', + productUuid: 'product-uuid', +} + +export const projectVitalsData = { + lastScan: 'projectVitals-lastScan', + lastUserScanned: { + uuid: 'projectVitals-lastUserScannedUserUuid', + name: 'projectVitals-lastUserScannedUserName', + email: 'projectVitals-lastUserScannedUserEmail', + userType: 'projectVitals-lastUserScannedUserType', + }, + requestToken: 'projectVitals-requestToken', + lastSourceFileMatch: 'projectVitals-lastSourceFileMatch', + lastScanComment: 'projectVitals-lastScanComment', + projectCreationDate: 'projectVitals-projectCreationDate', + pluginName: 'projectVitals-pluginName', + pluginVersion: 'projectVitals-pluginVersion', + libraryCount: 2, +} + +export const policyAlertsData = [ + { + uuid: 'policyAlert1-uuid', + name: 'policyAlert1-name', + type: 'policyAlert1', + component: { + uuid: 'policyAlert1-componentUuid', + name: 'policyAlert1-componentName', + description: 'policyAlert1-componentDescription', + libraryType: 'policyAlert1-libraryType', + }, + alertInfo: { + status: 'policyAlert1-alertInfoStatus', + comment: {}, + detectedAt: 'policyAlert1-alertInfoDetectedAt', + modifiedAt: 'policyAlert1-alertInfoModifiedAt', + }, + project: { + uuid: 'policyAlert1-projectUuid', + name: 'policyAlert1-projectName', + path: 'policyAlert1-path', + productUuid: 'policyAlert1-productUuid', + }, + policyName: 'policyAlert1-policyName', + }, + { + uuid: 'policyAlert2-uuid', + name: 'policyAlert2-name', + type: 'policyAlert2', + component: { + uuid: 'policyAlert2-componentUuid', + name: 'policyAlert2-componentName', + description: 'policyAlert2-componentDescription', + libraryType: 'policyAlert2-libraryType', + }, + alertInfo: { + status: 'policyAlert2-alertInfoStatus', + comment: {}, + detectedAt: 'policyAlert2-alertInfoDetectedAt', + modifiedAt: 'policyAlert2-alertInfoModifiedAt', + }, + project: { + uuid: 'policyAlert2-projectUuid', + name: 'policyAlert2-projectName', + path: 'policyAlert2-path', + productUuid: 'policyAlert2-productUuid', + }, + policyName: 'policyAlert2-policyName', + }, +] + +export const multipleLicensesAlertsData = [ + { + uuid: 'multipleLicensesAlert1-uuid', + name: 'multipleLicensesAlert1-name', + type: 'multipleLicensesAlert1', + component: { + uuid: 'multipleLicensesAlert1-componentUuid', + name: 'multipleLicensesAlert1-componentName', + description: 'multipleLicensesAlert1-componentDescription', + libraryType: 'multipleLicensesAlert1-libraryType', + }, + alertInfo: { + status: 'multipleLicensesAlert1-alertInfoStatus', + comment: {}, + detectedAt: 'multipleLicensesAlert1-alertInfoDetectedAt', + modifiedAt: 'multipleLicensesAlert1-alertInfoModifiedAt', + }, + project: { + uuid: 'multipleLicensesAlert1-projectUuid', + name: 'multipleLicensesAlert1-projectName', + path: 'multipleLicensesAlert1-path', + productUuid: 'multipleLicensesAlert1-productUuid', + }, + numberOfLicenses: 1, + licenses: ['multipleLicensesAlert1-licenses'], + }, + { + uuid: 'multipleLicensesAlert2-uuid', + name: 'multipleLicensesAlert2-name', + type: 'multipleLicensesAlert2', + component: { + uuid: 'multipleLicensesAlert2-componentUuid', + name: 'multipleLicensesAlert2-componentName', + description: 'multipleLicensesAlert2-componentDescription', + libraryType: 'multipleLicensesAlert2-libraryType', + }, + alertInfo: { + status: 'multipleLicensesAlert2-alertInfoStatus', + comment: {}, + detectedAt: 'multipleLicensesAlert2-alertInfoDetectedAt', + modifiedAt: 'multipleLicensesAlert2-alertInfoModifiedAt', + }, + project: { + uuid: 'multipleLicensesAlert2-projectUuid', + name: 'multipleLicensesAlert2-projectName', + path: 'multipleLicensesAlert2-path', + productUuid: 'multipleLicensesAlert2-productUuid', + }, + numberOfLicenses: 2, + licenses: ['multipleLicensesAlert2-licenses'], + }, +] + +export const newVersionsAlertsData = [ + { + uuid: 'newVersionsAlert1-uuid', + name: 'newVersionsAlert1-name', + type: 'newVersionsAlert1', + component: { + uuid: 'newVersionsAlert1-componentUuid', + name: 'newVersionsAlert1-componentName', + description: 'newVersionsAlert1-componentDescription', + libraryType: 'newVersionsAlert1-libraryType', + }, + alertInfo: { + status: 'newVersionsAlert1-alertInfoStatus', + comment: {}, + detectedAt: 'newVersionsAlert1-alertInfoDetectedAt', + modifiedAt: 'newVersionsAlert1-alertInfoModifiedAt', + }, + project: { + uuid: 'newVersionsAlert1-projectUuid', + name: 'newVersionsAlert1-projectName', + path: 'newVersionsAlert1-path', + productUuid: 'newVersionsAlert1-productUuid', + }, + availableVersion: 'newVersionsAlert1-availableVersion', + availableVersionType: 'newVersionsAlert1-availableVersionType', + }, + { + uuid: 'newVersionsAlert2-uuid', + name: 'newVersionsAlert2-name', + type: 'newVersionsAlert2', + component: { + uuid: 'newVersionsAlert2-componentUuid', + name: 'newVersionsAlert2-componentName', + description: 'newVersionsAlert2-componentDescription', + libraryType: 'newVersionsAlert2-libraryType', + }, + alertInfo: { + status: 'newVersionsAlert2-alertInfoStatus', + comment: {}, + detectedAt: 'newVersionsAlert2-alertInfoDetectedAt', + modifiedAt: 'newVersionsAlert2-alertInfoModifiedAt', + }, + project: { + uuid: 'newVersionsAlert2-projectUuid', + name: 'newVersionsAlert2-projectName', + path: 'newVersionsAlert2-path', + productUuid: 'newVersionsAlert2-productUuid', + }, + availableVersion: 'newVersionsAlert2-availableVersion', + availableVersionType: 'newVersionsAlert2-availableVersionType', + }, +] + +export const rejectedInUseAlertsData = [ + { + uuid: 'rejectedInUseAlert1-uuid', + name: 'rejectedInUseAlert1-name', + type: 'rejectedInUseAlert1', + component: { + uuid: 'rejectedInUseAlert1-componentUuid', + name: 'rejectedInUseAlert1-componentName', + description: 'rejectedInUseAlert1-componentDescription', + libraryType: 'rejectedInUseAlert1-libraryType', + }, + alertInfo: { + status: 'rejectedInUseAlert1-alertInfoStatus', + comment: {}, + detectedAt: 'rejectedInUseAlert1-alertInfoDetectedAt', + modifiedAt: 'rejectedInUseAlert1-alertInfoModifiedAt', + }, + project: { + uuid: 'rejectedInUseAlert1-projectUuid', + name: 'rejectedInUseAlert1-projectName', + path: 'rejectedInUseAlert1-path', + productUuid: 'rejectedInUseAlert1-productUuid', + }, + description: 'rejectedInUseAlert1-description', + }, + { + uuid: 'rejectedInUseAlert2-uuid', + name: 'rejectedInUseAlert2-name', + type: 'rejectedInUseAlert2', + component: { + uuid: 'rejectedInUseAlert2-componentUuid', + name: 'rejectedInUseAlert2-componentName', + description: 'rejectedInUseAlert2-componentDescription', + libraryType: 'rejectedInUseAlert2-libraryType', + }, + alertInfo: { + status: 'rejectedInUseAlert2-alertInfoStatus', + comment: {}, + detectedAt: 'rejectedInUseAlert2-alertInfoDetectedAt', + modifiedAt: 'rejectedInUseAlert2-alertInfoModifiedAt', + }, + project: { + uuid: 'rejectedInUseAlert2-projectUuid', + name: 'rejectedInUseAlert2-projectName', + path: 'rejectedInUseAlert2-path', + productUuid: 'rejectedInUseAlert2-productUuid', + }, + description: 'rejectedInUseAlert2-description', + }, +] + +export const securityAlertsData = [ + { + uuid: 'securityAlert1-uuid', + name: 'securityAlert1-name', + type: 'securityAlert1-type', + component: { + uuid: 'securityAlert1-componentUuid', + name: 'securityAlert1-componentName', + description: 'securityAlert1-componentDescription', + componentType: 'securityAlert1-componentType', + libraryType: 'securityAlert1-componentLibraryType', + directDependency: false, + references: { + url: 'securityAlert1-componentReferencesUrl', + homePage: 'securityAlert1-componentReferencesHomePage', + genericPackageIndex: + 'securityAlert1-componentReferencesGenericPackageIndex', + }, + groupId: 'securityAlert1-componentGroupId', + artifactId: 'securityAlert1-componentArtifactId', + version: 'securityAlert1-componentVersion', + path: 'securityAlert1-componentPath', + }, + alertInfo: { + status: 'securityAlert1-alertInfoStatus', + comment: {}, + detectedAt: 'securityAlert1-alertInfoDetectedAt', + modifiedAt: 'securityAlert1-alertInfoModifiedAt', + }, + project: { + uuid: 'securityAlert1-projectUuid', + name: 'securityAlert1-projectName', + path: 'securityAlert1-productName', + productUuid: 'securityAlert1-productUuid', + }, + product: { + uuid: 'securityAlert1-productUuid', + name: 'securityAlert1-productName', + }, + vulnerability: { + name: 'securityAlert1-vulnerabilityName', + type: 'securityAlert1-vulnerabilityType', + description: 'securityAlert1-vulnerabilityDescription', + score: 7.5, + severity: 'securityAlert1-vulnerabilitySeverity', + publishDate: 'securityAlert1-vulnerabilityPublishDate', + modifiedDate: 'securityAlert1-vulnerabiltyModifiedDate', + vulnerabilityScoring: [ + { + score: 9.9, + severity: 'securityAlert1-vulnerabilityScoringScore', + type: 'securityAlert1-vulnerabilityScoringType', + }, + ], + }, + topFix: { + id: 123456, + vulnerability: 'securityAlert1-topFixVulnerability', + type: 'securityAlert1-topFixType', + origin: 'securityAlert1-topFixOrigin', + url: 'securityAlert1-topFixUrl', + fixResolution: 'securityAlert1-topFixFixResolution', + date: 'securityAlert1-topFixDate', + message: 'securityAlert1-topFixMessage', + extraData: {}, + }, + effective: 'securityAlert1-effective', + }, + { + uuid: 'securityAlert2-uuid', + name: 'securityAlert2-name', + type: 'securityAlert2-type', + component: { + uuid: 'securityAlert2-componentUuid', + name: 'securityAlert2-componentName', + description: 'securityAlert2-componentDescription', + componentType: 'securityAlert2-componentType', + libraryType: 'securityAlert2-componentLibraryType', + directDependency: false, + references: { + url: 'securityAlert2-componentReferencesUrl', + homePage: 'securityAlert2-componentReferencesHomePage', + genericPackageIndex: + 'securityAlert2-componentReferencesGenericPackageIndex', + }, + groupId: 'securityAlert2-componentGroupId', + artifactId: 'securityAlert2-componentArtifactId', + version: 'securityAlert2-componentVersion', + path: 'securityAlert2-componentPath', + }, + alertInfo: { + status: 'securityAlert2-alertInfoStatus', + comment: {}, + detectedAt: 'securityAlert2-alertInfoDetectedAt', + modifiedAt: 'securityAlert2-alertInfoModifiedAt', + }, + project: { + uuid: 'securityAlert2-projectUuid', + name: 'securityAlert2-projectName', + path: 'securityAlert2-productName', + productUuid: 'securityAlert2-productUuid', + }, + product: { + uuid: 'securityAlert2-productUuid', + name: 'securityAlert2-productName', + }, + vulnerability: { + name: 'securityAlert2-vulnerabilityName', + type: 'securityAlert2-vulnerabilityType', + description: 'securityAlert2-vulnerabilityDescription', + score: 7.5, + severity: 'securityAlert2-vulnerabilitySeverity', + publishDate: 'securityAlert2-vulnerabilityPublishDate', + modifiedDate: 'securityAlert2-vulnerabiltyModifiedDate', + vulnerabilityScoring: [ + { + score: 9.9, + severity: 'securityAlert2-vulnerabilityScoringScore', + type: 'securityAlert2-vulnerabilityScoringType', + }, + ], + }, + topFix: { + id: 123456, + vulnerability: 'securityAlert2-topFixVulnerability', + type: 'securityAlert2-topFixType', + origin: 'securityAlert2-topFixOrigin', + url: 'securityAlert2-topFixUrl', + fixResolution: 'securityAlert2-topFixFixResolution', + date: 'securityAlert2-topFixDate', + message: 'securityAlert2-topFixMessage', + extraData: {}, + }, + effective: 'securityAlert2-effective', + }, +] + +export const librariesData = [ + { + uuid: 'library1-uuid', + name: 'library1', + artifactId: 'library1-artifactId', + groupId: 'library1-groupdId', + version: 'library1-version', + architecture: 'library1-architecture', + languageVersion: 'library1-languageVersion', + classifier: 'library1-classifier', + extension: 'library1-extension', + sha1: 'library1-sha1', + description: 'library1-description', + type: 'library1-type', + directDependency: false, + licenses: [ + { + uuid: 'license1-uuid', + name: 'license1', + assignedByUser: false, + licenseReferences: [ + { + uuid: 'licenseReference1-uuid', + type: 'licenseReference1-type', + liabilityReference: 'licenseReference1-liabilityRef', + information: 'licenseReference1-info', + }, + ], + }, + ], + copyrightReferences: [], + locations: [], + }, + { + uuid: 'library2-uuid', + name: 'library2', + artifactId: 'library2-artifactId', + groupId: 'library2-groupdId', + version: 'library2-version', + architecture: 'library2-architecture', + languageVersion: 'library2-languageVersion', + classifier: 'library2-classifier', + extension: 'library2-extension', + sha1: 'library2-sha1', + description: 'library2-description', + type: 'library2-type', + directDependency: false, + licenses: [ + { + uuid: 'license2-uuid', + name: 'license2', + assignedByUser: false, + licenseReferences: [ + { + uuid: 'licenseReference2-uuid', + type: 'licenseReference2-type', + liabilityReference: 'licenseReference2-liabilityRef', + information: 'licenseReference2-info', + }, + ], + }, + ], + copyrightReferences: [ + { + type: 'library1-copyrightReference1Type', + copyright: 'library1-copyrightReference1Copyright', + startYear: 'library1-copyrightReference1StartYear', + endYear: 'library1-copyrightReference1EndYear', + author: 'library1-copyrightReference1Author', + referenceInfo: 'library1-copyrightReferenceReference1Info', + }, + { + type: 'library1-copyrightReference2Type', + copyright: 'library1-copyrightReference2Copyright', + startYear: 'library1-copyrightReference2StartYear', + endYear: 'library1-copyrightReference2EndYear', + author: 'library1-copyrightReference2Author', + referenceInfo: 'library1-copyrightReference2ReferenceInfo', + }, + ], + locations: [ + { + localPath: 'library2-location1LocalPath', + dependencyFile: 'library2-location1DependencyFile', + }, + { + localPath: 'library2-location2LocalPath', + dependencyFile: 'library2-location2DependencyFile', + }, + ], + }, +] + +export const vulnerabilitiesData = [ + { + name: 'vulnerability1-name', + type: 'vulnerability1-type', + description: 'vulnerability1-description', + score: 9.9, + severity: 'vulnerability1-severity', + publishDate: 'vulnerability1-publishDate', + modifiedDate: 'vulnerability1-modifiedDate', + vulnerabilityScoring: [ + { + score: 9.9, + severity: 'vulnerability1-vulnerabilityScoringSeverity', + type: 'vulnerability1-vulnerabilityScoringType', + extraData: { + confidentialityImpact: + 'vulnerability1-vulnerabilityScoringExtraDataConfidentialityImpact', + attackComplexity: + 'vulnerability1-vulnerabilityScoringExtraDataAttackComplexity', + scope: 'vulnerability1-vulnerabilityScoringExtraDataScope', + availabilityImpact: + 'vulnerability1-vulnerabilityScoringExtraDataAvailabilityImpact', + attackVector: + 'vulnerability1-vulnerabilityScoringExtraDataAttackVector', + integrityImpact: + 'vulnerability1-vulnerabilityScoringExtraDataIntegrityImpact', + privilegesRequired: + 'vulnerability1-vulnerabilityScoringExtraDataPrivilegesRequired', + vectorString: + 'vulnerability1-vulnerabilityScoringExtraDataVectorString', + userInteraction: + 'vulnerability1-vulnerabilityScoringExtraDataUserInteraction', + }, + }, + ], + references: [], + }, + { + name: 'vulnerability2-name', + type: 'vulnerability2-type', + description: 'vulnerability2-description', + score: 9.9, + severity: 'vulnerability2-severity', + publishDate: 'vulnerability2-publishDate', + modifiedDate: 'vulnerability2-modifiedDate', + vulnerabilityScoring: [ + { + score: 9.9, + severity: 'vulnerability2-vulnerabilityScoringSeverity', + type: 'vulnerability2-vulnerabilityScoringType', + extraData: { + confidentialityImpact: + 'vulnerability2-vulnerabilityScoringExtraDataConfidentialityImpact', + attackComplexity: + 'vulnerability2-vulnerabilityScoringExtraDataAttackComplexity', + scope: 'vulnerability2-vulnerabilityScoringExtraDataScope', + availabilityImpact: + 'vulnerability2-vulnerabilityScoringExtraDataAvailabilityImpact', + attackVector: + 'vulnerability2-vulnerabilityScoringExtraDataAttackVector', + integrityImpact: + 'vulnerability2-vulnerabilityScoringExtraDataIntegrityImpact', + privilegesRequired: + 'vulnerability2-vulnerabilityScoringExtraDataPrivilegesRequired', + vectorString: + 'vulnerability2-vulnerabilityScoringExtraDataVectorString', + userInteraction: + 'vulnerability2-vulnerabilityScoringExtraDataUserInteraction', + }, + }, + ], + references: [ + { + value: 'vulnerability2-reference1Value', + source: 'vulnerability2-reference1Source', + url: 'vulnerability2-reference1Url', + signature: false, + advisory: false, + patch: false, + }, + { + value: 'vulnerability2-reference2Value', + source: 'vulnerability2-reference2Source', + url: 'vulnerability2-reference2Url', + signature: false, + advisory: false, + patch: false, + }, + ], + }, +] + +export const vulnerabilitiesFixSummaryData = [ + { + vulnerability: 'vulnerability1-name', + topRankedFix: { + id: 123456, + vulnerability: 'securityAlert1-topFixVulnerability', + type: 'securityAlert1-topFixType', + origin: 'securityAlert1-topFixOrigin', + url: 'securityAlert1-topFixUrl', + fixResolution: 'securityAlert1-topFixFixResolution', + date: 'securityAlert1-topFixDate', + message: 'securityAlert1-topFixMessage', + extraData: {}, + }, + allFixes: [ + { + id: 123456, + vulnerability: 'securityAlert1-topFixVulnerability', + type: 'securityAlert1-topFixType', + origin: 'securityAlert1-topFixOrigin', + url: 'securityAlert1-topFixUrl', + fixResolution: 'securityAlert1-topFixFixResolution', + date: 'securityAlert1-topFixDate', + message: 'securityAlert1-topFixMessage', + extraData: {}, + }, + { + id: 123457, + vulnerability: 'securityAlert2-topFixVulnerability', + type: 'securityAlert2-topFixType', + origin: 'securityAlert2-topFixOrigin', + url: 'securityAlert2-topFixUrl', + fixResolution: 'securityAlert2-topFixFixResolution', + date: 'securityAlert2-topFixDate', + message: 'securityAlert2-topFixMessage', + extraData: {}, + }, + ], + totalUpVotes: 12345, + totalDownVotes: 1234, + }, + { + vulnerability: 'vulnerability2-name', + topRankedFix: { + id: 456789, + vulnerability: 'securityAlert3-topFixVulnerability', + type: 'securityAlert3-topFixType', + origin: 'securityAlert3-topFixOrigin', + url: 'securityAlert3-topFixUrl', + fixResolution: 'securityAlert3-topFixFixResolution', + date: 'securityAlert3-topFixDate', + message: 'securityAlert3-topFixMessage', + extraData: {}, + }, + allFixes: [ + { + id: 456789, + vulnerability: 'securityAlert3-topFixVulnerability', + type: 'securityAlert3-topFixType', + origin: 'securityAlert3-topFixOrigin', + url: 'securityAlert3-topFixUrl', + fixResolution: 'securityAlert3-topFixFixResolution', + date: 'securityAlert3-topFixDate', + message: 'securityAlert3-topFixMessage', + extraData: {}, + }, + { + id: 567890, + vulnerability: 'securityAlert4-topFixVulnerability', + type: 'securityAlert4-topFixType', + origin: 'securityAlert4-topFixOrigin', + url: 'securityAlert4-topFixUrl', + fixResolution: 'securityAlert4-topFixFixResolution', + date: 'securityAlert4-topFixDate', + message: 'securityAlert4-topFixMessage', + extraData: {}, + }, + ], + totalUpVotes: 12345, + totalDownVotes: 1234, + }, +] diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/integration/fixtures/failedStatusFixtures.ts b/yaku-apps-typescript/apps/mend-fetcher/test/integration/fixtures/failedStatusFixtures.ts new file mode 100644 index 00000000..899b3a57 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/integration/fixtures/failedStatusFixtures.ts @@ -0,0 +1,229 @@ +import { MockServerOptions } from '../../../../../integration-tests/src/util' +import { + librariesData, + organizationData, + projectData, +} from '../../unit/fixtures/data' +import { projectVitalsData } from './data' + +export const getFAILEDEmptyFixture = async ( + port: number +): Promise => { + return { + port: port, + https: false, + responses: { + ['/api/v2.0/login']: { + post: { + responseStatus: 500, + }, + }, + }, + } +} + +export const getFAILEDLoginFixture = async ( + port: number +): Promise => { + return { + port: port, + https: false, + responses: { + ['/api/v2.0/login']: { + post: { + responseStatus: 401, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: 'Login failed' }, + }, + }, + }, + } +} + +export const getFAILEDProjectFixture = async ( + port: number, + options: { + org: string + project: string + } +): Promise => { + return { + port: port, + https: false, + responses: { + ['/api/v2.0/login']: { + post: { + responseStatus: 200, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: { jwtToken: 'jwt-token', jwtTTL: 1800000 } }, + }, + }, + [`/api/v2.0/orgs/${options.org}`]: { + get: { + responseStatus: 200, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: organizationData, additionalData: {} }, + }, + }, + [`/api/v2.0/projects/${options.project}`]: { + get: { + responseStatus: 404, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: { errorMessage: 'Entity not found' }, + additionalData: {}, + supportToken: 'boo!', + }, + }, + }, + }, + } +} + +export const getFAILEDRandomApiFailureFixture = async ( + port: number, + successResponseStatus: number, + failedResponseStatus: number, + options: { org: string; project: string } +) => { + const responses = { + ['/api/v2.0/login']: { + post: { + responseStatus: successResponseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: { jwtToken: 'jwt-token', jwtTTL: 1800000 } }, + }, + }, + [`/api/v2.0/orgs/${options.org}`]: { + get: { + responseStatus: successResponseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: organizationData, additionalData: {} }, + }, + }, + [`/api/v2.0/projects/${options.project}`]: { + get: { + responseStatus: successResponseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: projectData }, + }, + }, + [`/api/v2.0/projects/${options.project}/vitals`]: { + get: { + responseStatus: successResponseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: projectVitalsData }, + }, + }, + [`/api/v2.0/projects/${options.project}/libraries`]: { + get: [ + { + responseStatus: successResponseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: librariesData, + additionalData: {}, + supportToken: 'boo!', + }, + }, + { + responseStatus: successResponseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: [], + additionalData: {}, + supportToken: 'boo!', + }, + }, + ], + }, + [`/api/v2.0/projects/${options.project}/libraries/${librariesData[0].uuid}/vulnerabilities`]: + { + get: { + responseStatus: successResponseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: [], + additionalData: {}, + supportToken: 'boo!', + }, + }, + }, + [`/api/v2.0/projects/${options.project}/libraries/${librariesData[1].uuid}/vulnerabilities`]: + { + get: { + responseStatus: successResponseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: [], + additionalData: {}, + supportToken: 'boo!', + }, + }, + }, + } + const randomEndpoint = Object.entries(responses)[getRandomNumber(1, 6)][1] + const endpointResponse = (randomEndpoint['get'] ??= randomEndpoint['post']) + if (Array.isArray(endpointResponse)) { + const responseNumber = + endpointResponse[getRandomNumber(0, endpointResponse.length - 1)] + responseNumber.responseStatus = failedResponseStatus + responseNumber.responseBody = { + retVal: { errorMessage: 'Response Error Message' }, + supportToken: 'boo!', + } + } else { + endpointResponse.responseStatus = failedResponseStatus + endpointResponse.responseBody = { + retVal: { errorMessage: 'Response Error Message' }, + supportToken: 'boo!', + } + } + return { + port: port, + https: false, + responses: responses, + } +} + +const getRandomNumber = (min: number, max: number): number => { + const start = Math.ceil(min) + const end = Math.ceil(max) + return Math.floor(Math.random() * (end - start + 1)) + start +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/integration/fixtures/greenStatusFixtures.ts b/yaku-apps-typescript/apps/mend-fetcher/test/integration/fixtures/greenStatusFixtures.ts new file mode 100644 index 00000000..6c6d7404 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/integration/fixtures/greenStatusFixtures.ts @@ -0,0 +1,165 @@ +import { MockServerOptions } from '../../../../../integration-tests/src/util' +import { + librariesData, + organizationData, + projectData, + projectVitalsData, +} from './data' + +export const getGREENStatusFixture = async ( + port: number, + responseStatus: number, + options: { + org: string + project: string + } +): Promise => { + return { + port: port, + https: false, + responses: { + ['/api/v2.0/login']: { + post: { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: { jwtToken: 'jwt-token', jwtTTL: 1800000 } }, + }, + }, + [`/api/v2.0/orgs/${options.org}`]: { + get: { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: organizationData, additionalData: {} }, + }, + }, + [`/api/v2.0/projects/${options.project}`]: { + get: { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: projectData }, + }, + }, + [`/api/v2.0/projects/${options.project}/vitals`]: { + get: { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: projectVitalsData }, + }, + }, + [`/api/v2.0/projects/${options.project}/alerts/legal`]: { + get: { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: [], additionalData: {} }, + }, + }, + [`/api/v2.0/projects/${options.project}/alerts/security`]: { + get: { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: [], additionalData: {} }, + }, + }, + [`/api/v2.0/projects/${options.project}/libraries`]: { + get: [ + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: librariesData, + additionalData: {}, + supportToken: 'boo!', + }, + }, + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: [], + additionalData: {}, + supportToken: 'boo!', + }, + }, + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: librariesData, + additionalData: {}, + supportToken: 'boo!', + }, + }, + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: [], + additionalData: {}, + supportToken: 'boo!', + }, + }, + ], + }, + [`/api/v2.0/projects/${options.project}/libraries/${librariesData[0].uuid}/vulnerabilities`]: + { + get: { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: [], + additionalData: {}, + supportToken: 'boo!', + }, + }, + }, + [`/api/v2.0/projects/${options.project}/libraries/${librariesData[1].uuid}/vulnerabilities`]: + { + get: { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: [], + additionalData: {}, + supportToken: 'boo!', + }, + }, + }, + }, + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/integration/fixtures/redStatusFixtures.ts b/yaku-apps-typescript/apps/mend-fetcher/test/integration/fixtures/redStatusFixtures.ts new file mode 100644 index 00000000..f5f69643 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/integration/fixtures/redStatusFixtures.ts @@ -0,0 +1,332 @@ +import { MockServerOptions } from '../../../../../integration-tests/src/util' +import { + librariesData, + organizationData, + policyAlertsData, + newVersionsAlertsData, + multipleLicensesAlertsData, + rejectedInUseAlertsData, + projectData, + projectVitalsData, + securityAlertsData, + vulnerabilitiesData, + vulnerabilitiesFixSummaryData, +} from './data' + +export const getREDStatusFixture = async ( + port: number, + responseStatus: number, + options: { + org: string + project: string + vulnerabilityId: string + vulnerabilityId2: string + } +): Promise => { + return { + port: port, + https: false, + responses: { + ['/api/v2.0/login']: { + post: { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: { jwtToken: 'jwt-token', jwtTTL: 1800000 } }, + }, + }, + [`/api/v2.0/orgs/${options.org}`]: { + get: { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: organizationData, additionalData: {} }, + }, + }, + [`/api/v2.0/projects/${options.project}`]: { + get: { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: projectData }, + }, + }, + [`/api/v2.0/projects/${options.project}/vitals`]: { + get: { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: projectVitalsData }, + }, + }, + [`/api/v2.0/projects/${options.project}/alerts/legal`]: { + get: [ + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: policyAlertsData, additionalData: {} }, + }, + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: [], additionalData: {} }, + }, + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: multipleLicensesAlertsData, + additionalData: {}, + }, + }, + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: [], additionalData: {} }, + }, + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: newVersionsAlertsData, additionalData: {} }, + }, + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: [], additionalData: {} }, + }, + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: rejectedInUseAlertsData, + additionalData: {}, + }, + }, + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: [], additionalData: {} }, + }, + ], + }, + [`/api/v2.0/projects/${options.project}/alerts/security`]: { + get: [ + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: securityAlertsData, additionalData: {} }, + }, + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { retVal: [], additionalData: {} }, + }, + ], + }, + [`/api/v2.0/projects/${options.project}/libraries`]: { + get: [ + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: librariesData, + additionalData: {}, + supportToken: 'boo!', + }, + }, + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: [], + additionalData: {}, + supportToken: 'boo!', + }, + }, + ], + }, + [`/api/v2.0/projects/${options.project}/libraries/${librariesData[0].uuid}/vulnerabilities`]: + { + get: [ + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: [vulnerabilitiesData[0], vulnerabilitiesData[1]], + additionalData: {}, + supportToken: 'boo!', + }, + }, + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: [], + additionalData: {}, + supportToken: 'boo!', + }, + }, + ], + }, + [`/api/v2.0/projects/${options.project}/libraries/${librariesData[1].uuid}/vulnerabilities`]: + { + get: [ + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: [vulnerabilitiesData[1], vulnerabilitiesData[0]], + additionalData: {}, + supportToken: 'boo!', + }, + }, + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: [], + additionalData: {}, + supportToken: 'boo!', + }, + }, + ], + }, + [`/api/v2.0/vulnerabilities/${options.vulnerabilityId}/remediation`]: { + get: [ + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: vulnerabilitiesFixSummaryData[0], + additionalData: {}, + supportToken: 'boo!', + }, + }, + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: vulnerabilitiesFixSummaryData[0], + additionalData: {}, + supportToken: 'boo!', + }, + }, + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: [], + additionalData: {}, + supportToken: 'boo!', + }, + }, + ], + }, + [`/api/v2.0/vulnerabilities/${options.vulnerabilityId2}/remediation`]: { + get: [ + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: vulnerabilitiesFixSummaryData[1], + additionalData: {}, + supportToken: 'boo!', + }, + }, + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: vulnerabilitiesFixSummaryData[1], + additionalData: {}, + supportToken: 'boo!', + }, + }, + { + responseStatus: responseStatus, + responseHeaders: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + responseBody: { + retVal: [], + additionalData: {}, + supportToken: 'boo!', + }, + }, + ], + }, + }, + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/integration/mend-failed-status.int-spec.ts b/yaku-apps-typescript/apps/mend-fetcher/test/integration/mend-failed-status.int-spec.ts new file mode 100644 index 00000000..81e58ac4 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/integration/mend-failed-status.int-spec.ts @@ -0,0 +1,213 @@ +import { afterEach, beforeAll, describe, expect, it } from 'vitest' +import { + MockServer, + MockServerOptions, + RunProcessResult, + run, +} from '../../../../integration-tests/src/util' +import { + defaultEnvironment, + mendFetcherExecutable, + MOCK_SERVER_PORT, +} from './utils' +import { + getFAILEDEmptyFixture, + getFAILEDLoginFixture, + getFAILEDProjectFixture, + getFAILEDRandomApiFailureFixture, +} from './fixtures/failedStatusFixtures' +import * as fs_sync from 'fs' + +describe('FAILED status scenarios', () => { + let mockServer: MockServer + + beforeAll(() => { + expect(fs_sync.existsSync(mendFetcherExecutable)).to.be.equal(true) + }) + + afterEach(async () => { + await mockServer?.stop() + }) + + it('should set status to FAILED when environment variables are missing', async () => { + const env = {} + const options: MockServerOptions = await getFAILEDEmptyFixture( + MOCK_SERVER_PORT + ) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + mendFetcherExecutable, + undefined, + { env: env } + ) + + expect(result.exitCode).to.be.equal(0) + expect(result.stdout).to.not.have.length(0) + expect(result.stdout).to.include( + JSON.stringify({ + status: 'FAILED', + reason: + 'Environment validation failed: MEND_API_URL Required, MEND_SERVER_URL Required, MEND_ORG_TOKEN Required, MEND_PROJECT_TOKEN Required, MEND_USER_EMAIL Required, MEND_USER_KEY Required', + }) + ) + expect(result.stderr).to.not.have.length(0) + }) + + it.each([ + { name: 'MEND_API_URL', value: 'foo.bar', errorMessage: 'Invalid url' }, + { name: 'MEND_SERVER_URL', value: 'bar.foo', errorMessage: 'Invalid url' }, + { + name: 'MEND_ORG_TOKEN', + value: '', + errorMessage: 'String must contain at least 1 character(s)', + }, + { + name: 'MEND_PROJECT_ID', + value: 'confused', + errorMessage: 'Must be a number or numbers splitted by a comma.', + }, + { + name: 'MEND_PROJECT_TOKEN', + value: '', + errorMessage: 'String must contain at least 1 character(s)', + }, + { name: 'MEND_USER_EMAIL', value: 'foo', errorMessage: 'Invalid email' }, + { + name: 'MEND_USER_KEY', + value: '', + errorMessage: 'String must contain at least 1 character(s)', + }, + { + name: 'MEND_REPORT_TYPE', + value: 'invalid report type', + errorMessage: + `Invalid enum value. ` + + `Expected 'alerts' | 'vulnerabilities', received 'invalid report type'`, + }, + { + name: 'MEND_ALERTS_STATUS', + value: 'nonexisting alerts status', + errorMessage: + `Invalid enum value. Expected 'all' | 'active' | 'ignored' | 'library_removed' | 'library_in_house' | 'library_whitelist', ` + + `received 'nonexisting alerts status'`, + }, + { + name: 'MEND_MIN_CONNECTION_TIME', + value: 'infinity', + errorMessage: 'Expected number, received not a number', + }, + { + name: 'MEND_MAX_CONCURRENT_CONNECTIONS', + value: 'boatloads', + errorMessage: 'Expected number, received not a number', + }, + ])( + 'should set status to FAILED when environment variable $name fails validation', + async (envVariable) => { + const env = { ...defaultEnvironment } + env[`${envVariable.name}`] = envVariable.value + const options: MockServerOptions = await getFAILEDEmptyFixture( + MOCK_SERVER_PORT + ) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + mendFetcherExecutable, + undefined, + { env: env } + ) + + expect(result.exitCode).to.be.equal(0) + expect(result.stdout).to.not.have.length(0) + expect(result.stdout).to.include( + JSON.stringify({ + status: 'FAILED', + reason: + `Environment validation failed:` + + ` ${envVariable.name}` + + ` ${envVariable.errorMessage}`, + }) + ) + expect(result.stderr).to.not.have.length(0) + } + ) + + it('should set status to FAILED when login fails', async () => { + const env = { ...defaultEnvironment } + const options: MockServerOptions = await getFAILEDLoginFixture( + MOCK_SERVER_PORT + ) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + mendFetcherExecutable, + undefined, + { env: env } + ) + + expect(result.exitCode).to.be.equal(0) + expect(result.stdout).to.not.have.length(0) + expect(result.stdout).to.include( + JSON.stringify({ + status: 'FAILED', + reason: 'Response status code 401: Login failed', + }) + ) + expect(result.stderr).to.not.have.length(0) + }) + + it('should set status to FAILED when project does not exists', async () => { + const env = { ...defaultEnvironment } + const options: MockServerOptions = await getFAILEDProjectFixture( + MOCK_SERVER_PORT, + { org: env.MEND_ORG_TOKEN, project: env.MEND_PROJECT_TOKEN } + ) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + mendFetcherExecutable, + undefined, + { env: env } + ) + + expect(result.exitCode).to.be.equal(0) + expect(result.stdout).to.not.have.length(0) + expect(result.stdout).to.include( + JSON.stringify({ + status: 'FAILED', + reason: 'Response status code 404: Entity not found', + }) + ) + expect(result.stderr).to.not.have.length(0) + }) + + it('should set status to FAILED when Mend API endpoint fails', async () => { + const env = { ...defaultEnvironment } + const successResponseStatus = 200 + const failedResponseStatus = 500 + const options: MockServerOptions = await getFAILEDRandomApiFailureFixture( + MOCK_SERVER_PORT, + successResponseStatus, + failedResponseStatus, + { org: env.MEND_ORG_TOKEN, project: env.MEND_PROJECT_TOKEN } + ) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + mendFetcherExecutable, + undefined, + { env: env } + ) + + expect(result.exitCode).to.be.equal(0) + expect(result.stdout).to.not.have.length(0) + expect(result.stdout).to.include( + JSON.stringify({ + status: 'FAILED', + reason: `Response status code ${failedResponseStatus}: Response Error Message`, + }) + ) + expect(result.stderr).to.not.have.length(0) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/integration/mend-green-status.int-spec.ts b/yaku-apps-typescript/apps/mend-fetcher/test/integration/mend-green-status.int-spec.ts new file mode 100644 index 00000000..7773598f --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/integration/mend-green-status.int-spec.ts @@ -0,0 +1,111 @@ +import { afterEach, beforeAll, describe, expect, it } from 'vitest' +import { + MockServer, + MockServerOptions, + RunProcessResult, + run, +} from '../../../../integration-tests/src/util' +import { + defaultEnvironment, + mendFetcherExecutable, + MOCK_SERVER_PORT, +} from './utils' +import { getGREENStatusFixture } from './fixtures/greenStatusFixtures' +import { + organizationData, + projectData, + projectVitalsData, +} from './fixtures/data' +import * as fs_sync from 'fs' + +describe.each([ + { name: 'MEND_PROJECT_ID', value: '123321,124421' }, + { name: 'MEND_PROJECT_ID', value: undefined }, +])('GREEN status scenarios', (envVariable) => { + let mockServer: MockServer + + beforeAll(() => { + expect(fs_sync.existsSync(mendFetcherExecutable)).to.be.equal(true) + }) + + afterEach(async () => { + await mockServer?.stop() + }) + + it.each([ + { + report: 'alerts', + noOfRequests: 17, + reqInfo: + 'login + org + project + project vitals + policy alerts + new versions alerts + multiple licenses alerts + rejected in use alerts + security alerts', + }, + { + report: 'vulnerabilities', + noOfRequests: 15, + reqInfo: + 'login + org + project + project vitals + libraries(1 + 1) + vulns of lib 1 + vulns of lib 2', + }, + ])( + 'should set status to GREEN when there are no $report found', + async (testOptions) => { + const env = { + ...defaultEnvironment, + MEND_REPORT_TYPE: testOptions.report, + } + + env.MEND_PROJECT_TOKEN = 'project-uuid,project-uuid' + if (envVariable.value) { + env[`${envVariable.name}`] = envVariable.value + } + + const reasonLinkTemplate = (id: number) => { + if (envVariable.value) { + return ( + `${env.MEND_SERVER_URL}/Wss/WSS.html#!project;` + + `orgToken=${organizationData.uuid};` + + `id=` + + envVariable.value.split(',')[id] + ) + } + return ( + `${env.MEND_SERVER_URL}/Wss/WSS.html` + + ` in organization ${organizationData.name}` + + ` and project ${projectData.name}` + ) + } + const options: MockServerOptions = await getGREENStatusFixture( + MOCK_SERVER_PORT, + 200, + { org: env.MEND_ORG_TOKEN, project: 'project-uuid' } + ) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + mendFetcherExecutable, + undefined, + { env: env } + ) + + expect(result.exitCode).to.be.equal(0) + expect(result.stdout).to.not.have.length(0) + expect(result.stdout).to.include( + JSON.stringify({ + status: 'GREEN', + reason: + `No ${env.MEND_REPORT_TYPE} were found, last scan was executed on ${projectVitalsData.lastScan}` + + ', see more details in Mend ' + + reasonLinkTemplate(0) + + '; ' + + `No ${env.MEND_REPORT_TYPE} were found, last scan was executed on ${projectVitalsData.lastScan}` + + ', see more details in Mend ' + + reasonLinkTemplate(1) + + `;`, + }) + ) + expect(result.stderr).to.have.length(0) + + const noOfRequests = mockServer.getNumberOfRequests() + expect(noOfRequests).to.equal(testOptions.noOfRequests) + } + ) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/integration/mend-red-status.int-spec.ts b/yaku-apps-typescript/apps/mend-fetcher/test/integration/mend-red-status.int-spec.ts new file mode 100644 index 00000000..5c361b59 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/integration/mend-red-status.int-spec.ts @@ -0,0 +1,102 @@ +import { afterEach, beforeAll, describe, expect, it } from 'vitest' +import { + MockServer, + MockServerOptions, + RunProcessResult, + run, +} from '../../../../integration-tests/src/util' +import { + defaultEnvironment, + mendFetcherExecutable, + MOCK_SERVER_PORT, +} from './utils' +import { + organizationData, + projectData, + projectVitalsData, +} from './fixtures/data' +import { getREDStatusFixture } from './fixtures/redStatusFixtures' +import * as fs_sync from 'fs' + +describe.each([ + { name: 'MEND_PROJECT_ID', value: '123921' }, + { name: 'MEND_PROJECT_ID', value: undefined }, +])('RED status scenarios', (envVariable) => { + let mockServer: MockServer + let reasonLinkTemplate = ';' + + beforeAll(() => { + expect(fs_sync.existsSync(mendFetcherExecutable)).to.be.equal(true) + }) + + afterEach(async () => { + await mockServer?.stop() + }) + + it.each([ + { + report: 'alerts', + noOfRequests: 14, + noOfResults: 10, + }, + { + report: 'vulnerabilities', + noOfRequests: 14, + noOfResults: 4, + }, + ])('should set status to RED when $report are found', async (testOptions) => { + const env = { + ...defaultEnvironment, + MEND_REPORT_TYPE: testOptions.report, + VULNERABILITY_ID: 'vulnerability1-name', + VULNERABILITY_ID2: 'vulnerability2-name', + } + if (envVariable.value) { + env[`${envVariable.name}`] = envVariable.value + reasonLinkTemplate = + `${env.MEND_SERVER_URL}/Wss/WSS.html#!project;` + + `orgToken=${organizationData.uuid};` + + `id=` + + env[`${envVariable.name}`] + } else { + reasonLinkTemplate = + `${env.MEND_SERVER_URL}/Wss/WSS.html` + + ` in organization ${organizationData.name}` + + ` and project ${projectData.name}` + } + const options: MockServerOptions = await getREDStatusFixture( + MOCK_SERVER_PORT, + 200, + { + org: env.MEND_ORG_TOKEN, + project: env.MEND_PROJECT_TOKEN, + vulnerabilityId: env.VULNERABILITY_ID, + vulnerabilityId2: env.VULNERABILITY_ID2, + } + ) + mockServer = new MockServer(options) + + const result: RunProcessResult = await run( + mendFetcherExecutable, + undefined, + { env: env } + ) + + expect(result.exitCode).to.be.equal(0) + expect(result.stdout).to.not.have.length(0) + expect(result.stdout).to.include( + JSON.stringify({ + status: 'RED', + reason: + `${testOptions.noOfResults} ${testOptions.report} were found, last scan was executed on ${projectVitalsData.lastScan},` + + ' see more details in Mend ' + + reasonLinkTemplate + + `;`, + }) + ) + expect(result.stderr).to.have.length(0) + + const noOfRequests = mockServer.getNumberOfRequests() + expect(noOfRequests).to.equal(testOptions.noOfRequests) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/integration/utils.ts b/yaku-apps-typescript/apps/mend-fetcher/test/integration/utils.ts new file mode 100644 index 00000000..ddc28251 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/integration/utils.ts @@ -0,0 +1,10 @@ +export const mendFetcherExecutable = `${__dirname}/../../dist/index.js` +export const MOCK_SERVER_PORT = 8080 +export const defaultEnvironment = { + MEND_API_URL: 'http://localhost:8080', + MEND_SERVER_URL: 'http://localhost:8080', + MEND_ORG_TOKEN: 'org-token', + MEND_PROJECT_TOKEN: 'project-uuid', + MEND_USER_EMAIL: 'user@domain.gTLD', + MEND_USER_KEY: 'user-key', +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/auth/auth.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/auth/auth.test.ts new file mode 100644 index 00000000..7d4124a7 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/auth/auth.test.ts @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { Authenticator } from '../../../src/auth/auth' +import { Login } from '../../../src/model/login' +import { MendEnvironment } from '../../../src/model/mendEnvironment' +import * as authFetcher from '../../../src/fetcher/auth.fetcher' +import { envFixture } from '../fixtures/env' +import { organizationData } from '../fixtures/data' + +describe('auth', () => { + const env: MendEnvironment = envFixture + + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.clearAllMocks() + vi.restoreAllMocks() + vi.useRealTimers() + }) + + it('should only create one instance of Authenticator', () => { + const expected: Authenticator = Authenticator.getInstance(env) + + const result = Authenticator.getInstance(env) + + expect(result).toBeInstanceOf(Authenticator) + expect(result).to.be.equal(expected) + }) + + it('should return a valid Login object', async () => { + const fakeSystemDate = new Date(2023, 1, 1, 0, 0, 0, 0) + vi.setSystemTime(fakeSystemDate) + const spy = vi.spyOn(authFetcher, 'auth') + spy.mockReturnValue( + Promise.resolve( + new Login( + `${env.email.split('@')[0]}-userUuid`, + `${env.email.split('@')[0]}-userName`, + env.email, + 'jwtToken', + 'jwtRefresh', + 1800000, + organizationData.name, + organizationData.uuid, + fakeSystemDate.valueOf() + ) + ) + ) + + const auth: Authenticator = Authenticator.getInstance(env) + const expected = new Login( + `${env.email.split('@')[0]}-userUuid`, + `${env.email.split('@')[0]}-userName`, + env.email, + 'jwtToken', + 'jwtRefresh', + 1800000, + organizationData.name, + organizationData.uuid, + fakeSystemDate.valueOf() + ) + + const result: Login = await auth.authenticate() + + expect(result).toStrictEqual(expected) + }) + + it('should return the same Login object when authentication has not expired', async () => { + const fakeSystemDate = new Date(2023, 1, 1, 0, 20, 0, 0) + vi.setSystemTime(fakeSystemDate) + const spy = vi.spyOn(authFetcher, 'auth') + spy.mockReturnValue( + Promise.resolve( + new Login( + `${env.email.split('@')[0]}-userUuid`, + `${env.email.split('@')[0]}-userName`, + env.email, + 'jwtToken', + 'jwtRefresh', + 1800000, + organizationData.name, + organizationData.uuid, + new Date(2023, 1, 1, 0, 0, 0, 0).valueOf() + ) + ) + ) + + const auth: Authenticator = Authenticator.getInstance(env) + const expected = new Login( + `${env.email.split('@')[0]}-userUuid`, + `${env.email.split('@')[0]}-userName`, + env.email, + 'jwtToken', + 'jwtRefresh', + 1800000, + organizationData.name, + organizationData.uuid, + new Date(2023, 1, 1, 0, 0, 0, 0).valueOf() + ) + + const result: Login = await auth.authenticate() + + expect(result).toStrictEqual(expected) + }) + + it('should return a new Login object when authentication has expired', async () => { + const fakeSystemDate = new Date(2023, 1, 1, 0, 35, 0, 0) + vi.setSystemTime(fakeSystemDate) + const spy = vi.spyOn(authFetcher, 'auth') + spy.mockReturnValue( + Promise.resolve( + new Login( + `${env.email.split('@')[0]}-userUuid`, + `${env.email.split('@')[0]}-userName`, + env.email, + 'jwtToken2', + 'jwtRefresh2', + 1800000, + organizationData.name, + organizationData.uuid, + fakeSystemDate.valueOf() + ) + ) + ) + + const auth: Authenticator = Authenticator.getInstance(env) + const expected = new Login( + `${env.email.split('@')[0]}-userUuid`, + `${env.email.split('@')[0]}-userName`, + env.email, + 'jwtToken2', + 'jwtRefresh2', + 1800000, + organizationData.name, + organizationData.uuid, + fakeSystemDate.valueOf() + ) + + const result: Login = await auth.authenticate() + + expect(result).toStrictEqual(expected) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/fetcher/alert.fetcher.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fetcher/alert.fetcher.test.ts new file mode 100644 index 00000000..1b57ee5e --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fetcher/alert.fetcher.test.ts @@ -0,0 +1,924 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { afterEach, describe, expect, it, vi } from 'vitest' +import { Authenticator } from '../../../src/auth/auth' +import { axiosInstance } from '../../../src/fetcher/common.fetcher' +import { + getPolicyAlertDTOs, + getNewVersionsAlertDTOs, + getMultipleLicensesAlertDTOs, + getRejectedInUseAlertDTOs, + getSecurityAlertDTOs, +} from '../../../src/fetcher/alert.fetcher' +import { MendEnvironment } from '../../../src/model/mendEnvironment' +import { PolicyAlertDTO } from '../../../src/dto/policyAlert.dto' +import { NewVersionsAlertDTO } from '../../../src/dto/newVersionsAlert.dto' +import { MultipleLicensesAlertDTO } from '../../../src/dto/multipleLicensesAlert.dto' +import { RejectedInUseAlertDTO } from '../../../src/dto/rejectedInUseAlert.dto' +import { SecurityAlertDTO } from '../../../src/dto/securityAlert.dto' +import { envFixture } from '../fixtures/env' +import { FakeAuthenticator } from '../fixtures/fakeauth' +import { HTTPResponseStatusCodes } from '../fixtures/httpResponseStatus' +import { + policyAlertsData, + securityAlertsData, + newVersionsAlertsData, + multipleLicensesAlertsData, + rejectedInUseAlertsData, +} from '../fixtures/data' +import { + policyAlertsDTO, + securityAlertsDTO, + newVersionsAlertsDTO, + multipleLicensesAlertsDTO, + rejectedInUseAlertsDTO, +} from '../fixtures/dto' + +describe('alert.fetcher', () => { + const env: MendEnvironment = envFixture + + afterEach(() => { + vi.clearAllMocks() + vi.restoreAllMocks() + }) + + describe('policy.alerts', () => { + it("should retrieve 'active' policy alerts when there is a single page response", async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: policyAlertsData, + }, + }) + ) + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [], + }, + }) + ) + const expected: PolicyAlertDTO[] = policyAlertsDTO + + const result: PolicyAlertDTO[] = await getPolicyAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'active' }, + fakeAuth as unknown as Authenticator + ) + + expect(result).toStrictEqual(expected) + }) + + it("should retrieve 'all' policy alerts when there is a multipage response", async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [policyAlertsData[0]], + }, + }) + ) + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [policyAlertsData[1]], + }, + }) + ) + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [], + }, + }) + ) + const expected: PolicyAlertDTO[] = policyAlertsDTO + + const result: PolicyAlertDTO[] = await getPolicyAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'all' }, + fakeAuth as unknown as Authenticator + ) + + expect(result).toStrictEqual(expected) + }) + + it('should throw an error when unexpected data is returned', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce(Promise.resolve({ data: { not: 'expected' } })) + + const result = getPolicyAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError('No expected values returned') + }) + + it('should throw an error when request is not successful', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + request: {}, + message: 'Request Error Message', + }) + ) + + const result = getPolicyAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError('Request Error Message') + }) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when response falls out of the 2xx range', + async (httpStatus) => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + response: { + status: httpStatus.code, + data: {}, + statusText: httpStatus.message, + }, + }) + ) + + const result = getPolicyAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when response falls out of the 2xx range and supportToken is provided', + async (httpStatus) => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + response: { + data: { + retVal: { errorMessage: httpStatus.message }, + supportToken: 'boo!', + }, + statusText: '', + status: httpStatus.code, + }, + }) + ) + + const result = getPolicyAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it('should throw an error when it fails for unexpected reasons', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce(Promise.reject()) + + const result = getPolicyAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError() + }) + }) + + describe('newVersions.alerts', () => { + it("should retrieve 'active' New Versions alerts when there is a single page response", async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: newVersionsAlertsData, + }, + }) + ) + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [], + }, + }) + ) + const expected: NewVersionsAlertDTO[] = newVersionsAlertsDTO + + const result: NewVersionsAlertDTO[] = await getNewVersionsAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'active' }, + fakeAuth as unknown as Authenticator + ) + + expect(result).toStrictEqual(expected) + }) + + it("should retrieve 'all' New Versions alerts when there is a multipage response", async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [newVersionsAlertsData[0]], + }, + }) + ) + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [newVersionsAlertsData[1]], + }, + }) + ) + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [], + }, + }) + ) + const expected: NewVersionsAlertDTO[] = newVersionsAlertsDTO + + const result: NewVersionsAlertDTO[] = await getNewVersionsAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'all' }, + fakeAuth as unknown as Authenticator + ) + + expect(result).toStrictEqual(expected) + }) + + it('should throw an error when unexpected data is returned', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce(Promise.resolve({ data: { not: 'expected' } })) + + const result = getNewVersionsAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError('No expected values returned') + }) + + it('should throw an error when request is not successful', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + request: {}, + message: 'Request error message', + }) + ) + + const result = getNewVersionsAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError('Request error message') + }) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when response falls out of the 2xx range', + async (httpStatus) => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + response: { + status: httpStatus.code, + data: {}, + statusText: httpStatus.message, + }, + }) + ) + + const result = getNewVersionsAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when response falls out of the 2xx range and supportToken is provided', + async (httpStatus) => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + response: { + data: { + retVal: { errorMessage: httpStatus.message }, + supportToken: 'boo!', + }, + statusText: '', + status: httpStatus.code, + }, + }) + ) + + const result = getNewVersionsAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it('should throw an error when it fails for unexpected reasons', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce(Promise.reject()) + + const result = getNewVersionsAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError() + }) + }) + + describe('multipleLicenses.alerts', () => { + it("should retrieve 'active' Multiple Licenses alerts when there is a single page response", async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: multipleLicensesAlertsData, + }, + }) + ) + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [], + }, + }) + ) + const expected: MultipleLicensesAlertDTO[] = multipleLicensesAlertsDTO + + const result: MultipleLicensesAlertDTO[] = + await getMultipleLicensesAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'active' }, + fakeAuth as unknown as Authenticator + ) + + expect(result).toStrictEqual(expected) + }) + + it("should retrieve 'all' Multiple Licenses alerts when there is a multipage response", async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [multipleLicensesAlertsData[0]], + }, + }) + ) + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [multipleLicensesAlertsData[1]], + }, + }) + ) + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [], + }, + }) + ) + const expected: MultipleLicensesAlertDTO[] = multipleLicensesAlertsDTO + + const result: MultipleLicensesAlertDTO[] = + await getMultipleLicensesAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'all' }, + fakeAuth as unknown as Authenticator + ) + + expect(result).toStrictEqual(expected) + }) + + it('should throw an error when unexpected data is returned', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce(Promise.resolve({ data: { not: 'expected' } })) + + const result = getMultipleLicensesAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError('No expected values returned') + }) + + it('should throw an error when request is not successful', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + request: {}, + message: 'Request error message', + }) + ) + + const result = getMultipleLicensesAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError('Request error message') + }) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when response falls out of the 2xx range', + async (httpStatus) => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + response: { + status: httpStatus.code, + data: {}, + statusText: httpStatus.message, + }, + }) + ) + + const result = getMultipleLicensesAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when response falls out of the 2xx range and supportToken is provided', + async (httpStatus) => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + response: { + data: { + retVal: { errorMessage: httpStatus.message }, + supportToken: 'boo!', + }, + statusText: '', + status: httpStatus.code, + }, + }) + ) + + const result = getMultipleLicensesAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it('should throw an error when it fails for unexpected reasons', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce(Promise.reject()) + + const result = getMultipleLicensesAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError() + }) + }) + + describe('rejectedInUse.alerts', () => { + it("should retrieve 'active' Rejected In Use alerts when there is a single page response", async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: rejectedInUseAlertsData, + }, + }) + ) + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [], + }, + }) + ) + const expected: RejectedInUseAlertDTO[] = rejectedInUseAlertsDTO + + const result: RejectedInUseAlertDTO[] = await getRejectedInUseAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'active' }, + fakeAuth as unknown as Authenticator + ) + + expect(result).toStrictEqual(expected) + }) + + it("should retrieve 'all' Rejected In Use alerts when there is a multipage response", async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [rejectedInUseAlertsData[0]], + }, + }) + ) + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [rejectedInUseAlertsData[1]], + }, + }) + ) + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [], + }, + }) + ) + const expected: RejectedInUseAlertDTO[] = rejectedInUseAlertsDTO + + const result: RejectedInUseAlertDTO[] = await getRejectedInUseAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'all' }, + fakeAuth as unknown as Authenticator + ) + + expect(result).toStrictEqual(expected) + }) + + it('should throw an error when unexpected data is returned', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce(Promise.resolve({ data: { not: 'expected' } })) + + const result = getRejectedInUseAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError('No expected values returned') + }) + + it('should throw an error when request is not successful', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + request: {}, + message: 'Request error message', + }) + ) + + const result = getRejectedInUseAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError('Request error message') + }) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when response falls out of the 2xx range', + async (httpStatus) => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + response: { + status: httpStatus.code, + data: {}, + statusText: httpStatus.message, + }, + }) + ) + + const result = getRejectedInUseAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when response falls out of the 2xx range and supportToken is provided', + async (httpStatus) => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + response: { + data: { + retVal: { errorMessage: httpStatus.message }, + supportToken: 'boo!', + }, + statusText: '', + status: httpStatus.code, + }, + }) + ) + + const result = getRejectedInUseAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it('should throw an error when it fails for unexpected reasons', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce(Promise.reject()) + + const result = getRejectedInUseAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError() + }) + }) + + describe('security.alerts', () => { + it("should retrieve 'active' security alerts when there is a single page response", async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: securityAlertsData, + }, + }) + ) + spy.mockReturnValueOnce( + Promise.resolve({ data: { additionalData: {}, retVal: [] } }) + ) + const expected: SecurityAlertDTO[] = securityAlertsDTO + + const result: SecurityAlertDTO[] = await getSecurityAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'active' }, + fakeAuth as unknown as Authenticator + ) + + expect(result).toStrictEqual(expected) + }) + + it("should retrieve 'all' security alerts when there is a multipage response", async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [securityAlertsData[0]], + }, + }) + ) + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [securityAlertsData[1]], + }, + }) + ) + spy.mockReturnValueOnce( + Promise.resolve({ data: { additionalData: {}, retVal: [] } }) + ) + const expected: SecurityAlertDTO[] = securityAlertsDTO + + const result: SecurityAlertDTO[] = await getSecurityAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'all' }, + fakeAuth as unknown as Authenticator + ) + + expect(result).toStrictEqual(expected) + }) + + it('should throw an error when unexpected data is returned', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce(Promise.resolve({ data: { not: 'expected' } })) + + const result = getSecurityAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError('No expected values returned') + }) + + it('should throw an error when request is not successful', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + request: {}, + message: 'Request error message', + }) + ) + + const result = getSecurityAlertDTOs( + env.apiUrl, + + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError('Request error message') + }) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when response falls out of the 2xx range', + async (httpStatus) => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + response: { + status: httpStatus.code, + data: {}, + statusText: httpStatus.message, + }, + }) + ) + + const result = getSecurityAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when response falls out of the 2xx range and supportToken is provided', + async (httpStatus) => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + response: { + data: { + retVal: { errorMessage: httpStatus.message }, + supportToken: 'boo!', + }, + statusText: '', + status: httpStatus.code, + }, + }) + ) + + const result = getSecurityAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it('should throw an error when it fails for unexpected reasons', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce(Promise.reject()) + + const result = getSecurityAlertDTOs( + env.apiUrl, + { projectToken: env.projectToken, status: 'status' }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError() + }) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/fetcher/auth.fetcher.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fetcher/auth.fetcher.test.ts new file mode 100644 index 00000000..0370d0da --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fetcher/auth.fetcher.test.ts @@ -0,0 +1,132 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { afterEach, describe, expect, it, vi } from 'vitest' +import { auth } from '../../../src/fetcher/auth.fetcher' +import { axiosInstance } from '../../../src/fetcher/common.fetcher' +import { Login } from '../../../src/model/login' +import { MendEnvironment } from '../../../src/model/mendEnvironment' +import { envFixture } from '../fixtures/env' +import { HTTPResponseStatusCodes } from '../fixtures/httpResponseStatus' +import { organizationData } from '../fixtures/data' +import { AxiosError } from 'axios' + +describe('auth.fetcher', () => { + const env: MendEnvironment = envFixture + + afterEach(() => { + vi.clearAllMocks() + vi.restoreAllMocks() + }) + + it('should return a Login object when the request is successful', async () => { + const now = new Date().valueOf() + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValue( + Promise.resolve({ + data: { + retVal: { + userUuid: `${env.email.split('@')[0]}-userUuid`, + userName: `${env.email.split('@')[0]}-userName`, + email: env.email, + refreshToken: `jwtRefresh`, + jwtToken: `jwtToken`, + orgName: organizationData.name, + orgUuid: organizationData.uuid, + domainName: ``, + domainUuid: ``, + accountName: ``, + accountUuid: ``, + jwtTTL: 1800000, + sessionStartTime: now, + }, + }, + }) + ) + const expected = new Login( + `${env.email.split('@')[0]}-userUuid`, + `${env.email.split('@')[0]}-userName`, + env.email, + 'jwtToken', + 'jwtRefresh', + 1800000, + organizationData.name, + organizationData.uuid, + now + ) + + const result: Login = await auth( + env.apiUrl, + env.email, + env.orgToken, + env.userKey + ) + + expect(result).toStrictEqual(expected) + }) + + it('should throw an error when unexpected values are returned', async () => { + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValue(Promise.resolve({ data: {} })) + + const result = auth(env.apiUrl, env.email, env.orgToken, env.userKey) + await expect(result).rejects.toThrowError('No expected values returned') + }) + + it('should throw an error when request is not successful', async () => { + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValue( + Promise.reject({ + request: 'Request data', + message: 'Request error', + isAxiosError: true, + } as AxiosError) + ) + + const result = auth(env.apiUrl, env.email, env.orgToken, env.userKey) + await expect(result).rejects.toThrowError('Request error') + }) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when reponse falls out of the 2xx range', + async (httpStatus) => { + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValue( + Promise.reject({ + response: { + status: httpStatus.code, + data: { error: httpStatus.message }, + }, + isAxiosError: true, + } as AxiosError) + ) + + const result = auth(env.apiUrl, env.email, env.orgToken, env.userKey) + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when reponse falls out of the 2xx range and supportToken is provided', + async (httpStatus) => { + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValue( + Promise.reject({ + response: { + status: httpStatus.code, + data: { retVal: httpStatus.message, supportToken: 'boo!' }, + }, + isAxiosError: true, + } as AxiosError) + ) + + const result = auth(env.apiUrl, env.email, env.orgToken, env.userKey) + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/fetcher/errors.fetcher.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fetcher/errors.fetcher.test.ts new file mode 100644 index 00000000..cefc6094 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fetcher/errors.fetcher.test.ts @@ -0,0 +1,165 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { afterEach, describe, expect, it, vi } from 'vitest' +import { handleAxiosError } from '../../../src/fetcher/errors.fetcher' +import { AxiosError } from 'axios' + +describe('errors.fetcher', () => { + afterEach(() => { + vi.clearAllMocks() + vi.restoreAllMocks() + }) + + describe('handleAxiosError', () => { + it('should throw a ResponseError when AxiosError includes response information', async () => { + const responseError = { + isAxiosError: true, + response: { + data: { retVal: 123 }, + status: 599, + message: 'Custome Error Message', + }, + } + + const result = new Promise((resolve) => { + resolve(handleAxiosError(responseError as unknown as AxiosError)) + }) + await expect(result).rejects.toThrowError( + `Response status code ${responseError.response.status}: ${responseError.response?.message}` + ) + }) + + it('should throw a RequestError when AxiosError provides only request information', async () => { + const responseError = { + isAxiosError: true, + request: {}, + message: 'Custom Error Message', + } as AxiosError + + const result = new Promise((resolve) => { + resolve(handleAxiosError(responseError)) + }) + + await expect(result).rejects.toThrowError( + `RequestError: ${responseError.message}` + ) + }) + + it('should throw an error when AxiosError has unexpected format', async () => { + const unexpectedError = { + isAxiosError: true, + message: 'Custom Error Message', + } as AxiosError + + const result = new Promise((resolve) => { + resolve(handleAxiosError(unexpectedError)) + }) + + await expect(result).rejects.toThrowError( + `Error ${unexpectedError.message}` + ) + }) + }) + + describe('processResponseError', () => { + it('should throw a ResponseError when AxiosError response provides no data', async () => { + const responseError = { + isAxiosError: true, + response: { + status: 599, + statusText: '', + message: 'Custom Error Message', + }, + message: 'Custom Error Message', + status: 599, + } as unknown as AxiosError + + const result = new Promise((resolve) => { + resolve(handleAxiosError(responseError)) + }) + + await expect(result).rejects.toThrowError( + `Response status code ${responseError.status}: ${responseError.message}` + ) + }) + + it('should throw a ResponseError when AxiosError response data provides no retVal but a statusText', async () => { + const responseError = { + isAxiosError: true, + response: { status: 599, data: {}, statusText: 'Custom Error Message' }, + message: 'Custom Error Message', + status: 599, + } as AxiosError + + const result = new Promise((resolve) => { + resolve(handleAxiosError(responseError)) + }) + + await expect(result).rejects.toThrowError( + `Response status code ${responseError.response?.status}: ${responseError.response?.statusText}` + ) + }) + + it('should throw a ResponseError when AxiosError response data provides no retVal and no statusText', async () => { + const responseError = { + isAxiosError: true, + response: { + status: 599, + data: { error: 'Custom Error Message' }, + statusText: '', + }, + } + + const result = new Promise((resolve) => { + resolve(handleAxiosError(responseError as unknown as AxiosError)) + }) + + await expect(result).rejects.toThrowError( + `Response status code ${responseError.response.status}: ${responseError.response.data.error}` + ) + }) + + it('should throw a ResponseError when AxiosError response data retVal is a simple message', async () => { + const responseError = { + isAxiosError: true, + response: { + data: { retVal: 'Customer Error Message', supportToken: 'boo!' }, + status: 599, + statusText: '', + }, + } + + const result = new Promise((resolve) => { + resolve(handleAxiosError(responseError as unknown as AxiosError)) + }) + + await expect(result).rejects.toThrowError( + `Response status code ${responseError.response.status}: ${responseError.response.data.retVal}` + ) + }) + + it('should throw a ResponseError when AxiosError response data retVal is an object containing the error message', async () => { + const responseError = { + isAxiosError: true, + response: { + data: { + retVal: { errorMessage: 'Customer Error Message' }, + supportToken: 'boo!', + }, + statusText: '', + status: 599, + }, + } + + const result = new Promise((resolve) => { + resolve(handleAxiosError(responseError as AxiosError)) + }) + + await expect(result).rejects.toThrowError( + `Response status code ${responseError.response.status}: ${responseError.response.data.retVal.errorMessage}` + ) + }) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/fetcher/library.fetcher.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fetcher/library.fetcher.test.ts new file mode 100644 index 00000000..6f700b87 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fetcher/library.fetcher.test.ts @@ -0,0 +1,199 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { afterEach, describe, expect, it, vi } from 'vitest' +import { Authenticator } from '../../../src/auth/auth' +import { axiosInstance } from '../../../src/fetcher/common.fetcher' +import { getLibraryDTOs } from '../../../src/fetcher/library.fetcher' +import { LibraryDTO } from '../../../src/dto/library.dto' +import { MendEnvironment } from '../../../src/model/mendEnvironment' +import { envFixture } from '../fixtures/env' +import { HTTPResponseStatusCodes } from '../fixtures/httpResponseStatus' +import { FakeAuthenticator } from '../fixtures/fakeauth' +import { librariesData } from '../fixtures/data' +import { librariesDTO } from '../fixtures/dto' + +describe('library.fetcher', () => { + const env: MendEnvironment = envFixture + + afterEach(() => { + vi.clearAllMocks() + vi.restoreAllMocks() + }) + + it('should retrieve libraries when there is a single page response', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: librariesData, + }, + }) + ) + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [], + }, + }) + ) + const expected: LibraryDTO[] = librariesDTO + + const result: LibraryDTO[] = await getLibraryDTOs( + env.apiUrl, + { projectToken: env.projectToken }, + fakeAuth as unknown as Authenticator + ) + + expect(result).toStrictEqual(expected) + }) + + it('should retrieve libraries when there is a multipage response', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [librariesData[0]], + }, + }) + ) + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [librariesData[1]], + }, + }) + ) + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [], + }, + }) + ) + const expected: LibraryDTO[] = librariesDTO + + const result: LibraryDTO[] = await getLibraryDTOs( + env.apiUrl, + { projectToken: env.projectToken }, + fakeAuth as unknown as Authenticator + ) + + expect(result).toStrictEqual(expected) + }) + + it('should throw an error when unexpected data is returned', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce(Promise.resolve({ data: { not: 'expected' } })) + + const result = getLibraryDTOs( + env.apiUrl, + { projectToken: env.projectToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError('No expected values are returned') + }) + + it('should throw an error when request is not successful', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + request: {}, + message: 'Request Error Message', + }) + ) + + const result = getLibraryDTOs( + env.apiUrl, + { projectToken: env.projectToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError('Request Error Message') + }) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when response falls out of the 2xx range', + async (httpStatus) => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + response: { + status: httpStatus.code, + data: {}, + statusText: httpStatus.message, + }, + }) + ) + + const result = getLibraryDTOs( + env.apiUrl, + { projectToken: env.projectToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when response falls out of the 2xx range and supportToken is provided', + async (httpStatus) => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + response: { + data: { + retVal: { errorMessage: httpStatus.message }, + supportToken: 'boo!', + }, + statusText: '', + status: httpStatus.code, + }, + }) + ) + + const result = getLibraryDTOs( + env.apiUrl, + { projectToken: env.projectToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it('should throw an error when it fails for unexpected reasons', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce(Promise.reject()) + + const result = getLibraryDTOs( + env.apiUrl, + { projectToken: env.projectToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError() + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/fetcher/organization.fetcher.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fetcher/organization.fetcher.test.ts new file mode 100644 index 00000000..f4a6287d --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fetcher/organization.fetcher.test.ts @@ -0,0 +1,153 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { afterEach, describe, expect, it, vi } from 'vitest' +import { Authenticator } from '../../../src/auth/auth' +import { axiosInstance } from '../../../src/fetcher/common.fetcher' +import { getOrganizationDTO } from '../../../src/fetcher/organization.fetcher' +import { MendEnvironment } from '../../../src/model/mendEnvironment' +import { OrganizationDTO } from '../../../src/dto/organization.dto' +import { envFixture } from '../fixtures/env' +import { HTTPResponseStatusCodes } from '../fixtures/httpResponseStatus' +import { FakeAuthenticator } from '../fixtures/fakeauth' +import { organizationDTO } from '../fixtures/dto' +import { organizationData } from '../fixtures/data' + +describe('organization.fetcher', () => { + const env: MendEnvironment = envFixture + + afterEach(() => { + vi.clearAllMocks() + vi.restoreAllMocks() + }) + + it('should retrieve organization information', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: organizationData, + }, + }) + ) + const expected: OrganizationDTO = organizationDTO + + const result: OrganizationDTO = await getOrganizationDTO( + env.apiUrl, + { orgToken: env.orgToken }, + fakeAuth as unknown as Authenticator + ) + + expect(result).toStrictEqual(expected) + }) + + it('should throw an error when unexpected data is returned', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce(Promise.resolve({ data: { not: 'expected' } })) + + const result = getOrganizationDTO( + env.apiUrl, + { orgToken: env.orgToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError('No expected values are returned') + }) + + it('should throw an error when request is not successful', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + request: {}, + message: 'Request Error Message', + }) + ) + + const result = getOrganizationDTO( + env.apiUrl, + { orgToken: env.orgToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError('Request Error Message') + }) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when response falls out of the 2xx range', + async (httpStatus) => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + response: { + status: httpStatus.code, + data: {}, + statusText: httpStatus.message, + }, + }) + ) + + const result = getOrganizationDTO( + env.apiUrl, + { orgToken: env.orgToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when response falls out of the 2xx range and supportToken is provided', + async (httpStatus) => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + response: { + data: { + retVal: { errorMessage: httpStatus.message }, + supportToken: 'boo!', + }, + statusText: '', + status: httpStatus.code, + }, + }) + ) + + const result = getOrganizationDTO( + env.apiUrl, + { orgToken: env.orgToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it('should throw an error when it fails for unexpected reasons', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce(Promise.reject()) + + const result = getOrganizationDTO( + env.apiUrl, + { orgToken: env.orgToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError() + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/fetcher/project.fetcher.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fetcher/project.fetcher.test.ts new file mode 100644 index 00000000..76c90382 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fetcher/project.fetcher.test.ts @@ -0,0 +1,295 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { afterEach, describe, expect, it, vi } from 'vitest' +import { Authenticator } from '../../../src/auth/auth' +import { axiosInstance } from '../../../src/fetcher/common.fetcher' +import { + getProjectDTO, + getProjectVitalsDTO, +} from '../../../src/fetcher/project.fetcher' +import { MendEnvironment } from '../../../src/model/mendEnvironment' +import { ProjectDTO } from '../../../src/dto/project.dto' +import { ProjectVitalsDTO } from '../../../src/dto/projectVitals.dto' +import { envFixture } from '../fixtures/env' +import { HTTPResponseStatusCodes } from '../fixtures/httpResponseStatus' +import { FakeAuthenticator } from '../fixtures/fakeauth' +import { projectData, projectVitalsData } from '../fixtures/data' +import { projectDTO, projectVitalsDTO } from '../fixtures/dto' + +describe('project.fetcher', () => { + const env: MendEnvironment = envFixture + + afterEach(() => { + vi.clearAllMocks() + vi.restoreAllMocks() + }) + + describe('project', () => { + it('should retrieve project information', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: projectData, + }, + }) + ) + const expected: ProjectDTO = projectDTO + + const result: ProjectDTO = await getProjectDTO( + env.apiUrl, + { projectToken: env.projectToken }, + fakeAuth as unknown as Authenticator + ) + + expect(result).toStrictEqual(expected) + }) + + it('should throw an error when unexpected data is returned', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce(Promise.resolve({ data: { not: 'expected' } })) + + const result = getProjectDTO( + env.apiUrl, + { projectToken: env.projectToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + 'No expected values are returned' + ) + }) + + it('should throw an error when request is not successful', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + request: {}, + message: 'Request Error Message', + }) + ) + + const result = getProjectDTO( + env.apiUrl, + { projectToken: env.projectToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError('Request Error Message') + }) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when response falls out of the 2xx range', + async (httpStatus) => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + response: { + status: httpStatus.code, + data: {}, + statusText: httpStatus.message, + }, + }) + ) + + const result = getProjectDTO( + env.apiUrl, + { projectToken: env.projectToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when response falls out of the 2xx range and supportToken is provided', + async (httpStatus) => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + response: { + data: { + retVal: { errorMessage: httpStatus.message }, + supportToken: 'boo!', + }, + statusText: '', + status: httpStatus.code, + }, + }) + ) + + const result = getProjectDTO( + env.apiUrl, + { projectToken: env.projectToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it('should throw an error when it fails for unexpected reasons', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce(Promise.reject()) + + const result = getProjectDTO( + env.apiUrl, + { projectToken: env.projectToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError() + }) + }) + + describe('projectVitals', () => { + it('should retrieve project vitals information', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: projectVitalsData, + supportToken: 'supportToken', + }, + }) + ) + const expected: ProjectVitalsDTO = projectVitalsDTO + + const result: ProjectVitalsDTO = await getProjectVitalsDTO( + env.apiUrl, + { projectToken: env.projectToken }, + fakeAuth as unknown as Authenticator + ) + + expect(result).toStrictEqual(expected) + }) + + it('should throw an error when unexpected data is returned', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce(Promise.resolve({ data: { not: 'expected' } })) + + const result = getProjectVitalsDTO( + env.apiUrl, + { projectToken: env.projectToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + 'No expected values are returned' + ) + }) + + it('should throw an error when request is not successful', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + request: {}, + message: 'Request Error Message', + }) + ) + + const result = getProjectVitalsDTO( + env.apiUrl, + { projectToken: env.projectToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError('Request Error Message') + }) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when response falls out of the 2xx range', + async (httpStatus) => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + response: { + status: httpStatus.code, + data: {}, + statusText: httpStatus.message, + }, + }) + ) + + const result = getProjectVitalsDTO( + env.apiUrl, + { projectToken: env.projectToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when response falls out of the 2xx range and supportToken is provided', + async (httpStatus) => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + response: { + data: { + retVal: { errorMessage: httpStatus.message }, + supportToken: 'boo!', + }, + statusText: '', + status: httpStatus.code, + }, + }) + ) + + const result = getProjectVitalsDTO( + env.apiUrl, + { projectToken: env.projectToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it('should throw an error when it fails for unexpected reasons', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce(Promise.reject()) + + const result = getProjectVitalsDTO( + env.apiUrl, + { projectToken: env.projectToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError() + }) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/fetcher/vulnerability.fetcher.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fetcher/vulnerability.fetcher.test.ts new file mode 100644 index 00000000..792ad3fa --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fetcher/vulnerability.fetcher.test.ts @@ -0,0 +1,369 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { afterEach, describe, expect, it, vi } from 'vitest' +import { Authenticator } from '../../../src/auth/auth' +import { axiosInstance } from '../../../src/fetcher/common.fetcher' +import { + getLibraryVulnerabilityDTOs, + getVulnerabilityFixesDTOs, +} from '../../../src/fetcher/vulnerability.fetcher' +import { MendEnvironment } from '../../../src/model/mendEnvironment' +import { VulnerabilityDTO } from '../../../src/dto/vulnerability.dto' +import { VulnerabilityFixSummaryDTO } from '../../../src/dto/vulnerabilityFixSummary.dto' +import { envFixture } from '../fixtures/env' +import { HTTPResponseStatusCodes } from '../fixtures/httpResponseStatus' +import { FakeAuthenticator } from '../fixtures/fakeauth' +import { + vulnerabilitiesData, + vulnerabilitiesFixSummaryData, +} from '../fixtures/data' +import { + vulnerabilitiesDTO, + vulnerabilitiesFixSummaryDTO, +} from '../fixtures/dto' + +describe('vulnerability.fetcher', () => { + const env: MendEnvironment = envFixture + + afterEach(() => { + vi.clearAllMocks() + vi.restoreAllMocks() + }) + + it('should retrieve vulnerabilities when there is a single page response', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: vulnerabilitiesData, + }, + }) + ) + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [], + }, + }) + ) + const libraryToken = 'library1-Uuid' + const expected: VulnerabilityDTO[] = vulnerabilitiesDTO + + const result: VulnerabilityDTO[] = await getLibraryVulnerabilityDTOs( + env.apiUrl, + { projectToken: env.projectToken, libraryToken }, + fakeAuth as unknown as Authenticator + ) + + expect(result).toStrictEqual(expected) + }) + + it('should retrieve vulnerabilities when there is a multipage response', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [vulnerabilitiesData[0]], + }, + }) + ) + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [vulnerabilitiesData[1]], + }, + }) + ) + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [], + }, + }) + ) + const libraryToken = 'library1-Uuid' + const expected: VulnerabilityDTO[] = vulnerabilitiesDTO + + const result: VulnerabilityDTO[] = await getLibraryVulnerabilityDTOs( + env.apiUrl, + { projectToken: env.projectToken, libraryToken }, + fakeAuth as unknown as Authenticator + ) + + expect(result).toStrictEqual(expected) + }) + + it('should throw an error when unexpected data is returned', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce(Promise.resolve({ data: { not: 'expected' } })) + const libraryToken = 'library1-Uuid' + + const result = getLibraryVulnerabilityDTOs( + env.apiUrl, + { projectToken: env.projectToken, libraryToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError('No expected values are returned') + }) + + it('should throw an error when request is not successful', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + request: {}, + message: 'Request Error Message', + }) + ) + const libraryToken = 'library1-Uuid' + + const result = getLibraryVulnerabilityDTOs( + env.apiUrl, + { projectToken: env.projectToken, libraryToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError('Request Error Message') + }) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when response falls out of the 2xx range', + async (httpStatus) => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + response: { + status: httpStatus.code, + data: {}, + statusText: httpStatus.message, + }, + }) + ) + const libraryToken = 'library1-Uuid' + + const result = getLibraryVulnerabilityDTOs( + env.apiUrl, + { projectToken: env.projectToken, libraryToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when response falls out of the 2xx range and supportToken is provided', + async (httpStatus) => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + response: { + data: { + retVal: { errorMessage: httpStatus.message }, + supportToken: 'boo!', + }, + statusText: '', + status: httpStatus.code, + }, + }) + ) + const libraryToken = 'library1-Uuid' + + const result = getLibraryVulnerabilityDTOs( + env.apiUrl, + { projectToken: env.projectToken, libraryToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it('should throw an error when it fails for unexpected reasons', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce(Promise.reject()) + const libraryToken = 'library1-Uuid' + + const result = getLibraryVulnerabilityDTOs( + env.apiUrl, + { projectToken: env.projectToken, libraryToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError() + }) +}) + +describe('vulnerabilityFix.fetcher', () => { + const env: MendEnvironment = envFixture + + afterEach(() => { + vi.clearAllMocks() + vi.restoreAllMocks() + }) + + it('should retrieve vulnerabilities fix', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: vulnerabilitiesFixSummaryData, + }, + }) + ) + spy.mockReturnValueOnce( + Promise.resolve({ + data: { + additionalData: {}, + retVal: [], + }, + }) + ) + const vulnerabilityId = 'vulnerability1-name' + const expected: VulnerabilityFixSummaryDTO = vulnerabilitiesFixSummaryDTO + + const result: VulnerabilityFixSummaryDTO = await getVulnerabilityFixesDTOs( + env.apiUrl, + { vulnerabilityId: vulnerabilityId }, + fakeAuth as unknown as Authenticator + ) + + expect(result).toStrictEqual(expected) + }) + + it('should throw an error when unexpected data is returned', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce(Promise.resolve({ data: { not: 'expected' } })) + const libraryToken = 'library1-Uuid' + + const result = getLibraryVulnerabilityDTOs( + env.apiUrl, + { projectToken: env.projectToken, libraryToken }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError('No expected values are returned') + + spy.mockRestore() + }) + + it('should throw an error when request is not successful', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + request: 'Request data', + message: 'Request error message', + }) + ) + const vulnerabilityId = 'vulnerability1-name' + + const result = getVulnerabilityFixesDTOs( + env.apiUrl, + { vulnerabilityId: vulnerabilityId }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError('Request error message') + }) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when response falls out of the 2xx range', + async (httpStatus) => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + response: { + status: httpStatus.code, + data: {}, + statusText: httpStatus.message, + }, + }) + ) + const vulnerabilityId = 'vulnerability1-name' + + const result = getVulnerabilityFixesDTOs( + env.apiUrl, + { vulnerabilityId: vulnerabilityId }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it.each(HTTPResponseStatusCodes)( + 'should throw an error when response falls out of the 2xx range and supportToken is provided', + async (httpStatus) => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce( + Promise.reject({ + isAxiosError: true, + response: { + status: httpStatus.code, + statusText: '', + data: { + retVal: { errorMessage: httpStatus.message }, + supportToken: 'boo!', + }, + }, + }) + ) + const vulnerabilityId = 'vulnerability1-name' + + const result = getVulnerabilityFixesDTOs( + env.apiUrl, + { vulnerabilityId: vulnerabilityId }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError( + `Response status code ${httpStatus.code}: ${httpStatus.message}` + ) + } + ) + + it('should throw an error when it fails for unexpected reasons', async () => { + const fakeAuth: FakeAuthenticator = new FakeAuthenticator(env) + const spy = vi.spyOn(axiosInstance, 'request') + spy.mockReturnValueOnce(Promise.reject()) + const vulnerabilityId = 'vulnerability1-name' + + const result = getVulnerabilityFixesDTOs( + env.apiUrl, + { vulnerabilityId: vulnerabilityId }, + fakeAuth as unknown as Authenticator + ) + + await expect(result).rejects.toThrowError() + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/fixtures/data.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fixtures/data.ts new file mode 100644 index 00000000..1a48f47d --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fixtures/data.ts @@ -0,0 +1,600 @@ +export const organizationData = { + uuid: 'organization-uuid', + name: 'organization-name', +} + +export const projectData = { + uuid: 'project-uuid', + name: 'project-name', + path: 'product-name', + productName: 'product-name', + productUuid: 'product-uuid', +} + +export const projectVitalsData = { + lastScan: 'projectVitals-lastScan', + lastUserScanned: { + uuid: 'projectVitals-lastUserScannedUserUuid', + name: 'projectVitals-lastUserScannedUserName', + email: 'projectVitals-lastUserScannedUserEmail', + userType: 'projectVitals-lastUserScannedUserType', + }, + requestToken: 'projectVitals-requestToken', + lastSourceFileMatch: 'projectVitals-lastSourceFileMatch', + lastScanComment: 'projectVitals-lastScanComment', + projectCreationDate: 'projectVitals-projectCreationDate', + pluginName: 'projectVitals-pluginName', + pluginVersion: 'projectVitals-pluginVersion', + libraryCount: 2, +} + +export const policyAlertsData = [ + { + uuid: 'policyAlert1-uuid', + name: 'policyAlert1-name', + type: 'policyAlert1', + component: { + uuid: 'policyAlert1-componentUuid', + name: 'policyAlert1-componentName', + description: 'policyAlert1-componentDescription', + libraryType: 'policyAlert1-libraryType', + }, + alertInfo: { + status: 'policyAlert1-alertInfoStatus', + comment: {}, + detectedAt: 'policyAlert1-alertInfoDetectedAt', + modifiedAt: 'policyAlert1-alertInfoModifiedAt', + }, + project: { + uuid: 'policyAlert1-projectUuid', + name: 'policyAlert1-projectName', + path: 'policyAlert1-path', + productUuid: 'policyAlert1-productUuid', + }, + policyName: 'policyAlert1-policyName', + }, + { + uuid: 'policyAlert2-uuid', + name: 'policyAlert2-name', + type: 'policyAlert2', + component: { + uuid: 'policyAlert2-componentUuid', + name: 'policyAlert2-componentName', + description: 'policyAlert2-componentDescription', + libraryType: 'policyAlert2-libraryType', + }, + alertInfo: { + status: 'policyAlert2-alertInfoStatus', + comment: {}, + detectedAt: 'policyAlert2-alertInfoDetectedAt', + modifiedAt: 'policyAlert2-alertInfoModifiedAt', + }, + project: { + uuid: 'policyAlert2-projectUuid', + name: 'policyAlert2-projectName', + path: 'policyAlert2-path', + productUuid: 'policyAlert2-productUuid', + }, + policyName: 'policyAlert2-policyName', + }, +] + +export const multipleLicensesAlertsData = [ + { + uuid: 'multipleLicensesAlert1-uuid', + name: 'multipleLicensesAlert1-name', + type: 'multipleLicensesAlert1', + component: { + uuid: 'multipleLicensesAlert1-componentUuid', + name: 'multipleLicensesAlert1-componentName', + description: 'multipleLicensesAlert1-componentDescription', + libraryType: 'multipleLicensesAlert1-libraryType', + }, + alertInfo: { + status: 'multipleLicensesAlert1-alertInfoStatus', + comment: {}, + detectedAt: 'multipleLicensesAlert1-alertInfoDetectedAt', + modifiedAt: 'multipleLicensesAlert1-alertInfoModifiedAt', + }, + project: { + uuid: 'multipleLicensesAlert1-projectUuid', + name: 'multipleLicensesAlert1-projectName', + path: 'multipleLicensesAlert1-path', + productUuid: 'multipleLicensesAlert1-productUuid', + }, + numberOfLicenses: 1, + licenses: ['multipleLicensesAlert1-licenses'], + }, + { + uuid: 'multipleLicensesAlert2-uuid', + name: 'multipleLicensesAlert2-name', + type: 'multipleLicensesAlert2', + component: { + uuid: 'multipleLicensesAlert2-componentUuid', + name: 'multipleLicensesAlert2-componentName', + description: 'multipleLicensesAlert2-componentDescription', + libraryType: 'multipleLicensesAlert2-libraryType', + }, + alertInfo: { + status: 'multipleLicensesAlert2-alertInfoStatus', + comment: {}, + detectedAt: 'multipleLicensesAlert2-alertInfoDetectedAt', + modifiedAt: 'multipleLicensesAlert2-alertInfoModifiedAt', + }, + project: { + uuid: 'multipleLicensesAlert2-projectUuid', + name: 'multipleLicensesAlert2-projectName', + path: 'multipleLicensesAlert2-path', + productUuid: 'multipleLicensesAlert2-productUuid', + }, + numberOfLicenses: 2, + licenses: ['multipleLicensesAlert2-licenses'], + }, +] + +export const newVersionsAlertsData = [ + { + uuid: 'newVersionsAlert1-uuid', + name: 'newVersionsAlert1-name', + type: 'newVersionsAlert1', + component: { + uuid: 'newVersionsAlert1-componentUuid', + name: 'newVersionsAlert1-componentName', + description: 'newVersionsAlert1-componentDescription', + libraryType: 'newVersionsAlert1-libraryType', + }, + alertInfo: { + status: 'newVersionsAlert1-alertInfoStatus', + comment: {}, + detectedAt: 'newVersionsAlert1-alertInfoDetectedAt', + modifiedAt: 'newVersionsAlert1-alertInfoModifiedAt', + }, + project: { + uuid: 'newVersionsAlert1-projectUuid', + name: 'newVersionsAlert1-projectName', + path: 'newVersionsAlert1-path', + productUuid: 'newVersionsAlert1-productUuid', + }, + availableVersion: 'newVersionsAlert1-availableVersion', + availableVersionType: 'newVersionsAlert1-availableVersionType', + }, + { + uuid: 'newVersionsAlert2-uuid', + name: 'newVersionsAlert2-name', + type: 'newVersionsAlert2', + component: { + uuid: 'newVersionsAlert2-componentUuid', + name: 'newVersionsAlert2-componentName', + description: 'newVersionsAlert2-componentDescription', + libraryType: 'newVersionsAlert2-libraryType', + }, + alertInfo: { + status: 'newVersionsAlert2-alertInfoStatus', + comment: {}, + detectedAt: 'newVersionsAlert2-alertInfoDetectedAt', + modifiedAt: 'newVersionsAlert2-alertInfoModifiedAt', + }, + project: { + uuid: 'newVersionsAlert2-projectUuid', + name: 'newVersionsAlert2-projectName', + path: 'newVersionsAlert2-path', + productUuid: 'newVersionsAlert2-productUuid', + }, + availableVersion: 'newVersionsAlert2-availableVersion', + availableVersionType: 'newVersionsAlert2-availableVersionType', + }, +] + +export const rejectedInUseAlertsData = [ + { + uuid: 'rejectedInUseAlert1-uuid', + name: 'rejectedInUseAlert1-name', + type: 'rejectedInUseAlert1', + component: { + uuid: 'rejectedInUseAlert1-componentUuid', + name: 'rejectedInUseAlert1-componentName', + description: 'rejectedInUseAlert1-componentDescription', + libraryType: 'rejectedInUseAlert1-libraryType', + }, + alertInfo: { + status: 'rejectedInUseAlert1-alertInfoStatus', + comment: {}, + detectedAt: 'rejectedInUseAlert1-alertInfoDetectedAt', + modifiedAt: 'rejectedInUseAlert1-alertInfoModifiedAt', + }, + project: { + uuid: 'rejectedInUseAlert1-projectUuid', + name: 'rejectedInUseAlert1-projectName', + path: 'rejectedInUseAlert1-path', + productUuid: 'rejectedInUseAlert1-productUuid', + }, + description: 'rejectedInUseAlert1-description', + }, + { + uuid: 'rejectedInUseAlert2-uuid', + name: 'rejectedInUseAlert2-name', + type: 'rejectedInUseAlert2', + component: { + uuid: 'rejectedInUseAlert2-componentUuid', + name: 'rejectedInUseAlert2-componentName', + description: 'rejectedInUseAlert2-componentDescription', + libraryType: 'rejectedInUseAlert2-libraryType', + }, + alertInfo: { + status: 'rejectedInUseAlert2-alertInfoStatus', + comment: {}, + detectedAt: 'rejectedInUseAlert2-alertInfoDetectedAt', + modifiedAt: 'rejectedInUseAlert2-alertInfoModifiedAt', + }, + project: { + uuid: 'rejectedInUseAlert2-projectUuid', + name: 'rejectedInUseAlert2-projectName', + path: 'rejectedInUseAlert2-path', + productUuid: 'rejectedInUseAlert2-productUuid', + }, + description: 'rejectedInUseAlert2-description', + }, +] + +export const securityAlertsData = [ + { + uuid: 'securityAlert1-uuid', + name: 'securityAlert1-name', + type: 'securityAlert1-type', + component: { + uuid: 'securityAlert1-componentUuid', + name: 'securityAlert1-componentName', + description: 'securityAlert1-componentDescription', + componentType: 'securityAlert1-componentType', + libraryType: 'securityAlert1-componentLibraryType', + directDependency: false, + references: { + url: 'securityAlert1-componentReferencesUrl', + homePage: 'securityAlert1-componentReferencesHomePage', + genericPackageIndex: + 'securityAlert1-componentReferencesGenericPackageIndex', + }, + groupId: 'securityAlert1-componentGroupId', + artifactId: 'securityAlert1-componentArtifactId', + version: 'securityAlert1-componentVersion', + path: 'securityAlert1-componentPath', + }, + alertInfo: { + status: 'securityAlert1-alertInfoStatus', + comment: {}, + detectedAt: 'securityAlert1-alertInfoDetectedAt', + modifiedAt: 'securityAlert1-alertInfoModifiedAt', + }, + project: { + uuid: 'securityAlert1-projectUuid', + name: 'securityAlert1-projectName', + path: 'securityAlert1-productName', + productUuid: 'securityAlert1-productUuid', + }, + product: { + uuid: 'securityAlert1-productUuid', + name: 'securityAlert1-productName', + }, + vulnerability: { + name: 'securityAlert1-vulnerabilityName', + type: 'securityAlert1-vulnerabilityType', + description: 'securityAlert1-vulnerabilityDescription', + score: 7.5, + severity: 'securityAlert1-vulnerabilitySeverity', + publishDate: 'securityAlert1-vulnerabilityPublishDate', + modifiedDate: 'securityAlert1-vulnerabiltyModifiedDate', + vulnerabilityScoring: [ + { + score: 9.9, + severity: 'securityAlert1-vulnerabilityScoringScore', + type: 'securityAlert1-vulnerabilityScoringType', + }, + ], + }, + topFix: { + id: 123456, + vulnerability: 'securityAlert1-topFixVulnerability', + type: 'securityAlert1-topFixType', + origin: 'securityAlert1-topFixOrigin', + url: 'securityAlert1-topFixUrl', + fixResolution: 'securityAlert1-topFixFixResolution', + date: 'securityAlert1-topFixDate', + message: 'securityAlert1-topFixMessage', + extraData: {}, + }, + effective: 'securityAlert1-effective', + }, + { + uuid: 'securityAlert2-uuid', + name: 'securityAlert2-name', + type: 'securityAlert2-type', + component: { + uuid: 'securityAlert2-componentUuid', + name: 'securityAlert2-componentName', + description: 'securityAlert2-componentDescription', + componentType: 'securityAlert2-componentType', + libraryType: 'securityAlert2-componentLibraryType', + directDependency: false, + references: { + url: 'securityAlert2-componentReferencesUrl', + homePage: 'securityAlert2-componentReferencesHomePage', + genericPackageIndex: + 'securityAlert2-componentReferencesGenericPackageIndex', + }, + groupId: 'securityAlert2-componentGroupId', + artifactId: 'securityAlert2-componentArtifactId', + version: 'securityAlert2-componentVersion', + path: 'securityAlert2-componentPath', + }, + alertInfo: { + status: 'securityAlert2-alertInfoStatus', + comment: {}, + detectedAt: 'securityAlert2-alertInfoDetectedAt', + modifiedAt: 'securityAlert2-alertInfoModifiedAt', + }, + project: { + uuid: 'securityAlert2-projectUuid', + name: 'securityAlert2-projectName', + path: 'securityAlert2-productName', + productUuid: 'securityAlert2-productUuid', + }, + product: { + uuid: 'securityAlert2-productUuid', + name: 'securityAlert2-productName', + }, + vulnerability: { + name: 'securityAlert2-vulnerabilityName', + type: 'securityAlert2-vulnerabilityType', + description: 'securityAlert2-vulnerabilityDescription', + score: 7.5, + severity: 'securityAlert2-vulnerabilitySeverity', + publishDate: 'securityAlert2-vulnerabilityPublishDate', + modifiedDate: 'securityAlert2-vulnerabiltyModifiedDate', + vulnerabilityScoring: [ + { + score: 9.9, + severity: 'securityAlert2-vulnerabilityScoringScore', + type: 'securityAlert2-vulnerabilityScoringType', + }, + ], + }, + topFix: { + id: 123456, + vulnerability: 'securityAlert2-topFixVulnerability', + type: 'securityAlert2-topFixType', + origin: 'securityAlert2-topFixOrigin', + url: 'securityAlert2-topFixUrl', + fixResolution: 'securityAlert2-topFixFixResolution', + date: 'securityAlert2-topFixDate', + message: 'securityAlert2-topFixMessage', + extraData: {}, + }, + effective: 'securityAlert2-effective', + }, +] + +export const librariesData = [ + { + uuid: 'library1-uuid', + name: 'library1', + artifactId: 'library1-artifactId', + groupId: 'library1-groupdId', + version: 'library1-version', + architecture: 'library1-architecture', + languageVersion: 'library1-languageVersion', + classifier: 'library1-classifier', + extension: 'library1-extension', + sha1: 'library1-sha1', + description: 'library1-description', + type: 'library1-type', + directDependency: false, + licenses: [ + { + uuid: 'license1-uuid', + name: 'license1', + assignedByUser: false, + licenseReferences: [ + { + uuid: 'licenseReference1-uuid', + type: 'licenseReference1-type', + liabilityReference: 'licenseReference1-liabilityRef', + information: 'licenseReference1-info', + }, + ], + }, + ], + copyrightReferences: [], + locations: [], + }, + { + uuid: 'library2-uuid', + name: 'library2', + artifactId: 'library2-artifactId', + groupId: 'library2-groupdId', + version: 'library2-version', + architecture: 'library2-architecture', + languageVersion: 'library2-languageVersion', + classifier: 'library2-classifier', + extension: 'library2-extension', + sha1: 'library2-sha1', + description: 'library2-description', + type: 'library2-type', + directDependency: false, + licenses: [ + { + uuid: 'license2-uuid', + name: 'license2', + assignedByUser: false, + licenseReferences: [ + { + uuid: 'licenseReference2-uuid', + type: 'licenseReference2-type', + liabilityReference: 'licenseReference2-liabilityRef', + information: 'licenseReference2-info', + }, + ], + }, + ], + copyrightReferences: [ + { + type: 'library1-copyrightReference1Type', + copyright: 'library1-copyrightReference1Copyright', + startYear: 'library1-copyrightReference1StartYear', + endYear: 'library1-copyrightReference1EndYear', + author: 'library1-copyrightReference1Author', + referenceInfo: 'library1-copyrightReferenceReference1Info', + }, + { + type: 'library1-copyrightReference2Type', + copyright: 'library1-copyrightReference2Copyright', + startYear: 'library1-copyrightReference2StartYear', + endYear: 'library1-copyrightReference2EndYear', + author: 'library1-copyrightReference2Author', + referenceInfo: 'library1-copyrightReference2ReferenceInfo', + }, + ], + locations: [ + { + localPath: 'library2-location1LocalPath', + dependencyFile: 'library2-location1DependencyFile', + }, + { + localPath: 'library2-location2LocalPath', + dependencyFile: 'library2-location2DependencyFile', + }, + ], + }, +] + +export const vulnerabilitiesData = [ + { + name: 'vulnerability1-name', + type: 'vulnerability1-type', + description: 'vulnerability1-description', + score: 9.9, + severity: 'vulnerability1-severity', + publishDate: 'vulnerability1-publishDate', + modifiedDate: 'vulnerability1-modifiedDate', + vulnerabilityScoring: [ + { + score: 9.9, + severity: 'vulnerability1-vulnerabilityScoringSeverity', + type: 'vulnerability1-vulnerabilityScoringType', + extraData: { + confidentialityImpact: + 'vulnerability1-vulnerabilityScoringExtraDataConfidentialityImpact', + attackComplexity: + 'vulnerability1-vulnerabilityScoringExtraDataAttackComplexity', + scope: 'vulnerability1-vulnerabilityScoringExtraDataScope', + availabilityImpact: + 'vulnerability1-vulnerabilityScoringExtraDataAvailabilityImpact', + attackVector: + 'vulnerability1-vulnerabilityScoringExtraDataAttackVector', + integrityImpact: + 'vulnerability1-vulnerabilityScoringExtraDataIntegrityImpact', + privilegesRequired: + 'vulnerability1-vulnerabilityScoringExtraDataPrivilegesRequired', + vectorString: + 'vulnerability1-vulnerabilityScoringExtraDataVectorString', + userInteraction: + 'vulnerability1-vulnerabilityScoringExtraDataUserInteraction', + }, + }, + ], + references: [], + }, + { + name: 'vulnerability2-name', + type: 'vulnerability2-type', + description: 'vulnerability2-description', + score: 9.9, + severity: 'vulnerability2-severity', + publishDate: 'vulnerability2-publishDate', + modifiedDate: 'vulnerability2-modifiedDate', + vulnerabilityScoring: [ + { + score: 9.9, + severity: 'vulnerability2-vulnerabilityScoringSeverity', + type: 'vulnerability2-vulnerabilityScoringType', + extraData: { + confidentialityImpact: + 'vulnerability2-vulnerabilityScoringExtraDataConfidentialityImpact', + attackComplexity: + 'vulnerability2-vulnerabilityScoringExtraDataAttackComplexity', + scope: 'vulnerability2-vulnerabilityScoringExtraDataScope', + availabilityImpact: + 'vulnerability2-vulnerabilityScoringExtraDataAvailabilityImpact', + attackVector: + 'vulnerability2-vulnerabilityScoringExtraDataAttackVector', + integrityImpact: + 'vulnerability2-vulnerabilityScoringExtraDataIntegrityImpact', + privilegesRequired: + 'vulnerability2-vulnerabilityScoringExtraDataPrivilegesRequired', + vectorString: + 'vulnerability2-vulnerabilityScoringExtraDataVectorString', + userInteraction: + 'vulnerability2-vulnerabilityScoringExtraDataUserInteraction', + }, + }, + ], + references: [ + { + value: 'vulnerability2-reference1Value', + source: 'vulnerability2-reference1Source', + url: 'vulnerability2-reference1Url', + signature: false, + advisory: false, + patch: false, + }, + { + value: 'vulnerability2-reference2Value', + source: 'vulnerability2-reference2Source', + url: 'vulnerability2-reference2Url', + signature: false, + advisory: false, + patch: false, + }, + ], + }, +] + +export const vulnerabilitiesFixSummaryData = { + vulnerability: 'vulnerability1', + topRankedFix: { + id: 123456, + vulnerability: 'securityAlert1-topFixVulnerability', + type: 'securityAlert1-topFixType', + origin: 'securityAlert1-topFixOrigin', + url: 'securityAlert1-topFixUrl', + fixResolution: 'securityAlert1-topFixFixResolution', + date: 'securityAlert1-topFixDate', + message: 'securityAlert1-topFixMessage', + extraData: {}, + }, + allFixes: [ + { + id: 123456, + vulnerability: 'securityAlert1-topFixVulnerability', + type: 'securityAlert1-topFixType', + origin: 'securityAlert1-topFixOrigin', + url: 'securityAlert1-topFixUrl', + fixResolution: 'securityAlert1-topFixFixResolution', + date: 'securityAlert1-topFixDate', + message: 'securityAlert1-topFixMessage', + extraData: {}, + }, + { + id: 123457, + vulnerability: 'securityAlert2-topFixVulnerability', + type: 'securityAlert2-topFixType', + origin: 'securityAlert2-topFixOrigin', + url: 'securityAlert2-topFixUrl', + fixResolution: 'securityAlert2-topFixFixResolution', + date: 'securityAlert2-topFixDate', + message: 'securityAlert2-topFixMessage', + extraData: {}, + }, + ], + totalUpVotes: 12345, + totalDownVotes: 1234, +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/fixtures/dto.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fixtures/dto.ts new file mode 100644 index 00000000..f3734056 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fixtures/dto.ts @@ -0,0 +1,246 @@ +import { LibraryDTO } from '../../../src/dto/library.dto' +import { NewVersionsAlertDTO } from '../../../src/dto/newVersionsAlert.dto' +import { MultipleLicensesAlertDTO } from '../../../src/dto/multipleLicensesAlert.dto' +import { RejectedInUseAlertDTO } from '../../../src/dto/rejectedInUseAlert.dto' +import { OrganizationDTO } from '../../../src/dto/organization.dto' +import { PolicyAlertDTO } from '../../../src/dto/policyAlert.dto' +import { ProjectDTO } from '../../../src/dto/project.dto' +import { ProjectVitalsDTO } from '../../../src/dto/projectVitals.dto' +import { SecurityAlertDTO } from '../../../src/dto/securityAlert.dto' +import { VulnerabilityDTO } from '../../../src/dto/vulnerability.dto' +import { VulnerabilityFixSummaryDTO } from '../../../src/dto/vulnerabilityFixSummary.dto' + +import { + librariesData, + organizationData, + policyAlertsData, + newVersionsAlertsData, + multipleLicensesAlertsData, + rejectedInUseAlertsData, + projectData, + projectVitalsData, + securityAlertsData, + vulnerabilitiesData, + vulnerabilitiesFixSummaryData, +} from './data' +import { VulnerabilityFix } from '../../../src/model/vulnerabilityFix' + +export const organizationDTO = new OrganizationDTO( + organizationData.uuid, + organizationData.name +) + +export const projectDTO = new ProjectDTO( + projectData.uuid, + projectData.name, + projectData.path, + projectData.productName, + projectData.productUuid +) + +export const projectVitalsDTO = new ProjectVitalsDTO( + projectVitalsData.lastScan, + projectVitalsData.lastUserScanned, + projectVitalsData.requestToken, + projectVitalsData.lastSourceFileMatch, + projectVitalsData.lastScanComment, + projectVitalsData.projectCreationDate, + projectVitalsData.pluginName, + projectVitalsData.pluginVersion, + projectVitalsData.libraryCount +) + +export const policyAlertsDTO = [ + new PolicyAlertDTO( + policyAlertsData[0].uuid, + policyAlertsData[0].name, + policyAlertsData[0].type, + policyAlertsData[0].component, + policyAlertsData[0].alertInfo, + policyAlertsData[0].project, + policyAlertsData[0].policyName + ), + new PolicyAlertDTO( + policyAlertsData[1].uuid, + policyAlertsData[1].name, + policyAlertsData[1].type, + policyAlertsData[1].component, + policyAlertsData[1].alertInfo, + policyAlertsData[1].project, + policyAlertsData[1].policyName + ), +] + +export const newVersionsAlertsDTO = [ + new NewVersionsAlertDTO( + newVersionsAlertsData[0].uuid, + newVersionsAlertsData[0].name, + newVersionsAlertsData[0].type, + newVersionsAlertsData[0].component, + newVersionsAlertsData[0].alertInfo, + newVersionsAlertsData[0].project, + newVersionsAlertsData[0].availableVersion, + newVersionsAlertsData[0].availableVersionType + ), + new NewVersionsAlertDTO( + newVersionsAlertsData[1].uuid, + newVersionsAlertsData[1].name, + newVersionsAlertsData[1].type, + newVersionsAlertsData[1].component, + newVersionsAlertsData[1].alertInfo, + newVersionsAlertsData[1].project, + newVersionsAlertsData[1].availableVersion, + newVersionsAlertsData[1].availableVersionType + ), +] + +export const multipleLicensesAlertsDTO = [ + new MultipleLicensesAlertDTO( + multipleLicensesAlertsData[0].uuid, + multipleLicensesAlertsData[0].name, + multipleLicensesAlertsData[0].type, + multipleLicensesAlertsData[0].component, + multipleLicensesAlertsData[0].alertInfo, + multipleLicensesAlertsData[0].project, + multipleLicensesAlertsData[0].numberOfLicenses, + multipleLicensesAlertsData[0].licenses + ), + new MultipleLicensesAlertDTO( + multipleLicensesAlertsData[1].uuid, + multipleLicensesAlertsData[1].name, + multipleLicensesAlertsData[1].type, + multipleLicensesAlertsData[1].component, + multipleLicensesAlertsData[1].alertInfo, + multipleLicensesAlertsData[1].project, + multipleLicensesAlertsData[1].numberOfLicenses, + multipleLicensesAlertsData[1].licenses + ), +] + +export const rejectedInUseAlertsDTO = [ + new RejectedInUseAlertDTO( + rejectedInUseAlertsData[0].uuid, + rejectedInUseAlertsData[0].name, + rejectedInUseAlertsData[0].type, + rejectedInUseAlertsData[0].component, + rejectedInUseAlertsData[0].alertInfo, + rejectedInUseAlertsData[0].project, + rejectedInUseAlertsData[0].description + ), + new RejectedInUseAlertDTO( + rejectedInUseAlertsData[1].uuid, + rejectedInUseAlertsData[1].name, + rejectedInUseAlertsData[1].type, + rejectedInUseAlertsData[1].component, + rejectedInUseAlertsData[1].alertInfo, + rejectedInUseAlertsData[1].project, + rejectedInUseAlertsData[1].description + ), +] + +export const securityAlertsDTO = [ + new SecurityAlertDTO( + securityAlertsData[0].uuid, + securityAlertsData[0].name, + securityAlertsData[0].type, + securityAlertsData[0].component, + securityAlertsData[0].alertInfo, + securityAlertsData[0].project, + securityAlertsData[0].product, + securityAlertsData[0].vulnerability, + securityAlertsData[0].topFix, + securityAlertsData[0].effective + ), + new SecurityAlertDTO( + securityAlertsData[1].uuid, + securityAlertsData[1].name, + securityAlertsData[1].type, + securityAlertsData[1].component, + securityAlertsData[1].alertInfo, + securityAlertsData[1].project, + securityAlertsData[1].product, + securityAlertsData[1].vulnerability, + securityAlertsData[1].topFix, + securityAlertsData[1].effective + ), +] + +export const librariesDTO = [ + new LibraryDTO( + librariesData[0].uuid, + librariesData[0].name, + librariesData[0].artifactId, + librariesData[0].version, + librariesData[0].architecture, + librariesData[0].languageVersion, + librariesData[0].classifier, + librariesData[0].extension, + librariesData[0].sha1, + librariesData[0].description, + librariesData[0].type, + librariesData[0].directDependency, + librariesData[0].licenses, + librariesData[0].copyrightReferences, + librariesData[0].locations + ), + new LibraryDTO( + librariesData[1].uuid, + librariesData[1].name, + librariesData[1].artifactId, + librariesData[1].version, + librariesData[1].architecture, + librariesData[1].languageVersion, + librariesData[1].classifier, + librariesData[1].extension, + librariesData[1].sha1, + librariesData[1].description, + librariesData[1].type, + librariesData[1].directDependency, + librariesData[1].licenses, + librariesData[1].copyrightReferences, + librariesData[1].locations + ), +] + +export const vulnerabilitiesDTO = [ + new VulnerabilityDTO( + vulnerabilitiesData[0].name, + vulnerabilitiesData[0].type, + vulnerabilitiesData[0].description, + vulnerabilitiesData[0].score, + vulnerabilitiesData[0].severity, + vulnerabilitiesData[0].publishDate, + vulnerabilitiesData[0].modifiedDate, + vulnerabilitiesData[0].vulnerabilityScoring, + vulnerabilitiesData[0].references + ), + new VulnerabilityDTO( + vulnerabilitiesData[1].name, + vulnerabilitiesData[1].type, + vulnerabilitiesData[1].description, + vulnerabilitiesData[1].score, + vulnerabilitiesData[1].severity, + vulnerabilitiesData[1].publishDate, + vulnerabilitiesData[1].modifiedDate, + vulnerabilitiesData[1].vulnerabilityScoring, + vulnerabilitiesData[1].references + ), +] + +export const vulnerabilitiesFixSummaryDTO = new VulnerabilityFixSummaryDTO( + vulnerabilitiesFixSummaryData.vulnerability, + new VulnerabilityFix( + vulnerabilitiesFixSummaryData.topRankedFix.id, + vulnerabilitiesFixSummaryData.topRankedFix.vulnerability, + vulnerabilitiesFixSummaryData.topRankedFix.type, + vulnerabilitiesFixSummaryData.topRankedFix.origin, + vulnerabilitiesFixSummaryData.topRankedFix.url, + vulnerabilitiesFixSummaryData.topRankedFix.fixResolution, + vulnerabilitiesFixSummaryData.topRankedFix.date, + vulnerabilitiesFixSummaryData.topRankedFix.message, + vulnerabilitiesFixSummaryData.topRankedFix.extraData + ), + vulnerabilitiesFixSummaryData.allFixes, + vulnerabilitiesFixSummaryData.totalUpVotes, + vulnerabilitiesFixSummaryData.totalDownVotes +) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/fixtures/env.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fixtures/env.ts new file mode 100644 index 00000000..4ce6e91d --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fixtures/env.ts @@ -0,0 +1,15 @@ +import { MendEnvironment } from '../../../src/model/mendEnvironment' + +export const envFixture: MendEnvironment = { + alertsStatus: 'active', + apiUrl: 'https://foo.bar', + serverUrl: 'https://bar.foo', + email: 'dummy1@some.gTLD', + maxConcurrentConnections: 123, + minConnectionTime: 321, + orgToken: 'dummy2-token', + projectToken: 'dummy3-token', + reportType: 'vulnerabilities', + resultsPath: 'dummy4-path', + userKey: 'dummy1-userkey', +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/fixtures/fakeauth.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fixtures/fakeauth.ts new file mode 100644 index 00000000..0c005772 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fixtures/fakeauth.ts @@ -0,0 +1,24 @@ +import { MendEnvironment } from '../../../src/model/mendEnvironment' +import { Login } from '../../../src/model/login' +import { organizationData } from './data' + +export class FakeAuthenticator { + private env: MendEnvironment + constructor(env: MendEnvironment) { + this.env = env + } + + async authenticate() { + return new Login( + `${this.env.email.split('@')[0]}-userUuid`, + `${this.env.email.split('@')[0]}-userName`, + this.env.email, + 'jwtToken', + 'jwtRefresh', + 1800000, + organizationData.name, + organizationData.uuid, + new Date().valueOf() + ) + } +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/fixtures/httpResponseStatus.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fixtures/httpResponseStatus.ts new file mode 100644 index 00000000..8eca6475 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fixtures/httpResponseStatus.ts @@ -0,0 +1,42 @@ +// RFC9110 Defined HTTP Response Status Codes +export const HTTPResponseStatusCodes = [ + { code: 100, message: 'Continue' }, + { code: 101, message: 'Switching Protocols' }, + { code: 300, message: 'Multiple Choices' }, + { code: 301, message: 'Moved Permanently' }, + { code: 302, message: 'Found' }, + { code: 303, message: 'See Other' }, + { code: 304, message: 'Not Modified' }, + { code: 305, message: 'Use Proxy' }, + { code: 306, message: '(Unused)' }, + { code: 307, message: 'Temporary Redirect' }, + { code: 308, message: 'Permanent Redirect' }, + { code: 400, message: 'Bad Request' }, + { code: 401, message: 'Unauthorizad' }, + { code: 402, message: 'Payment Required' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not Found' }, + { code: 405, message: 'Method Not Allowd' }, + { code: 406, message: 'Not Acceptable' }, + { code: 407, message: 'Proxy Authentication Required' }, + { code: 408, message: 'Request Timeout' }, + { code: 409, message: 'Conflict' }, + { code: 410, message: 'Gone' }, + { code: 411, message: 'Length Required' }, + { code: 412, message: 'Precondition Failed' }, + { code: 413, message: 'Content Too Large' }, + { code: 414, message: 'URI Too Long' }, + { code: 415, message: 'Unsupported Media Type' }, + { code: 416, message: 'Range Not Satisfiable' }, + { code: 417, message: 'Expectation Failed' }, + { code: 418, message: '(Unused)' }, + { code: 421, message: 'Misredirected Request' }, + { code: 422, message: 'Unprocessable Content' }, + { code: 426, message: 'Upgrade Required' }, + { code: 500, message: 'Internal Server Error' }, + { code: 501, message: 'Not Implemented' }, + { code: 502, message: 'Bad Gateway' }, + { code: 503, message: 'Service Unavailable' }, + { code: 504, message: 'Gateway Timeout' }, + { code: 505, message: 'HTTP Version Not Supported' }, +] diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/fixtures/model.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fixtures/model.ts new file mode 100644 index 00000000..7daaadc2 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/fixtures/model.ts @@ -0,0 +1,465 @@ +import { CopyrightReference } from '../../../src/model/copyrightReference' +import { Library } from '../../../src/model/library' +import { License } from '../../../src/model/license' +import { LicenseReference } from '../../../src/model/licenseReference' +import { Organization } from '../../../src/model/organization' +import { PolicyAlert } from '../../../src/model/policyAlert' +import { MultipleLicensesAlert } from '../../../src/model/multipleLicensesAlert' +import { NewVersionsAlert } from '../../../src/model/newVersionsAlert' +import { RejectedInUseAlert } from '../../../src/model/rejectedInUseAlert' +import { Project } from '../../../src/model/project' +import { ProjectVitals } from '../../../src/model/projectVitals' +import { SecurityAlert } from '../../../src/model/securityAlert' +import { Vulnerability } from '../../../src/model/vulnerability' +import { VulnerabilityFix } from '../../../src/model/vulnerabilityFix' +import { VulnerabilityReference } from '../../../src/model/vulnerabilityReference' +import { VulnerabilityFixSummary } from '../../../src/model/vulnerabilityFixSummary' + +import { + librariesData, + organizationData, + policyAlertsData, + multipleLicensesAlertsData, + newVersionsAlertsData, + rejectedInUseAlertsData, + projectData, + projectVitalsData, + securityAlertsData, + vulnerabilitiesData, + vulnerabilitiesFixSummaryData, +} from './data' + +export const organizationModel = new Organization( + organizationData.uuid, + organizationData.name +) + +export const projectModel = new Project( + projectData.uuid, + projectData.name, + projectData.path, + projectData.productName, + projectData.productUuid +) + +export const projectVitalsModel = new ProjectVitals( + projectVitalsData.lastScan, + projectVitalsData.lastUserScanned, + projectVitalsData.requestToken, + projectVitalsData.lastSourceFileMatch, + projectVitalsData.lastScanComment, + projectVitalsData.projectCreationDate, + projectVitalsData.pluginName, + projectVitalsData.pluginVersion, + projectVitalsData.libraryCount +) + +export const policyAlertsModel = [ + new PolicyAlert( + policyAlertsData[0].uuid, + policyAlertsData[0].name, + policyAlertsData[0].type, + policyAlertsData[0].component, + policyAlertsData[0].alertInfo, + new Project( + policyAlertsData[0].project.uuid, + policyAlertsData[0].project.name, + policyAlertsData[0].project.path, + policyAlertsData[0].project.path, + policyAlertsData[0].project.productUuid + ), + { + uuid: policyAlertsData[0].project.productUuid, + name: policyAlertsData[0].project.path, + }, + policyAlertsData[0].policyName + ), + new PolicyAlert( + policyAlertsData[1].uuid, + policyAlertsData[1].name, + policyAlertsData[1].type, + policyAlertsData[1].component, + policyAlertsData[1].alertInfo, + new Project( + policyAlertsData[1].project.uuid, + policyAlertsData[1].project.name, + policyAlertsData[1].project.path, + policyAlertsData[1].project.path, + policyAlertsData[1].project.productUuid + ), + { + uuid: policyAlertsData[1].project.productUuid, + name: policyAlertsData[1].project.path, + }, + policyAlertsData[1].policyName + ), +] + +export const multipleLicensesAlertsModel = [ + new MultipleLicensesAlert( + multipleLicensesAlertsData[0].uuid, + multipleLicensesAlertsData[0].name, + multipleLicensesAlertsData[0].type, + multipleLicensesAlertsData[0].component, + multipleLicensesAlertsData[0].alertInfo, + new Project( + multipleLicensesAlertsData[0].project.uuid, + multipleLicensesAlertsData[0].project.name, + multipleLicensesAlertsData[0].project.path, + multipleLicensesAlertsData[0].project.path, + multipleLicensesAlertsData[0].project.productUuid + ), + { + uuid: multipleLicensesAlertsData[0].project.productUuid, + name: multipleLicensesAlertsData[0].project.path, + }, + multipleLicensesAlertsData[0].numberOfLicenses, + multipleLicensesAlertsData[0].licenses + ), + new MultipleLicensesAlert( + multipleLicensesAlertsData[1].uuid, + multipleLicensesAlertsData[1].name, + multipleLicensesAlertsData[1].type, + multipleLicensesAlertsData[1].component, + multipleLicensesAlertsData[1].alertInfo, + new Project( + multipleLicensesAlertsData[1].project.uuid, + multipleLicensesAlertsData[1].project.name, + multipleLicensesAlertsData[1].project.path, + multipleLicensesAlertsData[1].project.path, + multipleLicensesAlertsData[1].project.productUuid + ), + { + uuid: multipleLicensesAlertsData[1].project.productUuid, + name: multipleLicensesAlertsData[1].project.path, + }, + multipleLicensesAlertsData[1].numberOfLicenses, + multipleLicensesAlertsData[1].licenses + ), +] + +export const newVersionsAlertsModel = [ + new NewVersionsAlert( + newVersionsAlertsData[0].uuid, + newVersionsAlertsData[0].name, + newVersionsAlertsData[0].type, + newVersionsAlertsData[0].component, + newVersionsAlertsData[0].alertInfo, + new Project( + newVersionsAlertsData[0].project.uuid, + newVersionsAlertsData[0].project.name, + newVersionsAlertsData[0].project.path, + newVersionsAlertsData[0].project.path, + newVersionsAlertsData[0].project.productUuid + ), + { + uuid: newVersionsAlertsData[0].project.productUuid, + name: newVersionsAlertsData[0].project.path, + }, + newVersionsAlertsData[0].availableVersion, + newVersionsAlertsData[0].availableVersionType + ), + new NewVersionsAlert( + newVersionsAlertsData[1].uuid, + newVersionsAlertsData[1].name, + newVersionsAlertsData[1].type, + newVersionsAlertsData[1].component, + newVersionsAlertsData[1].alertInfo, + new Project( + newVersionsAlertsData[1].project.uuid, + newVersionsAlertsData[1].project.name, + newVersionsAlertsData[1].project.path, + newVersionsAlertsData[1].project.path, + newVersionsAlertsData[1].project.productUuid + ), + { + uuid: newVersionsAlertsData[1].project.productUuid, + name: newVersionsAlertsData[1].project.path, + }, + newVersionsAlertsData[1].availableVersion, + newVersionsAlertsData[1].availableVersionType + ), +] + +export const rejectedInUseAlertsModel = [ + new RejectedInUseAlert( + rejectedInUseAlertsData[0].uuid, + rejectedInUseAlertsData[0].name, + rejectedInUseAlertsData[0].type, + rejectedInUseAlertsData[0].component, + rejectedInUseAlertsData[0].alertInfo, + new Project( + rejectedInUseAlertsData[0].project.uuid, + rejectedInUseAlertsData[0].project.name, + rejectedInUseAlertsData[0].project.path, + rejectedInUseAlertsData[0].project.path, + rejectedInUseAlertsData[0].project.productUuid + ), + { + uuid: rejectedInUseAlertsData[0].project.productUuid, + name: rejectedInUseAlertsData[0].project.path, + }, + rejectedInUseAlertsData[0].description + ), + new RejectedInUseAlert( + rejectedInUseAlertsData[1].uuid, + rejectedInUseAlertsData[1].name, + rejectedInUseAlertsData[1].type, + rejectedInUseAlertsData[1].component, + rejectedInUseAlertsData[1].alertInfo, + new Project( + rejectedInUseAlertsData[1].project.uuid, + rejectedInUseAlertsData[1].project.name, + rejectedInUseAlertsData[1].project.path, + rejectedInUseAlertsData[1].project.path, + rejectedInUseAlertsData[1].project.productUuid + ), + { + uuid: rejectedInUseAlertsData[1].project.productUuid, + name: rejectedInUseAlertsData[1].project.path, + }, + rejectedInUseAlertsData[1].description + ), +] + +export const securityAlertsModel = [ + new SecurityAlert( + securityAlertsData[0].uuid, + securityAlertsData[0].name, + securityAlertsData[0].type, + securityAlertsData[0].component, + securityAlertsData[0].alertInfo, + new Project( + securityAlertsData[0].project.uuid, + securityAlertsData[0].project.name, + securityAlertsData[0].project.path, + securityAlertsData[0].project.path, + securityAlertsData[0].project.productUuid + ), + securityAlertsData[0].product, + new Vulnerability( + securityAlertsData[0].vulnerability.name, + securityAlertsData[0].vulnerability.type, + securityAlertsData[0].vulnerability.description, + securityAlertsData[0].vulnerability.score, + securityAlertsData[0].vulnerability.severity, + securityAlertsData[0].vulnerability.publishDate, + securityAlertsData[0].vulnerability.modifiedDate, + securityAlertsData[0].vulnerability.vulnerabilityScoring, + [] + ), + new VulnerabilityFix( + securityAlertsData[0].topFix.id, + securityAlertsData[0].topFix.vulnerability, + securityAlertsData[0].topFix.type, + securityAlertsData[0].topFix.origin, + securityAlertsData[0].topFix.url, + securityAlertsData[0].topFix.fixResolution, + securityAlertsData[0].topFix.date, + securityAlertsData[0].topFix.message, + securityAlertsData[0].topFix.extraData + ), + securityAlertsData[0].effective + ), + new SecurityAlert( + securityAlertsData[1].uuid, + securityAlertsData[1].name, + securityAlertsData[1].type, + securityAlertsData[1].component, + securityAlertsData[1].alertInfo, + new Project( + securityAlertsData[1].project.uuid, + securityAlertsData[1].project.name, + securityAlertsData[1].project.path, + securityAlertsData[1].project.path, + securityAlertsData[1].project.productUuid + ), + securityAlertsData[1].product, + new Vulnerability( + securityAlertsData[1].vulnerability.name, + securityAlertsData[1].vulnerability.type, + securityAlertsData[1].vulnerability.description, + securityAlertsData[1].vulnerability.score, + securityAlertsData[1].vulnerability.severity, + securityAlertsData[1].vulnerability.publishDate, + securityAlertsData[1].vulnerability.modifiedDate, + securityAlertsData[1].vulnerability.vulnerabilityScoring, + [] + ), + new VulnerabilityFix( + securityAlertsData[1].topFix.id, + securityAlertsData[1].topFix.vulnerability, + securityAlertsData[1].topFix.type, + securityAlertsData[1].topFix.origin, + securityAlertsData[1].topFix.url, + securityAlertsData[1].topFix.fixResolution, + securityAlertsData[1].topFix.date, + securityAlertsData[1].topFix.message, + securityAlertsData[1].topFix.extraData + ), + securityAlertsData[1].effective + ), +] + +export const librariesModel = [ + new Library( + librariesData[0].uuid, + librariesData[0].name, + librariesData[0].artifactId, + librariesData[0].version, + librariesData[0].architecture, + librariesData[0].languageVersion, + librariesData[0].classifier, + librariesData[0].extension, + librariesData[0].sha1, + librariesData[0].description, + librariesData[0].type, + librariesData[0].directDependency, + librariesData[0].licenses.map((license) => { + return new License( + license.uuid, + license.name, + license.assignedByUser, + license.licenseReferences.map((ref) => { + return new LicenseReference( + ref.uuid, + ref.type, + ref.liabilityReference, + ref.information + ) + }) + ) + }), + librariesData[0].copyrightReferences.map( + (copyrightRef: CopyrightReference) => { + return new CopyrightReference( + copyrightRef.type, + copyrightRef.copyright, + copyrightRef.author, + copyrightRef.referenceInfo, + copyrightRef.startYear, + copyrightRef.endYear + ) + } + ), + librariesData[0].locations + ), + new Library( + librariesData[1].uuid, + librariesData[1].name, + librariesData[1].artifactId, + librariesData[1].version, + librariesData[1].architecture, + librariesData[1].languageVersion, + librariesData[1].classifier, + librariesData[1].extension, + librariesData[1].sha1, + librariesData[1].description, + librariesData[1].type, + librariesData[1].directDependency, + librariesData[1].licenses.map((license) => { + return new License( + license.uuid, + license.name, + license.assignedByUser, + license.licenseReferences.map((ref) => { + return new LicenseReference( + ref.uuid, + ref.type, + ref.liabilityReference, + ref.information + ) + }) + ) + }), + librariesData[1].copyrightReferences.map( + (copyrightRef: CopyrightReference) => { + return new CopyrightReference( + copyrightRef.type, + copyrightRef.copyright, + copyrightRef.author, + copyrightRef.referenceInfo, + copyrightRef.startYear, + copyrightRef.endYear + ) + } + ), + librariesData[1].locations + ), +] + +export const vulnerabilitiesModel = [ + new Vulnerability( + vulnerabilitiesData[0].name, + vulnerabilitiesData[0].type, + vulnerabilitiesData[0].description, + vulnerabilitiesData[0].score, + vulnerabilitiesData[0].severity, + vulnerabilitiesData[0].publishDate, + vulnerabilitiesData[0].modifiedDate, + vulnerabilitiesData[0].vulnerabilityScoring, + vulnerabilitiesData[0].references.map( + (ref: VulnerabilityReference) => + new VulnerabilityReference( + ref.value, + ref.source, + ref.url, + ref.signature, + ref.advisory, + ref.patch + ) + ) + ), + new Vulnerability( + vulnerabilitiesData[1].name, + vulnerabilitiesData[1].type, + vulnerabilitiesData[1].description, + vulnerabilitiesData[1].score, + vulnerabilitiesData[1].severity, + vulnerabilitiesData[1].publishDate, + vulnerabilitiesData[1].modifiedDate, + vulnerabilitiesData[1].vulnerabilityScoring, + vulnerabilitiesData[1].references.map( + (ref: VulnerabilityReference) => + new VulnerabilityReference( + ref.value, + ref.source, + ref.url, + ref.signature, + ref.advisory, + ref.patch + ) + ) + ), +] + +export const vulnerabilityFixSummaryModel = new VulnerabilityFixSummary( + vulnerabilitiesFixSummaryData.vulnerability, + new VulnerabilityFix( + vulnerabilitiesFixSummaryData.topRankedFix.id, + vulnerabilitiesFixSummaryData.topRankedFix.vulnerability, + vulnerabilitiesFixSummaryData.topRankedFix.type, + vulnerabilitiesFixSummaryData.topRankedFix.origin, + vulnerabilitiesFixSummaryData.topRankedFix.url, + vulnerabilitiesFixSummaryData.topRankedFix.fixResolution, + vulnerabilitiesFixSummaryData.topRankedFix.date, + vulnerabilitiesFixSummaryData.topRankedFix.message, + vulnerabilitiesFixSummaryData.topRankedFix.extraData + ), + vulnerabilitiesFixSummaryData.allFixes.map( + (fix: VulnerabilityFix) => + new VulnerabilityFix( + fix.id, + fix.vulnerability, + fix.type, + fix.origin, + fix.url, + fix.fixResolution, + fix.date, + fix.message, + fix.extraData + ) + ), + vulnerabilitiesFixSummaryData.totalUpVotes, + vulnerabilitiesFixSummaryData.totalDownVotes +) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/library.mapper.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/library.mapper.test.ts new file mode 100644 index 00000000..13392b9e --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/library.mapper.test.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { describe, expect, it } from 'vitest' +import { Library } from '../../../src/model/library' +import { LibraryDTO } from '../../../src/dto/library.dto' +import { LibraryMap } from '../../../src/mapper/library.mapper' +import { librariesDTO } from '../fixtures/dto' +import { librariesModel } from '../fixtures/model' + +describe('library.mapper', () => { + it('should return a Library object when data has no locations', () => { + const expected = librariesModel[0] + + const result: Library = LibraryMap.toModel(librariesDTO[0]) + + expect(result).toStrictEqual(expected) + }) + + it('should return a Library object when data includes locations', () => { + const expected = librariesModel[1] + const result: Library = LibraryMap.toModel(librariesDTO[1]) + + expect(result).toStrictEqual(expected) + }) + + it('should return a Library DTO when data has no CopyrightReference', () => { + const expected = librariesDTO[0] + + const result: LibraryDTO = LibraryMap.toDTO(librariesModel[0]) + + expect(result).toStrictEqual(expected) + }) + + it('should return a Library DTO when data includes CopyrightReference', () => { + const expected = librariesDTO[1] + + const result: LibraryDTO = LibraryMap.toDTO(librariesModel[1]) + + expect(result).toStrictEqual(expected) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/multipleLicensesAlert.mapper.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/multipleLicensesAlert.mapper.test.ts new file mode 100644 index 00000000..c8963e2c --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/multipleLicensesAlert.mapper.test.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { describe, expect, it } from 'vitest' +import { MultipleLicensesAlert } from '../../../src/model/multipleLicensesAlert' +import { MultipleLicensesAlertDTO } from '../../../src/dto/multipleLicensesAlert.dto' +import { MultipleLicensesAlertMap } from '../../../src/mapper/multipleLicensesAlert.mapper' +import { multipleLicensesAlertsDTO } from '../fixtures/dto' +import { multipleLicensesAlertsModel } from '../fixtures/model' + +describe('multipleLicensesAlert.mapper', () => { + it('should return a MultipleLicensesAlert object', () => { + const expected = multipleLicensesAlertsModel[0] + + const result: MultipleLicensesAlert = MultipleLicensesAlertMap.toModel( + multipleLicensesAlertsDTO[0] + ) + + expect(result).toStrictEqual(expected) + }) + + it('should return a MultipleLicensesAlert DTO', () => { + const expected = multipleLicensesAlertsDTO[1] + + const result: MultipleLicensesAlertDTO = MultipleLicensesAlertMap.toDTO( + multipleLicensesAlertsModel[1] + ) + + expect(result).toStrictEqual(expected) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/newVersionsAlert.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/newVersionsAlert.test.ts new file mode 100644 index 00000000..cd55a0ab --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/newVersionsAlert.test.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { describe, expect, it } from 'vitest' +import { NewVersionsAlert } from '../../../src/model/newVersionsAlert' +import { NewVersionsAlertDTO } from '../../../src/dto/newVersionsAlert.dto' +import { NewVersionsAlertMap } from '../../../src/mapper/newVersionsAlert.mapper' +import { newVersionsAlertsDTO } from '../fixtures/dto' +import { newVersionsAlertsModel } from '../fixtures/model' + +describe('newVersionsAlert.mapper', () => { + it('should return a NewVersionsAlert object', () => { + const expected = newVersionsAlertsModel[0] + + const result: NewVersionsAlert = NewVersionsAlertMap.toModel( + newVersionsAlertsDTO[0] + ) + + expect(result).toStrictEqual(expected) + }) + + it('should return a NewVersionsAlert DTO', () => { + const expected = newVersionsAlertsDTO[1] + + const result: NewVersionsAlertDTO = NewVersionsAlertMap.toDTO( + newVersionsAlertsModel[1] + ) + + expect(result).toStrictEqual(expected) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/organization.mapper.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/organization.mapper.test.ts new file mode 100644 index 00000000..24508948 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/organization.mapper.test.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { describe, expect, it } from 'vitest' +import { Organization } from '../../../src/model/organization' +import { OrganizationDTO } from '../../../src/dto/organization.dto' +import { OrganizationMap } from '../../../src/mapper/organization.mapper' +import { organizationDTO } from '../fixtures/dto' +import { organizationModel } from '../fixtures/model' + +describe('organization.mapper', () => { + it('should return an Organization object', () => { + const expected = organizationModel + + const result: Organization = OrganizationMap.toModel(organizationDTO) + + expect(result).toStrictEqual(expected) + }) + it('should return an Organization DTO', () => { + const expected = organizationDTO + + const result: OrganizationDTO = OrganizationMap.toDTO(organizationModel) + + expect(result).toStrictEqual(expected) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/policyAlert.mapper.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/policyAlert.mapper.test.ts new file mode 100644 index 00000000..0e61d08e --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/policyAlert.mapper.test.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { describe, expect, it } from 'vitest' +import { PolicyAlert } from '../../../src/model/policyAlert' +import { PolicyAlertDTO } from '../../../src/dto/policyAlert.dto' +import { PolicyAlertMap } from '../../../src/mapper/policyAlert.mapper' +import { policyAlertsDTO } from '../fixtures/dto' +import { policyAlertsModel } from '../fixtures/model' + +describe('policyAlert.mapper', () => { + it('should return a PolicyAlert object', () => { + const expected = policyAlertsModel[0] + + const result: PolicyAlert = PolicyAlertMap.toModel(policyAlertsDTO[0]) + + expect(result).toStrictEqual(expected) + }) + + it('should return a PolicyAlert DTO', () => { + const expected = policyAlertsDTO[1] + + const result: PolicyAlertDTO = PolicyAlertMap.toDTO(policyAlertsModel[1]) + + expect(result).toStrictEqual(expected) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/project.mapper.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/project.mapper.test.ts new file mode 100644 index 00000000..65960971 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/project.mapper.test.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { describe, expect, it } from 'vitest' +import { Project } from '../../../src/model/project' +import { ProjectDTO } from '../../../src/dto/project.dto' +import { ProjectMap } from '../../../src/mapper/project.mapper' +import { projectDTO } from '../fixtures/dto' +import { projectModel } from '../fixtures/model' + +describe('project.mapper', () => { + it('should return a Project object', () => { + const expected = projectModel + + const result: Project = ProjectMap.toModel(projectDTO) + + expect(result).toStrictEqual(expected) + }) + + it('should return a Project DTO', () => { + const expected = projectDTO + + const result: ProjectDTO = ProjectMap.toDTO(projectModel) + + expect(result).toStrictEqual(expected) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/projectVitals.mapper.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/projectVitals.mapper.test.ts new file mode 100644 index 00000000..81fb8d01 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/projectVitals.mapper.test.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { describe, expect, it } from 'vitest' +import { ProjectVitals } from '../../../src/model/projectVitals' +import { ProjectVitalsDTO } from '../../../src/dto/projectVitals.dto' +import { ProjectVitalsMap } from '../../../src/mapper/projectVitals.mapper' +import { projectVitalsDTO } from '../fixtures/dto' +import { projectVitalsModel } from '../fixtures/model' + +describe('projectVitals.mapper', () => { + it('should return a ProjectVitals object', () => { + const expected = projectVitalsModel + + const result: ProjectVitals = ProjectVitalsMap.toModel(projectVitalsDTO) + + expect(result).toStrictEqual(expected) + }) + + it('should return a ProjectVitals DTO', () => { + const expected = projectVitalsDTO + + const result: ProjectVitalsDTO = ProjectVitalsMap.toDTO(projectVitalsModel) + + expect(result).toStrictEqual(expected) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/rejectedInUseAlert.mapper.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/rejectedInUseAlert.mapper.test.ts new file mode 100644 index 00000000..f88288a6 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/rejectedInUseAlert.mapper.test.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { describe, expect, it } from 'vitest' +import { RejectedInUseAlert } from '../../../src/model/rejectedInUseAlert' +import { RejectedInUseAlertDTO } from '../../../src/dto/rejectedInUseAlert.dto' +import { RejectedInUseAlertMap } from '../../../src/mapper/rejectedInUseAlert.mapper' +import { rejectedInUseAlertsDTO } from '../fixtures/dto' +import { rejectedInUseAlertsModel } from '../fixtures/model' + +describe('rejectedInUseAlert.mapper', () => { + it('should return a RejectedInUseAlert object', () => { + const expected = rejectedInUseAlertsModel[0] + + const result: RejectedInUseAlert = RejectedInUseAlertMap.toModel( + rejectedInUseAlertsDTO[0] + ) + + expect(result).toStrictEqual(expected) + }) + + it('should return a RejectedInUseAlert DTO', () => { + const expected = rejectedInUseAlertsDTO[1] + + const result: RejectedInUseAlertDTO = RejectedInUseAlertMap.toDTO( + rejectedInUseAlertsModel[1] + ) + + expect(result).toStrictEqual(expected) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/securityAlert.mapper.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/securityAlert.mapper.test.ts new file mode 100644 index 00000000..e530200b --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/securityAlert.mapper.test.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { describe, expect, it } from 'vitest' +import { SecurityAlert } from '../../../src/model/securityAlert' +import { SecurityAlertDTO } from '../../../src/dto/securityAlert.dto' +import { SecurityAlertMap } from '../../../src/mapper/securityAlert.mapper' +import { securityAlertsDTO } from '../fixtures/dto' +import { securityAlertsModel } from '../fixtures/model' + +describe('securityAlert.mapper', () => { + it('should return a SecurityAlert object', () => { + const expected = securityAlertsModel[0] + + const result: SecurityAlert = SecurityAlertMap.toModel(securityAlertsDTO[0]) + + expect(result).toStrictEqual(expected) + }) + + it('should return a SecurityAlert DTO', () => { + const expected = securityAlertsDTO[1] + + const result: SecurityAlertDTO = SecurityAlertMap.toDTO( + securityAlertsModel[1] + ) + + expect(result).toStrictEqual(expected) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/vulnerability.mapper.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/vulnerability.mapper.test.ts new file mode 100644 index 00000000..cebe218c --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/vulnerability.mapper.test.ts @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { describe, expect, it } from 'vitest' +import { Vulnerability } from '../../../src/model/vulnerability' +import { VulnerabilityDTO } from '../../../src/dto/vulnerability.dto' +import { VulnerabilityMap } from '../../../src/mapper/vulnerability.mapper' +import { vulnerabilitiesDTO } from '../fixtures/dto' +import { vulnerabilitiesModel } from '../fixtures/model' + +describe('vulnerability.mapper', () => { + it('should return a Vulnerability object when DTO has no references', () => { + const expected = vulnerabilitiesModel[0] + + const result: Vulnerability = VulnerabilityMap.toModel( + vulnerabilitiesDTO[0] + ) + + expect(result).toStrictEqual(expected) + }) + + it('should return a Vulnerability object when DTO includes references', () => { + const expected = vulnerabilitiesModel[1] + + const result: Vulnerability = VulnerabilityMap.toModel( + vulnerabilitiesDTO[1] + ) + + expect(result).toStrictEqual(expected) + }) + + it('should return a Vulnerability DTO when Model has no references', () => { + const expected = vulnerabilitiesDTO[0] + + const result: VulnerabilityDTO = VulnerabilityMap.toDTO( + vulnerabilitiesModel[0] + ) + + expect(result).toStrictEqual(expected) + }) + + it('should return a Vulnerability DTO when Model includes references', () => { + const expected = vulnerabilitiesDTO[1] + + const result: VulnerabilityDTO = VulnerabilityMap.toDTO( + vulnerabilitiesModel[1] + ) + + expect(result).toStrictEqual(expected) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/vulnerabilityFixSummary.mapper.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/vulnerabilityFixSummary.mapper.test.ts new file mode 100644 index 00000000..c8a5b492 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/mapper/vulnerabilityFixSummary.mapper.test.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { describe, expect, it } from 'vitest' +import { VulnerabilityFixSummary } from '../../../src/model/vulnerabilityFixSummary' +import { VulnerabilityFixSummaryDTO } from '../../../src/dto/vulnerabilityFixSummary.dto' +import { VulnerabilityFixSummaryMap } from '../../../src/mapper/vulnerabilityFixSummary.mapper' +import { vulnerabilityFixSummaryModel } from '../fixtures/model' +import { vulnerabilitiesFixSummaryDTO } from '../fixtures/dto' +describe('vulnerabilityFixSummary.mapper', () => { + it('should return a Vulnerability Fix Summary object', () => { + const expected = vulnerabilityFixSummaryModel + + const result: VulnerabilityFixSummary = VulnerabilityFixSummaryMap.toModel( + vulnerabilitiesFixSummaryDTO + ) + + expect(result).toStrictEqual(expected) + }) + + it('should return a Vulnerability Fix Summary DTO', () => { + const expected = vulnerabilitiesFixSummaryDTO + + const result: VulnerabilityFixSummaryDTO = VulnerabilityFixSummaryMap.toDTO( + vulnerabilityFixSummaryModel + ) + + expect(result).toStrictEqual(expected) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/run.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/run.test.ts new file mode 100644 index 00000000..8f79ec1d --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/run.test.ts @@ -0,0 +1,1136 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { run } from '../../src/run' +import { AlertService } from '../../src/service/alert.service' +import { LibraryService } from '../../src/service/library.service' +import { OrganizationService } from '../../src/service/organization.service' +import { ProjectService } from '../../src/service/project.service' +import { VulnerabilityService } from '../../src/service/vulnerability.service' +import { + RequestError, + ResponseError, + UnexpectedDataError, +} from '../../src/fetcher/errors.fetcher' +import { + librariesModel, + organizationModel, + policyAlertsModel, + newVersionsAlertsModel, + multipleLicensesAlertsModel, + rejectedInUseAlertsModel, + projectModel, + projectVitalsModel, + securityAlertsModel, + vulnerabilitiesModel, + vulnerabilityFixSummaryModel, +} from './fixtures/model' +import * as autopilotUtils from '@B-S-F/autopilot-utils' +import { vulnerabilitiesFixSummaryData } from './fixtures/data' + +describe('run', () => { + vi.mock('AlertService', () => { + const AlertService = vi.fn() + AlertService.prototype.getPolicyAlertsById = vi.fn() + AlertService.prototype.getSecurityAlertsById = vi.fn() + AlertService.prototype.getNewVersionsAlertsById = vi.fn() + AlertService.prototype.getMultipleLicensesAlertsById = vi.fn() + AlertService.prototype.getRejectedInUseAlertsById = vi.fn() + return { AlertService } + }) + + vi.mock('../../src/utils/export', () => ({ + exportJson: vi.fn(), + })) + + vi.mock('../src/service/library.service', () => { + const LibraryService = vi.fn() + LibraryService.prototype.getAllLibrariesById = vi.fn() + return { LibraryService } + }) + + vi.mock('../src/service/organization.service', () => { + const OrganizationService = vi.fn() + OrganizationService.prototype.getOrganizationById = vi.fn() + return { OrganizationService } + }) + + vi.mock('../src/service/project.service', () => { + const ProjectService = vi.fn() + ProjectService.prototype.getProjectByToken = vi.fn() + return { ProjectService } + }) + + vi.mock('../src/service/vulnerability.service', () => { + const VulnerabilityService = vi.fn() + VulnerabilityService.prototype.getAllVulnerabilitiesById = vi.fn() + VulnerabilityService.prototype.getAllVulnerabilitiesFixSummaryById = vi.fn() + return { VulnerabilityService } + }) + + process.exit = vi.fn() + + beforeEach(() => { + // clear environment + delete process.env.MEND_API_URL + delete process.env.MEND_SERVER_URL + delete process.env.MEND_ORG_TOKEN + delete process.env.MEND_PROJECT_ID + delete process.env.MEND_PROJECT_TOKEN + delete process.env.MEND_USER_EMAIL + delete process.env.MEND_USER_KEY + delete process.env.MEND_REPORT_TYPE + delete process.env.MEND_ALERTS_STATUS + delete process.env.MEND_MIN_CONNECTION_TIME + delete process.env.MEND_MAX_CONCURRENT_CONNECTIONS + delete process.env.MEND_RESULTS_PATH + }) + + afterEach(() => { + vi.unstubAllEnvs() + vi.clearAllMocks() + vi.restoreAllMocks() + }) + + describe('Environment variables validation', () => { + describe('Undefined required environment variables', () => { + beforeEach(() => { + vi.stubEnv('MEND_API_URL', 'https://foo.bar') + vi.stubEnv('MEND_SERVER_URL', 'https://bar.foo') + vi.stubEnv('MEND_ORG_TOKEN', 'dummy2-token') + vi.stubEnv('MEND_PROJECT_TOKEN', 'dummy3-token') + vi.stubEnv('MEND_USER_EMAIL', 'dummy1@some.gTLD') + vi.stubEnv('MEND_USER_KEY', 'dummy1-userkey') + }) + + it.each([ + { name: 'MEND_API_URL' }, + { name: 'MEND_SERVER_URL' }, + { name: 'MEND_ORG_TOKEN' }, + { name: 'MEND_PROJECT_TOKEN' }, + { name: 'MEND_USER_EMAIL' }, + { name: 'MEND_USER_KEY' }, + ])( + 'should set status FAILED when $name is not set', + async (envVariable) => { + delete process.env[`${envVariable.name}`] + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + + expect.assertions(2) + await run() + + expect(spyStatus).toHaveBeenCalledWith('FAILED') + expect(spyReason).toHaveBeenCalledWith( + 'Environment validation failed: ' + + `${envVariable.name} ` + + 'Required' + ) + } + ) + + it('should set status FAILED when no required env variables are set', async () => { + delete process.env.MEND_API_URL + delete process.env.MEND_SERVER_URL + delete process.env.MEND_ORG_TOKEN + delete process.env.MEND_PROJECT_TOKEN + delete process.env.MEND_USER_EMAIL + delete process.env.MEND_USER_KEY + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + + expect.assertions(2) + await run() + + expect(spyStatus).toHaveBeenCalledWith('FAILED') + expect(spyReason).toHaveBeenCalledWith( + `Environment validation failed:` + + ` MEND_API_URL Required,` + + ` MEND_SERVER_URL Required,` + + ` MEND_ORG_TOKEN Required,` + + ` MEND_PROJECT_TOKEN Required,` + + ` MEND_USER_EMAIL Required,` + + ` MEND_USER_KEY Required` + ) + }) + }) + + describe('Empty environment variables', () => { + beforeEach(() => { + vi.stubEnv('MEND_API_URL', 'https://foo.bar') + vi.stubEnv('MEND_SERVER_URL', 'https://bar.foo') + vi.stubEnv('MEND_ORG_TOKEN', 'dummy2-token') + vi.stubEnv('MEND_PROJECT_ID', '123321') + vi.stubEnv('MEND_PROJECT_TOKEN', 'dummy3-token') + vi.stubEnv('MEND_USER_EMAIL', 'dummy1@some.gTLD') + vi.stubEnv('MEND_USER_KEY', 'dummy1-userkey') + vi.stubEnv('MEND_REPORT_TYPE', 'vulnerabilities') + vi.stubEnv('MEND_ALERTS_STATUS', 'active') + vi.stubEnv('MEND_MIN_CONNECTION_TIME', '123') + vi.stubEnv('MEND_MAX_CONCURRENT_CONNECTIONS', '321') + vi.stubEnv('MEND_RESULTS_PATH', 'dummy4-path') + }) + + afterEach(() => { + vi.unstubAllEnvs() + vi.clearAllMocks() + }) + + it.each([ + { + name: 'MEND_ORG_TOKEN', + value: '', + errorMessage: 'String must contain at least 1 character(s)', + }, + { + name: 'MEND_PROJECT_TOKEN', + value: '', + errorMessage: 'String must contain at least 1 character(s)', + }, + { + name: 'MEND_USER_KEY', + value: '', + errorMessage: 'String must contain at least 1 character(s)', + }, + ])( + 'should set status to FAILED when $name is set, but empty', + async (envVariable) => { + delete process.env[`${envVariable.name}`] + vi.stubEnv(`${envVariable.name}`, envVariable.value) + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + + expect.assertions(2) + await run() + + expect(spyStatus).toHaveBeenCalledWith('FAILED') + expect(spyReason).toHaveBeenCalledWith( + `Environment validation failed:` + + ` ${envVariable.name}` + + ` ${envVariable.errorMessage}` + ) + } + ) + }) + + describe('Malformed environment variables', () => { + beforeEach(() => { + vi.stubEnv('MEND_API_URL', 'https://foo.bar') + vi.stubEnv('MEND_SERVER_URL', 'https://bar.foo') + vi.stubEnv('MEND_ORG_TOKEN', 'dummy2-token') + vi.stubEnv('MEND_PROJECT_ID', '123321,,125521') + vi.stubEnv( + 'MEND_PROJECT_TOKEN', + 'dummy3-token,dummy2-token,dummy1-token' + ) + vi.stubEnv('MEND_USER_EMAIL', 'dummy1@some.gTLD') + vi.stubEnv('MEND_USER_KEY', 'dummy1-userkey') + vi.stubEnv('MEND_REPORT_TYPE', 'vulnerabilities') + vi.stubEnv('MEND_ALERTS_STATUS', 'active') + vi.stubEnv('MEND_MIN_CONNECTION_TIME', '123') + vi.stubEnv('MEND_MAX_CONCURRENT_CONNECTIONS', '321') + vi.stubEnv('MEND_RESULTS_PATH', 'dummy4-path') + }) + + afterEach(() => { + vi.unstubAllEnvs() + vi.clearAllMocks() + }) + + it.each([ + { + name: 'MEND_API_URL', + value: 'foo.bar', + errorMessage: 'MEND_API_URL Invalid url', + }, + { + name: 'MEND_SERVER_URL', + value: 'bar.foo', + errorMessage: 'MEND_SERVER_URL Invalid url', + }, + { + name: 'MEND_PROJECT_ID', + value: 'confused', + errorMessage: + 'MEND_PROJECT_ID Must be a number or numbers splitted by a comma.', + }, + { + name: 'MEND_PROJECT_ID', + value: 'confused,double-confused,tripple-confused', + errorMessage: + 'MEND_PROJECT_ID Must be a number or numbers splitted by a comma.', + }, + { + name: 'MEND_PROJECT_ID', + value: '123321,', + errorMessage: + 'MEND_PROJECT_TOKEN and MEND_PROJECT_ID should be of equal length or MEND_PROJECT_ID should be empty.', + }, + { + name: 'MEND_PROJECT_TOKEN', + value: 'dummy3-token,', + errorMessage: 'MEND_PROJECT_TOKEN Unexpected trailing comma', + }, + { + name: 'MEND_PROJECT_TOKEN', + value: 'dummy3-token,dummy2-token', + errorMessage: + 'MEND_PROJECT_TOKEN and MEND_PROJECT_ID should be of equal length or MEND_PROJECT_ID should be empty.', + }, + { + name: 'MEND_USER_EMAIL', + value: 'foo', + errorMessage: 'MEND_USER_EMAIL Invalid email', + }, + { + name: 'MEND_REPORT_TYPE', + value: 'invalid report type', + errorMessage: + `MEND_REPORT_TYPE Invalid enum value. ` + + `Expected 'alerts' | 'vulnerabilities', received 'invalid report type'`, + }, + { + name: 'MEND_ALERTS_STATUS', + value: 'nonexisting alerts status', + errorMessage: + `MEND_ALERTS_STATUS Invalid enum value. Expected 'all' | 'active' | 'ignored' | 'library_removed' | 'library_in_house' | 'library_whitelist', ` + + `received 'nonexisting alerts status'`, + }, + { + name: 'MEND_MIN_CONNECTION_TIME', + value: 'infinity', + errorMessage: + 'MEND_MIN_CONNECTION_TIME Expected number, received not a number', + }, + { + name: 'MEND_MAX_CONCURRENT_CONNECTIONS', + value: 'boatloads', + errorMessage: + 'MEND_MAX_CONCURRENT_CONNECTIONS Expected number, received not a number', + }, + ])( + 'should set status to FAILED when $name is malformed', + async (envVariable) => { + delete process.env[`${envVariable.name}`] + vi.stubEnv(`${envVariable.name}`, envVariable.value) + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + + expect.assertions(2) + await run() + + expect(spyStatus).toHaveBeenCalledWith('FAILED') + expect(spyReason).toHaveBeenCalledWith( + `Environment validation failed:` + ` ${envVariable.errorMessage}` + ) + } + ) + }) + }) + + describe('Expected FAILED status', () => { + beforeEach(() => { + vi.stubEnv('MEND_API_URL', 'https://foo.bar') + vi.stubEnv('MEND_SERVER_URL', 'https://bar.foo') + vi.stubEnv('MEND_ORG_TOKEN', 'dummy2-token') + vi.stubEnv('MEND_RPOJECT_ID', '123321') + vi.stubEnv('MEND_PROJECT_TOKEN', 'dummy3-token') + vi.stubEnv('MEND_USER_EMAIL', 'dummy1@some.gTLD') + vi.stubEnv('MEND_USER_KEY', 'dummy1-userkey') + vi.stubEnv('MEND_REPORT_TYPE', 'vulnerabilities') + vi.stubEnv('MEND_ALERTS_STATUS', 'active') + vi.stubEnv('MEND_MIN_CONNECTION_TIME', '123') + vi.stubEnv('MEND_MAX_CONCURRENT_CONNECTIONS', '321') + vi.stubEnv('MEND_RESULTS_PATH', 'dummy4-path') + }) + + it('should set status FAILED when an UnexpectedDataError is thrown', async () => { + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + const spyOrg = vi.spyOn( + OrganizationService.prototype, + 'getOrganizationById' + ) + spyOrg.mockRejectedValueOnce( + new UnexpectedDataError('UnexpectedDataError was thrown') + ) + + expect.assertions(2) + await run() + + expect(spyStatus).toHaveBeenCalledWith('FAILED') + expect(spyReason).toHaveBeenCalledWith('UnexpectedDataError was thrown') + }) + + it('should set status FAILED when a RequestError is thrown', async () => { + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + const spyOrg = vi.spyOn( + OrganizationService.prototype, + 'getOrganizationById' + ) + spyOrg.mockRejectedValueOnce(new RequestError('Error message')) + + expect.assertions(2) + await run() + + expect(spyStatus).toHaveBeenCalledWith('FAILED') + expect(spyReason).toHaveBeenCalledWith('RequestError: Error message') + }) + + it('should set status FAILED when a ResponseError is thrown', async () => { + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + const spyOrg = vi.spyOn( + OrganizationService.prototype, + 'getOrganizationById' + ) + spyOrg.mockRejectedValueOnce( + new ResponseError('ResponseError was thrown') + ) + + expect.assertions(2) + await run() + + expect(spyStatus).toHaveBeenCalledWith('FAILED') + expect(spyReason).toHaveBeenCalledWith('ResponseError was thrown') + }) + }) + + describe.each([ + { name: 'MEND_PROJECT_ID', value: '123321' }, + { name: 'MEND_PROJECT_ID', value: '123321' }, + ])('Expected RED status', (envVariable) => { + beforeEach(() => { + vi.stubEnv('MEND_API_URL', 'https://foo.bar') + vi.stubEnv('MEND_SERVER_URL', 'https://bar.foo') + vi.stubEnv('MEND_ORG_TOKEN', 'dummy2-token') + if (envVariable.value) { + vi.stubEnv(envVariable.name, envVariable.value) + } + vi.stubEnv('MEND_PROJECT_TOKEN', 'dummy3-token') + vi.stubEnv('MEND_USER_EMAIL', 'dummy1@some.gTLD') + vi.stubEnv('MEND_USER_KEY', 'dummy1-userkey') + vi.stubEnv('MEND_REPORT_TYPE', 'vulnerabilities') + vi.stubEnv('MEND_ALERTS_STATUS', 'active') + vi.stubEnv('MEND_MIN_CONNECTION_TIME', '123') + vi.stubEnv('MEND_MAX_CONCURRENT_CONNECTIONS', '321') + vi.stubEnv('MEND_RESULTS_PATH', 'dummy4-path') + }) + + it('should set RED status when vulnerabilities are found', async () => { + if (!envVariable.value) { + delete process.env[`${envVariable.name}`] + } + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + const spyResult = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'addResult' + ) + const spyOrganizationService = vi.spyOn( + OrganizationService.prototype, + 'getOrganizationById' + ) + spyOrganizationService.mockReturnValueOnce( + Promise.resolve(organizationModel) + ) + const spyProjectService = vi.spyOn( + ProjectService.prototype, + 'getProjectByToken' + ) + spyProjectService.mockReturnValueOnce(Promise.resolve(projectModel)) + const spyVitalsProjectService = vi.spyOn( + ProjectService.prototype, + 'getProjectVitals' + ) + spyVitalsProjectService.mockReturnValueOnce( + Promise.resolve(projectVitalsModel) + ) + const spyLibraryService = vi.spyOn( + LibraryService.prototype, + 'getAllLibrariesById' + ) + spyLibraryService.mockReturnValueOnce(Promise.resolve(librariesModel)) + const spyVulnerabilityService = vi.spyOn( + VulnerabilityService.prototype, + 'getAllVulnerabilitiesById' + ) + + const spyVulnerabilityServiceFix = vi.spyOn( + VulnerabilityService.prototype, + 'getAllVulnerabilitiesFixSummaryById' + ) + + spyVulnerabilityService.mockReturnValueOnce( + Promise.resolve([vulnerabilitiesModel[0], vulnerabilitiesModel[1]]) + ) + spyVulnerabilityService.mockReturnValueOnce( + Promise.resolve([vulnerabilitiesModel[1], vulnerabilitiesModel[0]]) + ) + spyVulnerabilityServiceFix.mockReturnValueOnce( + Promise.resolve(vulnerabilityFixSummaryModel) + ) + spyVulnerabilityServiceFix.mockReturnValueOnce( + Promise.resolve(vulnerabilityFixSummaryModel) + ) + spyVulnerabilityServiceFix.mockReturnValueOnce( + Promise.resolve(vulnerabilityFixSummaryModel) + ) + spyVulnerabilityServiceFix.mockReturnValueOnce( + Promise.resolve(vulnerabilityFixSummaryModel) + ) + const reasonLinkTemplate = + envVariable.value !== undefined + ? `${process.env.MEND_SERVER_URL}/Wss/WSS.html#!project;` + + `orgToken=${organizationModel.uuid};` + + `id=${process.env.MEND_PROJECT_ID}` + : `${process.env.MEND_SERVER_URL}/Wss/WSS.html#!project;` + + `orgToken=${organizationModel.uuid}` + const resultLinkTemplate = + envVariable.value !== undefined + ? `${process.env.MEND_SERVER_URL}/Wss/WSS.html#!libraryDetails;` + + `orgToken=${organizationModel.uuid};` + + `project=${process.env.MEND_PROJECT_ID};` + : `${process.env.MEND_SERVER_URL}/Wss/WSS.html#!libraryDetails;` + + `orgToken=${organizationModel.uuid};` + + expect.assertions(vulnerabilitiesModel.length * librariesModel.length + 2) + await run() + expect(spyResult).toHaveBeenNthCalledWith(1, { + criterion: 'Open Vulnerability Mend', + justification: vulnerabilitiesModel[0].description, + fulfilled: false, + metadata: { + project: projectModel.name, + name: `${vulnerabilitiesModel[0].name}:${librariesModel[0].name}`, + severity: vulnerabilitiesModel[0].severity, + score: `${vulnerabilitiesModel[0].score}`, + link: resultLinkTemplate + `uuid=${librariesModel[0].uuid};`, + description: vulnerabilitiesModel[0].description, + topFix: + vulnerabilityFixSummaryModel?.topRankedFix.message + + ' ' + + vulnerabilitiesFixSummaryData?.topRankedFix.fixResolution, + }, + }) + expect(spyResult).toHaveBeenNthCalledWith(2, { + criterion: 'Open Vulnerability Mend', + justification: vulnerabilitiesModel[1].description, + fulfilled: false, + metadata: { + project: projectModel.name, + name: `${vulnerabilitiesModel[1].name}:${librariesModel[0].name}`, + severity: vulnerabilitiesModel[1].severity, + score: `${vulnerabilitiesModel[1].score}`, + link: resultLinkTemplate + `uuid=${librariesModel[0].uuid};`, + description: vulnerabilitiesModel[1].description, + topFix: + vulnerabilityFixSummaryModel?.topRankedFix.message + + ' ' + + vulnerabilitiesFixSummaryData?.topRankedFix.fixResolution, + }, + }) + expect(spyResult).toHaveBeenNthCalledWith(3, { + criterion: 'Open Vulnerability Mend', + justification: vulnerabilitiesModel[1].description, + fulfilled: false, + metadata: { + project: projectModel.name, + name: `${vulnerabilitiesModel[1].name}:${librariesModel[1].name}`, + severity: vulnerabilitiesModel[1].severity, + score: `${vulnerabilitiesModel[1].score}`, + link: resultLinkTemplate + `uuid=${librariesModel[1].uuid};`, + description: vulnerabilitiesModel[1].description, + topFix: + vulnerabilityFixSummaryModel?.topRankedFix.message + + ' ' + + vulnerabilitiesFixSummaryData?.topRankedFix.fixResolution, + }, + }) + expect(spyResult).toHaveBeenNthCalledWith(4, { + criterion: 'Open Vulnerability Mend', + justification: vulnerabilitiesModel[0].description, + fulfilled: false, + metadata: { + project: projectModel.name, + name: `${vulnerabilitiesModel[0].name}:${librariesModel[1].name}`, + severity: vulnerabilitiesModel[0].severity, + score: `${vulnerabilitiesModel[0].score}`, + link: resultLinkTemplate + `uuid=${librariesModel[1].uuid};`, + description: vulnerabilitiesModel[0].description, + topFix: + vulnerabilityFixSummaryModel?.topRankedFix.message + + ' ' + + vulnerabilitiesFixSummaryData?.topRankedFix.fixResolution, + }, + }) + expect(spyStatus).toHaveBeenCalledWith('RED') + expect(spyReason).toHaveBeenCalledWith( + `${ + vulnerabilitiesModel.length * librariesModel.length + } vulnerabilities were found, last scan was executed on ${ + projectVitalsModel.lastScan + }` + + `, see more details in Mend ` + + reasonLinkTemplate + + `${ + envVariable.value === undefined + ? ' in project ' + projectModel.name + ';' + : '' + }` + + `;` + ) + }) + + it('should set RED status when alerts are found', async () => { + vi.stubEnv('MEND_REPORT_TYPE', 'alerts') + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + const spyResult = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'addResult' + ) + const spyOrganizationService = vi.spyOn( + OrganizationService.prototype, + 'getOrganizationById' + ) + spyOrganizationService.mockReturnValueOnce( + Promise.resolve(organizationModel) + ) + const spyProjectService = vi.spyOn( + ProjectService.prototype, + 'getProjectByToken' + ) + spyProjectService.mockReturnValueOnce(Promise.resolve(projectModel)) + const spyVitalsProjectService = vi.spyOn( + ProjectService.prototype, + 'getProjectVitals' + ) + spyVitalsProjectService.mockReturnValueOnce( + Promise.resolve(projectVitalsModel) + ) + const spyPolicyAlertService = vi.spyOn( + AlertService.prototype, + 'getPolicyAlertsById' + ) + spyPolicyAlertService.mockReturnValueOnce( + Promise.resolve(policyAlertsModel) + ) + const spyNewVersionAlertService = vi.spyOn( + AlertService.prototype, + 'getNewVersionsAlertsById' + ) + spyNewVersionAlertService.mockReturnValueOnce( + Promise.resolve(newVersionsAlertsModel) + ) + const spyMultipleLicensesAlertService = vi.spyOn( + AlertService.prototype, + 'getMultipleLicensesAlertsById' + ) + spyMultipleLicensesAlertService.mockReturnValueOnce( + Promise.resolve(multipleLicensesAlertsModel) + ) + const spyRejectedInUseAlertService = vi.spyOn( + AlertService.prototype, + 'getRejectedInUseAlertsById' + ) + spyRejectedInUseAlertService.mockReturnValueOnce( + Promise.resolve(rejectedInUseAlertsModel) + ) + const spySecurityAlertService = vi.spyOn( + AlertService.prototype, + 'getSecurityAlertsById' + ) + spySecurityAlertService.mockReturnValueOnce( + Promise.resolve(securityAlertsModel) + ) + const reasonLinkTemplate = + envVariable.value !== undefined + ? `${process.env.MEND_SERVER_URL}/Wss/WSS.html#!project;` + + `orgToken=${organizationModel.uuid};` + + `id=${process.env.MEND_PROJECT_ID}` + : `${process.env.MEND_SERVER_URL}/Wss/WSS.html#!project;` + + `orgToken=${organizationModel.uuid}` + const resultLinkTemplate = + `${process.env.MEND_SERVER_URL}/Wss/WSS.html#!libraryDetails;` + + `orgToken=${organizationModel.uuid};` + + `project=${process.env.MEND_PROJECT_ID};` + + expect.assertions( + policyAlertsModel.length + + securityAlertsModel.length + + newVersionsAlertsModel.length + + multipleLicensesAlertsModel.length + + rejectedInUseAlertsModel.length + + 2 + ) + await run() + + expect(spyResult).toHaveBeenNthCalledWith(1, { + criterion: 'Open Policy Alert Mend', + justification: policyAlertsModel[0].component.name, + fulfilled: false, + metadata: { + project: projectModel.name, + name: policyAlertsModel[0].component.name, + status: policyAlertsModel[0].alertInfo.status, + link: + resultLinkTemplate + `uuid=${policyAlertsModel[0].component.uuid};`, + description: policyAlertsModel[0].component.description, + }, + }) + expect(spyResult).toHaveBeenNthCalledWith(2, { + criterion: 'Open Policy Alert Mend', + justification: policyAlertsModel[1].component.name, + fulfilled: false, + metadata: { + project: projectModel.name, + name: policyAlertsModel[1].component.name, + status: policyAlertsModel[1].alertInfo.status, + link: + resultLinkTemplate + `uuid=${policyAlertsModel[1].component.uuid};`, + description: policyAlertsModel[1].component.description, + }, + }) + + expect(spyResult).toHaveBeenNthCalledWith(3, { + criterion: 'Open Security Alert Mend', + justification: securityAlertsModel[0].vulnerability.name, + fulfilled: false, + metadata: { + project: projectModel.name, + name: `${securityAlertsModel[0].vulnerability.name}:${securityAlertsModel[0].component.name}`, + severity: securityAlertsModel[0].vulnerability.severity, + score: `${securityAlertsModel[0].vulnerability.score}`, + status: securityAlertsModel[0].alertInfo.status, + link: + resultLinkTemplate + + `uuid=${securityAlertsModel[0].component.uuid};`, + description: securityAlertsModel[0].vulnerability.description, + }, + }) + expect(spyResult).toHaveBeenNthCalledWith(4, { + criterion: 'Open Security Alert Mend', + justification: securityAlertsModel[1].vulnerability.name, + fulfilled: false, + metadata: { + project: projectModel.name, + name: `${securityAlertsModel[1].vulnerability.name}:${securityAlertsModel[1].component.name}`, + severity: securityAlertsModel[1].vulnerability.severity, + score: `${securityAlertsModel[1].vulnerability.score}`, + status: securityAlertsModel[1].alertInfo.status, + link: + resultLinkTemplate + + `uuid=${securityAlertsModel[1].component.uuid};`, + description: securityAlertsModel[1].vulnerability.description, + }, + }) + expect(spyResult).toHaveBeenNthCalledWith(5, { + criterion: 'New Versions Alert Mend', + justification: + newVersionsAlertsModel[0].component.name + + ':' + + newVersionsAlertsModel[0].availableVersionType + + ' ' + + newVersionsAlertsModel[0].availableVersion, + fulfilled: false, + metadata: { + project: projectModel.name, + name: newVersionsAlertsModel[0].component.name, + status: newVersionsAlertsModel[0].alertInfo.status, + link: + resultLinkTemplate + + `uuid=${newVersionsAlertsModel[0].component.uuid};`, + description: newVersionsAlertsModel[0].component.description, + }, + }) + expect(spyResult).toHaveBeenNthCalledWith(6, { + criterion: 'New Versions Alert Mend', + justification: + newVersionsAlertsModel[1].component.name + + ':' + + newVersionsAlertsModel[1].availableVersionType + + ' ' + + newVersionsAlertsModel[1].availableVersion, + fulfilled: false, + metadata: { + project: projectModel.name, + name: newVersionsAlertsModel[1].component.name, + status: newVersionsAlertsModel[1].alertInfo.status, + link: + resultLinkTemplate + + `uuid=${newVersionsAlertsModel[1].component.uuid};`, + description: newVersionsAlertsModel[1].component.description, + }, + }) + + expect(spyResult).toHaveBeenNthCalledWith(7, { + criterion: 'Multiple Licenses Alert Mend', + justification: + multipleLicensesAlertsModel[0].component.name + + ':' + + multipleLicensesAlertsModel[0].numberOfLicenses + + ' ' + + multipleLicensesAlertsModel[0].licenses, + fulfilled: false, + metadata: { + project: projectModel.name, + name: multipleLicensesAlertsModel[0].component.name, + status: multipleLicensesAlertsModel[0].alertInfo.status, + link: + resultLinkTemplate + + `uuid=${multipleLicensesAlertsModel[0].component.uuid};`, + description: multipleLicensesAlertsModel[0].component.description, + }, + }) + expect(spyResult).toHaveBeenNthCalledWith(8, { + criterion: 'Multiple Licenses Alert Mend', + justification: + multipleLicensesAlertsModel[1].component.name + + ':' + + multipleLicensesAlertsModel[1].numberOfLicenses + + ' ' + + multipleLicensesAlertsModel[1].licenses, + fulfilled: false, + metadata: { + project: projectModel.name, + name: multipleLicensesAlertsModel[1].component.name, + status: multipleLicensesAlertsModel[1].alertInfo.status, + link: + resultLinkTemplate + + `uuid=${multipleLicensesAlertsModel[1].component.uuid};`, + description: multipleLicensesAlertsModel[1].component.description, + }, + }) + expect(spyResult).toHaveBeenNthCalledWith(9, { + criterion: 'Rejected In Use Alert Mend', + justification: + rejectedInUseAlertsModel[0].component.name + + ':' + + rejectedInUseAlertsModel[0].description, + fulfilled: false, + metadata: { + project: projectModel.name, + name: rejectedInUseAlertsModel[0].component.name, + status: rejectedInUseAlertsModel[0].alertInfo.status, + link: + resultLinkTemplate + + `uuid=${rejectedInUseAlertsModel[0].component.uuid};`, + description: rejectedInUseAlertsModel[0].component.description, + }, + }) + expect(spyResult).toHaveBeenNthCalledWith(10, { + criterion: 'Rejected In Use Alert Mend', + justification: + rejectedInUseAlertsModel[1].component.name + + ':' + + rejectedInUseAlertsModel[1].description, + fulfilled: false, + metadata: { + project: projectModel.name, + name: rejectedInUseAlertsModel[1].component.name, + status: rejectedInUseAlertsModel[1].alertInfo.status, + link: + resultLinkTemplate + + `uuid=${rejectedInUseAlertsModel[1].component.uuid};`, + description: rejectedInUseAlertsModel[1].component.description, + }, + }) + expect(spyStatus).toHaveBeenCalledWith('RED') + expect(spyReason).toHaveBeenCalledWith( + `${ + policyAlertsModel.length + + securityAlertsModel.length + + newVersionsAlertsModel.length + + multipleLicensesAlertsModel.length + + rejectedInUseAlertsModel.length + } alerts were found, last scan was executed on ${ + projectVitalsModel.lastScan + }` + + `, see more details in Mend ` + + reasonLinkTemplate + + `${ + envVariable.value === undefined + ? ` in project ` + projectModel.name + : `` + }` + + `;` + ) + }) + }) + + describe.each([ + { name: 'MEND_PROJECT_ID', value: '123321' }, + { name: 'MEND_PROJECT_ID', value: undefined }, + ])('Expected GREEN status', (envVariable) => { + beforeEach(() => { + vi.stubEnv('MEND_API_URL', 'https://foo.bar') + vi.stubEnv('MEND_SERVER_URL', 'https://bar.foo') + vi.stubEnv('MEND_ORG_TOKEN', 'dummy2-token') + if (envVariable.value) { + vi.stubEnv(envVariable.name, envVariable.value) + } + vi.stubEnv('MEND_PROJECT_TOKEN', 'dummy3-token') + vi.stubEnv('MEND_USER_EMAIL', 'dummy1@some.gTLD') + vi.stubEnv('MEND_USER_KEY', 'dummy1-userkey') + vi.stubEnv('MEND_REPORT_TYPE', 'vulnerabilities') + vi.stubEnv('MEND_ALERTS_STATUS', 'active') + vi.stubEnv('MEND_MIN_CONNECTION_TIME', '123') + vi.stubEnv('MEND_MAX_CONCURRENT_CONNECTIONS', '321') + vi.stubEnv('MEND_RESULTS_PATH', 'dummy4-path') + }) + + it('should set GREEN status when no vulnerabilities are found', async () => { + if (!envVariable.value) { + delete process.env[`${envVariable.name}`] + } + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + const spyResult = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'addResult' + ) + const spyOrganizationService = vi.spyOn( + OrganizationService.prototype, + 'getOrganizationById' + ) + spyOrganizationService.mockReturnValueOnce( + Promise.resolve(organizationModel) + ) + const spyProjectService = vi.spyOn( + ProjectService.prototype, + 'getProjectByToken' + ) + spyProjectService.mockReturnValueOnce(Promise.resolve(projectModel)) + const spyVitalsProjectService = vi.spyOn( + ProjectService.prototype, + 'getProjectVitals' + ) + spyVitalsProjectService.mockReturnValueOnce( + Promise.resolve(projectVitalsModel) + ) + const spyLibraryService = vi.spyOn( + LibraryService.prototype, + 'getAllLibrariesById' + ) + spyLibraryService.mockReturnValueOnce(Promise.resolve(librariesModel)) + const spyVulnerabilityService = vi.spyOn( + VulnerabilityService.prototype, + 'getAllVulnerabilitiesById' + ) + + spyVulnerabilityService.mockReturnValueOnce(Promise.resolve([])) + spyVulnerabilityService.mockReturnValueOnce(Promise.resolve([])) + + const reasonLinkTemplate = + envVariable.value !== undefined + ? `${process.env.MEND_SERVER_URL}/Wss/WSS.html#!project;` + + `orgToken=${organizationModel.uuid};` + + `id=${process.env.MEND_PROJECT_ID}` + : `${process.env.MEND_SERVER_URL}/Wss/WSS.html` + + ` in organization ${organizationModel.name}` + + ` and project ${projectModel.name}` + + expect.assertions(3) + await run() + + expect(spyResult).toHaveBeenNthCalledWith(1, { + criterion: 'There are no open vulnerabilities in mend', + justification: 'No open vulnerabilities were found', + fulfilled: true, + metadata: {}, + }) + expect(spyStatus).toHaveBeenCalledWith('GREEN') + expect(spyReason).toHaveBeenCalledWith( + `No vulnerabilities were found, last scan was executed on ${projectVitalsModel.lastScan}` + + ',' + + ' see more details in Mend ' + + reasonLinkTemplate + + ';' + ) + }) + + it('should set GREEN status when no alerts are found', async () => { + if (!envVariable.value) { + delete process.env[`${envVariable.name}`] + } + vi.stubEnv('MEND_REPORT_TYPE', 'alerts') + const spyStatus = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setStatus' + ) + const spyReason = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'setReason' + ) + const spyResult = vi.spyOn( + autopilotUtils.AppOutput.prototype, + 'addResult' + ) + const spyOrganizationService = vi.spyOn( + OrganizationService.prototype, + 'getOrganizationById' + ) + spyOrganizationService.mockReturnValueOnce( + Promise.resolve(organizationModel) + ) + const spyProjectService = vi.spyOn( + ProjectService.prototype, + 'getProjectByToken' + ) + spyProjectService.mockReturnValueOnce(Promise.resolve(projectModel)) + const spyPolicyAlertService = vi.spyOn( + AlertService.prototype, + 'getPolicyAlertsById' + ) + const spyNewVersionAlertService = vi.spyOn( + AlertService.prototype, + 'getNewVersionsAlertsById' + ) + const spyMultipleLicensesAlertService = vi.spyOn( + AlertService.prototype, + 'getMultipleLicensesAlertsById' + ) + const spyRejectedInUseAlertService = vi.spyOn( + AlertService.prototype, + 'getRejectedInUseAlertsById' + ) + const spyVitalsProjectService = vi.spyOn( + ProjectService.prototype, + 'getProjectVitals' + ) + spyVitalsProjectService.mockReturnValueOnce( + Promise.resolve(projectVitalsModel) + ) + spyPolicyAlertService.mockReturnValueOnce(Promise.resolve([])) + spyNewVersionAlertService.mockReturnValueOnce(Promise.resolve([])) + spyMultipleLicensesAlertService.mockReturnValueOnce(Promise.resolve([])) + spyRejectedInUseAlertService.mockReturnValueOnce(Promise.resolve([])) + const spySecurityAlertService = vi.spyOn( + AlertService.prototype, + 'getSecurityAlertsById' + ) + spySecurityAlertService.mockReturnValueOnce(Promise.resolve([])) + const reasonLinkTemplate = + envVariable.value !== undefined + ? `${process.env.MEND_SERVER_URL}/Wss/WSS.html#!project;` + + `orgToken=${organizationModel.uuid};` + + `id=${process.env.MEND_PROJECT_ID}` + : `${process.env.MEND_SERVER_URL}/Wss/WSS.html` + + ` in organization ${organizationModel.name}` + + ` and project ${projectModel.name}` + + expect.assertions(3) + await run() + + expect(spyResult).toHaveBeenNthCalledWith(1, { + criterion: 'There are no open alerts in mend', + justification: 'No open alerts were found', + fulfilled: true, + metadata: {}, + }) + + expect(spyStatus).toHaveBeenCalledWith('GREEN') + expect(spyReason).toHaveBeenCalledWith( + `No alerts were found, last scan was executed on ${projectVitalsModel.lastScan}` + + `,` + + ` see more details in Mend ` + + reasonLinkTemplate + + `;` + ) + }) + }) + + describe('Unexpected app exit', () => { + beforeEach(() => { + vi.stubEnv('MEND_API_URL', 'https://foo.bar') + vi.stubEnv('MEND_SERVER_URL', 'https://bar.foo') + vi.stubEnv('MEND_ORG_TOKEN', 'dummy2-token') + vi.stubEnv('MEND_RPOJECT_ID', '123321') + vi.stubEnv('MEND_PROJECT_TOKEN', 'dummy3-token') + vi.stubEnv('MEND_USER_EMAIL', 'dummy1@some.gTLD') + vi.stubEnv('MEND_USER_KEY', 'dummy1-userkey') + vi.stubEnv('MEND_REPORT_TYPE', 'vulnerabilities') + vi.stubEnv('MEND_ALERTS_STATUS', 'active') + vi.stubEnv('MEND_MIN_CONNECTION_TIME', '123') + vi.stubEnv('MEND_MAX_CONCURRENT_CONNECTIONS', '321') + vi.stubEnv('MEND_RESULTS_PATH', 'dummy4-path') + }) + + it('should throw an error when unexpected ', async () => { + const spyOrg = vi.spyOn( + OrganizationService.prototype, + 'getOrganizationById' + ) + spyOrg.mockRejectedValueOnce(new Error()) + + expect.assertions(1) + const result = run() + + await expect(result).rejects.toThrowError() + }) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/service/alert.service.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/service/alert.service.test.ts new file mode 100644 index 00000000..3ac35d21 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/service/alert.service.test.ts @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { afterEach, describe, expect, it, vi } from 'vitest' +import { MendEnvironment } from '../../../src/model/mendEnvironment' +import { AlertService } from '../../../src/service/alert.service' +import { PolicyAlert } from '../../../src/model/policyAlert' +import { SecurityAlert } from '../../../src/model/securityAlert' +import * as AlertFetcher from '../../../src/fetcher/alert.fetcher' +import { envFixture } from '../fixtures/env' +import { FakeAuthenticator } from '../fixtures/fakeauth' +import { + multipleLicensesAlertsDTO, + newVersionsAlertsDTO, + policyAlertsDTO, + rejectedInUseAlertsDTO, +} from '../fixtures/dto' +import { + multipleLicensesAlertsModel, + newVersionsAlertsModel, + policyAlertsModel, + rejectedInUseAlertsModel, +} from '../fixtures/model' +import { securityAlertsDTO } from '../fixtures/dto' +import { securityAlertsModel } from '../fixtures/model' +import { MultipleLicensesAlert } from '../../../src/model/multipleLicensesAlert' +import { NewVersionsAlert } from '../../../src/model/newVersionsAlert' +import { RejectedInUseAlert } from '../../../src/model/rejectedInUseAlert' + +describe('alert.service', () => { + const env: MendEnvironment = envFixture + + vi.mock('Authenticator', () => { + const mock = { + getInstance: vi.fn(() => new FakeAuthenticator(env)), + } + return mock + }) + + afterEach(() => { + vi.clearAllMocks() + vi.restoreAllMocks() + }) + + describe('policy.alerts', () => { + it('should return a list of Policy Alerts', async () => { + const spy = vi.spyOn(AlertFetcher, 'getPolicyAlertDTOs') + spy.mockReturnValue(Promise.resolve(policyAlertsDTO)) + const expected: PolicyAlert[] = policyAlertsModel + + const alertService: AlertService = new AlertService(env) + const result: PolicyAlert[] = await alertService.getPolicyAlertsById( + env.projectToken, + 'all' + ) + + expect(result).toStrictEqual(expected) + }) + }) + + describe('multipleLicenses.alerts', () => { + it('should return a list of Multiple Licenses Alerts', async () => { + const spy = vi.spyOn(AlertFetcher, 'getMultipleLicensesAlertDTOs') + spy.mockReturnValue(Promise.resolve(multipleLicensesAlertsDTO)) + const expected: MultipleLicensesAlert[] = multipleLicensesAlertsModel + + const alertService: AlertService = new AlertService(env) + const result: MultipleLicensesAlert[] = + await alertService.getMultipleLicensesAlertsById( + env.projectToken, + 'all' + ) + + expect(result).toStrictEqual(expected) + }) + }) + + describe('newVersions.alerts', () => { + it('should return a list of New Versions Alerts', async () => { + const spy = vi.spyOn(AlertFetcher, 'getNewVersionsAlertDTOs') + spy.mockReturnValue(Promise.resolve(newVersionsAlertsDTO)) + const expected: NewVersionsAlert[] = newVersionsAlertsModel + + const alertService: AlertService = new AlertService(env) + const result: NewVersionsAlert[] = + await alertService.getNewVersionsAlertsById(env.projectToken, 'all') + + expect(result).toStrictEqual(expected) + }) + }) + + describe('rejectedInUse.alerts', () => { + it('should return a list of Rejected In Use Alerts', async () => { + const spy = vi.spyOn(AlertFetcher, 'getRejectedInUseAlertDTOs') + spy.mockReturnValue(Promise.resolve(rejectedInUseAlertsDTO)) + const expected: RejectedInUseAlert[] = rejectedInUseAlertsModel + + const alertService: AlertService = new AlertService(env) + const result: RejectedInUseAlert[] = + await alertService.getRejectedInUseAlertsById(env.projectToken, 'all') + + expect(result).toStrictEqual(expected) + }) + }) + + describe('security.alerts', () => { + it('should return a list of Security Alerts', async () => { + const spy = vi.spyOn(AlertFetcher, 'getSecurityAlertDTOs') + spy.mockReturnValue(Promise.resolve(securityAlertsDTO)) + const expected: SecurityAlert[] = securityAlertsModel + + const alertService: AlertService = new AlertService(env) + const result: SecurityAlert[] = await alertService.getSecurityAlertsById( + env.projectToken, + 'all' + ) + + expect(result).toStrictEqual(expected) + }) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/service/library.service.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/service/library.service.test.ts new file mode 100644 index 00000000..3ba5af68 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/service/library.service.test.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { afterEach, describe, expect, it, vi } from 'vitest' +import { MendEnvironment } from '../../../src/model/mendEnvironment' +import { Library } from '../../../src/model/library' +import { LibraryService } from '../../../src/service/library.service' +import * as LibraryFetcher from '../../../src/fetcher/library.fetcher' +import { envFixture } from '../fixtures/env' +import { FakeAuthenticator } from '../fixtures/fakeauth' +import { librariesDTO } from '../fixtures/dto' +import { librariesModel } from '../fixtures/model' + +describe('library.service', () => { + const env: MendEnvironment = envFixture + + vi.mock('Authenticator', () => { + const mock = { + getInstance: vi.fn(() => new FakeAuthenticator(env)), + } + return mock + }) + + afterEach(() => { + vi.clearAllMocks() + vi.restoreAllMocks() + }) + + it("should return list of Project's libraries", async () => { + const spy = vi.spyOn(LibraryFetcher, 'getLibraryDTOs') + spy.mockReturnValue(Promise.resolve(librariesDTO)) + const expected: Library[] = librariesModel + + const libraryService: LibraryService = new LibraryService(env) + const result: Library[] = await libraryService.getAllLibrariesById( + env.projectToken + ) + + expect(result).toStrictEqual(expected) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/service/organization.service.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/service/organization.service.test.ts new file mode 100644 index 00000000..1fe0c12e --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/service/organization.service.test.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { afterEach, describe, expect, it, vi } from 'vitest' +import { MendEnvironment } from '../../../src/model/mendEnvironment' +import { Organization } from '../../../src/model/organization' +import { OrganizationService } from '../../../src/service/organization.service' +import * as OrganizationFetcher from '../../../src/fetcher/organization.fetcher' +import { envFixture } from '../fixtures/env' +import { FakeAuthenticator } from '../fixtures/fakeauth' +import { organizationDTO } from '../fixtures/dto' +import { organizationModel } from '../fixtures/model' + +describe('organization.service', () => { + const env: MendEnvironment = envFixture + + vi.mock('Authenticator', () => { + const mock = { + getInstance: vi.fn(() => new FakeAuthenticator(env)), + } + return mock + }) + + afterEach(() => { + vi.clearAllMocks() + vi.restoreAllMocks() + }) + + it('should return an Organization object', async () => { + const spy = vi.spyOn(OrganizationFetcher, 'getOrganizationDTO') + spy.mockReturnValue(Promise.resolve(organizationDTO)) + const expected = organizationModel + + const organizationService = new OrganizationService(env) + const result: Organization = await organizationService.getOrganizationById( + env.orgToken + ) + + expect(result).toStrictEqual(expected) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/service/project.service.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/service/project.service.test.ts new file mode 100644 index 00000000..e03f4937 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/service/project.service.test.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { afterEach, describe, expect, it, vi } from 'vitest' +import { MendEnvironment } from '../../../src/model/mendEnvironment' +import { Project } from '../../../src/model/project' +import { ProjectService } from '../../../src/service/project.service' +import * as ProjectFetcher from '../../../src/fetcher/project.fetcher' +import { envFixture } from '../fixtures/env' +import { FakeAuthenticator } from '../fixtures/fakeauth' +import { projectDTO, projectVitalsDTO } from '../fixtures/dto' +import { projectModel, projectVitalsModel } from '../fixtures/model' +import { ProjectVitals } from '../../../src/model/projectVitals' + +describe('project.service', () => { + const env: MendEnvironment = envFixture + + vi.mock('Authenticator', () => { + const mock = { + getInstance: vi.fn(() => new FakeAuthenticator(env)), + } + return mock + }) + + afterEach(() => { + vi.clearAllMocks() + vi.restoreAllMocks() + }) + + it('should return a Project object', async () => { + const spy = vi.spyOn(ProjectFetcher, 'getProjectDTO') + spy.mockReturnValue(Promise.resolve(projectDTO)) + const expected = projectModel + + const projectService = new ProjectService(env) + const result: Project = await projectService.getProjectByToken( + env.projectToken + ) + + expect(result).toStrictEqual(expected) + }) + + it('should return a ProjectVitals object', async () => { + const spy = vi.spyOn(ProjectFetcher, 'getProjectVitalsDTO') + spy.mockReturnValue(Promise.resolve(projectVitalsDTO)) + const expected = projectVitalsModel + + const projectService = new ProjectService(env) + const result: ProjectVitals = await projectService.getProjectVitals( + env.projectToken + ) + + expect(result).toStrictEqual(expected) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/service/vulnerability.service.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/service/vulnerability.service.test.ts new file mode 100644 index 00000000..fd18c97e --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/service/vulnerability.service.test.ts @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { afterEach, describe, expect, it, vi } from 'vitest' +import { MendEnvironment } from '../../../src/model/mendEnvironment' +import { Vulnerability } from '../../../src/model/vulnerability' +import { VulnerabilityService } from '../../../src/service/vulnerability.service' +import * as VulnerabilityFetcher from '../../../src/fetcher/vulnerability.fetcher' +import { envFixture } from '../fixtures/env' +import { FakeAuthenticator } from '../fixtures/fakeauth' +import { vulnerabilitiesDTO } from '../fixtures/dto' +import { vulnerabilitiesFixSummaryDTO } from '../fixtures/dto' +import { vulnerabilitiesModel } from '../fixtures/model' +import { vulnerabilityFixSummaryModel } from '../fixtures/model' +import { VulnerabilityFixSummary } from '../../../src/model/vulnerabilityFixSummary' + +describe('vulnerability.service', () => { + const env: MendEnvironment = envFixture + + vi.mock('Authenticator', () => { + const mock = { + getInstance: vi.fn(() => new FakeAuthenticator(env)), + } + return mock + }) + + afterEach(() => { + vi.clearAllMocks() + vi.restoreAllMocks() + }) + + it('should return all library vulnerabilities', async () => { + const spy = vi.spyOn(VulnerabilityFetcher, 'getLibraryVulnerabilityDTOs') + spy.mockReturnValue(Promise.resolve(vulnerabilitiesDTO)) + const libraryUuid = 'library-uuid' + const expected: Vulnerability[] = vulnerabilitiesModel + + const vulnerabilityService = new VulnerabilityService(env) + const result: Vulnerability[] = + await vulnerabilityService.getAllVulnerabilitiesById( + libraryUuid, + env.projectToken + ) + + expect(result).toStrictEqual(expected) + }) + + it('should return fixes for a vulnerability', async () => { + const spy = vi.spyOn(VulnerabilityFetcher, 'getVulnerabilityFixesDTOs') + spy.mockReturnValue(Promise.resolve(vulnerabilitiesFixSummaryDTO)) + const vulnerabilityId = 'vulnerability-uuid' + const expected: VulnerabilityFixSummary = vulnerabilityFixSummaryModel + + const vulnerabilityService = new VulnerabilityService(env) + const result: VulnerabilityFixSummary = + await vulnerabilityService.getAllVulnerabilitiesFixSummaryById( + vulnerabilityId + ) + + expect(result).toStrictEqual(expected) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/test/unit/utils/export.utils.test.ts b/yaku-apps-typescript/apps/mend-fetcher/test/unit/utils/export.utils.test.ts new file mode 100644 index 00000000..0aa0c231 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/test/unit/utils/export.utils.test.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { afterEach, describe, expect, it, vi } from 'vitest' +import * as fs from 'fs/promises' +import * as fs_sync from 'fs' +import * as utils from '../../../src/utils/export' + +describe('export.utils', () => { + vi.mock('fs/promises') + vi.mock('fs') + + afterEach(() => { + vi.clearAllMocks() + vi.restoreAllMocks() + }) + + it('should write JSON content to a file into an exising directory', async () => { + const spyExistsSync = vi.spyOn(fs_sync, 'existsSync') + spyExistsSync.mockImplementation(() => false) + const content = { content: 'content' } + + expect.assertions(2) + await utils.exportJson(content, '/tmp/content.json') + + expect(fs_sync.mkdirSync).toHaveBeenCalled() + expect(fs.writeFile).toHaveBeenCalledWith( + '/tmp/content.json', + JSON.stringify(content) + ) + }) + + it('should write JSON content to a file into a non-exising directory', async () => { + const spyExistsSync = vi.spyOn(fs_sync, 'existsSync') + spyExistsSync.mockImplementation(() => true) + const content = { content: 'content' } + + expect.assertions(1) + await utils.exportJson(content, '/tmp/content.json') + + expect(fs.writeFile).toHaveBeenCalledWith( + '/tmp/content.json', + JSON.stringify(content) + ) + }) +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/tsconfig.json b/yaku-apps-typescript/apps/mend-fetcher/tsconfig.json new file mode 100644 index 00000000..e1f51365 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@B-S-F/typescript-config/base.json", + "ts-node": { + "compilerOptions": { + "module": "ESNext" + } + }, + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/yaku-apps-typescript/apps/mend-fetcher/tsup.config.ts b/yaku-apps-typescript/apps/mend-fetcher/tsup.config.ts new file mode 100644 index 00000000..0e6106f5 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/tsup.config.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/**/*.ts'], + splitting: false, + clean: true, + target: 'node18', + format: ['esm'], + bundle: false, + sourcemap: true, +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/vitest-integration.config.ts b/yaku-apps-typescript/apps/mend-fetcher/vitest-integration.config.ts new file mode 100644 index 00000000..8bec0725 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/vitest-integration.config.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['test/integration/**/*.int-spec.ts'], + pool: 'threads', + poolOptions: { + threads: { + maxThreads: 1, + minThreads: 1, + }, + }, + typecheck: { + tsconfig: 'tsconfig.json', + }, + reporters: ['junit', 'default'], + outputFile: 'reports/integration-test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/mend-fetcher/vitest.config.ts b/yaku-apps-typescript/apps/mend-fetcher/vitest.config.ts new file mode 100644 index 00000000..79161309 --- /dev/null +++ b/yaku-apps-typescript/apps/mend-fetcher/vitest.config.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + typecheck: { + tsconfig: 'tsconfig.json', + }, + coverage: { + provider: 'v8', + all: true, + enabled: true, + reporter: ['cobertura', 'json-summary', 'text-summary'], + include: ['src'], + exclude: ['**/src/index.ts', 'src/model', 'src/dto'], + }, + reporters: ['junit', 'default'], + outputFile: 'reports/test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/smb-fetcher/.env.sample b/yaku-apps-typescript/apps/smb-fetcher/.env.sample new file mode 100644 index 00000000..e2c456cb --- /dev/null +++ b/yaku-apps-typescript/apps/smb-fetcher/.env.sample @@ -0,0 +1,3 @@ +SMB_USERNAME= +SMB_PASSWORD= +SMB_CONFIG_PATH= diff --git a/yaku-apps-typescript/apps/smb-fetcher/.eslintrc.cjs b/yaku-apps-typescript/apps/smb-fetcher/.eslintrc.cjs new file mode 100644 index 00000000..9ef0a5d9 --- /dev/null +++ b/yaku-apps-typescript/apps/smb-fetcher/.eslintrc.cjs @@ -0,0 +1,5 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +module.exports = require("@B-S-F/eslint-config/eslint-preset"); diff --git a/yaku-apps-typescript/apps/smb-fetcher/README.md b/yaku-apps-typescript/apps/smb-fetcher/README.md new file mode 100644 index 00000000..ff18db9c --- /dev/null +++ b/yaku-apps-typescript/apps/smb-fetcher/README.md @@ -0,0 +1,43 @@ +# SMB Fetcher + +## Setup the environment variables + +After doing the [Installation and Build step](../../README.md#installation) make a copy of the `.env.sample` template + +```sh +cp .env.sample .env +``` + +Set the required environment variables in `.env` + +```sh +SMB_USERNAME= +SMB_PASSWORD= +SMB_CONFIG_PATH= +``` + +Export them to the current shell with + +```sh +export $(grep -v '^#' .env | xargs -0) +``` + +| Environment Variable | Description | +| -------------------- | ----------------------------------- | +| SMB_USERNAME | SMB Share username | +| SMB_PASSWORD | SMB Share password | +| SMB_CONFIG_PATH | SMB Fetcher configuration file path | + +## Configuration file + +The SMB Fetcher will read `SMB_CONFIG_PATH` configuration `yaml` file which contains SMB Share information + +For a given POSIX path to a SMB Share in the form of `smb://host.gTLD/path/to/smb/shared/resource` the configuration file must contain the NT path form: + +```yaml +share: '\\host.gTLD\path' +domain: 'NT-DOMAIN-OF-SMB-SHARE' +files: + - 'to\smb\shared\resource' + - 'to\smb\shared\another\resource' +``` diff --git a/yaku-apps-typescript/apps/smb-fetcher/package.json b/yaku-apps-typescript/apps/smb-fetcher/package.json new file mode 100644 index 00000000..da67b81a --- /dev/null +++ b/yaku-apps-typescript/apps/smb-fetcher/package.json @@ -0,0 +1,47 @@ +{ + "name": "@B-S-F/smb-fetcher", + "version": "0.1.1", + "description": "", + "main": "dist/index.js", + "type": "module", + "scripts": { + "build": "tsup", + "start": "node --openssl-legacy-provider ./dist/index.js", + "dev": "nodemon --watch \"src/**\" --exec npm run start", + "lint": "eslint '**/*.ts'", + "format": "prettier --write '**/*.{ts,md}'", + "test": "vitest run && npm run test:update-cobertura-file", + "test:update-cobertura-file": "sed -i'.bak' 's,package name=\",package name=\"'${npm_package_name}/',g' coverage/cobertura-coverage.xml", + "test:dev": "vitest -w", + "test:coverage": "vitest --coverage", + "test:ui": "vitest --ui" + }, + "keywords": [], + "author": "", + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@vitest/ui": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "nodemon": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + }, + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "v9u-smb2": "^1.0.6", + "yaml": "^2.3.2", + "zod": "^3.22.3" + }, + "bin": { + "smb-fetcher": "dist/index.js" + }, + "files": [ + "dist" + ] +} diff --git a/yaku-apps-typescript/apps/smb-fetcher/src/index.ts b/yaku-apps-typescript/apps/smb-fetcher/src/index.ts new file mode 100644 index 00000000..cb7829be --- /dev/null +++ b/yaku-apps-typescript/apps/smb-fetcher/src/index.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env -S node --openssl-legacy-provider +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { run } from './run.js' +run() diff --git a/yaku-apps-typescript/apps/smb-fetcher/src/run.ts b/yaku-apps-typescript/apps/smb-fetcher/src/run.ts new file mode 100644 index 00000000..a504e267 --- /dev/null +++ b/yaku-apps-typescript/apps/smb-fetcher/src/run.ts @@ -0,0 +1,186 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ +import { + AppError, + AppOutput, + GetLogger, + InitLogger, + Output, +} from '@B-S-F/autopilot-utils' +import fs from 'fs/promises' +import * as fs_sync from 'fs' +import path from 'path' +import SMB2, { IStats } from 'v9u-smb2' +import YAML from 'yaml' +import z from 'zod' + +class FetchError extends AppError { + constructor(reason: string) { + super(reason) + this.name = 'FetchError' + } + + Reason(): string { + return super.Reason() + } +} + +const validateFetcherConfig = async (filePath: string) => { + const config = await YAML.parse( + await fs.readFile(filePath, { encoding: 'utf-8' }) + ) + + return configSchema().parse(config) +} + +const validateEnvironment = () => { + const envSchema = z.object({ + SMB_USERNAME: z.string(), + SMB_PASSWORD: z.string(), + SMB_CONFIG_PATH: z.string(), + }) + + return envSchema.parse(process.env) +} + +const configSchema = () => { + const configSchema = z.object({ + share: z.string(), + domain: z.string().default('EMEA'), + files: z.array(z.string().min(1)), + }) + + return configSchema +} + +export const retry = async ( + fn: { (smbPath: string, smbClient: SMB2): Promise }, + smbPath: string, + smbClient: SMB2, + retries = 5, + delay = 100 +): Promise => { + const logger = GetLogger() + try { + return await fn(smbPath, smbClient) + } catch (error) { + if (retries > 1) { + logger.warn(`retry SMB operation on ${smbPath}`) + + await new Promise((resolve) => setTimeout(resolve, delay)) + + return await retry(fn, smbPath, smbClient, retries - 1, delay * 2) + } else { + logger.error(`All SMB operation retries on ${smbPath} have failed`) + throw new FetchError(`Failed SMB operation on ${smbPath}`) + } + } +} + +const outputs: Output[] = [] + +const fetchFile = async ( + destPath: string, + smbPath: string, + smbClient: SMB2 +) => { + const logger = GetLogger() + + const smbstats: IStats = await smbClient.stat(smbPath) + const content = await smbClient.readFile(smbPath) + + const dirName = path.dirname(destPath) + if (!fs_sync.existsSync(dirName)) { + fs_sync.mkdirSync(dirName, { recursive: true }) + } + + fs_sync.writeFileSync(destPath, content) + fs_sync.utimesSync(destPath, smbstats.atime, smbstats.mtime) + + logger.info(`${smbPath} is fetched successfuly`) + const currentPath = path.resolve('./') + + const relativePath = destPath.replace(currentPath, '.').replace('./', '') + outputs.push({ fetched: `${relativePath}` }) +} + +const traversePath = async ( + destPath: string, + smbPath: string, + smbClient: SMB2 +) => { + const smbstats: IStats = await smbClient.stat(smbPath) + if (smbstats.isDirectory()) { + const dirInfo = await smbClient.readdir(smbPath, { stats: true }) + + if (!fs_sync.existsSync(destPath)) { + fs_sync.mkdirSync(destPath, { recursive: true }) + fs_sync.utimesSync(destPath, smbstats.atime, smbstats.mtime) + } + + await Promise.all( + dirInfo.map(async (item: any) => { + const newDestPath = path.join(destPath, item.name) + const newsmbPath = `${smbPath}\\${item.name}` + + await traversePath(newDestPath, newsmbPath, smbClient) + }) + ) + } else { + await retry( + async () => fetchFile(destPath, smbPath, smbClient), + smbPath, + smbClient + ).catch((error: any) => { + throw error + }) + } +} + +export const run = async () => { + const logger = InitLogger('smb-fetcher', 'info') + const output = new AppOutput() + + try { + const env = validateEnvironment() + + const config = await validateFetcherConfig(env.SMB_CONFIG_PATH) + + const smbOptions = { + share: config.share, + domain: config.domain, + username: env.SMB_USERNAME, + password: env.SMB_PASSWORD, + autoCloseTimeout: 0, + } + + const smbClient = new SMB2(smbOptions) + + for (const file of config.files) { + const cleanSmbPath = file.replace(/(\\)\\+/g, '$1').replace(/\\+$/, '') + const cleanFsPath = file.replaceAll('\\', path.sep).replace(/\/+$/, '') + const filepath = { + smbpath: cleanSmbPath, + fspath: cleanFsPath.split(path.sep).at(-1) ?? '', + } + const outputPath = path.resolve('./', filepath.fspath) + await traversePath(outputPath, filepath.smbpath, smbClient) + } + await smbClient.disconnect() + + outputs.forEach((item) => output.addOutput(item)) + output.write() + } catch (error: any) { + if (error instanceof AppError) { + output.setStatus('FAILED') + output.setReason(error.Reason()) + logger.error(error.Reason()) + + output.write() + process.exit(0) + } else { + throw error + } + } +} diff --git a/yaku-apps-typescript/apps/smb-fetcher/test/run.test.ts b/yaku-apps-typescript/apps/smb-fetcher/test/run.test.ts new file mode 100644 index 00000000..d39c20e8 --- /dev/null +++ b/yaku-apps-typescript/apps/smb-fetcher/test/run.test.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2023 by grow platform GmbH + */ + +import { describe, expect, it } from 'vitest' + +describe('', () => { + it('', () => { + expect(true).toBeTruthy() + }) +}) diff --git a/yaku-apps-typescript/apps/smb-fetcher/tsconfig.json b/yaku-apps-typescript/apps/smb-fetcher/tsconfig.json new file mode 100644 index 00000000..0df56473 --- /dev/null +++ b/yaku-apps-typescript/apps/smb-fetcher/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@B-S-F/typescript-config/base.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/yaku-apps-typescript/apps/smb-fetcher/tsup.config.ts b/yaku-apps-typescript/apps/smb-fetcher/tsup.config.ts new file mode 100644 index 00000000..0e6106f5 --- /dev/null +++ b/yaku-apps-typescript/apps/smb-fetcher/tsup.config.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/**/*.ts'], + splitting: false, + clean: true, + target: 'node18', + format: ['esm'], + bundle: false, + sourcemap: true, +}) diff --git a/yaku-apps-typescript/apps/smb-fetcher/vitest.config.ts b/yaku-apps-typescript/apps/smb-fetcher/vitest.config.ts new file mode 100644 index 00000000..6fc40afa --- /dev/null +++ b/yaku-apps-typescript/apps/smb-fetcher/vitest.config.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + typecheck: { + tsconfig: 'tsconfig.json', + }, + coverage: { + provider: 'v8', + all: true, + enabled: true, + reporter: ['cobertura', 'json-summary', 'text-summary'], + include: ['src'], + exclude: ['**/src/index.ts'], + }, + reporters: ['junit', 'default'], + outputFile: 'reports/test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/sonarqube/.env.sample b/yaku-apps-typescript/apps/sonarqube/.env.sample new file mode 100644 index 00000000..08bb73c1 --- /dev/null +++ b/yaku-apps-typescript/apps/sonarqube/.env.sample @@ -0,0 +1,5 @@ +export SONARQUBE_ACCESS_TOKEN=7f59986804d56523d35240e982b89044019fab13 +export SONARQUBE_PROJECT_KEY=qg-cli-demo +export SONARQUBE_PROTOCOL=http +export SONARQUBE_HOST=localhost +export SONARQUBE_PORT=9000 \ No newline at end of file diff --git a/yaku-apps-typescript/apps/sonarqube/.eslintrc.cjs b/yaku-apps-typescript/apps/sonarqube/.eslintrc.cjs new file mode 100644 index 00000000..1eab256c --- /dev/null +++ b/yaku-apps-typescript/apps/sonarqube/.eslintrc.cjs @@ -0,0 +1 @@ +module.exports = require('@B-S-F/eslint-config/eslint-preset') diff --git a/yaku-apps-typescript/apps/sonarqube/README.md b/yaku-apps-typescript/apps/sonarqube/README.md new file mode 100644 index 00000000..798f8e9b --- /dev/null +++ b/yaku-apps-typescript/apps/sonarqube/README.md @@ -0,0 +1,19 @@ +# Sonarqube cli + +**_NOTE:_** Handling sonarqube integrations + +## Usage + +In order to see all use-cases please use `sonarqube --help` + +### Fetch a project's status + +```bash +sonarqube fetch project-status --project-key --hostname --access-token +``` + +You can also use the following environment variables: + +- `SONARQUBE_HOSTNAME` +- `SONARQUBE_PROJECT_KEY` +- `SONARQUBE_ACCESS_TOKEN` diff --git a/yaku-apps-typescript/apps/sonarqube/package.json b/yaku-apps-typescript/apps/sonarqube/package.json new file mode 100644 index 00000000..8b343fc0 --- /dev/null +++ b/yaku-apps-typescript/apps/sonarqube/package.json @@ -0,0 +1,48 @@ +{ + "name": "@B-S-F/sonarqube", + "version": "0.2.0", + "description": "", + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsup", + "dev": "nodemon --watch \"src/**\" --exec npm run start", + "start": "npm run build && node dist/index.js", + "lint": "eslint '**/*.ts'", + "format": "prettier --write '**/*.{ts,md}'", + "test": "vitest run && npm run test:update-cobertura-file", + "test:update-cobertura-file": "sed -i'.bak' 's,package name=\",package name=\"'${npm_package_name}/',g' coverage/cobertura-coverage.xml", + "test:dev": "vitest -w", + "test:coverage": "vitest --coverage", + "test:ui": "vitest --ui", + "test:integration:local": "npx rimraf dist && npm run build && npm run test:integration:ci", + "test:integration:ci": "npx vitest run --config vitest-integration.config.ts" + }, + "keywords": [], + "author": "", + "files": [ + "dist" + ], + "license": "", + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "node-fetch": "^3.3.2", + "commander": "^11.0.0" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/axios": "^0.14.0", + "@types/node": "*", + "c8": "*", + "eslint": "*", + "nodemon": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + }, + "bin": { + "sonarqube": "dist/index.js" + } +} diff --git a/yaku-apps-typescript/apps/sonarqube/src/commands/fetch/create-url.ts b/yaku-apps-typescript/apps/sonarqube/src/commands/fetch/create-url.ts new file mode 100644 index 00000000..336fac07 --- /dev/null +++ b/yaku-apps-typescript/apps/sonarqube/src/commands/fetch/create-url.ts @@ -0,0 +1,63 @@ +import { ConfigurationError } from './errors.js' + +export function createDashboardUrl( + hostname: string, + port: number, + protocol: 'http' | 'https', + projectKey: string +) { + try { + const url = new URL( + `${protocol}://${hostname}:${port.toString()}/dashboard` + ) + url.searchParams.append('id', projectKey) + return url + } catch (error: any) { + throw new ConfigurationError( + `Configuration could not be parsed as URL, ${error.message}` + ) + } +} + +export function createApiUrl( + hostname: string, + port: number, + protocol: 'http' | 'https', + apiPath: string, + searchParams: { [key: string]: string } +) { + try { + const url = new URL( + `${protocol}://${hostname}:${port.toString()}/${apiPath}` + ) + Object.entries(searchParams).forEach(([key, value]) => { + url.searchParams.append(key, value) + }) + return url + } catch (error: any) { + throw new ConfigurationError( + `Configuration could not be parsed as URL, ${error.message}` + ) + } +} + +export function createAuthHeader( + accessToken?: string, + username?: string, + password?: string +) { + if (!accessToken && !(username && password)) { + throw new Error( + 'Failed to create Auth Header, either access token or username and password have to be provided' + ) + } + let encoded = '' + if (accessToken) { + encoded = Buffer.from(`${accessToken}:`, 'binary').toString('base64') + } else { + encoded = Buffer.from(`${username}:${password}`, 'binary').toString( + 'base64' + ) + } + return `Basic ${encoded}` +} diff --git a/yaku-apps-typescript/apps/sonarqube/src/commands/fetch/errors.ts b/yaku-apps-typescript/apps/sonarqube/src/commands/fetch/errors.ts new file mode 100644 index 00000000..4876a390 --- /dev/null +++ b/yaku-apps-typescript/apps/sonarqube/src/commands/fetch/errors.ts @@ -0,0 +1,23 @@ +import { AppError } from '@B-S-F/autopilot-utils' + +export class ConfigurationError extends AppError { + constructor(message: string) { + super(message) + this.name = 'ConfigurationError' + } + + Reason(): string { + return super.Reason() + } +} + +export class RequestError extends AppError { + constructor(message: string) { + super(message) + this.name = 'RequestError' + } + + Reason(): string { + return super.Reason() + } +} diff --git a/yaku-apps-typescript/apps/sonarqube/src/commands/fetch/index.ts b/yaku-apps-typescript/apps/sonarqube/src/commands/fetch/index.ts new file mode 100644 index 00000000..df296e2b --- /dev/null +++ b/yaku-apps-typescript/apps/sonarqube/src/commands/fetch/index.ts @@ -0,0 +1,103 @@ +import { InitLogger } from '@B-S-F/autopilot-utils' +import { Command } from 'commander' +import { ConfigurationError } from './errors.js' +import { projectStatus } from './project-status.js' + +export type FetchOptions = { + hostname: string + port: number + accessToken: string + protocol: 'https' | 'http' + outputPath: string + enableProxy: boolean +} + +export function addFetchOptions(command: Command) { + command + .option( + '--hostname ', + 'Sonarqube hostname e.g. "sonarqube.bosch.com"', + process.env.SONARQUBE_HOSTNAME + ) + .option( + '--access-token ', + 'Sonarqube access token', + process.env.SONARQUBE_ACCESS_TOKEN + ) + .option( + '--port [port]', + 'Sonarqube port', + process.env.SONARQUBE_PORT ?? '443' + ) + .option( + '--protocol [protocol]', + 'Sonarqube protocol ("https", "http")', + process.env.SONARQUBE_PROTOCOL ?? 'https' + ) + .option( + '--output-path [output-path]', + 'File to write the fetched data to', + process.env.SONARQUBE_OUTPUT_PATH ?? 'sonarqube_data.json' + ) + .option('--debug', 'Enable debug logging', process.env.DEBUG ?? false) + .option('--enable-proxy', 'Enable proxy', process.env.ENABLE_PROXY ?? false) +} + +export function verifyOptions(options: any): options is FetchOptions { + if (options.debug) { + InitLogger('sonarqube', 'debug') + } else { + InitLogger('sonarqube', 'info') + } + const environmentErrors = [] + if (!options.hostname) { + environmentErrors.push(new ConfigurationError('hostname is not set')) + } + + if (!options.accessToken) { + environmentErrors.push(new ConfigurationError('access token is not set')) + } + if (Number(options.port) < 1 || Number(options.port) > 65535) { + environmentErrors.push( + new ConfigurationError( + `port ${options.port} is not set to a valid port number` + ) + ) + } + if (options.protocol !== 'http' && options.protocol !== 'https') { + environmentErrors.push( + new ConfigurationError('protocol not set to http or https') + ) + } + if (!options.outputPath) { + environmentErrors.push(new ConfigurationError('output path is not set')) + } + if (environmentErrors.length > 0) { + const concatenatedReasons = environmentErrors + .map((error) => error.Reason()) + .join('\n') + throw new ConfigurationError(concatenatedReasons) + } + return true +} + +const projectStatusCommand = new Command('project-status') + .description('Fetch project status from Sonarqube.') + .option( + '--project-key ', + 'Sonarqube project key', + process.env.SONARQUBE_PROJECT_KEY + ) + .action(async (options: any) => { + verifyOptions(options) + if (!options.projectKey) { + throw new ConfigurationError('project key is not set') + } + await projectStatus(options) + }) + +addFetchOptions(projectStatusCommand) + +export const command = new Command('fetch') + .description('Fetch data from Sonarqube.') + .addCommand(projectStatusCommand) diff --git a/yaku-apps-typescript/apps/sonarqube/src/commands/fetch/project-status.ts b/yaku-apps-typescript/apps/sonarqube/src/commands/fetch/project-status.ts new file mode 100644 index 00000000..4cc92caa --- /dev/null +++ b/yaku-apps-typescript/apps/sonarqube/src/commands/fetch/project-status.ts @@ -0,0 +1,121 @@ +import { AppOutput, GetLogger } from '@B-S-F/autopilot-utils' +import { writeFile } from 'fs/promises' +import { Agent as HttpAgent } from 'http' +import { Agent as HttpsAgent } from 'https' +import fetch from 'node-fetch' +import { configureProxyTunnel } from '../../utils/configure-proxy-tunnel.js' +import { + createApiUrl, + createAuthHeader, + createDashboardUrl, +} from './create-url.js' +import { RequestError } from './errors.js' +import { FetchOptions } from './index.js' + +const PROJECT_STATUS_API_PATH = 'api/qualitygates/project_status' + +export type FetchProjectStatusOptions = { + projectKey: string +} + +export async function projectStatus( + options: FetchOptions & FetchProjectStatusOptions +): Promise { + const logger = GetLogger() + logger.info(`Fetching project status for ${options.projectKey}`) + const dashboardUrl = createDashboardUrl( + options.hostname, + options.port, + options.protocol, + options.projectKey + ) + logger.debug(`dashboardUrl url: ${dashboardUrl.href}`) + + let proxyTunnel = undefined + if (options.enableProxy) { + logger.info('Configuring proxy tunnel') + const httpProxy = process.env.HTTPS_PROXY + const httpsProxy = process.env.HTTP_PROXY + logger.debug(`httpProxy: ${httpProxy}`) + logger.debug(`httpsProxy: ${httpsProxy}`) + proxyTunnel = configureProxyTunnel(options.protocol, httpsProxy, httpProxy) + } + + const projectStatus = await getProjectStatus( + options.hostname, + options.port, + options.protocol, + options.projectKey, + options.accessToken, + proxyTunnel + ) + + logger.info(`Writing response to ${options.outputPath}`) + await writeFile(options.outputPath, JSON.stringify(projectStatus, null, 2)) + + const result = new AppOutput() + result.addOutput({ + dashboardUrl: dashboardUrl.href, + sonarqubeResultPath: options.outputPath, + }) + result.write() +} + +type ProjectStatus = { + status: string + ignoredConditions: boolean + conditions: Array<{ + status: string + metricKey: string + comparator: string + errorThreshold: string + actualValue: string + }> + period: { + mode: string + date: string + parameter: string + } +} + +export async function getProjectStatus( + hostname: string, + port: number, + protocol: 'http' | 'https', + projectKey: string, + accessToken: string, + proxyTunnel?: HttpAgent | HttpsAgent +): Promise { + const logger = GetLogger() + const apiUrl = createApiUrl( + hostname, + port, + protocol, + PROJECT_STATUS_API_PATH, + { projectKey: projectKey } + ) + logger.debug(`apiUrl: ${apiUrl.href}`) + const response = await fetch(apiUrl.href, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: createAuthHeader(accessToken), + }, + agent: proxyTunnel, + }) + + const text = await response.text() + if (!response.ok) { + throw new RequestError( + `Failed to fetch project status with status ${response.status}, ${text}` + ) + } + + try { + return JSON.parse(text) + } catch (error: any) { + throw new Error( + `Could not parse sonarqube response as JSON, ${error.message}` + ) + } +} diff --git a/yaku-apps-typescript/apps/sonarqube/src/index.ts b/yaku-apps-typescript/apps/sonarqube/src/index.ts new file mode 100644 index 00000000..d928823a --- /dev/null +++ b/yaku-apps-typescript/apps/sonarqube/src/index.ts @@ -0,0 +1,12 @@ +#!/usr/bin/env node +import { AutopilotApp } from '@B-S-F/autopilot-utils' +import { createRequire } from 'module' +import { command as fetchCommand } from './commands/fetch/index.js' +const require = createRequire(import.meta.url) +const { version } = require('../package.json') + +const app = new AutopilotApp('sonarqube', version, 'Sonarqube connector', [ + fetchCommand, +]) + +app.run() diff --git a/yaku-apps-typescript/apps/sonarqube/src/utils/configure-proxy-tunnel.ts b/yaku-apps-typescript/apps/sonarqube/src/utils/configure-proxy-tunnel.ts new file mode 100644 index 00000000..748eed6b --- /dev/null +++ b/yaku-apps-typescript/apps/sonarqube/src/utils/configure-proxy-tunnel.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import * as tunnel from 'tunnel' + +export function configureProxyTunnel( + protocol: string, + httpsProxy: string | undefined, + httpProxy: string | undefined +) { + let proxy + + if (protocol === 'https') { + if (httpsProxy) { + proxy = { + host: `${httpsProxy.split(':')[1]}`.replace(/^\/\//, ''), + port: +httpsProxy.split(':')[2], + } + } + } else if (protocol === 'http') { + if (httpProxy) { + proxy = { + host: `${httpProxy.split(':')[1]}`.replace(/^\/\//, ''), + port: +httpProxy.split(':')[2], + } + } + } + + if (!proxy) return undefined + + return tunnel.httpsOverHttp({ + proxy: proxy, + }) +} diff --git a/yaku-apps-typescript/apps/sonarqube/test/integration/fetcher.int-spec.ts b/yaku-apps-typescript/apps/sonarqube/test/integration/fetcher.int-spec.ts new file mode 100644 index 00000000..cf6abeb7 --- /dev/null +++ b/yaku-apps-typescript/apps/sonarqube/test/integration/fetcher.int-spec.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from 'vitest' +import { run } from '../../../../integration-tests/src/util' +import { version } from '../../package.json' + +const executable = `${__dirname}/../../dist/index.js` + +describe('sonarqube', async () => { + it('should be available', async () => { + const { stderr, exitCode } = await run(`${executable}`, ['--help']) + expect(exitCode).toEqual(0) + expect(stderr).toHaveLength(0) + }) + it('should show current version', async () => { + const { stdout, stderr, exitCode } = await run(`${executable}`, [ + '--version', + ]) + expect(exitCode).toEqual(0) + expect(stderr).toHaveLength(0) + expect(stdout).toContain(version) + }) + it('should show help', async () => { + const { stdout, stderr, exitCode } = await run(`${executable}`, ['--help']) + expect(exitCode).toEqual(0) + expect(stderr).toHaveLength(0) + expect(stdout).toContain('Usage: sonarqube [options] [command]') + expect(stdout).toContain(' fetch Fetch data from Sonarqube.') + }) + it('should show help for fetch', async () => { + const { stdout, stderr, exitCode } = await run(`${executable}`, [ + 'fetch', + '--help', + ]) + expect(exitCode).toEqual(0) + expect(stderr).toHaveLength(0) + expect(stdout).toContain('Usage: sonarqube fetch [options] [command]') + expect(stdout).toContain( + ' project-status [options] Fetch project status from Sonarqube.' + ) + }) + it('should show help for fetch project-status', async () => { + const { stdout, stderr, exitCode } = await run(`${executable}`, [ + 'fetch', + 'project-status', + '--help', + ]) + expect(exitCode).toEqual(0) + expect(stderr).toHaveLength(0) + expect(stdout).toContain('Usage: sonarqube fetch project-status [options]') + expect(stdout).toContain('Fetch project status from Sonarqube.') + }) + it('should support environment variables', async () => { + const { stdout, exitCode } = await run(`${executable}`, [ + 'fetch', + 'project-status', + ]) + expect(exitCode).toEqual(0) + expect(stdout).toContain( + '{"status":"FAILED","reason":"hostname is not set\\naccess token is not set"}' + ) + const { stdout: stdout2, exitCode: exitCode2 } = await run( + `${executable}`, + ['fetch', 'project-status'], + { + env: { + SONARQUBE_HOSTNAME: 'hostname', + SONARQUBE_PORT: '8080', + SONARQUBE_PROTOCOL: 'https', + SONARQUBE_PROJECT_KEY: 'projectKey', + SONARQUBE_OUTPUT_PATH: 'outputPath', + }, + } + ) + expect(exitCode2).toEqual(0) + expect(stdout2).toContain( + '{"status":"FAILED","reason":"access token is not set"}' + ) + }) +}) diff --git a/yaku-apps-typescript/apps/sonarqube/test/unit/commands/fetch/create-url.spec.ts b/yaku-apps-typescript/apps/sonarqube/test/unit/commands/fetch/create-url.spec.ts new file mode 100644 index 00000000..d291f04f --- /dev/null +++ b/yaku-apps-typescript/apps/sonarqube/test/unit/commands/fetch/create-url.spec.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from 'vitest' +import { + createApiUrl, + createAuthHeader, + createDashboardUrl, +} from '../../../../src/commands/fetch/create-url' + +describe('createApiUrl', async () => { + it('should create a valid sonarqube api url', () => { + const hostname = 'hostname' + const port = 8080 + const protocol = 'https' + const path = 'path' + const token = 'token' + const url = createApiUrl(hostname, port, protocol, path, { + projectKey: token, + }) + expect(url.href).toEqual( + `${protocol}://${hostname}:8080/${path}?projectKey=${token}` + ) + expect(url.hostname).toEqual(hostname) + expect(url.port).toEqual(port.toString()) + expect(url.protocol).toEqual(`${protocol}:`) + expect(url.pathname).toEqual(`/${path}`) + expect(url.searchParams.get('projectKey')).toEqual(token) + }) +}) + +describe('createDashboardUrl', async () => { + it('should create a valid sonarqube dashboard url', () => { + const hostname = 'hostname' + const port = 8080 + const protocol = 'https' + const projectKey = 'projectKey' + const url = createDashboardUrl(hostname, port, protocol, projectKey) + expect(url.href).toEqual( + `${protocol}://${hostname}:8080/dashboard?id=${projectKey}` + ) + expect(url.hostname).toEqual(hostname) + expect(url.port).toEqual(port.toString()) + expect(url.protocol).toEqual(`${protocol}:`) + expect(url.pathname).toEqual('/dashboard') + expect(url.searchParams.get('id')).toEqual(projectKey) + }) +}) + +describe('createAuthHeader', async () => { + it('should create a valid basic auth header with access token', () => { + expect(createAuthHeader('token')).toEqual('Basic dG9rZW46') + }) + it('should create a valid basic auth header with username and password', () => { + expect(createAuthHeader(undefined, 'test', 'test')).toEqual( + 'Basic dGVzdDp0ZXN0' + ) + }) + it('should throw an error if neither access token nor username and password are provided', () => { + expect(() => createAuthHeader()).toThrowError( + 'Failed to create Auth Header, either access token or username and password have to be provided' + ) + }) + it('should throw an error if the necessary values are empty', () => { + expect(() => createAuthHeader('')).toThrowError( + 'Failed to create Auth Header, either access token or username and password have to be provided' + ) + expect(() => createAuthHeader(undefined, '', '')).toThrowError( + 'Failed to create Auth Header, either access token or username and password have to be provided' + ) + }) +}) diff --git a/yaku-apps-typescript/apps/sonarqube/test/unit/commands/fetch/project-status.spec.ts b/yaku-apps-typescript/apps/sonarqube/test/unit/commands/fetch/project-status.spec.ts new file mode 100644 index 00000000..62a1dd5c --- /dev/null +++ b/yaku-apps-typescript/apps/sonarqube/test/unit/commands/fetch/project-status.spec.ts @@ -0,0 +1,175 @@ +import { writeFile } from 'fs/promises' +import fetch, { Response } from 'node-fetch' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + createApiUrl, + createAuthHeader, + createDashboardUrl, +} from '../../../../src/commands/fetch/create-url' +import { + getProjectStatus, + projectStatus, +} from '../../../../src/commands/fetch/project-status' +import { configureProxyTunnel } from '../../../../src/utils/configure-proxy-tunnel' + +describe('getProjectStatus', async () => { + beforeEach(() => { + vi.resetAllMocks() + vi.mock('node-fetch') + vi.mock('../../../../src/commands/fetch/create-url') + }) + const options = { + hostname: 'test-host', + port: 8080, + protocol: 'https', + projectKey: 'test-key', + enableProxy: false, + outputPath: 'test-path', + accessToken: 'test-token', + } as any + it('should fetch project status', async () => { + vi.mocked(fetch).mockResolvedValue({ + ok: true, + text: async () => { + return '{"projectStatus": {"status": "OK"}}' + }, + status: 200, + statusText: 'OK', + } as Response) + vi.mocked(createApiUrl).mockReturnValue(new URL('https://test-url')) + + const result = await getProjectStatus( + options.hostname, + options.port, + options.protocol, + options.projectKey, + options.accessToken, + undefined + ) + expect(result).toEqual({ + projectStatus: { + status: 'OK', + }, + }) + }) + + it('should throw an error if the response is not ok', async () => { + vi.mocked(fetch).mockResolvedValue({ + ok: false, + text: async () => { + return 'some error message' + }, + status: 404, + statusText: 'OK', + } as Response) + vi.mocked(createApiUrl).mockReturnValue(new URL('https://test-url')) + + await expect( + getProjectStatus( + options.hostname, + options.port, + options.protocol, + options.projectKey, + options.accessToken, + undefined + ) + ).rejects.toThrowError( + 'Failed to fetch project status with status 404, some error message' + ) + }) + + it('should throw an error if the resoponse data is not valid json', async () => { + vi.mocked(fetch).mockResolvedValue({ + ok: true, + text: async () => { + return 'some invalid json' + }, + status: 404, + statusText: 'OK', + } as Response) + vi.mocked(createApiUrl).mockReturnValue(new URL('https://test-url')) + + await expect( + getProjectStatus( + options.hostname, + options.port, + options.protocol, + options.projectKey, + options.accessToken, + undefined + ) + ).rejects.toThrowError( + 'Could not parse sonarqube response as JSON, Unexpected token s in JSON at position 0' + ) + }) +}) + +describe('projectStatus', async () => { + beforeEach(() => { + vi.resetAllMocks() + vi.mock('node-fetch') + vi.mock('../../../../src/commands/fetch/create-url') + vi.mock('../../../../src/utils/configure-proxy-tunnel') + vi.mock('fs/promises') + }) + + it('should fetch project status', async () => { + const sonarqubeResult = { + projectStatus: { + status: 'OK', + }, + } + const mockedFetch = vi.mocked(fetch).mockResolvedValue({ + ok: true, + text: async () => { + return JSON.stringify(sonarqubeResult) + }, + status: 200, + statusText: 'OK', + } as Response) + const mockedCreateApiUrl = vi + .mocked(createApiUrl) + .mockReturnValue(new URL('https://test-url')) + const mockedCreateDashboardUrl = vi + .mocked(createDashboardUrl) + .mockReturnValue(new URL('https://test-url')) + const mockedCreateAuthHeader = vi + .mocked(createAuthHeader) + .mockReturnValue('Basic dG9rZW46') + const mockedWriteFile = vi.mocked(writeFile).mockResolvedValue(undefined) + const mockedConfigureProxyTunnel = vi + .mocked(configureProxyTunnel) + .mockReturnValue(undefined) + + const options = { + hostname: 'test-host', + port: 8080, + protocol: 'https', + projectKey: 'test-key', + enableProxy: false, + outputPath: 'test-path', + accessToken: 'test-token', + } as any + await expect(projectStatus(options)).resolves.toBeUndefined() + expect(mockedCreateAuthHeader).toHaveBeenCalledWith(options.accessToken) + expect(mockedCreateApiUrl).toHaveBeenCalledWith( + options.hostname, + options.port, + options.protocol, + 'api/qualitygates/project_status', + { projectKey: options.projectKey } + ) + expect(mockedFetch).toHaveBeenCalled() + expect(mockedCreateDashboardUrl).toHaveBeenCalledWith( + options.hostname, + options.port, + options.protocol, + options.projectKey + ) + expect(mockedConfigureProxyTunnel).not.toHaveBeenCalled() + expect(mockedWriteFile).toHaveBeenCalledWith( + options.outputPath, + JSON.stringify(sonarqubeResult, null, 2) + ) + }) +}) diff --git a/yaku-apps-typescript/apps/sonarqube/test/unit/fetch/project-status.spec.ts b/yaku-apps-typescript/apps/sonarqube/test/unit/fetch/project-status.spec.ts new file mode 100644 index 00000000..bea39fb0 --- /dev/null +++ b/yaku-apps-typescript/apps/sonarqube/test/unit/fetch/project-status.spec.ts @@ -0,0 +1,175 @@ +import { writeFile } from 'fs/promises' +import fetch, { Response } from 'node-fetch' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + createApiUrl, + createAuthHeader, + createDashboardUrl, +} from '../../../src/commands/fetch/create-url' +import { + getProjectStatus, + projectStatus, +} from '../../../src/commands/fetch/project-status' +import { configureProxyTunnel } from '../../../src/utils/configure-proxy-tunnel' + +describe('getProjectStatus', async () => { + beforeEach(() => { + vi.resetAllMocks() + vi.mock('node-fetch') + vi.mock('../../../src/commands/fetch/create-url') + }) + const options = { + hostname: 'test-host', + port: 8080, + protocol: 'https', + projectKey: 'test-key', + enableProxy: false, + outputPath: 'test-path', + accessToken: 'test-token', + } as any + it('should fetch project status', async () => { + vi.mocked(fetch).mockResolvedValue({ + ok: true, + text: async () => { + return '{"projectStatus": {"status": "OK"}}' + }, + status: 200, + statusText: 'OK', + } as Response) + vi.mocked(createApiUrl).mockReturnValue(new URL('https://test-url')) + + const result = await getProjectStatus( + options.hostname, + options.port, + options.protocol, + options.projectKey, + options.accessToken, + undefined + ) + expect(result).toEqual({ + projectStatus: { + status: 'OK', + }, + }) + }) + + it('should throw an error if the response is not ok', async () => { + vi.mocked(fetch).mockResolvedValue({ + ok: false, + text: async () => { + return 'some error message' + }, + status: 404, + statusText: 'OK', + } as Response) + vi.mocked(createApiUrl).mockReturnValue(new URL('https://test-url')) + + await expect( + getProjectStatus( + options.hostname, + options.port, + options.protocol, + options.projectKey, + options.accessToken, + undefined + ) + ).rejects.toThrowError( + 'Failed to fetch project status with status 404, some error message' + ) + }) + + it('should throw an error if the resoponse data is not valid json', async () => { + vi.mocked(fetch).mockResolvedValue({ + ok: true, + text: async () => { + return 'some invalid json' + }, + status: 404, + statusText: 'OK', + } as Response) + vi.mocked(createApiUrl).mockReturnValue(new URL('https://test-url')) + + await expect( + getProjectStatus( + options.hostname, + options.port, + options.protocol, + options.projectKey, + options.accessToken, + undefined + ) + ).rejects.toThrowError( + 'Could not parse sonarqube response as JSON, Unexpected token s in JSON at position 0' + ) + }) +}) + +describe('projectStatus', async () => { + beforeEach(() => { + vi.resetAllMocks() + vi.mock('node-fetch') + vi.mock('../../../src/commands/fetch/create-url') + vi.mock('../../../src/utils/configure-proxy-tunnel') + vi.mock('fs/promises') + }) + + it('should fetch project status', async () => { + const sonarqubeResult = { + projectStatus: { + status: 'OK', + }, + } + const mockedFetch = vi.mocked(fetch).mockResolvedValue({ + ok: true, + text: async () => { + return JSON.stringify(sonarqubeResult) + }, + status: 200, + statusText: 'OK', + } as Response) + const mockedCreateApiUrl = vi + .mocked(createApiUrl) + .mockReturnValue(new URL('https://test-url')) + const mockedCreateDashboardUrl = vi + .mocked(createDashboardUrl) + .mockReturnValue(new URL('https://test-url')) + const mockedCreateAuthHeader = vi + .mocked(createAuthHeader) + .mockReturnValue('Basic dG9rZW46') + const mockedWriteFile = vi.mocked(writeFile).mockResolvedValue(undefined) + const mockedConfigureProxyTunnel = vi + .mocked(configureProxyTunnel) + .mockReturnValue(undefined) + + const options = { + hostname: 'test-host', + port: 8080, + protocol: 'https', + projectKey: 'test-key', + enableProxy: false, + outputPath: 'test-path', + accessToken: 'test-token', + } as any + await expect(projectStatus(options)).resolves.toBeUndefined() + expect(mockedCreateAuthHeader).toHaveBeenCalledWith(options.accessToken) + expect(mockedCreateApiUrl).toHaveBeenCalledWith( + options.hostname, + options.port, + options.protocol, + 'api/qualitygates/project_status', + { projectKey: options.projectKey } + ) + expect(mockedFetch).toHaveBeenCalled() + expect(mockedCreateDashboardUrl).toHaveBeenCalledWith( + options.hostname, + options.port, + options.protocol, + options.projectKey + ) + expect(mockedConfigureProxyTunnel).not.toHaveBeenCalled() + expect(mockedWriteFile).toHaveBeenCalledWith( + options.outputPath, + JSON.stringify(sonarqubeResult, null, 2) + ) + }) +}) diff --git a/yaku-apps-typescript/apps/sonarqube/test/unit/utils/configure-proxy-tunnel.spec.ts b/yaku-apps-typescript/apps/sonarqube/test/unit/utils/configure-proxy-tunnel.spec.ts new file mode 100644 index 00000000..37dcab88 --- /dev/null +++ b/yaku-apps-typescript/apps/sonarqube/test/unit/utils/configure-proxy-tunnel.spec.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest' +import { configureProxyTunnel } from '../../../src/utils/configure-proxy-tunnel' + +describe('configureProxyTunnel', async () => { + it('should return a agent config for https', () => { + expect( + configureProxyTunnel('https', 'https://proxy', 'http://proxy') + ).toBeDefined() + }) + it('should return a agent config for http', () => { + expect( + configureProxyTunnel('http', 'https://proxy', 'http://proxy') + ).toBeDefined() + }) + it('should return undefined if no protocol is provided', () => { + expect(configureProxyTunnel('', 'https://proxy', 'http://proxy')).toEqual( + undefined + ) + }) + it('should return undefined for https if no https_proxy is provided', () => { + expect(configureProxyTunnel('https', undefined, undefined)).toEqual( + undefined + ) + }) + it('should return undefined for http if no http_proxy is provided', () => { + expect(configureProxyTunnel('http', undefined, undefined)).toEqual( + undefined + ) + }) + it('should return undefined if no proxy is provided', () => { + expect( + configureProxyTunnel('https', 'https://proxy', 'http://proxy') + ).toBeDefined() + }) +}) diff --git a/yaku-apps-typescript/apps/sonarqube/tsconfig.json b/yaku-apps-typescript/apps/sonarqube/tsconfig.json new file mode 100644 index 00000000..e1f51365 --- /dev/null +++ b/yaku-apps-typescript/apps/sonarqube/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@B-S-F/typescript-config/base.json", + "ts-node": { + "compilerOptions": { + "module": "ESNext" + } + }, + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/yaku-apps-typescript/apps/sonarqube/tsup.config.json b/yaku-apps-typescript/apps/sonarqube/tsup.config.json new file mode 100644 index 00000000..f35b9ed3 --- /dev/null +++ b/yaku-apps-typescript/apps/sonarqube/tsup.config.json @@ -0,0 +1,6 @@ +{ + "entry": ["src/index.ts"], + "splitting": false, + "sourcemap": true, + "clean": true +} diff --git a/yaku-apps-typescript/apps/sonarqube/tsup.config.ts b/yaku-apps-typescript/apps/sonarqube/tsup.config.ts new file mode 100644 index 00000000..94b5f89e --- /dev/null +++ b/yaku-apps-typescript/apps/sonarqube/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/**/*.ts'], + splitting: false, + sourcemap: true, + clean: true, + target: 'node18', + format: ['esm'], + bundle: false, +}) diff --git a/yaku-apps-typescript/apps/sonarqube/vitest-integration.config.ts b/yaku-apps-typescript/apps/sonarqube/vitest-integration.config.ts new file mode 100644 index 00000000..8bec0725 --- /dev/null +++ b/yaku-apps-typescript/apps/sonarqube/vitest-integration.config.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['test/integration/**/*.int-spec.ts'], + pool: 'threads', + poolOptions: { + threads: { + maxThreads: 1, + minThreads: 1, + }, + }, + typecheck: { + tsconfig: 'tsconfig.json', + }, + reporters: ['junit', 'default'], + outputFile: 'reports/integration-test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/apps/sonarqube/vitest.config.ts b/yaku-apps-typescript/apps/sonarqube/vitest.config.ts new file mode 100644 index 00000000..1cf5e21f --- /dev/null +++ b/yaku-apps-typescript/apps/sonarqube/vitest.config.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + typecheck: { + tsconfig: 'tsconfig.json', + }, + coverage: { + provider: 'v8', + all: true, + enabled: true, + reporter: ['cobertura', 'json-summary', 'text-summary'], + include: ['src'], + }, + reporters: ['junit', 'default'], + outputFile: 'reports/test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/integration-tests/.prettierrc b/yaku-apps-typescript/integration-tests/.prettierrc new file mode 100644 index 00000000..b2095be8 --- /dev/null +++ b/yaku-apps-typescript/integration-tests/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": false, + "singleQuote": true +} diff --git a/yaku-apps-typescript/integration-tests/src/util/cert.pem b/yaku-apps-typescript/integration-tests/src/util/cert.pem new file mode 100644 index 00000000..c8e39c55 --- /dev/null +++ b/yaku-apps-typescript/integration-tests/src/util/cert.pem @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFLDCCAxQCCQC9Qs6b7JpDGTANBgkqhkiG9w0BAQsFADBXMQswCQYDVQQGEwJE +RTELMAkGA1UECAwCQlcxFDASBgNVBAcMC0x1ZHdpZ3NidXJnMREwDwYDVQQKDAhH +Uk9XIFBBVDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTIzMDQxOTEzNTM1M1oYDzIw +NTMwNTMxMTM1MzUzWjBXMQswCQYDVQQGEwJERTELMAkGA1UECAwCQlcxFDASBgNV +BAcMC0x1ZHdpZ3NidXJnMREwDwYDVQQKDAhHUk9XIFBBVDESMBAGA1UEAwwJbG9j +YWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4v0dOcxp2sjc +6GQvAlnvDrNbPkzpa7taDP1UlxFe5HnYgAwLv/7HgCfja72AVy/rIIvAbAxTZF5u +4zzgxQJruDV2mUvmt3YV9qX/fiEFUswJbn7zzXYQlVP8lIddVH4Migm2RKLqPmT7 +bT0eBsQnrdT1i9JiW0Zvo3n72/L/8FfYVPAcatjp0s2pEEcPM5VpIAAsF1ezJfjt +xwTFjG/lIEiQO7WL7nlGMNQ5CY+7DH/kSPuCAFtKO5wUVqnTTlfrnoHdBxNSbe+4 +HhbAfR2FU36uqdR1eF+bf3AH5GqrdfMeQ6l9ybpa3SprC9m/2mIQZX2V/q7vv4f8 +KGoATb9xfIZSZJDcyu3oOgyTwBIPtoyUglVJjk6W/+RCqO+T7v7cwp1JQjugodNV +YULz0vuLRWiB8/q6gSrRJOXTXI7G9gl/nkVSBXoPBHhbAMzZ24NLy4d7C0Z7QHmJ +in0Iuo76atdd6o+Xf3IOFSByARm3Les6dRiJcDqOw81NtGEt9CXEsgK/gIcrQMGT +k1NVIM/lcQcMAdVqVEdqh2vYEqwGLujRHiEf5pvgAtv/ulxR+LynN6yTHYBbJ+/5 +TyuwzryqiA/kJNlK6kiEkeH3N3FR5xDZ6Q2AOt+xAIN9XAOJQhp+/DamzIUEBubB +UXzq4XHFwgbXVuCpn8WW5BKiB8Ybd+UCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEA +0HrBKwUpUi4fVYw5hKbLNEi767/roPQH4lNUhhnqncoHXt0GsrPwgmKOCeNoQsHd +UicsUKq+AW1jUycMTNq3WfD3Qyi+ccaPpjPblX9SMhaFoKd6oOXGqU5AA7pOhscM +DDT/kcoAJeAiiK+yJayk+fNyFFG1qZD9T/oOvkMhbGf8YgFZ3tA4FJtAtROkPVvE +E3X1XlXiy+fOi7bE37S8402uHNVO7+UwAaXQTfXJgdC/aEMXyfypV5ggojAz/Kki ++WHj9OobfTd7rl7A8dmCSM1878u/pb35yN6KT8k7kGvlaZcBeZbP079PPpj8s7i+ +f8/vlG/6VSIWAkh7s68m9IHRKhhvep05TSsUTM9fllmguNbBO51VJBy6P3aQUV69 +ZglHkucJ5HfP5pCzlCeHcVyqC7tseWHfLC9nGbzgmuABEuPK7yImRPkZ5wQ7wP3N +9QX81u5rxY9D9zCZtvd+MYoJPt4Al0N2HfjGBr6uCK/6qeWobt8jaqXu3IaLJCpA +3GIo0KPFIMwWnQgHROtbrus2fqqse6E2+KvS6K7ZqktDLyLtOj7ac4ntHXmZo/tM +wqw+OMUUwrPiLkJFoxqwW8c8pkZe42ZYU6l/nREg7KR0aV0BopTw/NRUNpUPFkaD +A2j+0kgfoWStVRlOcBTy9ATM8aHNeTVUywCyvNV6CyE= +-----END CERTIFICATE----- diff --git a/yaku-apps-typescript/integration-tests/src/util/index.ts b/yaku-apps-typescript/integration-tests/src/util/index.ts new file mode 100644 index 00000000..3d23ae0a --- /dev/null +++ b/yaku-apps-typescript/integration-tests/src/util/index.ts @@ -0,0 +1,2 @@ +export * from './mockserver' +export * from './process' diff --git a/yaku-apps-typescript/integration-tests/src/util/key.pem b/yaku-apps-typescript/integration-tests/src/util/key.pem new file mode 100644 index 00000000..61ba42b6 --- /dev/null +++ b/yaku-apps-typescript/integration-tests/src/util/key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQDi/R05zGnayNzo +ZC8CWe8Os1s+TOlru1oM/VSXEV7kediADAu//seAJ+NrvYBXL+sgi8BsDFNkXm7j +PODFAmu4NXaZS+a3dhX2pf9+IQVSzAlufvPNdhCVU/yUh11UfgyKCbZEouo+ZPtt +PR4GxCet1PWL0mJbRm+jefvb8v/wV9hU8Bxq2OnSzakQRw8zlWkgACwXV7Ml+O3H +BMWMb+UgSJA7tYvueUYw1DkJj7sMf+RI+4IAW0o7nBRWqdNOV+uegd0HE1Jt77ge +FsB9HYVTfq6p1HV4X5t/cAfkaqt18x5DqX3JulrdKmsL2b/aYhBlfZX+ru+/h/wo +agBNv3F8hlJkkNzK7eg6DJPAEg+2jJSCVUmOTpb/5EKo75Pu/tzCnUlCO6Ch01Vh +QvPS+4tFaIHz+rqBKtEk5dNcjsb2CX+eRVIFeg8EeFsAzNnbg0vLh3sLRntAeYmK +fQi6jvpq113qj5d/cg4VIHIBGbct6zp1GIlwOo7DzU20YS30JcSyAr+AhytAwZOT +U1Ugz+VxBwwB1WpUR2qHa9gSrAYu6NEeIR/mm+AC2/+6XFH4vKc3rJMdgFsn7/lP +K7DOvKqID+Qk2UrqSISR4fc3cVHnENnpDYA637EAg31cA4lCGn78NqbMhQQG5sFR +fOrhccXCBtdW4KmfxZbkEqIHxht35QIDAQABAoICAG1VO9GOO2KNo5IwR1Bbn0E2 +dPmiNECXAn4FO8x0Kn/kjLrIkpRNFS0OiYVoxru4MgoAJpQkr7pFniXIOf/K9bXJ +0rFuFNhDgbrHJDRNlXHXI6fccHSDrOMwjBXCydqbyFBo8ylGS4v4Of7ZFHBv5Sje +zdMmaAfM+pMEe6Lq/gp3VZU7/oQcrSvDse5MO+89xWALlTE9JeZha66UBs9pSjTt +nsOT463fLkbwwfRwDcmshHn+4xRm5G+n80f3DvfKc4xqwtrYnLilYcrkbJ0XLhQU +je7xjg+IBa8xeCu2kZYDn405w4P6RjAHcX5IyRdYastn3WrKOmLIDlYtEK5iSzem +e9tsbRiSvKaaRiDARLL7v9XEEpJU3UC2P7H0zwaXptrCONl+JWVETDl3aX+qcWCe ++BHT4VGpeQd+Ac40/x+WDl5Hj5mNIbwRi1/kDD3OqnOKtN7VVFD6bTU+m23FKLBA +HX2zSi8ZOMDBoZBorYBQaCV1C/hpmmi4TGo/ggqpkG850AypLqY7zZdjP510zVgN +CbfFSPK8F+ATeeh1sqOTYPJzWRH6WMvFohJOgk8j/j2SE5XwET4eKlStk0PDM3H+ +69YRNvDfIYPtXL0KBduSqkU4eKHdBii6BPPP9d/L/DvNVvyUjLvMrJUElkPmYj0F ++CgJCumASUFhw1BeB6tBAoIBAQD1nGPyLWwb6/A0bVjLz3Vb6DuKX4mXtvUq1fGv +balyn4rhs80nO4vjhUjBraTOVDHEXGse8cEsfhcSImeBuFR1eCXz9kBn6BBn9AoQ +BFP6dwxxiiwA6Owpfhr8/b7DBK0CvQoV26szgiCKKrWlmvi/sOzcKi0pEmtWLXHh +y9SOW4LdkLOO/cJicBxYqo4ZHtNjI1pfEtPqrmdZFnHrnkEX6MMpJZUU4astDyAC +Z5Iytqqo8Jb1v8J/TBo9bNAgnJjholH17VcxOX132+jwS+KpdS6wWwKeIWlbxIqa +6LqgKSas1VALZqAIOwz0b2yD7QEBbXe0D//3jXB4hQtWeGY1AoIBAQDslxKYywyG +Jkk4cnsK8Nu5wNaxSN3ZVjhrKZTvKGB/w0Kbdt6m+JtFeum5DZ1FuO/4xjK93b3e +0hO7AspzwZNqzLZVYcMR6icvge7qmrgD5vvJKabysm7sPJVNF5MvOjunmvM8MJsq +KwtU62crsORCjpoTX7qHNUTsWeQmSZLoLtxTne7Ask2rRBral59/6DD3e0Nz98pI +On93GSYSGkaqiUc4FL7VxfiFLyElg32oQDElAVHz3rKLUDzvkEnIdi7vu4CZJz0Y +Q2hPiKWe5o4BcGxTcoTfHdyOjEGhxVM2NpjfdXUlykMV9aQlQeG9gOG3ySUYQNF3 +nQsXedl/HkDxAoIBABDkB2+RNh0ZdbR1TKT/iGegqe+TMGgxmdyvR+azmPKcDDYH +YnVvP+iOsvk20t2ppp6FaIyBPbKsnTOPECU4ov5NG/cSGU3MBMzRWJvPYGMaKs3o +HayWWB9mX77ESIkq2icVDwt/xt9M9KXr2AAijzbHmRJvCBoJ/T249Fr95IlBu9c2 +61JLG0IfyaNDX3BU0V7BAKcHKXG7OrpCs+TrRji5tiovPnhoKJh5sM7ZhpFcRJJH +sWoHHP4aIrfUst97RXxG6HIMN3HYLUu/4N4dqeHTgDl6mMx9Kby44HtUw/jdu54p +MU8HaLwIK5Tn0MOl2eraN2A3tXe1z2VKaQQyVX0CggEAWeWVdWVB3v9RNxeY1TFr +7ArwCPENCvYN/foQ00beU+2Xs4bZV0yDg0UO5ffcWI/K7xYwVaCZ8r6ULK0EzDMz +lpMufQbmnjoApbaTV6VuYl00Mt2WyAUwzKbAfEiG7p0L6cWgwrAdZUpxxdSkoR9X +vEp/FPl84L9G7x/A5yNxpLOZmFQcUi/t/zOjmIegXXOWl5LIsJnozMUdhd2Sb7J4 +Q3hiDVckpAnTQpgD2kM6TeIGSm9T+nwWD4Vvgf+raXYuo3z1gjw8pKmISyA6/kPQ +lY9oOdT70+N+2NNGZPebhK/+KnpxBujx0LhDpLyB0AXWvoS5iZune/G6MzNjhz+x +oQKCAQBP6jp+rDscv/1p0hoqv2p5DyC9CGOcH5l2gGVLbzDUz62Bh0wzOfAwBUBt +olUCCc24McJuPT5Rko0UIZ7XM3JL6CMAUIVHdVcHq7oC45JB/Zd6RanWVOqVDZYg +S0R9At0uEgRVe49wfCKwLNldhHcBLH5O/Isb7S7fniCsY0lMxi9wlbcFHmk/0llH +OoDV4npiM5cbB5ohNEzBAvnYcOWbcggTVLeZc6EcBoaDq5fv47PfJUS+wKFshB4J +NO335MGAIH9rvl9b6atll3o3LMjCT4coYdNZKKxKN82V9EF6lC8xHAOq3cwvRH4A +A5V4jS3mN1a52lOBYMa52JUX/WIW +-----END PRIVATE KEY----- diff --git a/yaku-apps-typescript/integration-tests/src/util/mockserver.ts b/yaku-apps-typescript/integration-tests/src/util/mockserver.ts new file mode 100644 index 00000000..fc008735 --- /dev/null +++ b/yaku-apps-typescript/integration-tests/src/util/mockserver.ts @@ -0,0 +1,152 @@ +import express, { Express, Request, Response } from 'express' +import * as fs from 'fs' +import { IncomingHttpHeaders, Server } from 'http' +import * as https from 'https' +import path from 'path' + +export const MOCK_SERVER_CERT_PATH: string = path.join(__dirname, 'cert.pem') + +type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' +export interface MockServerOptions { + port?: number + https?: boolean + responses: { + [endpoint: string]: { + get?: MockResponse + post?: MockResponse + put?: MockResponse + patch?: MockResponse + delete?: MockResponse + } + } + delay?: number +} + +export type MockResponse = StaticMockResponse | StaticMockResponse[] + +export interface StaticMockResponse { + responseStatus: number + responseHeaders?: any + responseBody?: any +} + +export interface QueryParams { + [key: string]: undefined | string | string[] | QueryParams | QueryParams[] +} + +export interface ReceivedRequest { + body?: any + headers: IncomingHttpHeaders + cookies: any + query: QueryParams +} + +export class MockServer { + private readonly requests: { + [endpoint: string]: { + get?: ReceivedRequest[] + post?: ReceivedRequest[] + put?: ReceivedRequest[] + patch?: ReceivedRequest[] + delete?: ReceivedRequest[] + } + } = {} + private readonly app: Express + private server?: Server + + constructor(private readonly options: MockServerOptions) { + this.app = express() + this.start() + } + + async stop(): Promise { + return new Promise((resolve) => { + if (this.server == null) { + resolve() + } else { + this.server.close(() => resolve()) + } + }) + } + + getRequests(endpoint: string, method: HttpMethod): ReceivedRequest[] { + return this.requests[endpoint]?.[method] ?? [] + } + + getNumberOfRequests(): number { + let i = 0 + + Object.values(this.requests).forEach((endpoint) => { + Object.values(endpoint).forEach((method) => { + i += method.length + }) + }) + + return i + } + + private mockEndpoint( + endpoint: string, + method: HttpMethod, + mockResponse: MockResponse + ): void { + const mockRequestHandler = async (req: Request, res: Response) => { + this.requests[endpoint] ??= {} + this.requests[endpoint][method] ??= [] + this.requests[endpoint][method]!.push({ + body: req.body, + headers: req.headers, + cookies: req.cookies, + query: req.query, + }) + if (Array.isArray(mockResponse)) { + const numOfReceivedRequests = this.requests[endpoint][method]!.length + const currentRequest: StaticMockResponse = + mockResponse[numOfReceivedRequests - 1] + if (this.options.delay) { + await new Promise((resolve) => + setTimeout(resolve, this.options.delay) + ) + } + res.status(currentRequest.responseStatus) + res.header(currentRequest.responseHeaders) + res.send(currentRequest.responseBody) + } else { + if (this.options.delay) { + await new Promise((resolve) => + setTimeout(resolve, this.options.delay) + ) + } + res.status(mockResponse.responseStatus) + res.header(mockResponse.responseHeaders) + res.send(mockResponse.responseBody) + } + } + this.app[method](endpoint, mockRequestHandler) + } + + private start(): void { + this.app.use(express.json()) + for (const [endpoint, mockResponses] of Object.entries( + this.options.responses + )) { + for (const [method, mockResponse] of Object.entries(mockResponses)) { + this.mockEndpoint(endpoint, method as HttpMethod, mockResponse) + } + } + + if (this.options.https) { + this.server = https + .createServer( + { + key: fs.readFileSync(path.join(__dirname, 'key.pem')), + cert: fs.readFileSync(MOCK_SERVER_CERT_PATH), + }, + this.app + ) + .listen(this.options.port ?? 8080) + } else { + this.server = this.app.listen(this.options.port ?? 8080) + } + } +} diff --git a/yaku-apps-typescript/integration-tests/src/util/process.ts b/yaku-apps-typescript/integration-tests/src/util/process.ts new file mode 100644 index 00000000..6c792e66 --- /dev/null +++ b/yaku-apps-typescript/integration-tests/src/util/process.ts @@ -0,0 +1,71 @@ +import { + ChildProcessWithoutNullStreams, + spawn, + SpawnOptionsWithoutStdio, +} from 'child_process' +import { EOL } from 'os' + +export type RunProcessResult = { + stdout: string[] + stderr: string[] + exitCode: number +} + +function createProcess( + executable: string, + args: string[] = [], + options: SpawnOptionsWithoutStdio = {} +): ChildProcessWithoutNullStreams { + const nodeArgs: string[] = [executable, ...args] + options.env ??= {} + options.env = { + ...options.env, + PATH: process.env.PATH, + } + return spawn('node', nodeArgs, options) +} + +/** + * Run an executable in a child process using arguments. + * @param executable - path to the executable to run + * @param args - arguments the executable should be called with + * @param options - further configuration properties for the child process (e.g. env variables) + */ +export async function run( + executable: string, + args: string[] = [], + options?: SpawnOptionsWithoutStdio +): Promise { + const childProcess: ChildProcessWithoutNullStreams = createProcess( + executable, + args, + options + ) + childProcess.stdin.setDefaultEncoding('utf-8') + + return new Promise((resolve) => { + let stdout = '' + let stderr = '' + childProcess.stdout.on('data', (data) => { + console.log(data.toString()) + stdout = `${stdout}${data.toString()}` + }) + childProcess.stderr.on('data', (data) => { + console.warn(data.toString()) + stderr = `${stderr}${data.toString()}` + }) + childProcess.on('exit', (exitCode: number) => + resolve({ + stdout: stdout + .replace(/(\r\n|\n|\r)/gm, EOL) + .split(EOL) + .filter((s) => s.length > 0), + stderr: stderr + .replace(/(\r\n|\n|\r)/gm, EOL) + .split(EOL) + .filter((s) => s.length > 0), + exitCode: exitCode ?? 1, + }) + ) + }) +} diff --git a/yaku-apps-typescript/integration-tests/tsconfig.json b/yaku-apps-typescript/integration-tests/tsconfig.json new file mode 100644 index 00000000..9173c1cc --- /dev/null +++ b/yaku-apps-typescript/integration-tests/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2017", + "module": "ES2022", + "moduleResolution": "node", + "resolveJsonModule": true, + "sourceMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/yaku-apps-typescript/package-lock.json b/yaku-apps-typescript/package-lock.json new file mode 100644 index 00000000..1dc55c2f --- /dev/null +++ b/yaku-apps-typescript/package-lock.json @@ -0,0 +1,11059 @@ +{ + "name": "@B-S-F/qg-apps-typescript", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@B-S-F/qg-apps-typescript", + "version": "0.1.0", + "workspaces": [ + "apps/*", + "packages/*" + ], + "devDependencies": { + "@B-S-F/eslint-config": "^0.1.0", + "@B-S-F/typescript-config": "^0.1.0", + "@commitlint/cli": "^18.2.0", + "@commitlint/config-conventional": "^18.1.0", + "@types/node": "^18.14.2", + "@typescript-eslint/eslint-plugin": "^6.7.3", + "@typescript-eslint/parser": "^6.7.3", + "@vitest/coverage-v8": "^2.1.1", + "@vitest/ui": "^2.1.1", + "c8": "^7.13.0", + "eslint": "^8.35.0", + "eslint-config-prettier": "^8.6.0", + "eslint-plugin-json": "^3.1.0", + "express": "^5.0.0", + "husky": "^8.0.3", + "lint-staged": "^13.1.2", + "nodemon": "^3.1.7", + "prettier": "^2.8.4", + "tsup": "^6.6.3", + "turbo": "^1.10.13", + "typescript": "^4.9.5", + "vitest": "^2.1.1" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.3.0" + } + }, + "apps/ado-work-items-evaluator": { + "name": "@B-S-F/ado-work-items-evaluator", + "version": "0.8.0", + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "@B-S-F/issue-validators": "^0.1.0", + "js-yaml": "^4.1.0" + }, + "bin": { + "ado-work-items-evaluator": "dist/index.js" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "eslint": "*", + "nodemon": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + } + }, + "apps/ado-work-items-fetcher": { + "name": "@B-S-F/ado-work-items-fetcher", + "version": "0.7.2", + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "@B-S-F/issue-validators": "^0.1.0", + "axios": "^1.6.0", + "is-valid-hostname": "^1.0.2", + "tunnel": "^0.0.6", + "yaml": "^1.10.2", + "zod": "^3.22.3", + "zod-error": "^1.5.0" + }, + "bin": { + "ado-work-items-fetcher": "dist/index.js" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@types/tunnel": "^0.0.2", + "@vitest/ui": "*", + "c8": "*", + "eslint": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + } + }, + "apps/ado-work-items-fetcher/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, + "apps/defender-for-cloud": { + "name": "@B-S-F/defender-for-cloud", + "version": "0.3.1", + "license": "BIOSLv4", + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "axios": "^1.6.0", + "qs": "^6.11.0" + }, + "bin": { + "defender-for-cloud": "dist/index.js" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "@vitest/coverage-v8": "*", + "@vitest/ui": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "nodemon": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + } + }, + "apps/docupedia-fetcher": { + "name": "@B-S-F/docupedia-fetcher", + "version": "0.8.1", + "dependencies": { + "@ali-tas/htmldiff-js": "^1.1.3", + "@B-S-F/autopilot-utils": "^0.11.0", + "axios": "^1.6.7", + "commander": "^12.0.0", + "node-html-parser": "^6.1.12", + "node-stream-zip": "^1.15.0", + "proxy-agent": "^6.3.1", + "yaml": "^2.3.4", + "zod": "^3.22.4" + }, + "bin": { + "docupedia-fetcher": "dist/index.js" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/archiver": "^5.3.2", + "@types/node": "*", + "@vitest/ui": "*", + "archiver": "^5.3.1", + "c8": "*", + "eslint": "*", + "nodemon": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + } + }, + "apps/docupedia-fetcher/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "engines": { + "node": ">=18" + } + }, + "apps/gh-app": { + "name": "@B-S-F/gh-app", + "version": "0.3.1", + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "https-proxy-agent": "^7.0.4", + "octokit": "^3.1.2", + "undici": "^6.19.2", + "universal-github-app-jwt": "^2.2.0" + }, + "bin": { + "gh-app": "dist/index.js" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/jsonpath": "^0.2.0", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "@vitest/coverage-v8": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + } + }, + "apps/git-fetcher": { + "name": "@B-S-F/git-fetcher", + "version": "0.7.0", + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "date-fns": "^2.30.0", + "fs-extra": "^10.1.0", + "node-fetch": "^3.2.10", + "process": "^0.11.10", + "yaml": "^2.2.1", + "zod": "^3.22.3", + "zod-validation-error": "^1.3.0" + }, + "bin": { + "git-fetcher": "dist/index.js" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "@typescript-eslint/parser": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*" + } + }, + "apps/html-finalizer": { + "name": "@B-S-F/html-finalizer", + "version": "0.33.0", + "dependencies": { + "@B-S-F/markdown-utils": "~0.2.0", + "ejs": "^3.1.10", + "fs-extra": "^10.1.0", + "yaml": "^2.4.1" + }, + "bin": { + "html-finalizer": "dist/run.js" + }, + "devDependencies": { + "@types/ejs": "^3.1.1", + "@types/node": "*", + "eslint": "*", + "nodemon": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + } + }, + "apps/jira-evaluator": { + "name": "@B-S-F/jira-evaluator", + "version": "0.7.0", + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "@B-S-F/issue-validators": "^0.1.0" + }, + "bin": { + "jira-evaluator": "dist/index.js" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "@vitest/coverage-v8": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + } + }, + "apps/jira-fetcher": { + "name": "@B-S-F/jira-fetcher", + "version": "0.9.0", + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "@B-S-F/issue-validators": "*", + "node-fetch": "^3.2.6", + "proxy-agent": "^6.3.1", + "yaml": "*" + }, + "bin": { + "jira-fetcher": "dist/index.js" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "c8": "*", + "eslint": "*", + "nodemon": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + } + }, + "apps/jira-finalizer": { + "name": "@B-S-F/jira-finalizer", + "version": "0.1.1", + "dependencies": { + "commander": "^9.4.0", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "mime-types": "^2.1.35", + "node-fetch": "^3.3.0", + "yaml": "^2.1.1" + }, + "bin": { + "jira-finalizer": "dist/src/index.js" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "nodemon": "^3.0.1", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + } + }, + "apps/jira-finalizer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "apps/json-evaluator": { + "name": "@B-S-F/json-evaluator", + "version": "0.11.0", + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "@B-S-F/json-evaluator-lib": "^0.9.0", + "colors": "1.4.0", + "yaml": "^2.2.1", + "zod": "^3.22.3", + "zod-error": "^1.5.0" + }, + "bin": { + "json-evaluator": "dist/index.js" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/json2csv": "^5.0.3", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "prettier": "*", + "tsc-alias": "^1.8.8", + "tsup": "*", + "typescript": "*", + "vitest": "*" + } + }, + "apps/manual-answer-evaluator": { + "name": "@B-S-F/manual-answer-evaluator", + "version": "0.8.0", + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "parse-duration": "^1.0.2" + }, + "bin": { + "manual-answer-evaluator": "dist/evaluate.js" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@vitest/ui": "*", + "c8": "*", + "eslint": "*", + "nodemon": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + } + }, + "apps/mend-fetcher": { + "name": "@B-S-F/mend-fetcher", + "version": "0.7.0", + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "axios": "^1.6.0", + "bottleneck": "^2.19.5", + "zod": "^3.22.4" + }, + "bin": { + "mend-fetcher": "dist/index.js" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@vitest/ui": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "nodemon": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + } + }, + "apps/ocaas-app": { + "name": "@B-S-F/ocaas-app", + "version": "0.1.0", + "bin": { + "ocaas": "dist/index.js" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + } + }, + "apps/oneq-finalizer": { + "name": "@B-S-F/oneq-finalizer", + "version": "0.7.2", + "dependencies": { + "commander": "^9.4.0", + "fs-extra": "^10.1.0", + "yaml": "^2.1.1" + }, + "bin": { + "oneq": "dist/src/index.js" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/fs-extra": "^11.0.1", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "@vitest/coverage-v8": "*", + "@vitest/ui": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + } + }, + "apps/smb-fetcher": { + "name": "@B-S-F/smb-fetcher", + "version": "0.1.1", + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "v9u-smb2": "^1.0.6", + "yaml": "^2.3.2", + "zod": "^3.22.3" + }, + "bin": { + "smb-fetcher": "dist/index.js" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@vitest/ui": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "nodemon": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + } + }, + "apps/sonarqube": { + "name": "@B-S-F/sonarqube", + "version": "0.2.0", + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "commander": "^11.0.0", + "node-fetch": "^3.3.2" + }, + "bin": { + "sonarqube": "dist/index.js" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/axios": "^0.14.0", + "@types/node": "*", + "c8": "*", + "eslint": "*", + "nodemon": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + } + }, + "apps/sonarqube-evaluator": { + "name": "@B-S-F/sonarqube-evaluator", + "version": "0.4.0", + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "commander": "^9.5.0", + "title-case": "^3.0.3" + }, + "bin": { + "sonarqube-evaluator": "dist/index.js" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@vitest/ui": "*", + "c8": "*", + "eslint": "*", + "nodemon": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + } + }, + "apps/sonarqube/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "engines": { + "node": ">=16" + } + }, + "apps/xc-bosch-requirements-evaluator": { + "name": "@B-S-F/xc-bosch-requirements-evaluator", + "version": "0.7.0", + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0" + }, + "bin": { + "xc-bosch-requirements-evaluator": "dist/index.js" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + } + }, + "apps/xc-conformity-requirements-evaluator": { + "name": "@B-S-F/xc-conformity-requirements-evaluator", + "version": "0.7.0", + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0" + }, + "bin": { + "xc-conformity-requirements-evaluator": "dist/index.js" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + } + }, + "apps/xc-open-defects-evaluator": { + "name": "@B-S-F/xc-open-defects-evaluator", + "version": "0.7.1", + "dependencies": { + "@B-S-F/autopilot-utils": "^0.11.0", + "semver": "7.5.4" + }, + "bin": { + "xc-open-defects-evaluator": "dist/index.js" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + } + }, + "apps/xc-open-defects-evaluator/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ali-tas/htmldiff-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@ali-tas/htmldiff-js/-/htmldiff-js-1.1.3.tgz", + "integrity": "sha512-2QyK08A/O1pG5yRwFY1B4fQJbaxH8b0FxLiGTNuZP9yp8Y7uW6yat9UXzA2yjF5KOaNQ9y9/Eu94+ro74S9Qng==" + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", + "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@B-S-F/ado-work-items-evaluator": { + "resolved": "apps/ado-work-items-evaluator", + "link": true + }, + "node_modules/@B-S-F/ado-work-items-fetcher": { + "resolved": "apps/ado-work-items-fetcher", + "link": true + }, + "node_modules/@B-S-F/autopilot-utils": { + "resolved": "packages/autopilot-utils", + "link": true + }, + "node_modules/@B-S-F/defender-for-cloud": { + "resolved": "apps/defender-for-cloud", + "link": true + }, + "node_modules/@B-S-F/docupedia-fetcher": { + "resolved": "apps/docupedia-fetcher", + "link": true + }, + "node_modules/@B-S-F/eslint-config": { + "resolved": "packages/eslint-config", + "link": true + }, + "node_modules/@B-S-F/fonts": { + "resolved": "packages/fonts", + "link": true + }, + "node_modules/@B-S-F/gh-app": { + "resolved": "apps/gh-app", + "link": true + }, + "node_modules/@B-S-F/git-fetcher": { + "resolved": "apps/git-fetcher", + "link": true + }, + "node_modules/@B-S-F/html-finalizer": { + "resolved": "apps/html-finalizer", + "link": true + }, + "node_modules/@B-S-F/issue-validators": { + "resolved": "packages/issue-validators", + "link": true + }, + "node_modules/@B-S-F/jira-evaluator": { + "resolved": "apps/jira-evaluator", + "link": true + }, + "node_modules/@B-S-F/jira-fetcher": { + "resolved": "apps/jira-fetcher", + "link": true + }, + "node_modules/@B-S-F/jira-finalizer": { + "resolved": "apps/jira-finalizer", + "link": true + }, + "node_modules/@B-S-F/json-evaluator": { + "resolved": "apps/json-evaluator", + "link": true + }, + "node_modules/@B-S-F/json-evaluator-lib": { + "resolved": "packages/json-evaluator-lib", + "link": true + }, + "node_modules/@B-S-F/lint-staged-config": { + "resolved": "packages/lint-staged-config", + "link": true + }, + "node_modules/@B-S-F/log-utils": { + "resolved": "packages/log-utils", + "link": true + }, + "node_modules/@B-S-F/manual-answer-evaluator": { + "resolved": "apps/manual-answer-evaluator", + "link": true + }, + "node_modules/@B-S-F/markdown-utils": { + "resolved": "packages/markdown-utils", + "link": true + }, + "node_modules/@B-S-F/mend-fetcher": { + "resolved": "apps/mend-fetcher", + "link": true + }, + "node_modules/@B-S-F/ocaas-app": { + "resolved": "apps/ocaas-app", + "link": true + }, + "node_modules/@B-S-F/oneq-finalizer": { + "resolved": "apps/oneq-finalizer", + "link": true + }, + "node_modules/@B-S-F/smb-fetcher": { + "resolved": "apps/smb-fetcher", + "link": true + }, + "node_modules/@B-S-F/sonarqube": { + "resolved": "apps/sonarqube", + "link": true + }, + "node_modules/@B-S-F/sonarqube-evaluator": { + "resolved": "apps/sonarqube-evaluator", + "link": true + }, + "node_modules/@B-S-F/sync-versions": { + "resolved": "packages/sync-versions", + "link": true + }, + "node_modules/@B-S-F/typescript-config": { + "resolved": "packages/typescript-config", + "link": true + }, + "node_modules/@B-S-F/xc-bosch-requirements-evaluator": { + "resolved": "apps/xc-bosch-requirements-evaluator", + "link": true + }, + "node_modules/@B-S-F/xc-conformity-requirements-evaluator": { + "resolved": "apps/xc-conformity-requirements-evaluator", + "link": true + }, + "node_modules/@B-S-F/xc-open-defects-evaluator": { + "resolved": "apps/xc-open-defects-evaluator", + "link": true + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@commitlint/cli": { + "version": "18.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-18.6.1.tgz", + "integrity": "sha512-5IDE0a+lWGdkOvKH892HHAZgbAjcj1mT5QrfA/SVbLJV/BbBMGyKN0W5mhgjekPJJwEQdVNvhl9PwUacY58Usw==", + "dev": true, + "dependencies": { + "@commitlint/format": "^18.6.1", + "@commitlint/lint": "^18.6.1", + "@commitlint/load": "^18.6.1", + "@commitlint/read": "^18.6.1", + "@commitlint/types": "^18.6.1", + "execa": "^5.0.0", + "lodash.isfunction": "^3.0.9", + "resolve-from": "5.0.0", + "resolve-global": "1.0.0", + "yargs": "^17.0.0" + }, + "bin": { + "commitlint": "cli.js" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/config-conventional": { + "version": "18.6.3", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-18.6.3.tgz", + "integrity": "sha512-8ZrRHqF6je+TRaFoJVwszwnOXb/VeYrPmTwPhf0WxpzpGTcYy1p0SPyZ2eRn/sRi/obnWAcobtDAq6+gJQQNhQ==", + "dev": true, + "dependencies": { + "@commitlint/types": "^18.6.1", + "conventional-changelog-conventionalcommits": "^7.0.2" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/config-validator": { + "version": "18.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-18.6.1.tgz", + "integrity": "sha512-05uiToBVfPhepcQWE1ZQBR/Io3+tb3gEotZjnI4tTzzPk16NffN6YABgwFQCLmzZefbDcmwWqJWc2XT47q7Znw==", + "dev": true, + "dependencies": { + "@commitlint/types": "^18.6.1", + "ajv": "^8.11.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/ensure": { + "version": "18.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-18.6.1.tgz", + "integrity": "sha512-BPm6+SspyxQ7ZTsZwXc7TRQL5kh5YWt3euKmEIBZnocMFkJevqs3fbLRb8+8I/cfbVcAo4mxRlpTPfz8zX7SnQ==", + "dev": true, + "dependencies": { + "@commitlint/types": "^18.6.1", + "lodash.camelcase": "^4.3.0", + "lodash.kebabcase": "^4.1.1", + "lodash.snakecase": "^4.1.1", + "lodash.startcase": "^4.4.0", + "lodash.upperfirst": "^4.3.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/execute-rule": { + "version": "18.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-18.6.1.tgz", + "integrity": "sha512-7s37a+iWyJiGUeMFF6qBlyZciUkF8odSAnHijbD36YDctLhGKoYltdvuJ/AFfRm6cBLRtRk9cCVPdsEFtt/2rg==", + "dev": true, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/format": { + "version": "18.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-18.6.1.tgz", + "integrity": "sha512-K8mNcfU/JEFCharj2xVjxGSF+My+FbUHoqR+4GqPGrHNqXOGNio47ziiR4HQUPKtiNs05o8/WyLBoIpMVOP7wg==", + "dev": true, + "dependencies": { + "@commitlint/types": "^18.6.1", + "chalk": "^4.1.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/is-ignored": { + "version": "18.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-18.6.1.tgz", + "integrity": "sha512-MOfJjkEJj/wOaPBw5jFjTtfnx72RGwqYIROABudOtJKW7isVjFe9j0t8xhceA02QebtYf4P/zea4HIwnXg8rvA==", + "dev": true, + "dependencies": { + "@commitlint/types": "^18.6.1", + "semver": "7.6.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/lint": { + "version": "18.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-18.6.1.tgz", + "integrity": "sha512-8WwIFo3jAuU+h1PkYe5SfnIOzp+TtBHpFr4S8oJWhu44IWKuVx6GOPux3+9H1iHOan/rGBaiacicZkMZuluhfQ==", + "dev": true, + "dependencies": { + "@commitlint/is-ignored": "^18.6.1", + "@commitlint/parse": "^18.6.1", + "@commitlint/rules": "^18.6.1", + "@commitlint/types": "^18.6.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/load": { + "version": "18.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-18.6.1.tgz", + "integrity": "sha512-p26x8734tSXUHoAw0ERIiHyW4RaI4Bj99D8YgUlVV9SedLf8hlWAfyIFhHRIhfPngLlCe0QYOdRKYFt8gy56TA==", + "dev": true, + "dependencies": { + "@commitlint/config-validator": "^18.6.1", + "@commitlint/execute-rule": "^18.6.1", + "@commitlint/resolve-extends": "^18.6.1", + "@commitlint/types": "^18.6.1", + "chalk": "^4.1.0", + "cosmiconfig": "^8.3.6", + "cosmiconfig-typescript-loader": "^5.0.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/message": { + "version": "18.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-18.6.1.tgz", + "integrity": "sha512-VKC10UTMLcpVjMIaHHsY1KwhuTQtdIKPkIdVEwWV+YuzKkzhlI3aNy6oo1eAN6b/D2LTtZkJe2enHmX0corYRw==", + "dev": true, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/parse": { + "version": "18.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-18.6.1.tgz", + "integrity": "sha512-eS/3GREtvVJqGZrwAGRwR9Gdno3YcZ6Xvuaa+vUF8j++wsmxrA2En3n0ccfVO2qVOLJC41ni7jSZhQiJpMPGOQ==", + "dev": true, + "dependencies": { + "@commitlint/types": "^18.6.1", + "conventional-changelog-angular": "^7.0.0", + "conventional-commits-parser": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/read": { + "version": "18.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-18.6.1.tgz", + "integrity": "sha512-ia6ODaQFzXrVul07ffSgbZGFajpe8xhnDeLIprLeyfz3ivQU1dIoHp7yz0QIorZ6yuf4nlzg4ZUkluDrGN/J/w==", + "dev": true, + "dependencies": { + "@commitlint/top-level": "^18.6.1", + "@commitlint/types": "^18.6.1", + "git-raw-commits": "^2.0.11", + "minimist": "^1.2.6" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/resolve-extends": { + "version": "18.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-18.6.1.tgz", + "integrity": "sha512-ifRAQtHwK+Gj3Bxj/5chhc4L2LIc3s30lpsyW67yyjsETR6ctHAHRu1FSpt0KqahK5xESqoJ92v6XxoDRtjwEQ==", + "dev": true, + "dependencies": { + "@commitlint/config-validator": "^18.6.1", + "@commitlint/types": "^18.6.1", + "import-fresh": "^3.0.0", + "lodash.mergewith": "^4.6.2", + "resolve-from": "^5.0.0", + "resolve-global": "^1.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/rules": { + "version": "18.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-18.6.1.tgz", + "integrity": "sha512-kguM6HxZDtz60v/zQYOe0voAtTdGybWXefA1iidjWYmyUUspO1zBPQEmJZ05/plIAqCVyNUTAiRPWIBKLCrGew==", + "dev": true, + "dependencies": { + "@commitlint/ensure": "^18.6.1", + "@commitlint/message": "^18.6.1", + "@commitlint/to-lines": "^18.6.1", + "@commitlint/types": "^18.6.1", + "execa": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/to-lines": { + "version": "18.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-18.6.1.tgz", + "integrity": "sha512-Gl+orGBxYSNphx1+83GYeNy5N0dQsHBQ9PJMriaLQDB51UQHCVLBT/HBdOx5VaYksivSf5Os55TLePbRLlW50Q==", + "dev": true, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/top-level": { + "version": "18.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-18.6.1.tgz", + "integrity": "sha512-HyiHQZUTf0+r0goTCDs/bbVv/LiiQ7AVtz6KIar+8ZrseB9+YJAIo8HQ2IC2QT1y3N1lbW6OqVEsTHjbT6hGSw==", + "dev": true, + "dependencies": { + "find-up": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/types": { + "version": "18.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-18.6.1.tgz", + "integrity": "sha512-gwRLBLra/Dozj2OywopeuHj2ac26gjGkz2cZ+86cTJOdtWfiRRr4+e77ZDAGc6MDWxaWheI+mAV5TLWWRwqrFg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@octokit/app": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/app/-/app-14.1.0.tgz", + "integrity": "sha512-g3uEsGOQCBl1+W1rgfwoRFUIR6PtvB2T1E4RpygeUU5LrLvlOqcxrt5lfykIeRpUPpupreGJUYl70fqMDXdTpw==", + "dependencies": { + "@octokit/auth-app": "^6.0.0", + "@octokit/auth-unauthenticated": "^5.0.0", + "@octokit/core": "^5.0.0", + "@octokit/oauth-app": "^6.0.0", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/types": "^12.0.0", + "@octokit/webhooks": "^12.0.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/app/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==" + }, + "node_modules/@octokit/app/node_modules/@octokit/plugin-paginate-rest": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.1.tgz", + "integrity": "sha512-wfGhE/TAkXZRLjksFXuDZdmGnJQHvtU/joFQdweXUgzo1XwvBCD4o4+75NtFfjfLK5IwLf9vHTfSiU3sLRYpRw==", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/app/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/auth-app": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.1.2.tgz", + "integrity": "sha512-fWjIOpxnL8/YFY3kqquciFQ4o99aCqHw5kMFoGPYbz/h5HNZ11dJlV9zag5wS2nt0X1wJ5cs9BUo+CsAPfW4jQ==", + "dependencies": { + "@octokit/auth-oauth-app": "^7.1.0", + "@octokit/auth-oauth-user": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "deprecation": "^2.3.1", + "lru-cache": "^10.0.0", + "universal-github-app-jwt": "^1.1.2", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/@octokit/auth-app/node_modules/universal-github-app-jwt": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-1.2.0.tgz", + "integrity": "sha512-dncpMpnsKBk0eetwfN8D8OUHGfiDhhJ+mtsbMl+7PfW7mYjiH8LIcqRmYMtzYLgSh47HjfdBtrBwIQ/gizKR3g==", + "dependencies": { + "@types/jsonwebtoken": "^9.0.0", + "jsonwebtoken": "^9.0.2" + } + }, + "node_modules/@octokit/auth-oauth-app": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.1.0.tgz", + "integrity": "sha512-w+SyJN/b0l/HEb4EOPRudo7uUOSW51jcK1jwLa+4r7PA8FPFpoxEnHBHMITqCsc/3Vo2qqFjgQfz/xUUvsSQnA==", + "dependencies": { + "@octokit/auth-oauth-device": "^6.1.0", + "@octokit/auth-oauth-user": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "@types/btoa-lite": "^1.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-device": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.1.0.tgz", + "integrity": "sha512-FNQ7cb8kASufd6Ej4gnJ3f1QB5vJitkoV1O0/g6e6lUsQ7+VsSNRHRmFScN2tV4IgKA12frrr/cegUs0t+0/Lw==", + "dependencies": { + "@octokit/oauth-methods": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-user": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.1.0.tgz", + "integrity": "sha512-FrEp8mtFuS/BrJyjpur+4GARteUCrPeR/tZJzD8YourzoVhRics7u7we/aDcKv+yywRNwNi/P4fRi631rG/OyQ==", + "dependencies": { + "@octokit/auth-oauth-device": "^6.1.0", + "@octokit/oauth-methods": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-unauthenticated": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-5.0.1.tgz", + "integrity": "sha512-oxeWzmBFxWd+XolxKTc4zr+h3mt+yofn4r7OfoIkR/Cj/o70eEGmPsFbueyJE2iBAGpjgTnEOKM3pnuEGVmiqg==", + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-unauthenticated/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==" + }, + "node_modules/@octokit/auth-unauthenticated/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz", + "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.5.tgz", + "integrity": "sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.0.tgz", + "integrity": "sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==", + "dependencies": { + "@octokit/request": "^8.3.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-app": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-6.1.0.tgz", + "integrity": "sha512-nIn/8eUJ/BKUVzxUXd5vpzl1rwaVxMyYbQkNZjHrF7Vk/yu98/YDF/N2KeWO7uZ0g3b5EyiFXFkZI8rJ+DH1/g==", + "dependencies": { + "@octokit/auth-oauth-app": "^7.0.0", + "@octokit/auth-oauth-user": "^4.0.0", + "@octokit/auth-unauthenticated": "^5.0.0", + "@octokit/core": "^5.0.0", + "@octokit/oauth-authorization-url": "^6.0.2", + "@octokit/oauth-methods": "^4.0.0", + "@types/aws-lambda": "^8.10.83", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-authorization-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-6.0.2.tgz", + "integrity": "sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-methods": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.1.0.tgz", + "integrity": "sha512-4tuKnCRecJ6CG6gr0XcEXdZtkTDbfbnD5oaHBmLERTjTMZNi2CbfEHZxPU41xXLDG4DfKf+sonu00zvKI9NSbw==", + "dependencies": { + "@octokit/oauth-authorization-url": "^6.0.2", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "btoa-lite": "^1.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" + }, + "node_modules/@octokit/plugin-paginate-graphql": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-4.0.1.tgz", + "integrity": "sha512-R8ZQNmrIKKpHWC6V2gum4x9LG2qF1RxRjo27gjQcG3j+vf2tLsEfE7I/wRWEPzYMaenr1M+qDAtNcwZve1ce1A==", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.1.tgz", + "integrity": "sha512-ryqobs26cLtM1kQxqeZui4v8FeznirUsksiA+RYemMPJ7Micju0WSkv50dBksTuZks9O5cg4wp+t8fZ/cLY56g==", + "dependencies": { + "@octokit/types": "^13.5.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.2.tgz", + "integrity": "sha512-EI7kXWidkt3Xlok5uN43suK99VWqc8OaIMktY9d9+RNKl69juoTyxmLoWPIZgJYzi41qj/9zU7G/ljnNOJ5AFA==", + "dependencies": { + "@octokit/types": "^13.5.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5" + } + }, + "node_modules/@octokit/plugin-retry": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz", + "integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==", + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/plugin-retry/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==" + }, + "node_modules/@octokit/plugin-retry/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/plugin-throttling": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-8.2.0.tgz", + "integrity": "sha512-nOpWtLayKFpgqmgD0y3GqXafMFuKcA4tRPZIfu7BArd2lEZeb1988nhWhwx4aZWmjDmUfdgVf7W+Tt4AmvRmMQ==", + "dependencies": { + "@octokit/types": "^12.2.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5.0.0" + } + }, + "node_modules/@octokit/plugin-throttling/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==" + }, + "node_modules/@octokit/plugin-throttling/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz", + "integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==", + "dependencies": { + "@octokit/endpoint": "^9.0.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz", + "integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.0.tgz", + "integrity": "sha512-CrooV/vKCXqwLa+osmHLIMUb87brpgUqlqkPGc6iE2wCkUvTrHiXFMhAKoDDaAAYJrtKtrFTgSQTg5nObBEaew==", + "dependencies": { + "@octokit/openapi-types": "^22.2.0" + } + }, + "node_modules/@octokit/webhooks": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-12.2.0.tgz", + "integrity": "sha512-CyuLJ0/P7bKZ+kIYw+fnkeVdhUzNuDKgNSI7pU/m7Nod0T7kP+s4s2f0pNmG9HL8/RZN1S0ZWTDll3VTMrFLAw==", + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/webhooks-methods": "^4.1.0", + "@octokit/webhooks-types": "7.4.0", + "aggregate-error": "^3.1.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/webhooks-methods": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-4.1.0.tgz", + "integrity": "sha512-zoQyKw8h9STNPqtm28UGOYFE7O6D4Il8VJwhAtMHFt2C4L0VQT1qGKLeefUOqHNs1mNRYSadVv7x0z8U2yyeWQ==", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/webhooks-types": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.4.0.tgz", + "integrity": "sha512-FE2V+QZ2UYlh+9wWd5BPLNXG+J/XUD/PPq0ovS+nCcGX4+3qVbi3jYOmCTW48hg9SBBLtInx9+o7fFt4H5iP0Q==" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.23.0.tgz", + "integrity": "sha512-8OR+Ok3SGEMsAZispLx8jruuXw0HVF16k+ub2eNXKHDmdxL4cf9NlNpAzhlOhNyXzKDEJuFeq0nZm+XlNb1IFw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.23.0.tgz", + "integrity": "sha512-rEFtX1nP8gqmLmPZsXRMoLVNB5JBwOzIAk/XAcEPuKrPa2nPJ+DuGGpfQUR0XjRm8KjHfTZLpWbKXkA5BoFL3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.23.0.tgz", + "integrity": "sha512-ZbqlMkJRMMPeapfaU4drYHns7Q5MIxjM/QeOO62qQZGPh9XWziap+NF9fsqPHT0KzEL6HaPspC7sOwpgyA3J9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.23.0.tgz", + "integrity": "sha512-PfmgQp78xx5rBCgn2oYPQ1rQTtOaQCna0kRaBlc5w7RlA3TDGGo7m3XaptgitUZ54US9915i7KeVPHoy3/W8tA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.23.0.tgz", + "integrity": "sha512-WAeZfAAPus56eQgBioezXRRzArAjWJGjNo/M+BHZygUcs9EePIuGI1Wfc6U/Ki+tMW17FFGvhCfYnfcKPh18SA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.23.0.tgz", + "integrity": "sha512-v7PGcp1O5XKZxKX8phTXtmJDVpE20Ub1eF6w9iMmI3qrrPak6yR9/5eeq7ziLMrMTjppkkskXyxnmm00HdtXjA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.23.0.tgz", + "integrity": "sha512-nAbWsDZ9UkU6xQiXEyXBNHAKbzSAi95H3gTStJq9UGiS1v+YVXwRHcQOQEF/3CHuhX5BVhShKoeOf6Q/1M+Zhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.23.0.tgz", + "integrity": "sha512-5QT/Di5FbGNPaVw8hHO1wETunwkPuZBIu6W+5GNArlKHD9fkMHy7vS8zGHJk38oObXfWdsuLMogD4sBySLJ54g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.23.0.tgz", + "integrity": "sha512-Sefl6vPyn5axzCsO13r1sHLcmPuiSOrKIImnq34CBurntcJ+lkQgAaTt/9JkgGmaZJ+OkaHmAJl4Bfd0DmdtOQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.23.0.tgz", + "integrity": "sha512-o4QI2KU/QbP7ZExMse6ULotdV3oJUYMrdx3rBZCgUF3ur3gJPfe8Fuasn6tia16c5kZBBw0aTmaUygad6VB/hQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.23.0.tgz", + "integrity": "sha512-+bxqx+V/D4FGrpXzPGKp/SEZIZ8cIW3K7wOtcJAoCrmXvzRtmdUhYNbgd+RztLzfDEfA2WtKj5F4tcbNPuqgeg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.23.0.tgz", + "integrity": "sha512-I/eXsdVoCKtSgK9OwyQKPAfricWKUMNCwJKtatRYMmDo5N859tbO3UsBw5kT3dU1n6ZcM1JDzPRSGhAUkxfLxw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.23.0.tgz", + "integrity": "sha512-4ZoDZy5ShLbbe1KPSafbFh1vbl0asTVfkABC7eWqIs01+66ncM82YJxV2VtV3YVJTqq2P8HMx3DCoRSWB/N3rw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.23.0.tgz", + "integrity": "sha512-+5Ky8dhft4STaOEbZu3/NU4QIyYssKO+r1cD3FzuusA0vO5gso15on7qGzKdNXnc1gOrsgCqZjRw1w+zL4y4hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.23.0.tgz", + "integrity": "sha512-0SPJk4cPZQhq9qA1UhIRumSE3+JJIBBjtlGl5PNC///BoaByckNZd53rOYD0glpTkYFBQSt7AkMeLVPfx65+BQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.23.0.tgz", + "integrity": "sha512-lqCK5GQC8fNo0+JvTSxcG7YB1UKYp8yrNLhsArlvPWN+16ovSZgoehlVHg6X0sSWPUkpjRBR5TuR12ZugowZ4g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" + }, + "node_modules/@types/archiver": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-5.3.4.tgz", + "integrity": "sha512-Lj7fLBIMwYFgViVVZHEdExZC3lVYsl+QL0VmdNdIzGZH544jHveYWij6qdnBgJQDnR7pMKliN9z2cPZFEbhyPw==", + "dev": true, + "dependencies": { + "@types/readdir-glob": "*" + } + }, + "node_modules/@types/aws-lambda": { + "version": "8.10.145", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.145.tgz", + "integrity": "sha512-dtByW6WiFk5W5Jfgz1VM+YPA21xMXTuSFoLYIDY0L44jDLLflVPtZkYuu3/YxpGcvjzKFBZLU+GyKjR0HOYtyw==" + }, + "node_modules/@types/axios": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz", + "integrity": "sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==", + "deprecated": "This is a stub types definition for axios (https://github.com/mzabriskie/axios). axios provides its own type definitions, so you don't need @types/axios installed!", + "dev": true, + "dependencies": { + "axios": "*" + } + }, + "node_modules/@types/btoa-lite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/btoa-lite/-/btoa-lite-1.0.2.tgz", + "integrity": "sha512-ZYbcE2x7yrvNFJiU7xJGrpF/ihpkM7zKgw8bha3LNJSesvTtUNxbpzaT7WXBIryf6jovisrxTBvymxMeLLj1Mg==" + }, + "node_modules/@types/ejs": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz", + "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "dev": true, + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/json2csv": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/json2csv/-/json2csv-5.0.7.tgz", + "integrity": "sha512-Ma25zw9G9GEBnX8b12R4EYvnFT6dBh8L3jwsN5EUFXa+fl2dqmbLDbNWN0XuQU3rSXdsbBeCYjI9uHU2PUBxhA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jsonpath": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@types/jsonpath/-/jsonpath-0.2.4.tgz", + "integrity": "sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA==", + "dev": true + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true + }, + "node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dev": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true + }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.19.54", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.54.tgz", + "integrity": "sha512-+BRgt0G5gYjTvdLac9sIeE0iZcJxi4Jc4PV5EUzqi+88jmQLr+fRZdv2tCTV7IHKSGxM6SaLoOXQWWUiLUItMw==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true + }, + "node_modules/@types/readdir-glob": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", + "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" + }, + "node_modules/@types/tunnel": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.2.tgz", + "integrity": "sha512-2gYBYYPhn3/aozRdfS+DSxFnf3YZ9IM9q6Wjee1xVj/vtRm1MwT9EN9yFSiQW05eAVRLkWJj3qpMAhIA6EY9mw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, + "node_modules/@vitest/coverage-v8": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.1.tgz", + "integrity": "sha512-md/A7A3c42oTT8JUHSqjP5uKTWJejzUW4jalpvs+rZ27gsURsMU8DEb+8Jf8C6Kj2gwfSHJqobDNBuoqlm0cFw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.6", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.11", + "magicast": "^0.3.4", + "std-env": "^3.7.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "2.1.1", + "vitest": "2.1.1" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz", + "integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==", + "dev": true, + "dependencies": { + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz", + "integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==", + "dev": true, + "dependencies": { + "@vitest/spy": "^2.1.0-beta.1", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.11" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/spy": "2.1.1", + "msw": "^2.3.5", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.1.tgz", + "integrity": "sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==", + "dev": true, + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz", + "integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==", + "dev": true, + "dependencies": { + "@vitest/utils": "2.1.1", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz", + "integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.1.1", + "magic-string": "^0.30.11", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz", + "integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==", + "dev": true, + "dependencies": { + "tinyspy": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-2.1.1.tgz", + "integrity": "sha512-IIxo2LkQDA+1TZdPLYPclzsXukBWd5dX2CKpGqH8CCt8Wh0ZuDn4+vuQ9qlppEju6/igDGzjWF/zyorfsf+nHg==", + "dev": true, + "dependencies": { + "@vitest/utils": "2.1.1", + "fflate": "^0.8.2", + "flatted": "^3.3.1", + "pathe": "^1.1.2", + "sirv": "^2.0.4", + "tinyglobby": "^0.2.6", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "2.1.1" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz", + "integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.1.1", + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", + "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", + "dependencies": { + "type-fest": "^1.0.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dev": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/array-flatten": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz", + "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==", + "dev": true + }, + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.0.1.tgz", + "integrity": "sha512-PagxbjvuPH6tv0f/kdVbFGcb79D236SLcDTs6DrQ7GizJ88S1UWP4nMXFEo/I4fdhGRGabvFfFjVGm3M7U8JwA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "3.1.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.5.2", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "^3.0.0", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/body-parser/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/body-parser/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/btoa-lite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", + "integrity": "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/bundle-require": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-4.2.1.tgz", + "integrity": "sha512-7Q/6vkyYAwOmQNRw75x+4yRtZCZJXUDmHHlFdkiV0wgv/reNjtJwpu1jPJ0w2kbEpIM0uoKI3S4/f39dU7AjSA==", + "dev": true, + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.17" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/c8": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-7.14.0.tgz", + "integrity": "sha512-i04rtkkcNcCf7zsQcSv/T9EbUn4RXQ6mropeMcjFOsQXQ0iGLAr/xT6TImQg4+U9hmNpN9XdvPkjUL1IzbgxJw==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^2.0.0", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-reports": "^3.1.4", + "rimraf": "^3.0.2", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/c8/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/c8/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/c8/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/c8/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/c8/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/c8/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/c8/node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/c8/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/c8/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dev": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/conventional-changelog-angular": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", + "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", + "dev": true, + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz", + "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==", + "dev": true, + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-commits-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", + "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", + "dev": true, + "dependencies": { + "is-text-path": "^2.0.0", + "JSONStream": "^1.3.5", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.1.tgz", + "integrity": "sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==", + "dev": true, + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig-typescript-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-5.0.0.tgz", + "integrity": "sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==", + "dev": true, + "dependencies": { + "jiti": "^1.19.1" + }, + "engines": { + "node": ">=v16" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=8.2", + "typescript": ">=4" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dev": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/dargs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", + "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-json": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-json/-/eslint-plugin-json-3.1.0.tgz", + "integrity": "sha512-MrlG2ynFEHe7wDGwbUuFPsaT2b1uhuEFhJ+W1f1u+1C2EkXmTYJp4B1aAdQQ8M+CC3t//N/oRKiIVw14L2HR1g==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21", + "vscode-json-languageservice": "^4.1.6" + }, + "engines": { + "node": ">=12.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.0.1.tgz", + "integrity": "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==", + "dev": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.0.1", + "content-disposition": "^1.0.0", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "^1.2.1", + "debug": "4.3.6", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "^2.0.0", + "fresh": "2.0.0", + "http-errors": "2.0.0", + "merge-descriptors": "^2.0.0", + "methods": "~1.1.2", + "mime-types": "^3.0.0", + "on-finished": "2.4.1", + "once": "1.4.0", + "parseurl": "~1.3.3", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "router": "^2.0.0", + "safe-buffer": "5.2.1", + "send": "^1.1.0", + "serve-static": "^2.1.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "^2.0.0", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/express/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "node_modules/fast-uri": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz", + "integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.0.0.tgz", + "integrity": "sha512-MX6Zo2adDViYh+GcxxB1dpO43eypOGUOL12rLCOTMQv/DfIbpSJUy4oQIIZhVZkH9e+bZWKMon0XHFEju16tkQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4", + "fs-extra": "^11.2.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/git-raw-commits": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.11.tgz", + "integrity": "sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==", + "dev": true, + "dependencies": { + "dargs": "^7.0.0", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + }, + "bin": { + "git-raw-commits": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/git-raw-commits/node_modules/meow": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", + "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", + "dev": true, + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/git-raw-commits/node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "dependencies": { + "readable-stream": "^3.0.0" + } + }, + "node_modules/git-raw-commits/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==", + "dev": true, + "dependencies": { + "ini": "^1.3.4" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/husky": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", + "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/iconv-lite": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", + "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-text-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz", + "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==", + "dev": true, + "dependencies": { + "text-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-valid-hostname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-valid-hostname/-/is-valid-hostname-1.0.2.tgz", + "integrity": "sha512-X/kiF3Xndj6WI7l/yLyzR7V1IbQd6L4S4cewSL0fRciemPmHbaXIKR2qtf+zseH+lbMG0vFp4HvCUe7amGZVhw==" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/isolated-vm": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/isolated-vm/-/isolated-vm-4.7.2.tgz", + "integrity": "sha512-JVEs5gzWObzZK5+OlBplCdYSpokMcdhLSs/xWYYxmYWVfOOFF4oZJsYh7E/FmfX8e7gMioXMpMMeEyX1afuKrg==", + "hasInstallScript": true, + "dependencies": { + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/jsonpath": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", + "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", + "dependencies": { + "esprima": "1.2.2", + "static-eval": "2.0.2", + "underscore": "1.12.1" + } + }, + "node_modules/jsonpath/node_modules/esprima": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", + "integrity": "sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/lint-staged": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.3.0.tgz", + "integrity": "sha512-mPRtrYnipYYv1FEE134ufbWpeggNTo+O/UPzngoaKzbzHAthvR55am+8GfHTnqNRQVRRrYQLGW9ZyUoD7DsBHQ==", + "dev": true, + "dependencies": { + "chalk": "5.3.0", + "commander": "11.0.0", + "debug": "4.3.4", + "execa": "7.2.0", + "lilconfig": "2.1.0", + "listr2": "6.6.1", + "micromatch": "4.0.5", + "pidtree": "0.6.0", + "string-argv": "0.3.2", + "yaml": "2.3.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", + "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/lint-staged/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/lint-staged/node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/lint-staged/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/lint-staged/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/lint-staged/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/yaml": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", + "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/listr2": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-6.6.1.tgz", + "integrity": "sha512-+rAXGHh0fkEWdXBmX+L6mmfmXmXvDGEKzkjxO+8mP3+nI/r/CWznVBvsibXdxda9Zz0OW2e2ikphN3OwCT/jSg==", + "dev": true, + "dependencies": { + "cli-truncate": "^3.1.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^5.0.1", + "rfdc": "^1.3.0", + "wrap-ansi": "^8.1.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "dev": true + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "dev": true + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "dev": true + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "node_modules/lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "dev": true + }, + "node_modules/log-update": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-5.0.1.tgz", + "integrity": "sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==", + "dependencies": { + "ansi-escapes": "^5.0.0", + "cli-cursor": "^4.0.0", + "slice-ansi": "^5.0.0", + "strip-ansi": "^7.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/logform": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.1.tgz", + "integrity": "sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/loupe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.0.tgz", + "integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", + "dev": true, + "dependencies": { + "mime-db": "^1.53.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", + "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/mylas": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.13.tgz", + "integrity": "sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/raouldeheer" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-abi": { + "version": "3.68.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.68.0.tgz", + "integrity": "sha512-7vbj10trelExNjFSBm5kTvZXXa7pZyKWx9RCKIyqe6I9Ev3IzGpQoqBP3a+cOdxY+pWj6VkP28n/2wWysBHD/A==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-html-parser": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", + "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "node_modules/node-stream-zip": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", + "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", + "engines": { + "node": ">=0.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/antelle" + } + }, + "node_modules/nodemon": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", + "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/ntlm2": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/ntlm2/-/ntlm2-0.3.0.tgz", + "integrity": "sha512-q4QRTBGjLhxMnoUFPak6IKehlGMsWp14Vu+VU4JdxMeS4Olm7/N7+HJlqrYesUCI0RBoc40rXvFO3WENpGDBbw==", + "dependencies": { + "extend": "^3.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/octokit": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/octokit/-/octokit-3.2.1.tgz", + "integrity": "sha512-u+XuSejhe3NdIvty3Jod00JvTdAE/0/+XbhIDhefHbu+2OcTRHd80aCiH6TX19ZybJmwPQBKFQmHGxp0i9mJrg==", + "dependencies": { + "@octokit/app": "^14.0.2", + "@octokit/core": "^5.0.0", + "@octokit/oauth-app": "^6.0.0", + "@octokit/plugin-paginate-graphql": "^4.0.0", + "@octokit/plugin-paginate-rest": "11.3.1", + "@octokit/plugin-rest-endpoint-methods": "13.2.2", + "@octokit/plugin-retry": "^6.0.0", + "@octokit/plugin-throttling": "^8.0.0", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^13.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", + "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.5", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-duration": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-1.1.0.tgz", + "integrity": "sha512-z6t9dvSJYaPoQq7quMzdEagSFtpGu+utzHqqxmpVWNNZRIXnvqyCvn9XsTdh7c/w0Bqmdz3RB3YnRaKtpRtEXQ==" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/plimit-lit": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", + "integrity": "sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==", + "dev": true, + "dependencies": { + "queue-lit": "^1.5.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-lit": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz", + "integrity": "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/replace-in-file": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-6.3.5.tgz", + "integrity": "sha512-arB9d3ENdKva2fxRnSjwBEXfK1npgyci7ZZuwysgAp7ORjHSyxz6oqIjTEv8R0Ydl4Ll7uOAZXL4vbkhGIizCg==", + "dependencies": { + "chalk": "^4.1.2", + "glob": "^7.2.0", + "yargs": "^17.2.1" + }, + "bin": { + "replace-in-file": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-global": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-global/-/resolve-global-1.0.0.tgz", + "integrity": "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==", + "dev": true, + "dependencies": { + "global-dirs": "^0.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.0.0.tgz", + "integrity": "sha512-dIM5zVoG8xhC6rnSN8uoAgFARwTE7BQs8YwHEvK0VCmfxQXMaOuA1uiR1IPwsW7JyK5iTt7Od/TC9StasS2NPQ==", + "dev": true, + "dependencies": { + "array-flatten": "3.0.0", + "is-promise": "4.0.0", + "methods": "~1.1.2", + "parseurl": "~1.3.3", + "path-to-regexp": "^8.0.0", + "setprototypeof": "1.2.0", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.1.0.tgz", + "integrity": "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==", + "dev": true, + "dependencies": { + "debug": "^4.3.5", + "destroy": "^1.2.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^0.5.2", + "http-errors": "^2.0.0", + "mime-types": "^2.1.35", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/send/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.1.0.tgz", + "integrity": "sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==", + "dev": true, + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/smartquotes": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/smartquotes/-/smartquotes-2.3.2.tgz", + "integrity": "sha512-0R6YJ5hLpDH4mZR7N5eZ12oCMLspvGOHL9A9SEm2e3b/CQmQidekW4SWSKEmor/3x6m3NCBBEqLzikcZC9VJNQ==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "dev": true + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "engines": { + "node": "*" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/static-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", + "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", + "dependencies": { + "escodegen": "^1.8.1" + } + }, + "node_modules/static-eval/node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/static-eval/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/static-eval/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/static-eval/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/static-eval/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/static-eval/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "dev": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sucrase/node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/text-extensions": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", + "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz", + "integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==", + "dev": true + }, + "node_modules/tinyglobby": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.9.tgz", + "integrity": "sha512-8or1+BGEdk1Zkkw2ii16qSS7uVrQJPre5A9o/XkWPATkk23FZh/15BKFxPnlTy6vkljZxLqYCzzBMj30ZrSvjw==", + "dev": true, + "dependencies": { + "fdir": "^6.4.0", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.0.tgz", + "integrity": "sha512-3oB133prH1o4j/L5lLW7uOCF1PlD+/It2L0eL/iAqWMB91RBbqTewABqxhj0ibBd90EEmWZq7ntIWzVaWcXTGQ==", + "dev": true, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz", + "integrity": "sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/title-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", + "integrity": "sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tsc-alias": { + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.10.tgz", + "integrity": "sha512-Ibv4KAWfFkFdKJxnWfVtdOmB0Zi1RJVxcbPGiCDsFpCQSsmpWyuzHG3rQyI5YkobWwxFPEyQfu1hdo4qLG2zPw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.3", + "commander": "^9.0.0", + "globby": "^11.0.4", + "mylas": "^2.1.9", + "normalize-path": "^3.0.0", + "plimit-lit": "^1.2.6" + }, + "bin": { + "tsc-alias": "dist/bin/index.js" + } + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/tsup": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-6.7.0.tgz", + "integrity": "sha512-L3o8hGkaHnu5TdJns+mCqFsDBo83bJ44rlK7e6VdanIvpea4ArPcU3swWGsLVbXak1PqQx/V+SSmFPujBK+zEQ==", + "dev": true, + "dependencies": { + "bundle-require": "^4.0.0", + "cac": "^6.7.12", + "chokidar": "^3.5.1", + "debug": "^4.3.1", + "esbuild": "^0.17.6", + "execa": "^5.0.0", + "globby": "^11.0.3", + "joycon": "^3.0.1", + "postcss-load-config": "^3.0.1", + "resolve-from": "^5.0.0", + "rollup": "^3.2.5", + "source-map": "0.8.0-beta.0", + "sucrase": "^3.20.3", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=14.18" + }, + "peerDependencies": { + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsup/node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/turbo": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/turbo/-/turbo-1.13.4.tgz", + "integrity": "sha512-1q7+9UJABuBAHrcC4Sxp5lOqYS5mvxRrwa33wpIyM18hlOCpRD/fTJNxZ0vhbMcJmz15o9kkVm743mPn7p6jpQ==", + "dev": true, + "bin": { + "turbo": "bin/turbo" + }, + "optionalDependencies": { + "turbo-darwin-64": "1.13.4", + "turbo-darwin-arm64": "1.13.4", + "turbo-linux-64": "1.13.4", + "turbo-linux-arm64": "1.13.4", + "turbo-windows-64": "1.13.4", + "turbo-windows-arm64": "1.13.4" + } + }, + "node_modules/turbo-darwin-64": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-1.13.4.tgz", + "integrity": "sha512-A0eKd73R7CGnRinTiS7txkMElg+R5rKFp9HV7baDiEL4xTG1FIg/56Vm7A5RVgg8UNgG2qNnrfatJtb+dRmNdw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/turbo-darwin-arm64": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.13.4.tgz", + "integrity": "sha512-eG769Q0NF6/Vyjsr3mKCnkG/eW6dKMBZk6dxWOdrHfrg6QgfkBUk0WUUujzdtVPiUIvsh4l46vQrNVd9EOtbyA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/turbo-linux-64": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-1.13.4.tgz", + "integrity": "sha512-Bq0JphDeNw3XEi+Xb/e4xoKhs1DHN7OoLVUbTIQz+gazYjigVZvtwCvgrZI7eW9Xo1eOXM2zw2u1DGLLUfmGkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/turbo-linux-arm64": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-1.13.4.tgz", + "integrity": "sha512-BJcXw1DDiHO/okYbaNdcWN6szjXyHWx9d460v6fCHY65G8CyqGU3y2uUTPK89o8lq/b2C8NK0yZD+Vp0f9VoIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/turbo-windows-64": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-1.13.4.tgz", + "integrity": "sha512-OFFhXHOFLN7A78vD/dlVuuSSVEB3s9ZBj18Tm1hk3aW1HTWTuAw0ReN6ZNlVObZUHvGy8d57OAGGxf2bT3etQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/turbo-windows-arm64": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-1.13.4.tgz", + "integrity": "sha512-u5A+VOKHswJJmJ8o8rcilBfU5U3Y1TTAfP9wX8bFh8teYF1ghP0EhtMRLjhtp6RPa+XCxHHVA2CiC3gbh5eg5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.0.tgz", + "integrity": "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==", + "dev": true, + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" + }, + "node_modules/undici": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", + "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/universal-github-app-jwt": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.0.tgz", + "integrity": "sha512-G5o6f95b5BggDGuUfKDApKaCgNYy2x7OdHY0zSMF081O0EJobw+1130VONhrA7ezGSV2FNOGyM+KQpQZAr9bIQ==" + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v9u-smb2": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/v9u-smb2/-/v9u-smb2-1.0.6.tgz", + "integrity": "sha512-xXFGQzOL0X6E//le+1rlEeyam+3jgk2vNSqs/2h1XuyIog5l47+hL3KcZh4fA4qTBuwFiEfH1sG7ZFpPT6VtWQ==", + "dependencies": { + "ntlm2": "^0.3.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", + "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz", + "integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.6", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite/node_modules/rollup": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.23.0.tgz", + "integrity": "sha512-vXB4IT9/KLDrS2WRXmY22sVB2wTsTwkpxjB8Q3mnakTENcYw3FRmfdYDy/acNmls+lHmDazgrRjK/yQ6hQAtwA==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.23.0", + "@rollup/rollup-android-arm64": "4.23.0", + "@rollup/rollup-darwin-arm64": "4.23.0", + "@rollup/rollup-darwin-x64": "4.23.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.23.0", + "@rollup/rollup-linux-arm-musleabihf": "4.23.0", + "@rollup/rollup-linux-arm64-gnu": "4.23.0", + "@rollup/rollup-linux-arm64-musl": "4.23.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.23.0", + "@rollup/rollup-linux-riscv64-gnu": "4.23.0", + "@rollup/rollup-linux-s390x-gnu": "4.23.0", + "@rollup/rollup-linux-x64-gnu": "4.23.0", + "@rollup/rollup-linux-x64-musl": "4.23.0", + "@rollup/rollup-win32-arm64-msvc": "4.23.0", + "@rollup/rollup-win32-ia32-msvc": "4.23.0", + "@rollup/rollup-win32-x64-msvc": "4.23.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/vitest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz", + "integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==", + "dev": true, + "dependencies": { + "@vitest/expect": "2.1.1", + "@vitest/mocker": "2.1.1", + "@vitest/pretty-format": "^2.1.1", + "@vitest/runner": "2.1.1", + "@vitest/snapshot": "2.1.1", + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", + "chai": "^5.1.1", + "debug": "^4.3.6", + "magic-string": "^0.30.11", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.1", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.1", + "@vitest/ui": "2.1.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vscode-json-languageservice": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-4.2.1.tgz", + "integrity": "sha512-xGmv9QIWs2H8obGbWg+sIPI/3/pFgj/5OWBhNzs00BkYQ9UaB2F6JJaGB/2/YOZJ3BvLXQTC4Q7muqU25QgAhA==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.0.0", + "vscode-languageserver-textdocument": "^1.0.3", + "vscode-languageserver-types": "^3.16.0", + "vscode-nls": "^5.0.0", + "vscode-uri": "^3.0.3" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "dev": true + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "dev": true + }, + "node_modules/vscode-nls": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.2.0.tgz", + "integrity": "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==", + "dev": true + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "dev": true + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/winston": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.14.2.tgz", + "integrity": "sha512-CO8cdpBB2yqzEf8v895L+GNKYJiEq8eKlHU38af3snQBQ+sdAIUepjMSguOIJC7ICbzm0ZI+Af2If4vIJrtmOg==", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.6.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.8.0.tgz", + "integrity": "sha512-qxSTKswC6llEMZKgCQdaWgDuMJQnhuvF5f2Nk3SNXc4byfQ+voo2mX1Px9dkNOuR8p0KAjfPG29PuYUSIb+vSA==", + "dependencies": { + "logform": "^2.6.1", + "readable-stream": "^4.5.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/winston-transport/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yaml": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dev": true, + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dev": true, + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-error": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/zod-error/-/zod-error-1.5.0.tgz", + "integrity": "sha512-zzopKZ/skI9iXpqCEPj+iLCKl9b88E43ehcU+sbRoHuwGd9F1IDVGQ70TyO6kmfiRL1g4IXkjsXK+g1gLYl4WQ==", + "dependencies": { + "zod": "^3.20.2" + } + }, + "node_modules/zod-validation-error": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-1.5.0.tgz", + "integrity": "sha512-/7eFkAI4qV0tcxMBB/3+d2c1P6jzzZYdYSlBuAklzMuCrJu5bzJfHS0yVAS87dRHVlhftd6RFJDIvv03JgkSbw==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "zod": "^3.18.0" + } + }, + "packages/autopilot-utils": { + "name": "@B-S-F/autopilot-utils", + "version": "0.11.0", + "dependencies": { + "commander": "^11.0.0", + "winston": "^3.10.0" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + } + }, + "packages/autopilot-utils/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "engines": { + "node": ">=16" + } + }, + "packages/eslint-config": { + "name": "@B-S-F/eslint-config", + "version": "0.1.0", + "dependencies": { + "eslint-config-prettier": "^8.6.0" + } + }, + "packages/fonts": { + "name": "@B-S-F/fonts", + "version": "0.1.0", + "devDependencies": {} + }, + "packages/issue-validators": { + "name": "@B-S-F/issue-validators", + "version": "0.1.0", + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "@vitest/coverage-v8": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "nodemon": "*", + "prettier": "*", + "tsup": "*", + "vitest": "*" + } + }, + "packages/json-evaluator-lib": { + "name": "@B-S-F/json-evaluator-lib", + "version": "0.9.0", + "dependencies": { + "ajv": "^8.12.0", + "colors": "1.4.0", + "isolated-vm": "^4.6.0", + "jsonpath": "^1.1.1", + "yaml": "^2.2.1", + "zod": "^3.22.3" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/jsonpath": "^0.2.0", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + } + }, + "packages/lint-staged-config": { + "name": "@B-S-F/lint-staged-config", + "version": "0.1.0" + }, + "packages/log-utils": { + "name": "@B-S-F/log-utils", + "version": "0.1.0", + "dependencies": { + "log-update": "^5.0.1" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@vitest/coverage-v8": "*", + "eslint": "*", + "nodemon": "*", + "prettier": "*", + "tsup": "*", + "vitest": "*" + } + }, + "packages/markdown-utils": { + "name": "@B-S-F/markdown-utils", + "version": "0.2.0", + "dependencies": { + "markdown-it": "^14.1.0", + "smartquotes": "^2.3.2", + "title-case": "^3.0.3" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/markdown-it": "^12.2.3", + "eslint": "*", + "nodemon": "*", + "prettier": "*", + "tsup": "*", + "vitest": "*" + } + }, + "packages/sync-versions": { + "name": "@B-S-F/sync-versions", + "version": "0.1.0", + "dependencies": { + "replace-in-file": "^6.3.5" + }, + "bin": { + "sync-versions": "src/index.js" + } + }, + "packages/typescript-config": { + "name": "@B-S-F/typescript-config", + "version": "0.1.0" + } + } +} diff --git a/yaku-apps-typescript/package.json b/yaku-apps-typescript/package.json new file mode 100644 index 00000000..ffe40300 --- /dev/null +++ b/yaku-apps-typescript/package.json @@ -0,0 +1,54 @@ +{ + "name": "@B-S-F/qg-apps-typescript", + "version": "0.1.0", + "private": true, + "workspaces": [ + "apps/*", + "packages/*" + ], + "scripts": { + "build": "npx turbo run build --continue --cache-dir=.turbo", + "dev": "npx turbo run dev --parallel --cache-dir=.turbo", + "lint": "npx turbo run lint --parallel --cache-dir=.turbo", + "lint:prettier": "npx prettier --check \"**/*.{ts,tsx,md}\"", + "lint-staged": "npx lint-staged", + "test": "npx turbo run test --parallel --cache-dir=.turbo", + "test:integration:ci": "npx turbo run test:integration:ci --concurrency=1 --cache-dir=.turbo", + "format": "npx prettier --write \"**/*.{ts,tsx,md}\" && npx turbo run format --parallel --cache-dir=.turbo", + "prepare": "npx husky install" + }, + "devDependencies": { + "@B-S-F/eslint-config": "^0.1.0", + "@B-S-F/typescript-config": "^0.1.0", + "@commitlint/cli": "^18.2.0", + "@commitlint/config-conventional": "^18.1.0", + "@types/node": "^18.14.2", + "@typescript-eslint/eslint-plugin": "^6.7.3", + "@typescript-eslint/parser": "^6.7.3", + "@vitest/coverage-v8": "^2.1.1", + "@vitest/ui": "^2.1.1", + "c8": "^7.13.0", + "eslint": "^8.35.0", + "eslint-config-prettier": "^8.6.0", + "eslint-plugin-json": "^3.1.0", + "express": "^5.0.0", + "husky": "^8.0.3", + "lint-staged": "^13.1.2", + "nodemon": "^3.1.7", + "prettier": "^2.8.4", + "tsup": "^6.6.3", + "turbo": "^1.10.13", + "typescript": "^4.9.5", + "vitest": "^2.1.1" + }, + "engines": { + "npm": ">=9.3.0", + "node": ">=18.17.0" + }, + "packageManager": "npm@9.3.1", + "lint-staged": { + "*.{ts,json,yml,yaml,md}": [ + "prettier --write" + ] + } +} diff --git a/yaku-apps-typescript/packages/autopilot-utils/.eslintrc.cjs b/yaku-apps-typescript/packages/autopilot-utils/.eslintrc.cjs new file mode 100644 index 00000000..87d861fe --- /dev/null +++ b/yaku-apps-typescript/packages/autopilot-utils/.eslintrc.cjs @@ -0,0 +1,15 @@ +module.exports = { + "extends": [ + "@B-S-F/eslint-config/eslint-preset" + ], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + 'no-sparse-arrays': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { destructuredArrayIgnorePattern: '^([.]{3})?_' }, + ], + "no-control-regex": 0 + }, +} \ No newline at end of file diff --git a/yaku-apps-typescript/packages/autopilot-utils/.prettierrc b/yaku-apps-typescript/packages/autopilot-utils/.prettierrc new file mode 100644 index 00000000..b2095be8 --- /dev/null +++ b/yaku-apps-typescript/packages/autopilot-utils/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": false, + "singleQuote": true +} diff --git a/yaku-apps-typescript/packages/autopilot-utils/package.json b/yaku-apps-typescript/packages/autopilot-utils/package.json new file mode 100644 index 00000000..19eba037 --- /dev/null +++ b/yaku-apps-typescript/packages/autopilot-utils/package.json @@ -0,0 +1,43 @@ +{ + "name": "@B-S-F/autopilot-utils", + "version": "0.11.0", + "description": "", + "main": "dist/index.js", + "types": "src/index.d.ts", + "files": [ + "dist" + ], + "type": "module", + "scripts": { + "build": "tsup && cp src/index.d.ts dist", + "lint": "eslint '**/*.ts'", + "format": "prettier --write '**/*.{ts,md}'", + "test": "vitest run && npm run test:update-cobertura-file", + "test:update-cobertura-file": "sed -i'.bak' 's,package name=\",package name=\"'${npm_package_name}/',g' coverage/cobertura-coverage.xml", + "test:dev": "vitest -w", + "test:coverage": "vitest --coverage", + "test:ui": "vitest --ui", + "test:integration:local": "npx rimraf dist && npm run build && npm run test:integration:ci", + "test:integration:ci": "npx vitest run --config vitest-integration.config.ts" + }, + "keywords": [], + "author": "", + "license": "", + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + }, + "dependencies": { + "commander": "^11.0.0", + "winston": "^3.10.0" + } +} diff --git a/yaku-apps-typescript/packages/autopilot-utils/src/app-error.ts b/yaku-apps-typescript/packages/autopilot-utils/src/app-error.ts new file mode 100644 index 00000000..bc4234b7 --- /dev/null +++ b/yaku-apps-typescript/packages/autopilot-utils/src/app-error.ts @@ -0,0 +1,12 @@ +export class AppError extends Error { + private reason: string + constructor(reason: string) { + super(reason) + this.reason = reason + // Set the prototype explicitly. + Object.setPrototypeOf(this, AppError.prototype) + } + Reason(): string { + return this.reason + } +} diff --git a/yaku-apps-typescript/packages/autopilot-utils/src/app-output.ts b/yaku-apps-typescript/packages/autopilot-utils/src/app-output.ts new file mode 100644 index 00000000..652eeb42 --- /dev/null +++ b/yaku-apps-typescript/packages/autopilot-utils/src/app-output.ts @@ -0,0 +1,54 @@ +import { Output, Result, Status } from './types.js' + +export class AppOutput { + data: { + status: Status | undefined + reason: string | undefined + outputs: Output[] + results: Result[] + } + + constructor() { + this.data = { + status: undefined, + reason: undefined, + outputs: [], + results: [], + } + } + + addResult(result: Result): void { + this.data.results.push(result) + } + + addOutput(output: Output): void { + this.data.outputs.push(output) + } + + setStatus(status: Status): void { + this.data.status = status + } + + setReason(reason: string): void { + this.data.reason = reason + } + + write(): void { + for (const output of this.data.outputs) { + console.log(JSON.stringify({ output: output })) + } + for (const result of this.data.results || []) { + console.log(JSON.stringify({ result: result })) + } + const out: any = {} + if (this.data.status) { + out['status'] = this.data.status + } + if (this.data.reason) { + out['reason'] = this.data.reason + } + if (Object.keys(out).length !== 0) { + console.log(JSON.stringify(out)) + } + } +} diff --git a/yaku-apps-typescript/packages/autopilot-utils/src/cli.ts b/yaku-apps-typescript/packages/autopilot-utils/src/cli.ts new file mode 100644 index 00000000..8235649d --- /dev/null +++ b/yaku-apps-typescript/packages/autopilot-utils/src/cli.ts @@ -0,0 +1,70 @@ +import { Command, program } from 'commander' +import { exit } from 'process' +import { AppError } from './app-error.js' +import { AppOutput } from './app-output.js' +import { InitLogger } from './logger.js' +import { Result } from './types' +/* + * AutopilotApp is a wrapper around commander.js + * It provides a simple way to create a CLI app + * with a common set of features. + * example: + * const app = new AutopilotApp( + * 'my-app', + * '0.0.1', + * 'My App Description', + * [ + * new Command('evaluate') + * .description('Evaluate the app') + * .action(() => { + * const appOutput = new AppOutput() + * appOutput.setStatus('GREEN') + * appOutput.setReason('Everything is fine.') + * appOutput.write() + * }) + * ] + * ) + * app.run() + * + * The above example will create a CLI app with a single command + * called evaluate. When the evaluate command is run, the app will + * output a green status with the reason "Everything is fine." + * + * If you want to use a logger, you can use the GetLogger function + * to get a logger instance. The logger will be initialized with + * the name of the app and the log level set to info (unless the + * DEBUG environment variable is set) + */ +export class AutopilotApp { + public results: Result[] = [] + constructor( + name: string, + version: string, + description: string, + commands: Command[] + ) { + program.name(name).version(version).description(description) + commands.forEach((command) => { + program.addCommand(command) + }) + const logLevel = process.env.DEBUG ? 'debug' : 'info' + InitLogger(name, logLevel) + } + + async run() { + try { + await program.parseAsync(process.argv) + } catch (e) { + if (e instanceof AppError) { + const appOutput = new AppOutput() + appOutput.setStatus('FAILED') + appOutput.setReason(e.Reason()) + appOutput.write() + exit(0) + } + throw e + } + } +} + +export const AutopilotAppCommand = Command diff --git a/yaku-apps-typescript/packages/autopilot-utils/src/index.d.ts b/yaku-apps-typescript/packages/autopilot-utils/src/index.d.ts new file mode 100644 index 00000000..73670f07 --- /dev/null +++ b/yaku-apps-typescript/packages/autopilot-utils/src/index.d.ts @@ -0,0 +1,6 @@ +export { Logger } from 'winston' +export { AppError } from './app-error.js' +export { AppOutput } from './app-output.js' +export { AutopilotApp, AutopilotAppCommand } from './cli.js' +export { GetLogger, InitLogger, LogLevel, toLogLevel } from './logger.js' +export { Output, Result, Status } from './types.js' diff --git a/yaku-apps-typescript/packages/autopilot-utils/src/index.ts b/yaku-apps-typescript/packages/autopilot-utils/src/index.ts new file mode 100644 index 00000000..ad36e3f0 --- /dev/null +++ b/yaku-apps-typescript/packages/autopilot-utils/src/index.ts @@ -0,0 +1,4 @@ +export { AppOutput } from './app-output.js' +export { AppError } from './app-error.js' +export { InitLogger, GetLogger, toLogLevel } from './logger.js' +export { AutopilotApp, AutopilotAppCommand } from './cli.js' diff --git a/yaku-apps-typescript/packages/autopilot-utils/src/logger.ts b/yaku-apps-typescript/packages/autopilot-utils/src/logger.ts new file mode 100644 index 00000000..19c9f484 --- /dev/null +++ b/yaku-apps-typescript/packages/autopilot-utils/src/logger.ts @@ -0,0 +1,66 @@ +import { createLogger, Logger, format, transports } from 'winston' + +let logger: Logger | null = null + +export type LogLevel = + | 'error' + | 'warn' + | 'info' + | 'http' + | 'verbose' + | 'debug' + | 'silly' + +export function InitLogger( + app: string, + logLevel: LogLevel = 'info', + logFile?: string +) { + logger = createLogger({ + level: logLevel, + format: format.combine( + format.label({ label: app }), + format.prettyPrint(), + format.cli() + ), + transports: [new transports.Console()], + }) + + if (logFile) { + logger.add( + new transports.File({ + filename: logFile, + format: format.combine(format.timestamp(), format.json()), + }) + ) + } + return logger +} + +export function GetLogger(): Logger { + if (!logger) { + logger = InitLogger('unknown-app') + } + return logger +} + +export function toLogLevel(logLevel: string): LogLevel { + switch (logLevel.toLowerCase()) { + case 'error': + return 'error' + case 'warn': + return 'warn' + case 'info': + return 'info' + case 'http': + return 'http' + case 'verbose': + return 'verbose' + case 'debug': + return 'debug' + case 'silly': + return 'silly' + default: + return 'info' + } +} diff --git a/yaku-apps-typescript/packages/autopilot-utils/src/types.ts b/yaku-apps-typescript/packages/autopilot-utils/src/types.ts new file mode 100644 index 00000000..5d206df7 --- /dev/null +++ b/yaku-apps-typescript/packages/autopilot-utils/src/types.ts @@ -0,0 +1,14 @@ +export type Output = { + [key: string]: string +} + +export type Status = 'GREEN' | 'YELLOW' | 'RED' | 'FAILED' + +export type Result = { + criterion: string + justification: string + fulfilled: boolean + metadata?: { + [key: string]: string + } +} diff --git a/yaku-apps-typescript/packages/autopilot-utils/test/integration/app.int-spec.ts b/yaku-apps-typescript/packages/autopilot-utils/test/integration/app.int-spec.ts new file mode 100644 index 00000000..2295852b --- /dev/null +++ b/yaku-apps-typescript/packages/autopilot-utils/test/integration/app.int-spec.ts @@ -0,0 +1,58 @@ +import { describe, it, expect } from 'vitest' +import { exec } from 'child_process' +import path from 'path' + +function executeApp(args: string[], command: string): Promise { + return new Promise((resolve, reject) => { + exec( + `node ${path.join(__dirname, 'app.js')} ${command} ${args.join(' ')}`, + (error, stdout, stderr) => { + if (error) { + reject(error) + } + if (stderr) { + reject(stderr) + } + resolve(stdout) + } + ) + }) +} + +describe('evaluate', () => { + it('should return help', async () => { + const output = await executeApp([], '--help') + expect(output).toContain('Usage: app [options] [command]') + }) + + it('should return green status', async () => { + const output = await executeApp(['--green'], 'evaluate') + expect(output).toContain('"status":"GREEN"') + }) + + it('should return red status', async () => { + const output = await executeApp(['--red'], 'evaluate') + expect(output).toContain('"status":"RED"') + }) + + it('should fail the evaluation', async () => { + const output = await executeApp(['--fail'], 'evaluate') + expect(output).toContain('"status":"FAILED"') + }) + + it('should throw an error', async () => { + expect(executeApp(['--throw'], 'evaluate')).rejects.toThrow() + }) +}) + +describe('fetch', () => { + it('should return help', async () => { + const output = await executeApp([], '--help') + expect(output).toContain('Usage: app [options] [command]') + }) + + it('should return green status', async () => { + const output = await executeApp([], 'fetch') + expect(output).toContain('{"output":{"test":"test"}}') + }) +}) diff --git a/yaku-apps-typescript/packages/autopilot-utils/test/integration/app.js b/yaku-apps-typescript/packages/autopilot-utils/test/integration/app.js new file mode 100644 index 00000000..73313af0 --- /dev/null +++ b/yaku-apps-typescript/packages/autopilot-utils/test/integration/app.js @@ -0,0 +1,56 @@ +import { Command } from 'commander' +import { AppError, AppOutput, AutopilotApp, GetLogger } from '../../dist/index.js' + +/* + * This is a command of the autopilot app. + * It mirrors a simple evaluator + * that can return green, red, fail or throw an error. + * It is used for testing the autopilot app. +*/ +const evaluateCommand = new Command('evaluate') +evaluateCommand.description('Evaluator test command.') +evaluateCommand.option('--green', 'Return green status.') +evaluateCommand.option('--red', 'Return red status.') +evaluateCommand.option('--fail', 'Fail the evaluation.') +evaluateCommand.option('--throw', 'Throw an unexpected error.') +evaluateCommand.action((options) => { + GetLogger().info('Evaluate command called.') + const appOutput = new AppOutput() + if (options.green) { + appOutput.setStatus('GREEN') + appOutput.setReason('Test reason.') + } else if (options.red) { + appOutput.setStatus('RED') + appOutput.setReason('Test reason.') + } else if (options.fail) { + throw new AppError('Test error.') + } else if (options.throw) { + throw new Error('Test error.') + } + appOutput.write() +}) + +const fetchCommand = new Command('fetch') +fetchCommand.description('Fetch test command.') +fetchCommand.option('--fail', 'Fail the fetch.') +fetchCommand.option('--throw', 'Throw an unexpected error.') +fetchCommand.action((options) => { + GetLogger().info('Fetch command called.') + const appOutput = new AppOutput() + if (options.fail) { + throw new AppError('Test error.') + } else if (options.throw) { + throw new Error('Test error.') + } + appOutput.addOutput({ test: 'test' }) + appOutput.write() +}) + +const app = new AutopilotApp( + 'app', + '0.0.1', + 'some description', + [evaluateCommand, fetchCommand] +) + +app.run() \ No newline at end of file diff --git a/yaku-apps-typescript/packages/autopilot-utils/test/unit/index.spec.ts b/yaku-apps-typescript/packages/autopilot-utils/test/unit/index.spec.ts new file mode 100644 index 00000000..ac31457d --- /dev/null +++ b/yaku-apps-typescript/packages/autopilot-utils/test/unit/index.spec.ts @@ -0,0 +1,130 @@ +import { describe, it, expect, vi } from 'vitest' +import { AppOutput } from '../../src' + +// mock console + +describe('app-interface', () => { + it('should create an instance of AppInterface', () => { + const appInterface = new AppOutput() + expect(appInterface).toBeInstanceOf(AppOutput) + }) + + it('should not output status if no status is set', () => { + const consoleSpy = vi.spyOn(console, 'log') + const appInterface = new AppOutput() + appInterface.setReason('The app did not set a status.') + appInterface.write() + expect(consoleSpy).toHaveBeenCalledWith( + JSON.stringify({ + reason: 'The app did not set a status.', + }) + ) + }) + + it('should not output reason if no reason is set', () => { + const consoleSpy = vi.spyOn(console, 'log') + const appInterface = new AppOutput() + appInterface.setStatus('GREEN') + appInterface.write() + expect(consoleSpy).toHaveBeenCalledWith( + JSON.stringify({ + status: 'GREEN', + }) + ) + }) + + it('should serve happy path', () => { + const consoleSpy = vi.spyOn(console, 'log') + const appInterface = new AppOutput() + appInterface.setStatus('GREEN') + appInterface.setReason('Everything is fine.') + appInterface.addOutput({ foo: 'bar' }) + appInterface.addResult({ + criterion: 'foo', + justification: 'bar', + fulfilled: true, + }) + appInterface.write() + expect(consoleSpy).toHaveBeenCalledWith( + JSON.stringify({ output: { foo: 'bar' } }) + ) + expect(consoleSpy).toHaveBeenCalledWith( + JSON.stringify({ + result: { + criterion: 'foo', + justification: 'bar', + fulfilled: true, + }, + }) + ) + expect(consoleSpy).toHaveBeenCalledWith( + JSON.stringify({ status: 'GREEN', reason: 'Everything is fine.' }) + ) + }) + + it('should print results in multiple json lines', () => { + const consoleSpy = vi.spyOn(console, 'log') + const appInterface = new AppOutput() + appInterface.addResult({ + criterion: 'foo', + justification: 'bar', + fulfilled: true, + }) + appInterface.addResult({ + criterion: 'foo2', + justification: 'bar2', + fulfilled: false, + }) + appInterface.addResult({ + criterion: 'foo3', + justification: 'bar3', + fulfilled: true, + }) + appInterface.write() + expect(consoleSpy).toHaveBeenCalledWith( + JSON.stringify({ + result: { + criterion: 'foo', + justification: 'bar', + fulfilled: true, + }, + }) + ) + expect(consoleSpy).toHaveBeenCalledWith( + JSON.stringify({ + result: { + criterion: 'foo2', + justification: 'bar2', + fulfilled: false, + }, + }) + ) + expect(consoleSpy).toHaveBeenCalledWith( + JSON.stringify({ + result: { + criterion: 'foo3', + justification: 'bar3', + fulfilled: true, + }, + }) + ) + }) + + it('should print outputs in multiple json lines', () => { + const consoleSpy = vi.spyOn(console, 'log') + const appInterface = new AppOutput() + appInterface.addOutput({ foo: 'bar' }) + appInterface.addOutput({ foo2: 'bar2' }) + appInterface.addOutput({ foo3: 'bar3' }) + appInterface.write() + expect(consoleSpy).toHaveBeenCalledWith( + JSON.stringify({ output: { foo: 'bar' } }) + ) + expect(consoleSpy).toHaveBeenCalledWith( + JSON.stringify({ output: { foo2: 'bar2' } }) + ) + expect(consoleSpy).toHaveBeenCalledWith( + JSON.stringify({ output: { foo3: 'bar3' } }) + ) + }) +}) diff --git a/yaku-apps-typescript/packages/autopilot-utils/tsup.config.json b/yaku-apps-typescript/packages/autopilot-utils/tsup.config.json new file mode 100644 index 00000000..267a68e2 --- /dev/null +++ b/yaku-apps-typescript/packages/autopilot-utils/tsup.config.json @@ -0,0 +1,6 @@ +{ + "entry": ["index.ts"], + "splitting": false, + "sourcemap": true, + "clean": true +} diff --git a/yaku-apps-typescript/packages/autopilot-utils/tsup.config.ts b/yaku-apps-typescript/packages/autopilot-utils/tsup.config.ts new file mode 100644 index 00000000..0e6106f5 --- /dev/null +++ b/yaku-apps-typescript/packages/autopilot-utils/tsup.config.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/**/*.ts'], + splitting: false, + clean: true, + target: 'node18', + format: ['esm'], + bundle: false, + sourcemap: true, +}) diff --git a/yaku-apps-typescript/packages/autopilot-utils/vitest-integration.config.ts b/yaku-apps-typescript/packages/autopilot-utils/vitest-integration.config.ts new file mode 100644 index 00000000..8bec0725 --- /dev/null +++ b/yaku-apps-typescript/packages/autopilot-utils/vitest-integration.config.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['test/integration/**/*.int-spec.ts'], + pool: 'threads', + poolOptions: { + threads: { + maxThreads: 1, + minThreads: 1, + }, + }, + typecheck: { + tsconfig: 'tsconfig.json', + }, + reporters: ['junit', 'default'], + outputFile: 'reports/integration-test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/packages/autopilot-utils/vitest.config.ts b/yaku-apps-typescript/packages/autopilot-utils/vitest.config.ts new file mode 100644 index 00000000..fd00b735 --- /dev/null +++ b/yaku-apps-typescript/packages/autopilot-utils/vitest.config.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + typecheck: { + tsconfig: 'tsconfig.json', + }, + coverage: { + provider: 'v8', + all: true, + enabled: true, + reporter: ['cobertura', 'json-summary', 'text-summary'], + include: ['src'], + exclude: ['src/index.d.ts', 'src/types.ts'], + }, + reporters: ['junit', 'default'], + outputFile: 'reports/test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/packages/eslint-config/eslint-preset.js b/yaku-apps-typescript/packages/eslint-config/eslint-preset.js new file mode 100644 index 00000000..8ebbadb3 --- /dev/null +++ b/yaku-apps-typescript/packages/eslint-config/eslint-preset.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +module.exports = { + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'prettier', + ], + // parser: '@typescript-eslint/parser', + // parserOptions: { + // ecmaVersion: 12, + // sourceType: module, + // }, + plugins: ['@typescript-eslint'], + settings: { + next: { + rootDir: ['apps/*/', 'packages/*/'], + }, + }, + env: { + browser: true, + es2021: true, + }, + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + 'no-sparse-arrays': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { destructuredArrayIgnorePattern: '^([.]{3})?_' }, + ], + }, + ignorePatterns: [ + 'node_modules/', + '*/node_modules', + '**/tsconfig.json', + '**/dist', + ], +} diff --git a/yaku-apps-typescript/packages/eslint-config/package.json b/yaku-apps-typescript/packages/eslint-config/package.json new file mode 100644 index 00000000..4363eb83 --- /dev/null +++ b/yaku-apps-typescript/packages/eslint-config/package.json @@ -0,0 +1,14 @@ +{ + "name": "@B-S-F/eslint-config", + "version": "0.1.0", + "main": "index.js", + "files": [ + "eslint-preset.js" + ], + "scripts": { + "build": "echo 'No build needed'" + }, + "dependencies": { + "eslint-config-prettier": "^8.6.0" + } +} diff --git a/yaku-apps-typescript/packages/fonts/package.json b/yaku-apps-typescript/packages/fonts/package.json new file mode 100644 index 00000000..3b3213df --- /dev/null +++ b/yaku-apps-typescript/packages/fonts/package.json @@ -0,0 +1,21 @@ +{ + "name": "@B-S-F/fonts", + "version": "0.1.0", + "description": "Truetype fonts for rendering", + "devDependencies": {}, + "scripts": { + "build": "echo 'No build needed'", + "test": "echo \"No tests\"" + }, + "repository": { + "type": "git", + "url": "https://github.com/B-S-F/qg-apps-typescript.git" + }, + "keywords": [ + "fonts" + ], + "files": [ + "fonts", + "install.sh" + ] +} diff --git a/yaku-apps-typescript/packages/issue-validators/.eslintrc.cjs b/yaku-apps-typescript/packages/issue-validators/.eslintrc.cjs new file mode 100644 index 00000000..cc850e07 --- /dev/null +++ b/yaku-apps-typescript/packages/issue-validators/.eslintrc.cjs @@ -0,0 +1,6 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + + module.exports = require("@B-S-F/eslint-config/eslint-preset") + \ No newline at end of file diff --git a/yaku-apps-typescript/packages/issue-validators/.prettierrc b/yaku-apps-typescript/packages/issue-validators/.prettierrc new file mode 100644 index 00000000..b2095be8 --- /dev/null +++ b/yaku-apps-typescript/packages/issue-validators/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": false, + "singleQuote": true +} diff --git a/yaku-apps-typescript/packages/issue-validators/package.json b/yaku-apps-typescript/packages/issue-validators/package.json new file mode 100644 index 00000000..6e24bef2 --- /dev/null +++ b/yaku-apps-typescript/packages/issue-validators/package.json @@ -0,0 +1,43 @@ +{ + "name": "@B-S-F/issue-validators", + "version": "0.1.0", + "description": "Issue evaluator functions", + "keywords": [ + "environment" + ], + "repository": { + "type": "git", + "url": "https://github.com/B-S-F/qg-apps-typescript.git" + }, + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsup && tsc --emitDeclarationOnly --declaration", + "dev": "nodemon --watch \"src/**\" --exec npm run start", + "lint": "eslint '**/*.ts'", + "format": "prettier --write '**/*.{ts,md}'", + "test": "vitest run && npm run test:update-cobertura-file", + "test:update-cobertura-file": "sed -i'.bak' 's,package name=\",package name=\"'${npm_package_name}/',g' coverage/cobertura-coverage.xml", + "test:dev": "vitest -w", + "test:coverage": "vitest --coverage", + "test:ui": "vitest --ui" + }, + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "@vitest/coverage-v8": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "nodemon": "*", + "prettier": "*", + "tsup": "*", + "vitest": "*" + } +} diff --git a/yaku-apps-typescript/packages/issue-validators/src/conditions.ts b/yaku-apps-typescript/packages/issue-validators/src/conditions.ts new file mode 100644 index 00000000..fb2c0568 --- /dev/null +++ b/yaku-apps-typescript/packages/issue-validators/src/conditions.ts @@ -0,0 +1,145 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { CheckGeneralResult, InvalidResolvedValues, Issue } from './types.js' + +/** + * @param issues the list of all issues + * @param fieldName the field to check for the issues + * @param acceptedValues if the field has at least one of the accepted values, it is valid + * + * @returns a list of issues that don't have expected values + */ +export function checkExpectedValues( + issues: Issue[], + fieldName: string, + acceptedValues: string[] +) { + return issues.filter((issue) => { + const field = issue[fieldName] + if (!field) return false + + if (typeof field !== 'object') { + return !acceptedValues.includes(field) + } else { + for (const property in field) { + if (acceptedValues.includes(field[property])) return false + } + return true + } + }) +} + +/** + * @param issues the list of all issues + * @param fieldName the field to check for the issues + * @param acceptedValues if the field has at least one of the accepted values, it is valid + * + * @returns a list of issues that have expected values + */ +export function checkExpectedValuesExist( + issues: Issue[], + fieldName: string, + acceptedValues: string[] +) { + return issues.filter((issue) => { + const field = issue[fieldName] + if (!field) return false + + if (typeof field !== 'object') { + return acceptedValues.includes(field) + } else { + for (const property in field) { + if (acceptedValues.includes(field[property])) return true + } + return false + } + }) +} + +/** + * @param issues the list of all issues + * @param fieldName the field to check for the issues + * @param unacceptedValues the values that are not accepted for that field + * + * @returns a list of issues with unaccepted field values + */ +export const checkIllegalValuesExist = checkExpectedValuesExist + +/** + * @param issues the list of all issues + * @param fieldName the field to check for the issues + * @param unacceptedValues the illegal values that are not accepted for that field + * + * @returns a list of issues without unaccepted field values + */ +export const checkIllegalValues = checkExpectedValues + +/** + * @param issue an issue object + * @param dueDateFieldName the field name of the actual date of the issue + * + * @returns a `CheckGeneralResult` stating if the dueDate is defined, overdue or valid + */ +export const checkDueDate = (issue: Issue, dueDateFieldName: string) => { + const dueDateField = issue[dueDateFieldName] + if (!dueDateField) { + return CheckGeneralResult.undefinedDueDate + } + + const dueDate = new Date(String(dueDateField)) + if (dueDate < new Date(Date.now())) { + return CheckGeneralResult.overdue + } + return CheckGeneralResult.valid +} + +/** + * @param issues the list of all issues + * @param fieldName the field to check for the issues + * @param acceptedValues if the field has at least one of the accepted values, it is valid + * @param dueDateFieldName the field name of the actual date of the issue + * + * @returns an `InvalidResolvedValues` containing information about overdue or undefined due date items + */ +export function checkResolvedValues( + issues: Issue[], + fieldName: string, + acceptedValues: string[], + dueDateFieldName: string +): InvalidResolvedValues { + const invalidissues: InvalidResolvedValues = { + [CheckGeneralResult.undefinedDueDate]: [], + [CheckGeneralResult.overdue]: [], + } + + const issuesWithoutExpectedValue = checkExpectedValues( + issues, + fieldName, + acceptedValues + ) + issuesWithoutExpectedValue.forEach((issue: Issue) => { + const response = checkDueDate(issue, dueDateFieldName) + if (response !== CheckGeneralResult.valid) { + console.log(issue) + invalidissues[response].push(issue) + } + }) + + return invalidissues +} + +/** + * @param issues the list of all issues + * @param fieldName the field to check for the issues + * + * @returns a list of issues which don't have the specified field + */ +export function checkFieldNotExist(issues: Issue[], fieldName: string) { + return issues.filter((issue) => !issue[fieldName]) +} + +export function checkFieldExists(issues: Issue[], fieldName: string) { + return issues.filter((issue) => issue[fieldName]) +} diff --git a/yaku-apps-typescript/packages/issue-validators/src/index.ts b/yaku-apps-typescript/packages/issue-validators/src/index.ts new file mode 100644 index 00000000..a6d31176 --- /dev/null +++ b/yaku-apps-typescript/packages/issue-validators/src/index.ts @@ -0,0 +1,132 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { + checkFieldNotExist, + checkFieldExists, + checkExpectedValues, + checkExpectedValuesExist, + checkIllegalValues, + checkIllegalValuesExist, + checkResolvedValues, +} from './conditions.js' +import { Issue, InvalidResolvedValues, Conditions } from './types.js' + +/** + * @param issues the list of all issues + * @param fieldName the field to check for the issues + * @param acceptedValues if the field has at least one of the accepted values, it is performed + * @param days the number of days in a cycle + * @param dueDateFieldName the field name of the actual date of the issue + * + * @returns a `CheckGeneralResult` stating if the the latest issue date is still before the cycle or not + */ +export const checkInCycle = ( + issues: Issue[], + fieldName: string, + acceptedValues: string[], + days: number, + dueDateFieldName: string +) => { + acceptedValues = acceptedValues.map((value) => value.toLowerCase()) + const validIssues = issues.filter((issue) => + acceptedValues.includes(issue[fieldName].toLowerCase()) + ) + + if (validIssues.length === 0) return false + + const dates = validIssues.map((issue) => new Date(issue[dueDateFieldName])) + const mostRecentDate: Date = new Date( + Math.max(...dates.map((d) => d.getTime())) + ) + + const cycleStartDate = new Date() + cycleStartDate.setDate(cycleStartDate.getDate() - days) + if (mostRecentDate < cycleStartDate) { + return false + } + return true +} + +/** + * @param issues the list of all issues + * @param fieldName the field to check for the issues + * @param acceptedValues if the field has at least one of the accepted values, it is performed + * @param dueDateFieldName the field name of the actual date of the issue + * @param afterDate the date after which to take issues into consideration + * + * @returns a list of issues that are either not closed, or after afterDate + */ +export const checkClosedIssuesAfterDate = ( + issues: Issue[], + fieldName: string, + acceptedValues: string[], + dueDateFieldName: string, + afterDate?: Date +) => { + if (afterDate && dueDateFieldName) { + return issues.filter((issue) => { + const isClosed = acceptedValues.includes(issue[fieldName]) + let dueDate + if (issue[dueDateFieldName]) dueDate = new Date(issue[dueDateFieldName]) + + if ( + !isClosed || + (isClosed && dueDate && dueDate > afterDate) || + (isClosed && !dueDate) // keep closed issues without due date + ) + return issue + }) + } + return issues +} + +/** + * @param issues the list of all issues + * @param fieldName the field to check for the issues + * @param conditionType the condition to check + * @param values the list of elements to check the issues against + * @param params additional information required for the check + * + * @returns the value of the chosen condition check + */ +export const checkProperty = ( + returnValidIssues = false, + issues: Issue[], + fieldName: string, + conditionType: Conditions, + values?: string[], + dueDateFieldName?: string +): Issue[] | InvalidResolvedValues => { + switch (conditionType) { + case Conditions.exists: + if (returnValidIssues == false) { + return checkFieldNotExist(issues, fieldName) + } else { + return checkFieldExists(issues, fieldName) + } + case Conditions.expected: + if (returnValidIssues == false) { + return checkExpectedValues(issues, fieldName, values!) + } else { + return checkExpectedValuesExist(issues, fieldName, values!) + } + case Conditions.illegal: + if (returnValidIssues == false) { + return checkIllegalValuesExist(issues, fieldName, values!) + } else { + return checkIllegalValues(issues, fieldName, values!) + } + case Conditions.resolved: + return checkResolvedValues(issues, fieldName, values!, dueDateFieldName!) + default: + console.warn(`Condition ${conditionType} is not implemented!`) + } + + return [] +} + +export * from './types.js' +export * from './conditions.js' +export * from './output.js' diff --git a/yaku-apps-typescript/packages/issue-validators/src/output.ts b/yaku-apps-typescript/packages/issue-validators/src/output.ts new file mode 100644 index 00000000..d3686a1d --- /dev/null +++ b/yaku-apps-typescript/packages/issue-validators/src/output.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +export function generatePropertyOutput( + property: string, + invalidPropertiesOutput: string[] +) { + if (!invalidPropertiesOutput.length) return '' + + let output = `Result for property \`${property}\`:\n` + invalidPropertiesOutput.forEach((propertyOutput) => { + output += ' * ' + propertyOutput + '\n' + }) + + return output +} + +export function generateGlobalOutput(outputs: string[]) { + const reason = outputs.join('\n') + const status = + reason.includes('invalid') || reason.includes('no ') ? 'RED' : 'GREEN' + console.log(JSON.stringify({ status, reason })) +} diff --git a/yaku-apps-typescript/packages/issue-validators/src/types.ts b/yaku-apps-typescript/packages/issue-validators/src/types.ts new file mode 100644 index 00000000..3c85b36f --- /dev/null +++ b/yaku-apps-typescript/packages/issue-validators/src/types.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +export enum CheckGeneralResult { + undefinedField = 'undefinedField', + invalid = 'invalid', + overdue = 'overdue', + undefinedDueDate = 'undefinedDueDate', + valid = 'valid', +} + +export enum Conditions { + exists = 'exists', + expected = 'expected', + illegal = 'illegal', + resolved = 'resolved', +} + +export interface Issue { + [key: string]: any +} + +export interface Dictionary { + [key: string]: any +} + +export interface InvalidResolvedValues { + [CheckGeneralResult.overdue]: Issue[] + [CheckGeneralResult.undefinedDueDate]: Issue[] +} + +export interface InvalidIssues { + [key: string]: { + [Conditions.exists]: Issue[] + [Conditions.expected]: Issue[] + [Conditions.illegal]: Issue[] + [Conditions.resolved]: InvalidResolvedValues + } +} diff --git a/yaku-apps-typescript/packages/issue-validators/test/conditions.test.ts b/yaku-apps-typescript/packages/issue-validators/test/conditions.test.ts new file mode 100644 index 00000000..985422d0 --- /dev/null +++ b/yaku-apps-typescript/packages/issue-validators/test/conditions.test.ts @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { describe, expect, it, vi } from 'vitest' + +import * as conditions from '../src/conditions' + +const issues = [ + { + State: 'Closed', + }, + { + State: { + name: 'Closed', + type: 'performed states', + }, + }, + { + State: { + name: 'Open', + }, + }, + { + Id: 4, + }, +] +const fieldName = 'State' +const dueDateFieldName = 'DueDate' + +describe('validators', () => { + it("checkExpectedValues() should return issues which don't have the accepted values", () => { + const acceptedValues = ['Closed'] + const expectedOutput = [issues[2]] + const result = conditions.checkExpectedValues( + issues, + fieldName, + acceptedValues + ) + expect(result).toEqual(expectedOutput) + }) + it('checkExpectedValuesExist() should return issues which have the accepted values', () => { + const acceptedValues = ['Closed'] + const expectedOutput = [issues[0], issues[1]] + const result = conditions.checkExpectedValuesExist( + issues, + fieldName, + acceptedValues + ) + expect(result).toEqual(expectedOutput) + }) + it('checkIllegalValuesExist() should return issues which have illegal values', () => { + const unacceptedValues = ['Open'] + const result = conditions.checkIllegalValuesExist( + issues, + fieldName, + unacceptedValues + ) + const expectedOutput = [issues[2]] + expect(result).toEqual(expectedOutput) + }) + it('checkIllegalValues() should return issues which do not have illegal values', () => { + const unacceptedValues = ['Open'] + const result = conditions.checkIllegalValues( + issues, + fieldName, + unacceptedValues + ) + const expectedOutput = [issues[0], issues[1]] + expect(result).toEqual(expectedOutput) + }) + it('checkDueDate() should return undefined due date', () => { + const issue = {} + const result = conditions.checkDueDate(issue, dueDateFieldName) + expect(result).toEqual('undefinedDueDate') + }) + it('checkDueDate() should return overdue', () => { + const issue = { + DueDate: '2022-01-01', + } + const result = conditions.checkDueDate(issue, dueDateFieldName) + expect(result).toEqual('overdue') + }) + it('checkDueDate() should return overdue', () => { + vi.useFakeTimers() + const date = new Date('2022-01-01') + vi.setSystemTime(date) + const issue = { + DueDate: '2022-02-01', + } + const result = conditions.checkDueDate(issue, dueDateFieldName) + expect(result).toEqual('valid') + vi.useRealTimers() + }) + it('checkResolvedValues() should find a valid and an invalid issue', () => { + const acceptedValues = ['Closed'] + const issuesResolved = [ + { + State: 'Closed', + DueDate: '2022-01-01', + }, + { + State: 'Open', + DueDate: '2022-01-01', + }, + ] + const expectedResult = { + undefinedDueDate: [], + overdue: [issuesResolved[1]], + } + const result = conditions.checkResolvedValues( + issuesResolved, + fieldName, + acceptedValues, + dueDateFieldName + ) + expect(result).toEqual(expectedResult) + }) + it('checkFieldNotExists() should return issues without the specified field', () => { + const expectedResult = [issues[3]] + const result = conditions.checkFieldNotExist(issues, fieldName) + expect(result).toEqual(expectedResult) + }) + it('checkFieldExists() should return issues with the specified field', () => { + const expectedResult = [issues[0], issues[1], issues[2]] + const result = conditions.checkFieldExists(issues, fieldName) + expect(result).toEqual(expectedResult) + }) +}) diff --git a/yaku-apps-typescript/packages/issue-validators/test/index.test.ts b/yaku-apps-typescript/packages/issue-validators/test/index.test.ts new file mode 100644 index 00000000..332a1405 --- /dev/null +++ b/yaku-apps-typescript/packages/issue-validators/test/index.test.ts @@ -0,0 +1,239 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { describe, expect, it, vi } from 'vitest' +import { Conditions } from '../src/types.js' +import * as evaluators from '../src/index' + +const fieldName = 'State' +const dueDateFieldName = 'DueDate' +const acceptedValues = ['Closed'] + +describe('checkInCycle()', () => { + const issues = [ + { + Id: 1, + State: 'Closed', + DueDate: '2022-01-01', + }, + { + Id: 2, + State: 'Open', + DueDate: '2022-02-01', + }, + ] + const days = 30 + + it('should return false if all issues are invalid', () => { + const result = evaluators.checkInCycle( + [], + fieldName, + acceptedValues, + days, + dueDateFieldName + ) + expect(result).toBeFalsy() + }) + + it('should return false', () => { + vi.useFakeTimers() + const date = new Date('2022-03-01') + vi.setSystemTime(date) + const result = evaluators.checkInCycle( + issues, + fieldName, + acceptedValues, + days, + dueDateFieldName + ) + expect(result).toBeFalsy() + vi.useRealTimers() + }) + + it('should return true', () => { + vi.useFakeTimers() + const date = new Date('2022-01-30') + vi.setSystemTime(date) + const result = evaluators.checkInCycle( + issues, + fieldName, + acceptedValues, + days, + dueDateFieldName + ) + expect(result).toBeTruthy() + vi.useRealTimers() + }) +}) + +describe('checkClosedIssuesAfterDate()', () => { + const issues = [ + { + Id: 1, + State: 'Closed', + }, + { + Id: 2, + State: 'Open', + }, + { + Id: 3, + State: 'Closed', + DueDate: '2022-01-01', + }, + { + Id: 4, + State: 'Closed', + DueDate: '2021-12-30', + }, + ] + it('should return all issues because of no date', () => { + const result = evaluators.checkClosedIssuesAfterDate( + issues, + fieldName, + acceptedValues, + dueDateFieldName + ) + expect(result).toEqual(issues) + }) + it('should return open issues, closed issues after date, closed issues without date', () => { + const afterDate = new Date('2021-12-31') + const result = evaluators.checkClosedIssuesAfterDate( + issues, + fieldName, + acceptedValues, + dueDateFieldName, + afterDate + ) + expect(result).toEqual(issues.slice(0, 3)) + }) +}) + +describe('checkProperty()', () => { + it('should return empty array for unknown condition type', () => { + const conditionType = 'condition1' as Conditions + const mockedConsole = vi.spyOn(console, 'warn') + const result = evaluators.checkProperty( + false, + [], + fieldName, + conditionType, + acceptedValues + ) + expect(result).toEqual([]) + expect(mockedConsole).toHaveBeenCalledWith( + 'Condition condition1 is not implemented!' + ) + }) + it('should return issues with undefined fields', () => { + const issues = [ + { + Id: 1, + }, + ] + const conditionType = 'exists' as Conditions + const expectedResult = issues + const result = evaluators.checkProperty( + false, + issues, + fieldName, + conditionType, + acceptedValues + ) + expect(result).toEqual(expectedResult) + }) + it('should return issues with unexpected fields', () => { + const issues = [ + { + Id: 1, + State: 'Open', + }, + ] + const conditionType = 'expected' as Conditions + const expectedResult = issues + const result = evaluators.checkProperty( + false, + issues, + fieldName, + conditionType, + acceptedValues + ) + expect(result).toEqual(expectedResult) + }) + it('should return issues with expected fields', () => { + const issues = [ + { + Id: 1, + State: 'Closed', + }, + ] + const conditionType = 'expected' as Conditions + const expectedResult = issues + const result = evaluators.checkProperty( + true, + issues, + fieldName, + conditionType, + acceptedValues + ) + expect(result).toEqual(expectedResult) + }) + it('should return issues with illegal fields', () => { + const issues = [ + { + Id: 1, + State: 'Closed', + }, + ] + const conditionType = 'illegal' as Conditions + const expectedResult = issues + const result = evaluators.checkProperty( + false, + issues, + fieldName, + conditionType, + acceptedValues + ) + expect(result).toEqual(expectedResult) + }) + it('should return issues without illegal fields', () => { + const issues = [ + { + Id: 1, + State: 'Open', + }, + ] + const conditionType = 'illegal' as Conditions + const expectedResult = issues + const result = evaluators.checkProperty( + true, + issues, + fieldName, + conditionType, + acceptedValues + ) + expect(result).toEqual(expectedResult) + }) + it('should return issues with unresolved values', () => { + const issues = [ + { + Id: 1, + State: 'Open', + }, + ] + const conditionType = 'resolved' as Conditions + const expectedResult = { + undefinedDueDate: issues, + overdue: [], + } + const result = evaluators.checkProperty( + false, + issues, + fieldName, + conditionType, + acceptedValues + ) + expect(result).toEqual(expectedResult) + }) +}) diff --git a/yaku-apps-typescript/packages/issue-validators/test/output.test.ts b/yaku-apps-typescript/packages/issue-validators/test/output.test.ts new file mode 100644 index 00000000..fe770c18 --- /dev/null +++ b/yaku-apps-typescript/packages/issue-validators/test/output.test.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { generatePropertyOutput, generateGlobalOutput } from '../src/output' + +describe('generatePropertyOutput()', () => { + it('should return output for each property', () => { + const invalidPropertiesOutputList = [ + 'property output 1', + 'property output 2', + ] + const expectedOutput = + `Result for property \`property\`:\n` + + ` * ${invalidPropertiesOutputList[0]}\n` + + ` * ${invalidPropertiesOutputList[1]}\n` + const output = generatePropertyOutput( + 'property', + invalidPropertiesOutputList + ) + expect(output).toEqual(expectedOutput) + }) + + it('should return empty string if there are no invalid properties', () => { + const invalidPropertiesOutput = [] + const expectedOutput = '' + const output = generatePropertyOutput('property', invalidPropertiesOutput) + expect(output).toEqual(expectedOutput) + }) +}) + +describe('generateGlobalOutput()', () => { + afterEach(() => { + vi.clearAllMocks() + }) + it('should return GREEN status', () => { + const outputs = ['Results from level 1:', 'All work items are valid'] + const spyConsole = vi.spyOn(console, 'log') + generateGlobalOutput(outputs) + expect(spyConsole).toHaveBeenCalledWith( + '{"status":"GREEN","reason":"Results from level 1:\\nAll work items are valid"}' + ) + }) + + it('should return RED status', () => { + const outputs = ['Results from level 1:', 'Some work items are invalid'] + const spyConsole = vi.spyOn(console, 'log') + generateGlobalOutput(outputs) + expect(spyConsole).toHaveBeenCalledWith( + `{"status":"RED","reason":"Results from level 1:\\nSome work items are invalid"}` + ) + }) +}) diff --git a/yaku-apps-typescript/packages/issue-validators/tsconfig.json b/yaku-apps-typescript/packages/issue-validators/tsconfig.json new file mode 100644 index 00000000..0df56473 --- /dev/null +++ b/yaku-apps-typescript/packages/issue-validators/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@B-S-F/typescript-config/base.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/yaku-apps-typescript/packages/issue-validators/tsup.config.ts b/yaku-apps-typescript/packages/issue-validators/tsup.config.ts new file mode 100644 index 00000000..0e6106f5 --- /dev/null +++ b/yaku-apps-typescript/packages/issue-validators/tsup.config.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/**/*.ts'], + splitting: false, + clean: true, + target: 'node18', + format: ['esm'], + bundle: false, + sourcemap: true, +}) diff --git a/yaku-apps-typescript/packages/issue-validators/vitest.config.ts b/yaku-apps-typescript/packages/issue-validators/vitest.config.ts new file mode 100644 index 00000000..1cf5e21f --- /dev/null +++ b/yaku-apps-typescript/packages/issue-validators/vitest.config.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + typecheck: { + tsconfig: 'tsconfig.json', + }, + coverage: { + provider: 'v8', + all: true, + enabled: true, + reporter: ['cobertura', 'json-summary', 'text-summary'], + include: ['src'], + }, + reporters: ['junit', 'default'], + outputFile: 'reports/test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/packages/json-evaluator-lib/.eslintrc.cjs b/yaku-apps-typescript/packages/json-evaluator-lib/.eslintrc.cjs new file mode 100644 index 00000000..87d861fe --- /dev/null +++ b/yaku-apps-typescript/packages/json-evaluator-lib/.eslintrc.cjs @@ -0,0 +1,15 @@ +module.exports = { + "extends": [ + "@B-S-F/eslint-config/eslint-preset" + ], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + 'no-sparse-arrays': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { destructuredArrayIgnorePattern: '^([.]{3})?_' }, + ], + "no-control-regex": 0 + }, +} \ No newline at end of file diff --git a/yaku-apps-typescript/packages/json-evaluator-lib/.prettierrc b/yaku-apps-typescript/packages/json-evaluator-lib/.prettierrc new file mode 100644 index 00000000..b2095be8 --- /dev/null +++ b/yaku-apps-typescript/packages/json-evaluator-lib/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": false, + "singleQuote": true +} diff --git a/yaku-apps-typescript/packages/json-evaluator-lib/package.json b/yaku-apps-typescript/packages/json-evaluator-lib/package.json new file mode 100644 index 00000000..2d2323b4 --- /dev/null +++ b/yaku-apps-typescript/packages/json-evaluator-lib/package.json @@ -0,0 +1,48 @@ +{ + "name": "@B-S-F/json-evaluator-lib", + "version": "0.9.0", + "description": "", + "main": "dist/index.js", + "types": "src/index.d.ts", + "files": [ + "dist" + ], + "type": "module", + "scripts": { + "build": "tsup && cp src/index.d.ts dist", + "dev": "nodemon --watch \"src/**\" --exec npm run start", + "start": "node ./dist/index.js", + "lint": "eslint '**/*.ts'", + "format": "prettier --write '**/*.{ts,md}'", + "test": "vitest run && npm run test:update-cobertura-file", + "test:update-cobertura-file": "sed -i'.bak' 's,package name=\",package name=\"'${npm_package_name}/',g' coverage/cobertura-coverage.xml", + "test:dev": "vitest -w", + "test:coverage": "vitest --coverage", + "test:ui": "vitest --ui" + }, + "keywords": [], + "author": "", + "license": "", + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/jsonpath": "^0.2.0", + "@types/node": "*", + "@typescript-eslint/eslint-plugin": "*", + "c8": "*", + "eslint": "*", + "eslint-config-prettier": "*", + "prettier": "*", + "tsup": "*", + "typescript": "*", + "vitest": "*" + }, + "dependencies": { + "ajv": "^8.12.0", + "colors": "1.4.0", + "isolated-vm": "^4.6.0", + "jsonpath": "^1.1.1", + "yaml": "^2.2.1", + "zod": "^3.22.3" + } +} diff --git a/yaku-apps-typescript/packages/json-evaluator-lib/src/conditions.ts b/yaku-apps-typescript/packages/json-evaluator-lib/src/conditions.ts new file mode 100644 index 00000000..388011c3 --- /dev/null +++ b/yaku-apps-typescript/packages/json-evaluator-lib/src/conditions.ts @@ -0,0 +1,179 @@ +import jp from 'jsonpath' +import ivm from 'isolated-vm' + +import { ReasonPackage, Status } from './types.js' +import { searchOnFail } from './util.js' + +export const evaluateCondition = ( + ref: unknown, + condition: string +): [boolean, any] => { + let newCondition = '' + const query = condition.match( + /(?<=\()[$](?=\))|\$(\S+?)(?=[\s=!()])|\$(\S+)/g + ) + if (!query || query.length === 0) { + throw new Error(`Error in condition: ${condition}`) + } + if (query.length > 1) { + throw new Error( + `Error in condition: ${condition}. Only one reference is allowed.` + ) + } + let values: unknown[] = [] + if (typeof ref === 'object') { + values = jp.query(ref, query[0]) + } else { + values = [ref] + } + if (condition.includes('.includes(')) { + newCondition = condition.replace(query[0], `values`) + } else { + newCondition = condition.replace(query[0], `values[0]`) + } + const isolate = new ivm.Isolate() + const context = isolate.createContextSync() + const jail = context.global + jail.setSync('global', jail.derefInto()) + jail.setSync('values', new ivm.ExternalCopy(values).copyInto()) + const script = isolate.compileScriptSync(newCondition) + const resultBool = script.runSync(context, { timeout: 5000 }) + const resultReasons = values + return [resultBool, resultReasons] +} + +export const all = ( + iterable: any[], + condition: string +): { result: boolean; reasonPackage?: ReasonPackage[] } => { + const continueSearchOnFail = searchOnFail() + const invalidElements: ReasonPackage[] = [] + + for (const value of iterable) { + if (!value) { + invalidElements.push({ reasons: value, context: value }) + if (!continueSearchOnFail) { + return { result: false, reasonPackage: invalidElements } + } + continue + } + const [result, reasons] = evaluateCondition(value, condition) + if (!result) { + invalidElements.push({ reasons, context: value }) + if (!continueSearchOnFail) { + return { result: false, reasonPackage: invalidElements } + } + } + } + + if (invalidElements.length > 0) { + return { result: false, reasonPackage: invalidElements } + } + return { result: true } +} + +export const any = ( + iterable: any[], + condition: string +): { result: boolean; reasonPackage?: ReasonPackage[] } => { + const invalidElements: ReasonPackage[] = [] + + for (const value of iterable) { + if (!value) { + continue + } + const [result, reason] = evaluateCondition(value, condition) + invalidElements.push({ reasons: reason, context: value }) + if (result) { + return { result: true } + } + } + return { result: false, reasonPackage: invalidElements } +} + +export const one = ( + iterable: any[], + condition: string +): { result: boolean; reasonPackage?: ReasonPackage[] } => { + const invalidElements: ReasonPackage[] = [] + const validElements: ReasonPackage[] = [] + const continueSearchOnFail = searchOnFail() + + for (const value of iterable) { + if (!value) { + invalidElements.push({ reasons: value, context: value }) + continue + } + const [result, reason] = evaluateCondition(value, condition) + if (result) { + validElements.push({ reasons: reason, context: value }) + if (validElements.length > 1 && !continueSearchOnFail) { + return { result: false, reasonPackage: validElements } + } + } else { + invalidElements.push({ reasons: reason, context: value }) + } + } + + if (validElements.length == 1) { + return { result: true } + } else if (validElements.length > 1) { + return { result: false, reasonPackage: validElements } + } + return { result: false, reasonPackage: invalidElements } +} + +export const none = ( + iterable: any[], + condition: string +): { result: boolean; reasonPackage?: ReasonPackage[] } => { + const invalidElements: ReasonPackage[] = [] + const continueSearchOnFail = searchOnFail() + + for (const value of iterable) { + if (!value) { + continue + } + const [result, reasons] = evaluateCondition(value, condition) + if (result) { + invalidElements.push({ reasons: reasons, context: value }) + if (!continueSearchOnFail) { + return { result: false, reasonPackage: invalidElements } + } + } + } + + if (invalidElements.length > 0) { + return { result: false, reasonPackage: invalidElements } + } + return { result: true } +} + +export function evaluateConcatenationCondition(condition: string): Status { + const isolate = new ivm.Isolate() + const context = isolate.createContextSync() + const jail = context.global + jail.setSync('global', jail.derefInto()) + jail.setSync('GREEN', 'GREEN') + jail.setSync('YELLOW', 'YELLOW') + jail.setSync('RED', false) + const script = isolate.compileScriptSync(condition) + const result = script.runSync(context, { timeout: 5000 }) + if (result === false) { + return 'RED' + } else if (result === 'GREEN' || result === 'YELLOW') { + const isolate = new ivm.Isolate() + const context = isolate.createContextSync() + const jail = context.global + jail.setSync('global', jail.derefInto()) + jail.setSync('GREEN', 'GREEN') + jail.setSync('YELLOW', false) + jail.setSync('RED', false) + const script = isolate.compileScriptSync(condition) + const result = script.runSync(context, { timeout: 5000 }) + if (result === false) { + return 'YELLOW' + } + } + return 'GREEN' +} diff --git a/yaku-apps-typescript/packages/json-evaluator-lib/src/evaluate.ts b/yaku-apps-typescript/packages/json-evaluator-lib/src/evaluate.ts new file mode 100644 index 00000000..b1ad57b9 --- /dev/null +++ b/yaku-apps-typescript/packages/json-evaluator-lib/src/evaluate.ts @@ -0,0 +1,176 @@ +import jp from 'jsonpath' +import ivm from 'isolated-vm' + +import { + all as all_cond, + any as any_cond, + one as one_cond, + none as none_cond, + evaluateCondition, + evaluateConcatenationCondition, +} from './conditions.js' + +import { + CheckResults, + ConcatenationResult, + ReasonPackage, + Status, +} from './types.js' +import { GetLogger } from '@B-S-F/autopilot-utils' + +const all = all_cond +const any = any_cond +const one = one_cond +const none = none_cond + +export const evalCheck = ( + condition: string, + reference: string, + data: unknown, + options: { + log?: string + true?: Status + false?: Status + return_if_empty?: Status + return_if_not_found?: Status + } +): CheckResults => { + const ref = jp.query(data, reference) + + const result: CheckResults = { + ref: reference, + condition: condition, + status: 'RED', + bool: false, + reasonPackages: [], + } + + if (ref.length === 0) { + if (reference.includes('[?(')) { + const logger = GetLogger() + logger.warn( + 'Warning: no JSON data was found. Could be ok, but if in doubt, ' + + `double-check your JSONPath reference expression (\`${reference}\`).` + ) + } else { + result.status = options.return_if_not_found || 'RED' + return result + } + } else if (ref.length === 1) { + let emptyCondition = + (Array.isArray(ref[0]) && ref[0].length === 0) || + (typeof ref[0] === 'object' && !Object.keys(ref[0]).length) + if (emptyCondition) { + result.status = options.return_if_empty || 'RED' + return result + } else { + if (ref[0].length !== undefined) { + let emptyFlag = true + for (let i = 0; i < ref[0].length; i++) { + emptyCondition = + (Array.isArray(ref[0][i]) && ref[0][i].length === 0) || + (typeof ref[0][i] === 'object' && !Object.keys(ref[0][i]).length) + if (!emptyCondition) { + emptyFlag = false + } + } + if (emptyFlag) { + result.status = options.return_if_empty || 'RED' + return result + } + } + } + } + + if (condition.match(/all|any|one|none/)) { + const isolate = new ivm.Isolate() + const context = isolate.createContextSync() + const jail = context.global + jail.setSync('global', jail.derefInto()) + jail.setSync('ref', new ivm.ExternalCopy(ref).copyInto()) + jail.setSync('all', all) + jail.setSync('any', any) + jail.setSync('one', one) + jail.setSync('none', none) + const script = isolate.compileScriptSync(condition) + const output: { result: boolean; reasonPackage?: ReasonPackage[] } = + script.runSync(context, { timeout: 5000, copy: true }) + result.bool = output.result + result.reasonPackages = output.reasonPackage ?? [] + + if (result.bool) { + result.status = options.true ?? 'GREEN' + } else { + result.status = options.false ?? 'RED' + } + + let emptyCondition = + result.reasonPackages[0] === undefined && ref.length === 0 + const greenStatusCondition = + result.reasonPackages[0] === undefined && ref.length > 0 + + if (emptyCondition) { + result.status = options.return_if_empty || 'RED' + } else if (greenStatusCondition) { + result.status = 'GREEN' + } else if (result.reasonPackages[0].reasons.length === 1) { + const reasons = result.reasonPackages[0].reasons + emptyCondition = + (Array.isArray(reasons[0]) && reasons[0].length === 0) || + (typeof reasons[0] === 'object' && !Object.keys(reasons[0]).length) + if (emptyCondition) { + result.status = options.return_if_empty || 'RED' + } + } else if (result.reasonPackages[0].reasons.length === 0) { + result.status = options.return_if_not_found || 'RED' + } + } else { + let reasons + ;[result.bool, reasons] = evaluateCondition(ref, condition) + result.reasonPackages = [{ reasons, context: ref }] + if (result.bool) { + result.status = options.true ?? 'GREEN' + } else { + result.status = options.false ?? 'RED' + } + } + + //Trim context to contain only what's specified in the log + if (result.reasonPackages!.length > 0) { + result.reasonPackages = result.reasonPackages!.map((reasonPackage) => { + if (options.log && typeof reasonPackage.context === 'object') { + reasonPackage.context = jp.query(reasonPackage.context, options.log!)[0] + } else { + reasonPackage.context = undefined + } + return reasonPackage + }) + } + return result +} + +export const evalConcatenation = ( + condition: string, + checks: Record +) => { + const splitExpression = condition.split(/(&&|\|\|)/g).map((str) => str.trim()) + const concatExpression = splitExpression + .map((str) => { + if (str === '&&' || str === '||') { + return str + } + try { + return checks[str].status + } catch (error) { + throw new Error( + 'Error in concatenation condition. Please check the concatenation condition.' + ) + } + }) + .join(' ') + const concatenationResult: ConcatenationResult = { + condition: condition, + status: evaluateConcatenationCondition(concatExpression), + } + return concatenationResult +} diff --git a/yaku-apps-typescript/packages/json-evaluator-lib/src/index.d.ts b/yaku-apps-typescript/packages/json-evaluator-lib/src/index.d.ts new file mode 100644 index 00000000..c4e8465c --- /dev/null +++ b/yaku-apps-typescript/packages/json-evaluator-lib/src/index.d.ts @@ -0,0 +1,3 @@ +export { evalCheck, evalConcatenation } from './evaluate.js' +export { readJson } from './read-json.js' +export { CheckResults, ConcatenationResult, Status } from './types.js' diff --git a/yaku-apps-typescript/packages/json-evaluator-lib/src/index.ts b/yaku-apps-typescript/packages/json-evaluator-lib/src/index.ts new file mode 100644 index 00000000..23e1ba11 --- /dev/null +++ b/yaku-apps-typescript/packages/json-evaluator-lib/src/index.ts @@ -0,0 +1,2 @@ +export { evalCheck, evalConcatenation } from './evaluate.js' +export { readJson } from './read-json.js' diff --git a/yaku-apps-typescript/packages/json-evaluator-lib/src/read-json.ts b/yaku-apps-typescript/packages/json-evaluator-lib/src/read-json.ts new file mode 100644 index 00000000..b501af36 --- /dev/null +++ b/yaku-apps-typescript/packages/json-evaluator-lib/src/read-json.ts @@ -0,0 +1,19 @@ +import { readFile } from 'fs/promises' + +export const readJson = async (filePath: string): Promise => { + try { + const data = await readFile(filePath, 'utf-8') + const stringStrippedData = data.replace(/"([^"]*?)":/g, (_, group1) => { + return `"${group1.replace(/\s/g, '_')}"` + ':' + }) + + return JSON.parse(stringStrippedData) + } catch (error) { + if (error.code === 'ENOENT') { + throw new Error(`File ${filePath} does not exist`) + } + throw new Error( + `File ${filePath} could not be parsed, failed with error: ${error}` + ) + } +} diff --git a/yaku-apps-typescript/packages/json-evaluator-lib/src/types.ts b/yaku-apps-typescript/packages/json-evaluator-lib/src/types.ts new file mode 100644 index 00000000..4b64d8f0 --- /dev/null +++ b/yaku-apps-typescript/packages/json-evaluator-lib/src/types.ts @@ -0,0 +1,29 @@ +import { z } from 'zod' + +const statusSchema = z.enum(['GREEN', 'YELLOW', 'RED']) +const SearchOnFailSchema = z.boolean() +const ReasonPackageSchema = z + .object({ + context: z.any(), + reasons: z.array(z.any()), + }) + .strict() + +const checkResultsSchema = z.object({ + ref: z.string(), + condition: z.string(), + status: statusSchema, + bool: z.boolean(), + reasonPackages: z.array(ReasonPackageSchema).optional(), +}) + +const concatenationResultSchema = z.object({ + condition: z.string(), + status: statusSchema, +}) + +export type CheckResults = z.infer +export type ConcatenationResult = z.infer +export type Status = z.infer +export type SearchOnFail = z.infer +export type ReasonPackage = z.infer diff --git a/yaku-apps-typescript/packages/json-evaluator-lib/src/util.ts b/yaku-apps-typescript/packages/json-evaluator-lib/src/util.ts new file mode 100644 index 00000000..2fbcf93c --- /dev/null +++ b/yaku-apps-typescript/packages/json-evaluator-lib/src/util.ts @@ -0,0 +1,26 @@ +import { SearchOnFail } from './types' +import { AppError } from '@B-S-F/autopilot-utils' + +/** + * returns the search on fail bool from the default environment variable CONTINUE_SEARCH_ON_FAIL + * + * @param optional envVariableName + * @returns true | false + */ +export function searchOnFail( + envVariableName: string = 'CONTINUE_SEARCH_ON_FAIL' +): SearchOnFail { + const logLevel: string = process.env[envVariableName] || 'TRUE' + return validateSearchOnFail(logLevel) +} + +function validateSearchOnFail(logLevel: string): boolean { + const buff = logLevel.toUpperCase() + if (buff === 'TRUE') { + return true + } else if (buff === 'FALSE') { + return false + } else { + throw new AppError(`CONTINUE_SEARCH_ON_FAIL: ${logLevel}, is not valid!`) + } +} diff --git a/yaku-apps-typescript/packages/json-evaluator-lib/tests/conditions.spec.ts b/yaku-apps-typescript/packages/json-evaluator-lib/tests/conditions.spec.ts new file mode 100644 index 00000000..e802b556 --- /dev/null +++ b/yaku-apps-typescript/packages/json-evaluator-lib/tests/conditions.spec.ts @@ -0,0 +1,400 @@ +import { describe, it, expect, vi } from 'vitest' + +import { + evaluateCondition, + all, + any, + one, + none, + evaluateConcatenationCondition, +} from '../src/conditions.js' + +describe('evaluateCondition', () => { + it('should evaluate a basic condition with a simple reference', () => { + const ref = [{ foo: 'bar' }] + const condition = '$[*].foo === "bar"' + const result = evaluateCondition(ref, condition) + const expectedResult = [true, ['bar']] + expect(result).toStrictEqual(expectedResult) + }) + + it('should handle conditions with array references', () => { + const ref = [{ foo: 'bar' }, { foo: 'baz' }] + const condition = '$[0].foo === "bar"' + const result = evaluateCondition(ref, condition) + const expectedResult = [true, ['bar']] + expect(result).toStrictEqual(expectedResult) + }) + + it('should handle conditions with the .includes() method', () => { + const ref = ['foo', 'bar', 'baz'] + const condition = '($[*]).includes("baz")' + const result = evaluateCondition(ref, condition) + const expectedResult = [true, ['foo', 'bar', 'baz']] + expect(result).toStrictEqual(expectedResult) + }) + + it('should throw an error if there are multiple references', () => { + const ref = [{ foo: { bar: 'baz' }, qux: 'quux' }] + const condition = '$[*].foo.bar === "baz" && $[*].qux === "quux"' + expect(() => evaluateCondition(ref, condition)).toThrow( + 'Error in condition: $[*].foo.bar === "baz" && $[*].qux === "quux". Only one reference is allowed.' + ) + }) + + it('should throw an error if the condition is invalid', () => { + const ref = { foo: 'bar' } + const condition = 'foo === "bar"' // missing $ symbol + expect(() => evaluateCondition([ref], condition)).toThrow( + 'Error in condition: foo === "bar"' + ) + }) +}) + +describe('all', () => { + it('should return true if all elements pass the condition', () => { + const iterable = [{ foo: 'bar' }, { foo: 'baz' }, { foo: 'qux' }] + const condition = '$.foo !== undefined' + const result = all(iterable, condition) + expect(result).toEqual({ result: true }) + }) + + it('should return false if at least one element fails the condition', () => { + const iterable = [{ foo: 'bar' }, { foo: 'baz' }, { foo: 'bar' }] + const condition = '$.foo === "bar"' + const result = all(iterable, condition) + expect(result).toEqual({ + result: false, + reasonPackage: [ + { + context: { foo: 'baz' }, + reasons: ['baz'], + }, + ], + }) + }) + + it('should return false if any element is falsy, stoppping at the first breaking element', () => { + const iterable = [{ foo: 'bar' }, null, { foo: 'baz' }] + const condition = '$.foo !== undefined' + const result = all(iterable, condition) + expect(result).toEqual({ + result: false, + reasonPackage: [ + { + reasons: null, + context: null, + }, + ], + }) + }) + + it('should return false if any element is falsy, returning all breaking elements', () => { + const iterable = [{ foo: 'baz' }, null, { foo: 'baz' }, { foo: 'biz' }] + const condition = '$.foo !== "baz"' + + vi.stubEnv('CONTINUE_SEARCH_ON_FAIL', 'true') + + const result = all(iterable, condition) + expect(result).toEqual({ + result: false, + reasonPackage: [ + { + reasons: ['baz'], + context: { foo: 'baz' }, + }, + { + reasons: null, + context: null, + }, + { + reasons: ['baz'], + context: { foo: 'baz' }, + }, + ], + }) + }) +}) + +describe('all', () => { + const testCases = [ + { + input: [{ foo: 'bar' }, { foo: 'baz' }, { foo: 'qux' }], + condition: '$.foo !== undefined', + expectedOutput: { result: true }, + continueSearchOnFail: 'false', + }, + { + input: [{ foo: 'bar' }, { foo: 'baz' }, { foo: 'bar' }], + condition: '$.foo === "bar"', + expectedOutput: { + result: false, + reasonPackage: [{ reasons: ['baz'], context: { foo: 'baz' } }], + }, + continueSearchOnFail: 'false', + }, + { + input: [{ foo: 'bar' }, { foo: 'baz' }, { foo: 'bar' }], + condition: '$.foo !== "bar"', + expectedOutput: { + result: false, + reasonPackage: [ + { reasons: ['bar'], context: { foo: 'bar' } }, + { reasons: ['bar'], context: { foo: 'bar' } }, + ], + }, + continueSearchOnFail: 'true', + }, + { + input: [{ foo: 'bar' }, null, { foo: 'baz' }], + condition: '$.foo !== undefined', + expectedOutput: { + result: false, + reasonPackage: [{ reasons: null, context: null }], + }, + continueSearchOnFail: 'false', + }, + { + input: [null, undefined, false], + condition: '$.foo === "nothing"', + expectedOutput: { + result: false, + reasonPackage: [{ reasons: null, context: null }], + }, + continueSearchOnFail: 'false', + }, + { + input: [null, undefined, false], + condition: '$.foo === "nothing"', + expectedOutput: { + result: false, + reasonPackage: [ + { reasons: null, context: null }, + { reasons: undefined, context: undefined }, + { reasons: false, context: false }, + ], + }, + continueSearchOnFail: 'true', + }, + ] + testCases.forEach((testCase) => { + it(`should return ${JSON.stringify( + testCase.expectedOutput + )} when called with ${JSON.stringify(testCase.input)} and "${ + testCase.condition + }" continue search on fail set to "${ + testCase.continueSearchOnFail + }"`, () => { + vi.stubEnv('CONTINUE_SEARCH_ON_FAIL', testCase.continueSearchOnFail) + const result = all(testCase.input, testCase.condition) + expect(result).toEqual(testCase.expectedOutput) + }) + }) +}) + +describe('any', () => { + const testCases = [ + { + input: [{ foo: 'bar' }, { qux: 'quux' }, { foo: 'baz' }], + condition: '$.foo !== undefined', + expectedOutput: { result: true }, + }, + { + input: [{ foo: 'bar' }, { foo: 'baz' }, { foo: 'baz' }], + condition: '$.foo === "quux"', + expectedOutput: { + result: false, + reasonPackage: [ + { reasons: ['bar'], context: { foo: 'bar' } }, + { reasons: ['baz'], context: { foo: 'baz' } }, + { reasons: ['baz'], context: { foo: 'baz' } }, + ], + }, + }, + { + input: [null, undefined, false], + condition: '$.foo !== "nothing"', + expectedOutput: { result: false, reasonPackage: [] }, + }, + ] + testCases.forEach((testCase) => { + it(`should return ${JSON.stringify( + testCase.expectedOutput + )} when called with ${JSON.stringify(testCase.input)} and "${ + testCase.condition + }"`, () => { + const result = any(testCase.input, testCase.condition) + expect(result).toEqual(testCase.expectedOutput) + }) + }) +}) + +describe('one', () => { + const testCases = [ + { + input: [{ foo: 'bar' }, { qux: 'quux' }, { foo: 'baz' }], + condition: '$.foo === "bar"', + expectedOutput: { result: true }, + continueSearchOnFail: 'false', + }, + { + input: [{ foo: 'bar' }, { foo: 'bar' }, { foo: 'bar' }], + condition: '$.foo === "baz"', + expectedOutput: { + result: false, + reasonPackage: [ + { reasons: ['bar'], context: { foo: 'bar' } }, + { reasons: ['bar'], context: { foo: 'bar' } }, + { reasons: ['bar'], context: { foo: 'bar' } }, + ], + }, + continueSearchOnFail: 'false', + }, + { + input: [{ foo: 'bar' }, { foo: 'bar' }, { foo: 'baz' }], + condition: '$.foo === "bar"', + expectedOutput: { + result: false, + reasonPackage: [ + { reasons: ['bar'], context: { foo: 'bar' } }, + { reasons: ['bar'], context: { foo: 'bar' } }, + ], + }, + continueSearchOnFail: 'false', + }, + { + input: [null, undefined, false], + condition: '$.foo === "nothing"', + expectedOutput: { + result: false, + reasonPackage: [ + { reasons: null, context: null }, + { reasons: undefined, context: undefined }, + { reasons: false, context: false }, + ], + }, + continueSearchOnFail: 'false', + }, + { + input: [{ foo: 'bar' }, { foo: 'bar' }, { foo: 'bar' }], + condition: '$.foo === "bar"', + expectedOutput: { + result: false, + reasonPackage: [ + { reasons: ['bar'], context: { foo: 'bar' } }, + { reasons: ['bar'], context: { foo: 'bar' } }, + { reasons: ['bar'], context: { foo: 'bar' } }, + ], + }, + continueSearchOnFail: 'true', + }, + ] + testCases.forEach((testCase) => { + it(`should return ${JSON.stringify( + testCase.expectedOutput + )} when called with ${JSON.stringify(testCase.input)} and "${ + testCase.condition + }"`, () => { + vi.stubEnv('CONTINUE_SEARCH_ON_FAIL', testCase.continueSearchOnFail) + const result = one(testCase.input, testCase.condition) + expect(result).toEqual(testCase.expectedOutput) + }) + }) +}) + +describe('none', () => { + const testCases = [ + { + input: [], + condition: '$.foo === "bar"', + expectedOutput: { result: true }, + continueSearchOnFail: 'false', + }, + { + input: [{ foo: 'bar' }, { foo: 'baz' }], + condition: '$.foo === "quux"', + expectedOutput: { result: true }, + continueSearchOnFail: 'false', + }, + { + input: [{ foo: 'bar' }, { foo: 'baz' }], + condition: '$.foo === "bar"', + expectedOutput: { + result: false, + reasonPackage: [{ reasons: ['bar'], context: { foo: 'bar' } }], + }, + continueSearchOnFail: 'false', + }, + { + input: [{ foo: 'baz' }, { foo: 'bar' }], + condition: '$.foo === "bar"', + expectedOutput: { + result: false, + reasonPackage: [{ reasons: ['bar'], context: { foo: 'bar' } }], + }, + continueSearchOnFail: 'false', + }, + { + input: [{ foo: 'baz' }, null, { foo: 'qux' }], + condition: '$.foo === "bar"', + expectedOutput: { result: true }, + continueSearchOnFail: 'false', + }, + { + input: [null, undefined, false], + condition: '$.foo === "bar"', + expectedOutput: { result: true }, + continueSearchOnFail: 'false', + }, + { + input: [{ foo: 'bar' }, { foo: 'bar' }], + condition: '$.foo === "bar"', + expectedOutput: { + result: false, + reasonPackage: [ + { reasons: ['bar'], context: { foo: 'bar' } }, + { reasons: ['bar'], context: { foo: 'bar' } }, + ], + }, + continueSearchOnFail: 'true', + }, + ] + testCases.forEach((testCase) => { + it(`should return ${JSON.stringify( + testCase.expectedOutput + )} when called with ${JSON.stringify(testCase.input)} and "${ + testCase.condition + }"`, () => { + vi.stubEnv('CONTINUE_SEARCH_ON_FAIL', testCase.continueSearchOnFail) + const result = none(testCase.input, testCase.condition) + expect(result).toEqual(testCase.expectedOutput) + }) + }) +}) + +describe('evaluateConcatenationCondition', () => { + const testCases = [ + { + condition: 'GREEN && YELLOW', + expectedOutput: 'YELLOW', + }, + { + condition: 'GREEN && YELLOW && RED', + expectedOutput: 'RED', + }, + { + condition: '(GREEN && YELLOW) || RED', + expectedOutput: 'YELLOW', + }, + { + condition: 'GREEN && (YELLOW || RED)', + expectedOutput: 'YELLOW', + }, + ] + testCases.forEach((testCase) => { + it(`should return ${testCase.expectedOutput} when called with ${testCase.condition}`, () => { + const result = evaluateConcatenationCondition(testCase.condition) + expect(result).toEqual(testCase.expectedOutput) + }) + }) +}) diff --git a/yaku-apps-typescript/packages/json-evaluator-lib/tests/evaluator.spec.ts b/yaku-apps-typescript/packages/json-evaluator-lib/tests/evaluator.spec.ts new file mode 100644 index 00000000..d1919b82 --- /dev/null +++ b/yaku-apps-typescript/packages/json-evaluator-lib/tests/evaluator.spec.ts @@ -0,0 +1,1013 @@ +import { describe, it, expect, vi } from 'vitest' + +import { evalCheck, evalConcatenation } from '../src/evaluate.js' +import { Status } from '../src/types' + +describe('evalCheck', () => { + it('should ignore empty filtered data', () => { + const condition = '$.length === 0' + const reference = '$[?(@.a==2)]' + const data = [{ a: 1 }, { a: 1 }] + const options = {} + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + }) + + it('should evaluate condition and return true status', () => { + const condition = '($[*]).length === 3' + const reference = '$.foo' + const data = { foo: [1, 2, 3] } + const options = { + true: 'GREEN' as Status, + } + const expectedResult = [ + { + reasons: [[1, 2, 3]], + context: undefined, + }, + ] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(true) + expect(result.status).toEqual('GREEN') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should evaluate condition and return false status', () => { + const condition = '($[*]).includes(4)' + const reference = '$.foo' + const data = { foo: [1, 2, 3] } + const options = { + false: 'YELLOW' as Status, + } + const expectedResult = [ + { + reasons: [[1, 2, 3]], + context: undefined, + }, + ] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(false) + expect(result.status).toEqual('YELLOW') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should evaluate all function and return true status', () => { + const condition = 'all(ref, "($) === true")' + const reference = '$.foo[*]' + const data = { foo: [true, true, true] } + const options = { + true: 'GREEN' as Status, + log: '$', + } + const expectedResult = [] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(true) + expect(result.status).toEqual('GREEN') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should evaluate all function and return false status with reasons', () => { + const condition = 'all(ref, "($) < 5")' + const reference = '$.foo[*]' + const data = { foo: [1, 2, 6, 4] } + const options = { + false: 'RED' as Status, + log: '$', + } + const expectedResult = [ + { + reasons: [6], + context: undefined, + }, + ] + + vi.stubEnv('CONTINUE_SEARCH_ON_FAIL', 'true') + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(false) + expect(result.status).toEqual('RED') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should return GREEN when ref is not found and return_if_not_found is set to GREEN', () => { + const condition = `all(ref, "'EXAMPLE_BRANCH' === $.branch")` + const reference = '$.otherExample[*]' + const data = { + example: [ + { + branch_name: 'EXAMPLE_BRANCH', + state: 'OPEN', + }, + + { + branch_id: 1, + state: 'OPEN', + }, + ], + example1: [], + } + const options = { + true: 'GREEN' as Status, + return_if_not_found: 'GREEN' as Status, + } + const expectedResult = [] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(false) + expect(result.status).toEqual('GREEN') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should return YELLOW when ref is not found and return_if_not_found is set to YELLOW', () => { + const condition = `all(ref, "'EXAMPLE_BRANCH' === $.branch")` + const reference = '$.otherExample[*]' + const data = { + example: [ + { + branch_name: 'EXAMPLE_BRANCH', + state: 'OPEN', + }, + + { + branch_id: 1, + state: 'OPEN', + }, + ], + example1: [], + } + const options = { + true: 'GREEN' as Status, + return_if_not_found: 'YELLOW' as Status, + } + const expectedResult = [] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(false) + expect(result.status).toEqual('YELLOW') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should return RED when ref is not found and return_if_not_found is set to RED', () => { + const condition = `all(ref, "'EXAMPLE_BRANCH' === $.branch")` + const reference = '$.otherExample[*]' + const data = { + example: [ + { + branch_name: 'EXAMPLE_BRANCH', + state: 'OPEN', + }, + + { + branch_id: 1, + state: 'OPEN', + }, + ], + example1: [], + } + const options = { + true: 'GREEN' as Status, + return_if_not_found: 'RED' as Status, + } + const expectedResult = [] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(false) + expect(result.status).toEqual('RED') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should return GREEN when condition is not found and return_if_not_found is set to GREEN', () => { + const condition = `all(ref, "'EXAMPLE_BRANCH' === $.branch_name")` + const reference = '$.example[*]' + const data = { + example: [ + { + branch_name: 'EXAMPLE_BRANCH', + state: 'OPEN', + }, + + { + branch_id: 1, + state: 'OPEN', + }, + ], + example1: [], + } + const options = { + true: 'GREEN' as Status, + return_if_not_found: 'GREEN' as Status, + } + const expectedResult = [ + { + reasons: [], + context: undefined, + }, + ] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(false) + expect(result.status).toEqual('GREEN') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should return YELLOW when condition is not found and return_if_not_found is set to YELLOW', () => { + const condition = `all(ref, "'EXAMPLE_BRANCH' === $.branch_name")` + const reference = '$.example[*]' + const data = { + example: [ + { + branch_name: 'EXAMPLE_BRANCH', + state: 'OPEN', + }, + + { + branch_id: 1, + state: 'OPEN', + }, + ], + example1: [], + } + const options = { + true: 'GREEN' as Status, + return_if_not_found: 'YELLOW' as Status, + } + const expectedResult = [ + { + reasons: [], + context: undefined, + }, + ] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(false) + expect(result.status).toEqual('YELLOW') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should return RED when condition is not found and return_if_not_found is set to RED', () => { + const condition = `all(ref, "'EXAMPLE_BRANCH' === $.branch_name")` + const reference = '$.example[*]' + const data = { + example: [ + { + branch_name: 'EXAMPLE_BRANCH', + state: 'OPEN', + }, + + { + branch_id: 1, + state: 'OPEN', + }, + ], + example1: [], + } + const options = { + true: 'RED' as Status, + return_if_not_found: 'RED' as Status, + } + const expectedResult = [ + { + reasons: [], + context: undefined, + }, + ] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(false) + expect(result.status).toEqual('RED') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should return RED when ref is not found and return_if_not_found is not set', () => { + const condition = `all(ref, "'EXAMPLE_BRANCH' === $.branch")` + const reference = '$.otherExample[*]' + const data = { + example: [ + { + branch_name: 'EXAMPLE_BRANCH', + state: 'OPEN', + }, + + { + branch_id: 1, + state: 'OPEN', + }, + ], + example1: [], + } + const options = { + true: 'GREEN' as Status, + } + const expectedResult = [] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(false) + expect(result.status).toEqual('RED') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should return RED when condition is not found and return_if_not_found is not set', () => { + const condition = `all(ref, "'EXAMPLE_BRANCH' === $.branch_name")` + const reference = '$.example[*]' + const data = { + example: [ + { + branch_name: 'EXAMPLE_BRANCH', + state: 'OPEN', + }, + + { + branch_id: 1, + state: 'OPEN', + }, + ], + example1: [], + } + const options = { + true: 'RED' as Status, + } + const expectedResult = [ + { + reasons: [], + context: undefined, + }, + ] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(false) + expect(result.status).toEqual('RED') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should return GREEN when ref is found, but empty and return_if_empty is set to GREEN', () => { + const condition = 'all(ref, "5 < $.total_commits")' + const reference = '$.stats' + const data = { + example: [ + { + branch: 'EXAMPLE_BRANCH', + state: 'OPEN', + contributors: [], + }, + { + branch: 'OTHER_BRANCH', + state: 'CLOSED', + contributors: [ + { + name: 'John', + age: 25, + }, + ], + }, + ], + stats: [], + } + + const options = { + true: 'GREEN' as Status, + return_if_empty: 'GREEN' as Status, + } + const expectedResult = [] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(false) + expect(result.status).toEqual('GREEN') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should return YELLOW when ref is found, but empty and return_if_empty is set to YELLOW', () => { + const condition = 'all(ref, "5 < $.total_commits")' + const reference = '$.stats' + const data = { + example: [ + { + branch: 'EXAMPLE_BRANCH', + state: 'OPEN', + contributors: [], + }, + { + branch: 'OTHER_BRANCH', + state: 'CLOSED', + contributors: [ + { + name: 'John', + age: 25, + }, + ], + }, + ], + stats: [], + } + + const options = { + true: 'GREEN' as Status, + return_if_empty: 'YELLOW' as Status, + } + const expectedResult = [] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(false) + expect(result.status).toEqual('YELLOW') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should return RED when ref is found, but empty and return_if_empty is set to RED', () => { + const condition = 'all(ref, "5 < $.total_commits")' + const reference = '$.stats' + const data = { + example: [ + { + branch: 'EXAMPLE_BRANCH', + state: 'OPEN', + contributors: [], + }, + { + branch: 'OTHER_BRANCH', + state: 'CLOSED', + contributors: [ + { + name: 'John', + age: 25, + }, + ], + }, + ], + stats: [], + } + + const options = { + true: 'GREEN' as Status, + return_if_empty: 'RED' as Status, + } + const expectedResult = [] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(false) + expect(result.status).toEqual('RED') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should return GREEN when condition is found, but empty and return_if_empty is set to GREEN', () => { + const condition = `all(ref, "'John' === $.name")` + const reference = '$.example[*].contributors[*]' + const data = { + example: [ + { + branch: 'EXAMPLE_BRANCH', + state: 'OPEN', + contributors: [], + }, + { + branch: 'OTHER_BRANCH', + state: 'CLOSED', + contributors: [ + { + name: {}, + age: 25, + }, + ], + }, + ], + stats: [], + } + + const options = { + true: 'GREEN' as Status, + return_if_empty: 'GREEN' as Status, + } + const expectedResult = [ + { + reasons: [{}], + context: undefined, + }, + ] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(false) + expect(result.status).toEqual('GREEN') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should return YELLOW when condition is found, but empty and return_if_empty is set to YELLOW', () => { + const condition = `all(ref, "'John' === $.name")` + const reference = '$.example[*].contributors[*]' + const data = { + example: [ + { + branch: 'EXAMPLE_BRANCH', + state: 'OPEN', + contributors: [], + }, + { + branch: 'OTHER_BRANCH', + state: 'CLOSED', + contributors: [ + { + name: {}, + age: 25, + }, + ], + }, + ], + stats: [], + } + + const options = { + true: 'GREEN' as Status, + return_if_empty: 'YELLOW' as Status, + } + const expectedResult = [ + { + reasons: [{}], + context: undefined, + }, + ] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(false) + expect(result.status).toEqual('YELLOW') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should return RED when condition is found, but empty and return_if_empty is set to RED', () => { + const condition = `all(ref, "'John' === $.name")` + const reference = '$.example[*].contributors[*]' + const data = { + example: [ + { + branch: 'EXAMPLE_BRANCH', + state: 'OPEN', + contributors: [], + }, + { + branch: 'OTHER_BRANCH', + state: 'CLOSED', + contributors: [ + { + name: {}, + age: 25, + }, + ], + }, + ], + stats: [], + } + + const options = { + true: 'GREEN' as Status, + return_if_empty: 'RED' as Status, + } + const expectedResult = [ + { + reasons: [{}], + context: undefined, + }, + ] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(false) + expect(result.status).toEqual('RED') + + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should return GREEN when condition is found, but once empty and once correct and return_if_empty is set to RED', () => { + const condition = `all(ref, "'John' === $.name")` + const reference = '$.example[*].contributors[*]' + const data = { + example: [ + { + branch: 'EXAMPLE_BRANCH', + state: 'OPEN', + contributors: [], + }, + { + branch: 'OTHER_BRANCH', + state: 'CLOSED', + contributors: [ + { + name: 'John', + age: 25, + }, + ], + }, + ], + stats: [], + } + + const options = { + true: 'GREEN' as Status, + return_if_empty: 'RED' as Status, + } + const expectedResult = [] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(true) + expect(result.status).toEqual('GREEN') + + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should return RED when ref is found, but empty and return_if_empty is not set', () => { + const condition = 'all(ref, "5 < $.total_commits")' + const reference = '$.stats' + const data = { + example: [ + { + branch: 'EXAMPLE_BRANCH', + state: 'OPEN', + contributors: [], + }, + { + branch: 'OTHER_BRANCH', + state: 'CLOSED', + contributors: [ + { + name: 'John', + age: 25, + }, + ], + }, + ], + stats: [], + } + + const options = { + true: 'GREEN' as Status, + } + const expectedResult = [] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(false) + expect(result.status).toEqual('RED') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should return RED when condition is found, but empty and return_if_empty is not set', () => { + const condition = `all(ref, "'John' === $.name")` + const reference = '$.example[*].contributors[*]' + const data = { + example: [ + { + branch: 'EXAMPLE_BRANCH', + state: 'OPEN', + contributors: [], + }, + { + branch: 'OTHER_BRANCH', + state: 'CLOSED', + contributors: [ + { + name: {}, + age: 25, + }, + ], + }, + ], + stats: [], + } + + const options = { + true: 'GREEN' as Status, + } + const expectedResult = [ + { + reasons: [{}], + context: undefined, + }, + ] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(false) + expect(result.status).toEqual('RED') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should pick return_if_not_found when both set and the ref is not found', () => { + const condition = `all(ref, "'EXAMPLE_BRANCH' === $.branch")` + const reference = '$.otherExample[*]' + const data = { + example: [ + { + branch_name: 'EXAMPLE_BRANCH', + state: 'OPEN', + }, + + { + branch_id: 1, + state: 'OPEN', + }, + ], + example1: [], + } + const options = { + true: 'GREEN' as Status, + return_if_not_found: 'YELLOW' as Status, + return_if_empty: 'GREEN' as Status, + } + const expectedResult = [] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(false) + expect(result.status).toEqual('YELLOW') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should pick return_if_not_found when both set and the condition is not found', () => { + const condition = `all(ref, "'EXAMPLE_BRANCH' === $.branch_name")` + const reference = '$.example[*]' + const data = { + example: [ + { + branch_name: 'EXAMPLE_BRANCH', + state: 'OPEN', + }, + + { + branch_id: 1, + state: 'OPEN', + }, + ], + example1: [], + } + const options = { + true: 'GREEN' as Status, + return_if_not_found: 'YELLOW' as Status, + return_if_empty: 'GREEN' as Status, + } + const expectedResult = [ + { + reasons: [], + context: undefined, + }, + ] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(false) + expect(result.status).toEqual('YELLOW') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should pick return_if_empty when both set and the ref exists, but it is empty', () => { + const condition = 'all(ref, "5 < $.total_commits")' + const reference = '$.stats' + const data = { + example: [ + { + branch: 'EXAMPLE_BRANCH', + state: 'OPEN', + contributors: [], + }, + { + branch: 'OTHER_BRANCH', + state: 'CLOSED', + contributors: [ + { + name: 'John', + age: 25, + }, + ], + }, + ], + stats: [], + } + + const options = { + true: 'GREEN' as Status, + return_if_not_found: 'GREEN' as Status, + return_if_empty: 'YELLOW' as Status, + } + const expectedResult = [] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(false) + expect(result.status).toEqual('YELLOW') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should pick return_if_empty when both set and the condition exists, but it is empty', () => { + const condition = `all(ref, "'John' === $.name")` + const reference = '$.example[*].contributors[*]' + const data = { + example: [ + { + branch: 'EXAMPLE_BRANCH', + state: 'OPEN', + contributors: [], + }, + { + branch: 'OTHER_BRANCH', + state: 'CLOSED', + contributors: [ + { + name: {}, + age: 25, + }, + ], + }, + ], + stats: [], + } + + const options = { + true: 'GREEN' as Status, + return_if_not_found: 'GREEN' as Status, + return_if_empty: 'YELLOW' as Status, + } + const expectedResult = [ + { + reasons: [{}], + context: undefined, + }, + ] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(false) + expect(result.status).toEqual('YELLOW') + expect(result.reasonPackages).toEqual(expectedResult) + }) + + it('should return GREEN when both variables do no influence the result', () => { + const condition = `all(ref, "12 == $.objects")` + const reference = '$.stats[*]' + const data = { + example: [ + { + branch: 'EXAMPLE_BRANCH', + state: 'OPEN', + contributors: [], + }, + { + branch: 'OTHER_BRANCH', + state: 'CLOSED', + contributors: [ + { + name: 'John', + age: 25, + }, + ], + }, + ], + stats: [{ objects: 12 }], + } + + const options = { + true: 'GREEN' as Status, + return_if_not_found: 'RED' as Status, + return_if_empty: 'YELLOW' as Status, + } + const expectedResult = [] + + const result = evalCheck(condition, reference, data, options) + + expect(result.ref).toEqual(reference) + expect(result.condition).toEqual(condition) + expect(result.bool).toEqual(true) + expect(result.status).toEqual('GREEN') + expect(result.reasonPackages).toEqual(expectedResult) + }) +}) + +describe('evalConcatenation', () => { + const checks = { + check1: { + ref: '', + condition: '', + status: 'GREEN' as Status, + bool: true, + reasonPackages: [ + { + reasons: [], + context: undefined, + }, + ], + }, + check2: { + ref: '', + condition: '', + status: 'YELLOW' as Status, + bool: false, + reasonPackages: [ + { + reasons: [], + context: undefined, + }, + ], + }, + check3: { + ref: '', + condition: '', + status: 'RED' as Status, + bool: false, + reasonPackages: [ + { + reasons: [], + context: undefined, + }, + ], + }, + } + + it('should evaluate an "AND" concatenation', () => { + const result = evalConcatenation('check1 && check2 && check3', checks) + expect(result).toEqual({ + condition: 'check1 && check2 && check3', + status: 'RED', + }) + }) + + it('should evaluate an "OR" concatenation', () => { + const result = evalConcatenation('check1 || check2 || check3', checks) + expect(result).toEqual({ + condition: 'check1 || check2 || check3', + status: 'GREEN', + }) + }) + + it('should throw an error if a referenced check does not exist', () => { + expect(() => evalConcatenation('check1 && check4', checks)).toThrow( + Error( + 'Error in concatenation condition. Please check the concatenation condition.' + ) + ) + }) +}) diff --git a/yaku-apps-typescript/packages/json-evaluator-lib/tests/read-json.spec.ts b/yaku-apps-typescript/packages/json-evaluator-lib/tests/read-json.spec.ts new file mode 100644 index 00000000..a484f3e7 --- /dev/null +++ b/yaku-apps-typescript/packages/json-evaluator-lib/tests/read-json.spec.ts @@ -0,0 +1,53 @@ +import { afterEach, describe, it, expect, vi } from 'vitest' + +import { readJson } from '../src/read-json.js' + +import { readFile } from 'fs/promises' + +vi.mock('fs/promises') + +class FileNotFoundError extends Error { + code = 'ENOENT' +} + +describe('readJson', () => { + afterEach(() => { + vi.resetAllMocks() + }) + it('should read and parse a JSON file', async () => { + vi.mocked(readFile).mockResolvedValueOnce( + '{ "name": "John Doe", "age": 30 }' + ) + + const data = await readJson('./example.json') + + expect(data).toEqual({ name: 'John Doe', age: 30 }) + }) + + it('should replace white spaces in keys with underscores', async () => { + vi.mocked(readFile).mockResolvedValueOnce( + '{ "first name": "John", "last name": "Doe", "age": 30 }' + ) + + const data = await readJson('./example_with_spaces.json') + + expect(data).toEqual({ first_name: 'John', last_name: 'Doe', age: 30 }) + }) + + it('should throw an error if the file could not be parsed', async () => { + vi.mocked(readFile).mockResolvedValueOnce('invalid json') + + await expect(readJson('./invalid.json')).rejects.toThrow( + 'File ./invalid.json could not be parsed, failed with error: SyntaxError: Unexpected token i in JSON at position 0' + ) + }) + + it('should throw an error if the file could not be found', async () => { + const error = new FileNotFoundError('File not found') + vi.mocked(readFile).mockRejectedValueOnce(error) + + await expect(readJson('./non_existing_file.json')).rejects.toThrow( + 'File ./non_existing_file.json does not exist' + ) + }) +}) diff --git a/yaku-apps-typescript/packages/json-evaluator-lib/tests/util.spec.ts b/yaku-apps-typescript/packages/json-evaluator-lib/tests/util.spec.ts new file mode 100644 index 00000000..8dcae87f --- /dev/null +++ b/yaku-apps-typescript/packages/json-evaluator-lib/tests/util.spec.ts @@ -0,0 +1,29 @@ +import { searchOnFail } from '../src/util' +import { describe, it, expect, vi } from 'vitest' + +describe('validateLogLevel', () => { + it('should return false when CONTINUE_SEARCH_ON_FAIL is "false"', () => { + vi.stubEnv('CONTINUE_SEARCH_ON_FAIL', 'false') + const result = searchOnFail() + expect(result).toBe(false) + }) + + it('should return true when CONTINUE_SEARCH_ON_FAIL is "true"', () => { + vi.stubEnv('CONTINUE_SEARCH_ON_FAIL', 'true') + const result = searchOnFail() + expect(result).toBe(true) + }) + + it('should return true when CONTINUE_SEARCH_ON_FAIL is "TRUE"', () => { + vi.stubEnv('CONTINUE_SEARCH_ON_FAIL', 'TRUE') + const result = searchOnFail() + expect(result).toBe(true) + }) + + it('should throw an error when CONTINUE_SEARCH_ON_FAIL is not "true" or "false"', () => { + vi.stubEnv('CONTINUE_SEARCH_ON_FAIL', 'INVALID') + expect(() => { + searchOnFail() + }).toThrowError('CONTINUE_SEARCH_ON_FAIL: INVALID, is not valid!') + }) +}) diff --git a/yaku-apps-typescript/packages/json-evaluator-lib/tsup.config.json b/yaku-apps-typescript/packages/json-evaluator-lib/tsup.config.json new file mode 100644 index 00000000..267a68e2 --- /dev/null +++ b/yaku-apps-typescript/packages/json-evaluator-lib/tsup.config.json @@ -0,0 +1,6 @@ +{ + "entry": ["index.ts"], + "splitting": false, + "sourcemap": true, + "clean": true +} diff --git a/yaku-apps-typescript/packages/json-evaluator-lib/tsup.config.ts b/yaku-apps-typescript/packages/json-evaluator-lib/tsup.config.ts new file mode 100644 index 00000000..0e6106f5 --- /dev/null +++ b/yaku-apps-typescript/packages/json-evaluator-lib/tsup.config.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/**/*.ts'], + splitting: false, + clean: true, + target: 'node18', + format: ['esm'], + bundle: false, + sourcemap: true, +}) diff --git a/yaku-apps-typescript/packages/json-evaluator-lib/vitest.config.ts b/yaku-apps-typescript/packages/json-evaluator-lib/vitest.config.ts new file mode 100644 index 00000000..f4e55f9a --- /dev/null +++ b/yaku-apps-typescript/packages/json-evaluator-lib/vitest.config.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + typecheck: { + tsconfig: 'tsconfig.json', + }, + coverage: { + provider: 'v8', + all: true, + enabled: true, + reporter: ['cobertura', 'json-summary', 'text-summary'], + include: ['src'], + exclude: ['src/index.ts', 'src/index.d.ts', 'src/types.ts'], + }, + reporters: ['junit', 'default'], + outputFile: 'reports/test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/packages/lint-staged-config/lint-staged-preset.js b/yaku-apps-typescript/packages/lint-staged-config/lint-staged-preset.js new file mode 100644 index 00000000..b5944c2f --- /dev/null +++ b/yaku-apps-typescript/packages/lint-staged-config/lint-staged-preset.js @@ -0,0 +1,8 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +module.exports = { + "*.{json,yml,md}": ["prettier --write"], + "*.ts": ["prettier --write"], +} \ No newline at end of file diff --git a/yaku-apps-typescript/packages/lint-staged-config/package.json b/yaku-apps-typescript/packages/lint-staged-config/package.json new file mode 100644 index 00000000..4d25d642 --- /dev/null +++ b/yaku-apps-typescript/packages/lint-staged-config/package.json @@ -0,0 +1,10 @@ +{ + "name": "@B-S-F/lint-staged-config", + "version": "0.1.0", + "scripts": { + "build": "echo 'No build needed'" + }, + "files": [ + "lint-staged-config.js" + ] +} diff --git a/yaku-apps-typescript/packages/log-utils/.eslintrc.cjs b/yaku-apps-typescript/packages/log-utils/.eslintrc.cjs new file mode 100644 index 00000000..cc850e07 --- /dev/null +++ b/yaku-apps-typescript/packages/log-utils/.eslintrc.cjs @@ -0,0 +1,6 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + + module.exports = require("@B-S-F/eslint-config/eslint-preset") + \ No newline at end of file diff --git a/yaku-apps-typescript/packages/log-utils/.prettierrc b/yaku-apps-typescript/packages/log-utils/.prettierrc new file mode 100644 index 00000000..b2095be8 --- /dev/null +++ b/yaku-apps-typescript/packages/log-utils/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": false, + "singleQuote": true +} diff --git a/yaku-apps-typescript/packages/log-utils/package.json b/yaku-apps-typescript/packages/log-utils/package.json new file mode 100644 index 00000000..c5a2f43c --- /dev/null +++ b/yaku-apps-typescript/packages/log-utils/package.json @@ -0,0 +1,42 @@ +{ + "name": "@B-S-F/log-utils", + "version": "0.1.0", + "description": "Log utils for TypeScript", + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@vitest/coverage-v8": "*", + "eslint": "*", + "nodemon": "*", + "prettier": "*", + "tsup": "*", + "vitest": "*" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "scripts": { + "build": "tsup && tsc --emitDeclarationOnly --declaration", + "dev": "nodemon --watch \"src/**\" --exec npm run start", + "lint": "eslint '**/*.ts'", + "format": "prettier --write '**/*.{ts,md}'", + "test": "vitest run && npm run test:update-cobertura-file", + "test:update-cobertura-file": "sed -i'.bak' 's,package name=\",package name=\"'${npm_package_name}/',g' coverage/cobertura-coverage.xml", + "test:dev": "vitest -w", + "test:coverage": "vitest --coverage", + "test:ui": "vitest --ui" + }, + "repository": { + "type": "git", + "url": "https://github.com/B-S-F/qg-apps-typescript.git" + }, + "keywords": [ + "yaml" + ], + "files": [ + "dist" + ], + "dependencies": { + "log-update": "^5.0.1" + } +} diff --git a/yaku-apps-typescript/packages/log-utils/src/index.ts b/yaku-apps-typescript/packages/log-utils/src/index.ts new file mode 100644 index 00000000..9c460047 --- /dev/null +++ b/yaku-apps-typescript/packages/log-utils/src/index.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import logUpdate from 'log-update' + +const animation = '|/-\\' + +/** + * Prints out `text` to the console and appends an animated spinner to the end. + * + * Uses the `log-update` package. Calling `stop()` on the returned object will only + * stop the animation but not close the current log-update session, so it can be reused + * and has to be closed with `logUpdate.done()` separately. + * + * @param {*} text to print, can be of any type that can be represented as string + * @param {*} interval as number in ms to call the setInterval method with + * @returns an object with a `stop()` function to stop the animation + */ +export function animateLog(text: any, interval = 150) { + let animationIndex = 0 + const intervalId = setInterval(() => { + logUpdate(`${text} ${animation[animationIndex]}`) + animationIndex = (animationIndex + 1) % animation.length + }, interval) + + return { + stop(overwrite?: boolean) { + clearInterval(intervalId) + logUpdate(text) + if (overwrite) logUpdate.clear() + }, + } +} diff --git a/yaku-apps-typescript/packages/log-utils/test/index.test.ts b/yaku-apps-typescript/packages/log-utils/test/index.test.ts new file mode 100644 index 00000000..f4db998b --- /dev/null +++ b/yaku-apps-typescript/packages/log-utils/test/index.test.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import logUpdate from 'log-update' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { animateLog } from '../src/index' + +vi.mock('log-update', () => { + return { + default: vi.fn(), + } +}) + +describe('animateLog', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + afterEach(() => { + vi.clearAllTimers() + vi.restoreAllMocks() + }) + it('logs with spinner at the end', () => { + animateLog('test') + vi.advanceTimersToNextTimer() + expect(logUpdate).toHaveBeenCalledWith('test |') + vi.advanceTimersToNextTimer() + expect(logUpdate).toHaveBeenCalledWith('test /') + vi.advanceTimersToNextTimer() + expect(logUpdate).toHaveBeenCalledWith('test -') + vi.advanceTimersToNextTimer() + expect(logUpdate).toHaveBeenCalledWith('test \\') + }) + + it('returns a function that stops the animation', () => { + const result = animateLog('test', 150) + expect(result.stop).toBeDefined() + result.stop() + vi.advanceTimersByTime(300) + expect(logUpdate).toHaveBeenCalledTimes(1) + }) +}) diff --git a/yaku-apps-typescript/packages/log-utils/tsconfig.json b/yaku-apps-typescript/packages/log-utils/tsconfig.json new file mode 100644 index 00000000..0df56473 --- /dev/null +++ b/yaku-apps-typescript/packages/log-utils/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@B-S-F/typescript-config/base.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/yaku-apps-typescript/packages/log-utils/tsup.config.ts b/yaku-apps-typescript/packages/log-utils/tsup.config.ts new file mode 100644 index 00000000..0e6106f5 --- /dev/null +++ b/yaku-apps-typescript/packages/log-utils/tsup.config.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/**/*.ts'], + splitting: false, + clean: true, + target: 'node18', + format: ['esm'], + bundle: false, + sourcemap: true, +}) diff --git a/yaku-apps-typescript/packages/log-utils/vitest.config.ts b/yaku-apps-typescript/packages/log-utils/vitest.config.ts new file mode 100644 index 00000000..1cf5e21f --- /dev/null +++ b/yaku-apps-typescript/packages/log-utils/vitest.config.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + typecheck: { + tsconfig: 'tsconfig.json', + }, + coverage: { + provider: 'v8', + all: true, + enabled: true, + reporter: ['cobertura', 'json-summary', 'text-summary'], + include: ['src'], + }, + reporters: ['junit', 'default'], + outputFile: 'reports/test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/packages/markdown-utils/.eslintrc.cjs b/yaku-apps-typescript/packages/markdown-utils/.eslintrc.cjs new file mode 100644 index 00000000..cc850e07 --- /dev/null +++ b/yaku-apps-typescript/packages/markdown-utils/.eslintrc.cjs @@ -0,0 +1,6 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + + module.exports = require("@B-S-F/eslint-config/eslint-preset") + \ No newline at end of file diff --git a/yaku-apps-typescript/packages/markdown-utils/.prettierrc b/yaku-apps-typescript/packages/markdown-utils/.prettierrc new file mode 100644 index 00000000..b2095be8 --- /dev/null +++ b/yaku-apps-typescript/packages/markdown-utils/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": false, + "singleQuote": true +} diff --git a/yaku-apps-typescript/packages/markdown-utils/package.json b/yaku-apps-typescript/packages/markdown-utils/package.json new file mode 100644 index 00000000..f231bc4e --- /dev/null +++ b/yaku-apps-typescript/packages/markdown-utils/package.json @@ -0,0 +1,44 @@ +{ + "name": "@B-S-F/markdown-utils", + "version": "0.2.0", + "description": "Markdown utils for TypeScript", + "devDependencies": { + "@B-S-F/eslint-config": "*", + "@B-S-F/typescript-config": "*", + "@types/markdown-it": "^12.2.3", + "eslint": "*", + "nodemon": "*", + "prettier": "*", + "tsup": "*", + "vitest": "*" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "scripts": { + "build": "tsup && tsc --emitDeclarationOnly --declaration", + "dev": "nodemon --watch \"src/**\" --exec npm run start", + "lint": "eslint '**/*.ts'", + "format": "prettier --write '**/*.{ts,md}'", + "test": "vitest run && npm run test:update-cobertura-file", + "test:update-cobertura-file": "sed -i'.bak' 's,package name=\",package name=\"'${npm_package_name}/',g' coverage/cobertura-coverage.xml", + "test:dev": "vitest -w", + "test:coverage": "vitest --coverage", + "test:ui": "vitest --ui" + }, + "repository": { + "type": "git", + "url": "https://github.com/B-S-F/qg-apps-typescript.git" + }, + "keywords": [ + "markdown" + ], + "files": [ + "dist" + ], + "dependencies": { + "markdown-it": "^14.1.0", + "smartquotes": "^2.3.2", + "title-case": "^3.0.3" + } +} diff --git a/yaku-apps-typescript/packages/markdown-utils/src/index.ts b/yaku-apps-typescript/packages/markdown-utils/src/index.ts new file mode 100644 index 00000000..e39be309 --- /dev/null +++ b/yaku-apps-typescript/packages/markdown-utils/src/index.ts @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import MarkdownIt from 'markdown-it' +import { createRequire } from 'module' +import { titleCase } from 'title-case' + +const require = createRequire(import.meta.url) +const smartquotes = require('smartquotes') + +const md = new MarkdownIt({ + breaks: true, + linkify: true, +}) + +function smartquotesNoCode(markdown: any) { + if (!markdown) return '' + // Don't convert to smart quotes in code blocks: + return String(markdown) + .split(/(```.*```|`[^`]*`|^(?:\t| {4})[^\n]*$)/ms) + .map((t) => + t.startsWith('`') || t.startsWith('\t') || t.startsWith(' ') + ? t + : smartquotes(t) + ) + .join('') +} + +const markdown = { + blockquote(text: string) { + if (!text) return '' + return text.replace(/^/gm, '> ') + }, + + buildOptionalLink(text: any, url: any) { + if (!url) return text + return `[${text}](${url})` + }, + + smartquotes: smartquotesNoCode, + titleCase, + + render(markdownSrc: any) { + return md.render(smartquotesNoCode(markdownSrc) || '') + }, + + renderInline(markdownSrc: any) { + return md.renderInline(smartquotesNoCode(markdownSrc) || '') + }, + + prettyTime(date?: Date | number) { + date ??= new Date() + if (typeof date === 'number') date = new Date(date) + return date.toLocaleString('sv', { timeZoneName: 'short' }) + }, +} + +export default markdown diff --git a/yaku-apps-typescript/packages/markdown-utils/test/markdown.test.ts b/yaku-apps-typescript/packages/markdown-utils/test/markdown.test.ts new file mode 100644 index 00000000..ef70c618 --- /dev/null +++ b/yaku-apps-typescript/packages/markdown-utils/test/markdown.test.ts @@ -0,0 +1,167 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { describe, expect, it } from 'vitest' +import markdown from '../src/index' + +const textblock = `abc +def` + +const textblock__quoted = `> abc +> def` + +describe('blockquote', function () { + it('replaces the first character of a string with a quote', function () { + expect(markdown.blockquote('test')).toEqual('> test') + }) + it('replaces the first character of each line with a quote', function () { + expect(markdown.blockquote(textblock)).toEqual(textblock__quoted) + }) + it('does not quote an empty string', function () { + expect(markdown.blockquote('')).toEqual('') + }) + it('increases quotation level', function () { + expect(markdown.blockquote('> test')).toEqual('> > test') + }) +}) + +describe('buildOptionalLink', function () { + it('returns the text if no url is provided', function () { + expect(markdown.buildOptionalLink('test', '')).toEqual('test') + }) + it('returns a valid markdown link if url is provided', function () { + expect(markdown.buildOptionalLink('test', 'https://test.com')).toEqual( + '[test](https://test.com)' + ) + }) + it('returns the text as is if no url is provided', function () { + expect(markdown.buildOptionalLink(3, null)).toEqual(3) + }) + it('converts a number to a string and returns valid markdown link if url is provided', function () { + expect(markdown.buildOptionalLink(3, 'https://test.com')).toEqual( + '[3](https://test.com)' + ) + }) +}) + +const textWithSourceBlock = `"Hello, it's 'me'!" + +"foo":\t"bar" + +\`\`\`json +{ + "foo": "bar", + "baz": "qux" +} +\`\`\` + +\tfoo: 'bar' +\tbaz: 'qux' +` + +const textWithSourceBlock__typography = `“Hello, it’s ‘me’!” + +“foo”:\t“bar” + +\`\`\`json +{ + "foo": "bar", + "baz": "qux" +} +\`\`\` + +\tfoo: 'bar' +\tbaz: 'qux' +` + +describe('smartquotes', function () { + it('converts double quotes to typography quotes', function () { + expect(markdown.smartquotes('"test"')).toEqual('“test”') + }) + it('converts single quotes to typography quotes', function () { + expect(markdown.smartquotes("'test'")).toEqual('‘test’') + }) + it('converts apostrophies quotes to typography apostrophies', function () { + expect(markdown.smartquotes('"Hello, it\'s me')).toEqual('“Hello, it’s me') + }) + it('ignores quotes in source code', function () { + expect(markdown.smartquotes('"Hello" `"world"`')).toEqual( + '“Hello” `"world"`' + ) + }) + it('ignores quotes in code blocks', function () { + expect(markdown.smartquotes(textWithSourceBlock)).toEqual( + textWithSourceBlock__typography + ) + }) + it('can handle null', function () { + expect(markdown.smartquotes(null)).toEqual('') + }) + it('converts a number', function () { + expect(markdown.smartquotes(123)).toEqual('123') + }) +}) + +describe('titleCase', function () { + it('converts a string to title case', function () { + expect(markdown.titleCase('test test')).toEqual('Test Test') + }) + it('converts umlauts to title case', function () { + expect(markdown.titleCase('über test')).toEqual('Über Test') + }) + it('does not convert articles and some small prepositions', function () { + expect(markdown.titleCase('test in a world')).toEqual('Test in a World') + }) + it('converts articles as the first letter', function () { + expect(markdown.titleCase('in a world')).toEqual('In a World') + }) +}) + +const markdownBlock = `# "Hello" + +World!\t"Tab" + + foo: 'bar' + baz: 'qux' + +More: + +\tfoo: 'bar' +\tbaz: 'qux' +\`'foo2bar'\` +` + +const markdownBlock__rendered = `

“Hello”

+

World!\t“Tab”

+
foo: 'bar'
+baz: 'qux'
+
+

More:

+
foo: 'bar'
+baz: 'qux'
+
+

'foo2bar'

+` + +describe('render', function () { + it('renders markdown format', function () { + expect(markdown.render('**test**')).toEqual( + '

test

\n' + ) + }) + it('renders markdown block and uses typographic quotes outside of code', function () { + expect(markdown.render(markdownBlock)).toEqual(markdownBlock__rendered) + }) +}) + +describe('renderInline', function () { + it('renders markdown format and uses typographic quotes', function () { + expect(markdown.renderInline('**"Test"**')).toEqual( + '“Test”' + ) + }) + it('converts a number to a string', function () { + expect(markdown.renderInline(3)).toEqual('3') + }) +}) diff --git a/yaku-apps-typescript/packages/markdown-utils/tsconfig.json b/yaku-apps-typescript/packages/markdown-utils/tsconfig.json new file mode 100644 index 00000000..0df56473 --- /dev/null +++ b/yaku-apps-typescript/packages/markdown-utils/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@B-S-F/typescript-config/base.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/yaku-apps-typescript/packages/markdown-utils/tsup.config.ts b/yaku-apps-typescript/packages/markdown-utils/tsup.config.ts new file mode 100644 index 00000000..0e6106f5 --- /dev/null +++ b/yaku-apps-typescript/packages/markdown-utils/tsup.config.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/**/*.ts'], + splitting: false, + clean: true, + target: 'node18', + format: ['esm'], + bundle: false, + sourcemap: true, +}) diff --git a/yaku-apps-typescript/packages/markdown-utils/vitest.config.ts b/yaku-apps-typescript/packages/markdown-utils/vitest.config.ts new file mode 100644 index 00000000..1cf5e21f --- /dev/null +++ b/yaku-apps-typescript/packages/markdown-utils/vitest.config.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022, 2023 by grow platform GmbH + */ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + typecheck: { + tsconfig: 'tsconfig.json', + }, + coverage: { + provider: 'v8', + all: true, + enabled: true, + reporter: ['cobertura', 'json-summary', 'text-summary'], + include: ['src'], + }, + reporters: ['junit', 'default'], + outputFile: 'reports/test-results.xml', + }, +}) diff --git a/yaku-apps-typescript/packages/sync-versions/package.json b/yaku-apps-typescript/packages/sync-versions/package.json new file mode 100644 index 00000000..2b717b66 --- /dev/null +++ b/yaku-apps-typescript/packages/sync-versions/package.json @@ -0,0 +1,20 @@ +{ + "name": "@B-S-F/sync-versions", + "version": "0.1.0", + "description": "", + "main": "src/index.js", + "scripts": { + "build": "echo 'No build needed'" + }, + "repository": { + "type": "git", + "url": "https://github.com/B-S-F/qg-apps-typescript.git" + }, + "keywords": [], + "bin": { + "sync-versions": "src/index.js" + }, + "dependencies": { + "replace-in-file": "^6.3.5" + } +} diff --git a/yaku-apps-typescript/packages/sync-versions/src/index.js b/yaku-apps-typescript/packages/sync-versions/src/index.js new file mode 100755 index 00000000..deee929e --- /dev/null +++ b/yaku-apps-typescript/packages/sync-versions/src/index.js @@ -0,0 +1,36 @@ +#! /usr/bin/env node + +/* + * Copyright (c) 2022, 2023 by grow platform GmbH + */ + +const replace = require('replace-in-file') + +async function run() { + if (process.argv.length < 4) { + console.error('Usage: sync-versions ') + return process.exit(1) + } + + const [, , scope, version] = process.argv + + const regex = new RegExp(`(?<="dependencies":\\s*\\{[^\}]*"${scope}\\/[^"]+": ")[^"]*(?=")`, 'gs') + + try { + const options = { + files: 'package.json', + from: regex, + to: `^${version}`, + } + + const results = await replace(options) + + if (results[0].hasChanged) { + console.log(process.cwd(), 'dependencies synced') + } + } catch (error) { + console.error('Error occurred:', error) + } +} + +run() diff --git a/yaku-apps-typescript/packages/typescript-config/base.json b/yaku-apps-typescript/packages/typescript-config/base.json new file mode 100644 index 00000000..c42f252a --- /dev/null +++ b/yaku-apps-typescript/packages/typescript-config/base.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Default", + "compilerOptions": { + "target": "es2021", + "module": "es2022", + "moduleResolution": "node", + "resolveJsonModule": true, + "sourceMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "types": ["vitest/importMeta"] + } +} diff --git a/yaku-apps-typescript/packages/typescript-config/package.json b/yaku-apps-typescript/packages/typescript-config/package.json new file mode 100644 index 00000000..5a7d222f --- /dev/null +++ b/yaku-apps-typescript/packages/typescript-config/package.json @@ -0,0 +1,11 @@ +{ + "name": "@B-S-F/typescript-config", + "version": "0.1.0", + "main": "index.js", + "scripts": { + "build": "echo 'No build needed'" + }, + "files": [ + "base.json" + ] +} diff --git a/yaku-apps-typescript/turbo.json b/yaku-apps-typescript/turbo.json new file mode 100644 index 00000000..3c039040 --- /dev/null +++ b/yaku-apps-typescript/turbo.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://turborepo.org/schema.json", + "pipeline": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**"] + }, + "lint": { + "outputs": [] + }, + "test": { + "outputs": ["coverage/**"] + }, + "test:integration:ci": { + "outputs": ["coverage/**"] + }, + "setup": { + "outputs": [], + "dependsOn": ["^setup"], + "cache": false + }, + "format": { + "outputs": [] + }, + "dev": { + "cache": false + } + } +}