From 15b2da6e5dd3c29664a8f886959e1e884c9e1861 Mon Sep 17 00:00:00 2001
From: gotbadger
Date: Wed, 18 Feb 2026 16:33:46 +0000
Subject: [PATCH 1/2] CM-59844: update deps
---
poetry.lock | 228 +++++++++++++++++++++++++++++++++++++++++++++----
pyproject.toml | 4 +-
2 files changed, 212 insertions(+), 20 deletions(-)
diff --git a/poetry.lock b/poetry.lock
index b20290ed..807fb2f8 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -107,6 +107,104 @@ files = [
{file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"},
]
+[[package]]
+name = "cffi"
+version = "2.0.0"
+description = "Foreign Function Interface for Python calling C code."
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+markers = "python_version >= \"3.10\" and platform_python_implementation != \"PyPy\""
+files = [
+ {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"},
+ {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"},
+ {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"},
+ {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"},
+ {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"},
+ {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"},
+ {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"},
+ {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"},
+ {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"},
+ {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"},
+ {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"},
+ {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"},
+ {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"},
+ {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"},
+ {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"},
+ {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"},
+ {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"},
+ {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"},
+ {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"},
+ {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"},
+ {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"},
+ {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"},
+ {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"},
+ {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"},
+ {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"},
+ {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"},
+ {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"},
+ {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"},
+ {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"},
+ {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"},
+ {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"},
+ {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"},
+ {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"},
+ {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"},
+ {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"},
+ {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"},
+ {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"},
+ {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"},
+ {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"},
+ {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"},
+ {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"},
+ {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"},
+ {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"},
+ {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"},
+ {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"},
+ {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"},
+ {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"},
+ {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"},
+ {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"},
+ {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"},
+ {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"},
+]
+
+[package.dependencies]
+pycparser = {version = "*", markers = "implementation_name != \"PyPy\""}
+
[[package]]
name = "chardet"
version = "5.2.0"
@@ -342,6 +440,80 @@ files = [
[package.extras]
toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
+[[package]]
+name = "cryptography"
+version = "46.0.5"
+description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
+optional = false
+python-versions = "!=3.9.0,!=3.9.1,>=3.8"
+groups = ["main"]
+markers = "python_version >= \"3.10\""
+files = [
+ {file = "cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad"},
+ {file = "cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b"},
+ {file = "cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b"},
+ {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263"},
+ {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d"},
+ {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed"},
+ {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2"},
+ {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2"},
+ {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0"},
+ {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731"},
+ {file = "cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82"},
+ {file = "cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1"},
+ {file = "cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48"},
+ {file = "cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4"},
+ {file = "cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2"},
+ {file = "cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678"},
+ {file = "cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87"},
+ {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee"},
+ {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981"},
+ {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9"},
+ {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648"},
+ {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4"},
+ {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0"},
+ {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663"},
+ {file = "cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826"},
+ {file = "cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d"},
+ {file = "cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a"},
+ {file = "cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4"},
+ {file = "cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31"},
+ {file = "cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18"},
+ {file = "cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235"},
+ {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a"},
+ {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76"},
+ {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614"},
+ {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229"},
+ {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1"},
+ {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d"},
+ {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c"},
+ {file = "cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4"},
+ {file = "cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9"},
+ {file = "cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72"},
+ {file = "cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595"},
+ {file = "cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c"},
+ {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a"},
+ {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356"},
+ {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da"},
+ {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257"},
+ {file = "cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7"},
+ {file = "cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d"},
+]
+
+[package.dependencies]
+cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""}
+typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""}
+
+[package.extras]
+docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"]
+docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
+nox = ["nox[uv] (>=2024.4.15)"]
+pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"]
+sdist = ["build (>=1.0.0)"]
+ssh = ["bcrypt (>=3.1.5)"]
+test = ["certifi (>=2024)", "cryptography-vectors (==46.0.5)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
+test-randomorder = ["pytest-randomly"]
+
[[package]]
name = "dunamai"
version = "1.21.2"
@@ -620,35 +792,35 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
[[package]]
name = "marshmallow"
-version = "3.22.0"
+version = "3.26.2"
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
groups = ["main"]
files = [
- {file = "marshmallow-3.22.0-py3-none-any.whl", hash = "sha256:71a2dce49ef901c3f97ed296ae5051135fd3febd2bf43afe0ae9a82143a494d9"},
- {file = "marshmallow-3.22.0.tar.gz", hash = "sha256:4972f529104a220bb8637d595aa4c9762afbe7f7a77d82dc58c1615d70c5823e"},
+ {file = "marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73"},
+ {file = "marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57"},
]
[package.dependencies]
packaging = ">=17.0"
[package.extras]
-dev = ["marshmallow[tests]", "pre-commit (>=3.5,<4.0)", "tox"]
-docs = ["alabaster (==1.0.0)", "autodocsumm (==0.2.13)", "sphinx (==8.0.2)", "sphinx-issues (==4.1.0)", "sphinx-version-warning (==1.1.2)"]
-tests = ["pytest", "pytz", "simplejson"]
+dev = ["marshmallow[tests]", "pre-commit (>=3.5,<5.0)", "tox"]
+docs = ["autodocsumm (==0.2.14)", "furo (==2024.8.6)", "sphinx (==8.1.3)", "sphinx-copybutton (==0.5.2)", "sphinx-issues (==5.0.0)", "sphinxext-opengraph (==0.9.1)"]
+tests = ["pytest", "simplejson"]
[[package]]
name = "mcp"
-version = "1.18.0"
+version = "1.26.0"
description = "Model Context Protocol SDK"
optional = false
python-versions = ">=3.10"
groups = ["main"]
markers = "python_version >= \"3.10\""
files = [
- {file = "mcp-1.18.0-py3-none-any.whl", hash = "sha256:42f10c270de18e7892fdf9da259029120b1ea23964ff688248c69db9d72b1d0a"},
- {file = "mcp-1.18.0.tar.gz", hash = "sha256:aa278c44b1efc0a297f53b68df865b988e52dd08182d702019edcf33a8e109f6"},
+ {file = "mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca"},
+ {file = "mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66"},
]
[package.dependencies]
@@ -658,10 +830,13 @@ httpx-sse = ">=0.4"
jsonschema = ">=4.20.0"
pydantic = ">=2.11.0,<3.0.0"
pydantic-settings = ">=2.5.2"
+pyjwt = {version = ">=2.10.1", extras = ["crypto"]}
python-multipart = ">=0.0.9"
pywin32 = {version = ">=310", markers = "sys_platform == \"win32\""}
sse-starlette = ">=1.6.1"
starlette = ">=0.27"
+typing-extensions = ">=4.9.0"
+typing-inspection = ">=0.4.1"
uvicorn = {version = ">=0.31.1", markers = "sys_platform != \"emscripten\""}
[package.extras]
@@ -767,6 +942,19 @@ files = [
dev = ["pre-commit", "tox"]
testing = ["coverage", "pytest", "pytest-benchmark"]
+[[package]]
+name = "pycparser"
+version = "3.0"
+description = "C parser in Python"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+markers = "python_version >= \"3.10\" and platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""
+files = [
+ {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"},
+ {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"},
+]
+
[[package]]
name = "pydantic"
version = "2.12.3"
@@ -1038,6 +1226,9 @@ files = [
{file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"},
]
+[package.dependencies]
+cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""}
+
[package.extras]
crypto = ["cryptography (>=3.4.0)"]
dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
@@ -1785,20 +1976,21 @@ typing-extensions = ">=4.12.0"
[[package]]
name = "urllib3"
-version = "1.26.19"
+version = "2.6.3"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
+python-versions = ">=3.9"
groups = ["main", "test"]
files = [
- {file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"},
- {file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"},
+ {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"},
+ {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"},
]
[package.extras]
-brotli = ["brotli (==1.0.9) ; os_name != \"nt\" and python_version < \"3\" and platform_python_implementation == \"CPython\"", "brotli (>=1.0.9) ; python_version >= \"3\" and platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation != \"CPython\"", "brotlipy (>=0.6.0) ; os_name == \"nt\" and python_version < \"3\""]
-secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress ; python_version == \"2.7\"", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
-socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""]
+h2 = ["h2 (>=4,<5)"]
+socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
+zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
[[package]]
name = "uvicorn"
@@ -1845,4 +2037,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.9"
-content-hash = "d705f54b6e814ba9b361cda482e5f23a7fbd0a41ae652f76ece6bfb78b00973f"
+content-hash = "593c613fcd6438e2133d90f3777c2050738bfa42bc7f5512e43c612b784a9870"
diff --git a/pyproject.toml b/pyproject.toml
index 06c69b28..cc6297c9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -36,12 +36,12 @@ version = "0.0.0" # DON'T TOUCH. Placeholder. Will be filled automatically on po
click = ">=8.1.0,<8.2.0"
colorama = ">=0.4.3,<0.5.0"
pyyaml = ">=6.0,<7.0"
-marshmallow = ">=3.15.0,<3.23.0" # 3.23 dropped support for Python 3.8
+marshmallow = ">=3.15.0,<4.0.0"
gitpython = ">=3.1.30,<3.2.0"
arrow = ">=1.0.0,<1.4.0"
binaryornot = ">=0.4.4,<0.5.0"
requests = ">=2.32.4,<3.0"
-urllib3 = "1.26.19" # lock v1 to avoid issues with openssl and old Python versions (<3.9.11) on macOS
+urllib3 = ">=2.4.0,<3.0.0"
pyjwt = ">=2.8.0,<3.0"
rich = ">=13.9.4, <14"
patch-ng = "1.18.1"
From efee709d69a657a2bdaf936fd1f1588b5660c89f Mon Sep 17 00:00:00 2001
From: gotbadger
Date: Thu, 19 Feb 2026 08:27:49 +0000
Subject: [PATCH 2/2] CM-59844: add some basic test coverage to validate
updates
---
tests/cli/apps/__init__.py | 0
tests/cli/apps/mcp/__init__.py | 0
tests/cli/apps/mcp/test_mcp_command.py | 315 ++++++++++++
tests/cyclient/test_client_base_exceptions.py | 162 +++++++
tests/test_models_deserialization.py | 451 ++++++++++++++++++
5 files changed, 928 insertions(+)
create mode 100644 tests/cli/apps/__init__.py
create mode 100644 tests/cli/apps/mcp/__init__.py
create mode 100644 tests/cli/apps/mcp/test_mcp_command.py
create mode 100644 tests/cyclient/test_client_base_exceptions.py
create mode 100644 tests/test_models_deserialization.py
diff --git a/tests/cli/apps/__init__.py b/tests/cli/apps/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/cli/apps/mcp/__init__.py b/tests/cli/apps/mcp/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/cli/apps/mcp/test_mcp_command.py b/tests/cli/apps/mcp/test_mcp_command.py
new file mode 100644
index 00000000..ebcc2373
--- /dev/null
+++ b/tests/cli/apps/mcp/test_mcp_command.py
@@ -0,0 +1,315 @@
+import json
+import os
+import sys
+from unittest.mock import AsyncMock, patch
+
+import pytest
+
+if sys.version_info < (3, 10):
+ pytest.skip('MCP requires Python 3.10+', allow_module_level=True)
+
+from cycode.cli.apps.mcp.mcp_command import (
+ _sanitize_file_path,
+ _TempFilesManager,
+)
+
+pytestmark = pytest.mark.anyio
+
+
+@pytest.fixture
+def anyio_backend() -> str:
+ return 'asyncio'
+
+
+# --- _sanitize_file_path input validation ---
+
+
+def test_sanitize_file_path_rejects_empty_string() -> None:
+ with pytest.raises(ValueError, match='non-empty string'):
+ _sanitize_file_path('')
+
+
+def test_sanitize_file_path_rejects_none() -> None:
+ with pytest.raises(ValueError, match='non-empty string'):
+ _sanitize_file_path(None)
+
+
+def test_sanitize_file_path_rejects_non_string() -> None:
+ with pytest.raises(ValueError, match='non-empty string'):
+ _sanitize_file_path(123)
+
+
+def test_sanitize_file_path_strips_null_bytes() -> None:
+ result = _sanitize_file_path('foo/bar\x00baz.py')
+ assert '\x00' not in result
+
+
+def test_sanitize_file_path_passes_valid_path_through() -> None:
+ result = _sanitize_file_path('src/main.py')
+ assert os.path.normpath(result) == os.path.normpath('src/main.py')
+
+
+# --- _TempFilesManager: path traversal prevention ---
+#
+# _sanitize_file_path delegates to pathvalidate which does NOT block
+# path traversal (../ passes through). The real security boundary is
+# the normpath containment check in _TempFilesManager.__enter__ (lines 136-139).
+# These tests verify that the two layers together prevent escaping the temp dir.
+
+
+def test_traversal_simple_dotdot_rejected() -> None:
+ """../../../etc/passwd must not escape the temp directory."""
+ files = {
+ '../../../etc/passwd': 'malicious',
+ 'safe.py': 'ok',
+ }
+ with _TempFilesManager(files, 'test-traversal') as temp_files:
+ assert len(temp_files) == 1
+ assert temp_files[0].endswith('safe.py')
+ for tf in temp_files:
+ assert '/etc/passwd' not in tf
+
+
+def test_traversal_backslash_dotdot_rejected() -> None:
+ """..\\..\\windows\\system32 must not escape the temp directory."""
+ files = {
+ '..\\..\\windows\\system32\\config': 'malicious',
+ 'safe.py': 'ok',
+ }
+ with _TempFilesManager(files, 'test-backslash') as temp_files:
+ assert len(temp_files) == 1
+ assert temp_files[0].endswith('safe.py')
+
+
+def test_traversal_embedded_dotdot_rejected() -> None:
+ """foo/../../../etc/passwd resolves outside temp dir and must be rejected."""
+ files = {
+ 'foo/../../../etc/passwd': 'malicious',
+ 'safe.py': 'ok',
+ }
+ with _TempFilesManager(files, 'test-embedded') as temp_files:
+ assert len(temp_files) == 1
+ assert temp_files[0].endswith('safe.py')
+
+
+def test_traversal_absolute_path_rejected() -> None:
+ """Absolute paths must not be written outside the temp directory."""
+ files = {
+ '/etc/passwd': 'malicious',
+ 'safe.py': 'ok',
+ }
+ with _TempFilesManager(files, 'test-absolute') as temp_files:
+ assert len(temp_files) == 1
+ assert temp_files[0].endswith('safe.py')
+
+
+def test_traversal_dotdot_only_rejected() -> None:
+ """A bare '..' path must be rejected."""
+ files = {
+ '..': 'malicious',
+ 'safe.py': 'ok',
+ }
+ with _TempFilesManager(files, 'test-bare-dotdot') as temp_files:
+ assert len(temp_files) == 1
+
+
+def test_traversal_all_malicious_raises() -> None:
+ """If every file path is a traversal attempt, no files are created and ValueError is raised."""
+ files = {
+ '../../../etc/passwd': 'malicious',
+ '../../shadow': 'also malicious',
+ }
+ with pytest.raises(ValueError, match='No valid files'), _TempFilesManager(files, 'test-all-malicious'):
+ pass
+
+
+def test_all_created_files_are_inside_temp_dir() -> None:
+ """Every created file must be under the temp base directory."""
+ files = {
+ 'a.py': 'aaa',
+ 'sub/b.py': 'bbb',
+ 'sub/deep/c.py': 'ccc',
+ }
+ manager = _TempFilesManager(files, 'test-containment')
+ with manager as temp_files:
+ base = os.path.normcase(os.path.normpath(manager.temp_base_dir))
+ for tf in temp_files:
+ normalized = os.path.normcase(os.path.normpath(tf))
+ assert normalized.startswith(base + os.sep), f'{tf} escaped temp dir {base}'
+
+
+def test_mixed_valid_and_traversal_only_creates_valid() -> None:
+ """Valid files are created, traversal attempts are silently skipped."""
+ files = {
+ '../escape.py': 'bad',
+ 'legit.py': 'good',
+ 'foo/../../escape2.py': 'bad',
+ 'src/app.py': 'good',
+ }
+ manager = _TempFilesManager(files, 'test-mixed')
+ with manager as temp_files:
+ base = os.path.normcase(os.path.normpath(manager.temp_base_dir))
+ assert len(temp_files) == 2
+ for tf in temp_files:
+ assert os.path.normcase(os.path.normpath(tf)).startswith(base + os.sep)
+ basenames = [os.path.basename(tf) for tf in temp_files]
+ assert 'legit.py' in basenames
+ assert 'app.py' in basenames
+
+
+# --- _TempFilesManager: general functionality ---
+
+
+def test_temp_files_manager_creates_files() -> None:
+ files = {
+ 'test1.py': 'print("hello")',
+ 'subdir/test2.js': 'console.log("world")',
+ }
+ with _TempFilesManager(files, 'test-call-id') as temp_files:
+ assert len(temp_files) == 2
+ for tf in temp_files:
+ assert os.path.exists(tf)
+
+
+def test_temp_files_manager_writes_correct_content() -> None:
+ files = {'hello.py': 'print("hello world")'}
+ with _TempFilesManager(files, 'test-content') as temp_files, open(temp_files[0]) as f:
+ assert f.read() == 'print("hello world")'
+
+
+def test_temp_files_manager_cleans_up_on_exit() -> None:
+ files = {'cleanup.py': 'code'}
+ manager = _TempFilesManager(files, 'test-cleanup')
+ with manager as temp_files:
+ temp_dir = manager.temp_base_dir
+ assert os.path.exists(temp_dir)
+ assert len(temp_files) == 1
+ assert not os.path.exists(temp_dir)
+
+
+def test_temp_files_manager_empty_path_raises() -> None:
+ files = {'': 'empty path'}
+ with pytest.raises(ValueError, match='No valid files'), _TempFilesManager(files, 'test-empty-path'):
+ pass
+
+
+def test_temp_files_manager_preserves_subdirectory_structure() -> None:
+ files = {
+ 'src/main.py': 'main',
+ 'src/utils/helper.py': 'helper',
+ }
+ with _TempFilesManager(files, 'test-dirs') as temp_files:
+ assert len(temp_files) == 2
+ paths = [os.path.basename(tf) for tf in temp_files]
+ assert 'main.py' in paths
+ assert 'helper.py' in paths
+
+
+# --- _run_cycode_command (async) ---
+
+
+@pytest.mark.anyio
+async def test_run_cycode_command_returns_dict() -> None:
+ from cycode.cli.apps.mcp.mcp_command import _run_cycode_command
+
+ mock_process = AsyncMock()
+ mock_process.communicate.return_value = (b'', b'error output')
+ mock_process.returncode = 1
+
+ with patch('asyncio.create_subprocess_exec', return_value=mock_process):
+ result = await _run_cycode_command('--invalid-flag-for-test')
+ assert isinstance(result, dict)
+ assert 'error' in result
+
+
+@pytest.mark.anyio
+async def test_run_cycode_command_parses_json_output() -> None:
+ from cycode.cli.apps.mcp.mcp_command import _run_cycode_command
+
+ mock_process = AsyncMock()
+ mock_process.communicate.return_value = (b'{"status": "ok"}', b'')
+ mock_process.returncode = 0
+
+ with patch('asyncio.create_subprocess_exec', return_value=mock_process):
+ result = await _run_cycode_command('version')
+ assert result == {'status': 'ok'}
+
+
+@pytest.mark.anyio
+async def test_run_cycode_command_handles_invalid_json() -> None:
+ from cycode.cli.apps.mcp.mcp_command import _run_cycode_command
+
+ mock_process = AsyncMock()
+ mock_process.communicate.return_value = (b'not json{', b'')
+ mock_process.returncode = 0
+
+ with patch('asyncio.create_subprocess_exec', return_value=mock_process):
+ result = await _run_cycode_command('version')
+ assert result['error'] == 'Failed to parse JSON output'
+
+
+@pytest.mark.anyio
+async def test_run_cycode_command_timeout() -> None:
+ import asyncio
+
+ from cycode.cli.apps.mcp.mcp_command import _run_cycode_command
+
+ async def slow_communicate() -> tuple[bytes, bytes]:
+ await asyncio.sleep(10)
+ return b'', b''
+
+ mock_process = AsyncMock()
+ mock_process.communicate = slow_communicate
+
+ with patch('asyncio.create_subprocess_exec', return_value=mock_process):
+ result = await _run_cycode_command('status', timeout=0.001)
+ assert isinstance(result, dict)
+ assert 'error' in result
+ assert 'timeout' in result['error'].lower()
+
+
+# --- _cycode_scan_tool ---
+
+
+@pytest.mark.anyio
+async def test_cycode_scan_tool_no_files() -> None:
+ from cycode.cli.apps.mcp.mcp_command import _cycode_scan_tool
+ from cycode.cli.cli_types import ScanTypeOption
+
+ result = await _cycode_scan_tool(ScanTypeOption.SECRET, {})
+ parsed = json.loads(result)
+ assert 'error' in parsed
+ assert 'No files provided' in parsed['error']
+
+
+@pytest.mark.anyio
+async def test_cycode_scan_tool_invalid_files() -> None:
+ from cycode.cli.apps.mcp.mcp_command import _cycode_scan_tool
+ from cycode.cli.cli_types import ScanTypeOption
+
+ result = await _cycode_scan_tool(ScanTypeOption.SECRET, {'': 'content'})
+ parsed = json.loads(result)
+ assert 'error' in parsed
+
+
+# --- _create_mcp_server ---
+
+
+def test_create_mcp_server() -> None:
+ from cycode.cli.apps.mcp.mcp_command import _create_mcp_server
+
+ server = _create_mcp_server('127.0.0.1', 8000)
+ assert server is not None
+ assert server.name == 'cycode'
+
+
+def test_create_mcp_server_registers_tools() -> None:
+ from cycode.cli.apps.mcp.mcp_command import _create_mcp_server
+
+ server = _create_mcp_server('127.0.0.1', 8000)
+ tool_names = [t.name for t in server._tool_manager._tools.values()]
+ assert 'cycode_status' in tool_names
+ assert 'cycode_secret_scan' in tool_names
+ assert 'cycode_sca_scan' in tool_names
+ assert 'cycode_iac_scan' in tool_names
+ assert 'cycode_sast_scan' in tool_names
diff --git a/tests/cyclient/test_client_base_exceptions.py b/tests/cyclient/test_client_base_exceptions.py
new file mode 100644
index 00000000..f99453d3
--- /dev/null
+++ b/tests/cyclient/test_client_base_exceptions.py
@@ -0,0 +1,162 @@
+from unittest.mock import MagicMock
+
+import pytest
+import responses
+from requests.exceptions import (
+ ConnectionError as RequestsConnectionError,
+)
+from requests.exceptions import (
+ HTTPError,
+ SSLError,
+ Timeout,
+)
+
+from cycode.cli.exceptions.custom_exceptions import (
+ HttpUnauthorizedError,
+ RequestConnectionError,
+ RequestHttpError,
+ RequestSslError,
+ RequestTimeoutError,
+)
+from cycode.cyclient import config
+from cycode.cyclient.cycode_client_base import CycodeClientBase
+
+
+def _make_client() -> CycodeClientBase:
+ return CycodeClientBase(config.cycode_api_url)
+
+
+# --- _handle_exception mapping ---
+
+
+def test_handle_exception_timeout() -> None:
+ client = _make_client()
+ with pytest.raises(RequestTimeoutError):
+ client._handle_exception(Timeout('timed out'))
+
+
+def test_handle_exception_ssl_error() -> None:
+ client = _make_client()
+ with pytest.raises(RequestSslError):
+ client._handle_exception(SSLError('cert verify failed'))
+
+
+def test_handle_exception_connection_error() -> None:
+ client = _make_client()
+ with pytest.raises(RequestConnectionError):
+ client._handle_exception(RequestsConnectionError('refused'))
+
+
+def test_handle_exception_http_error_401() -> None:
+ response = MagicMock()
+ response.status_code = 401
+ response.text = 'Unauthorized'
+ error = HTTPError(response=response)
+
+ client = _make_client()
+ with pytest.raises(HttpUnauthorizedError):
+ client._handle_exception(error)
+
+
+def test_handle_exception_http_error_500() -> None:
+ response = MagicMock()
+ response.status_code = 500
+ response.text = 'Internal Server Error'
+ error = HTTPError(response=response)
+
+ client = _make_client()
+ with pytest.raises(RequestHttpError) as exc_info:
+ client._handle_exception(error)
+ assert exc_info.value.status_code == 500
+
+
+def test_handle_exception_unknown_error_reraises() -> None:
+ client = _make_client()
+ with pytest.raises(RuntimeError, match='something unexpected'):
+ client._handle_exception(RuntimeError('something unexpected'))
+
+
+# --- HTTP integration via responses mock ---
+
+
+@responses.activate
+def test_get_returns_response_on_success() -> None:
+ client = _make_client()
+ url = f'{client.api_url}/test-endpoint'
+ responses.add(responses.GET, url, json={'ok': True}, status=200)
+
+ response = client.get('test-endpoint')
+ assert response.status_code == 200
+ assert response.json() == {'ok': True}
+
+
+@responses.activate
+def test_post_returns_response_on_success() -> None:
+ client = _make_client()
+ url = f'{client.api_url}/test-endpoint'
+ responses.add(responses.POST, url, json={'created': True}, status=201)
+
+ response = client.post('test-endpoint', body={'data': 'value'})
+ assert response.status_code == 201
+
+
+@responses.activate
+def test_get_raises_timeout_error() -> None:
+ client = _make_client()
+ url = f'{client.api_url}/slow-endpoint'
+ responses.add(responses.GET, url, body=Timeout('Connection timed out'))
+
+ with pytest.raises(RequestTimeoutError):
+ client.get('slow-endpoint')
+
+
+@responses.activate
+def test_get_raises_ssl_error() -> None:
+ client = _make_client()
+ url = f'{client.api_url}/ssl-endpoint'
+ responses.add(responses.GET, url, body=SSLError('certificate verify failed'))
+
+ with pytest.raises(RequestSslError):
+ client.get('ssl-endpoint')
+
+
+@responses.activate
+def test_get_raises_connection_error() -> None:
+ client = _make_client()
+ url = f'{client.api_url}/down-endpoint'
+ responses.add(responses.GET, url, body=RequestsConnectionError('Connection refused'))
+
+ with pytest.raises(RequestConnectionError):
+ client.get('down-endpoint')
+
+
+@responses.activate
+def test_get_raises_http_unauthorized_error() -> None:
+ client = _make_client()
+ url = f'{client.api_url}/auth-endpoint'
+ responses.add(responses.GET, url, json={'error': 'unauthorized'}, status=401)
+
+ with pytest.raises(HttpUnauthorizedError):
+ client.get('auth-endpoint')
+
+
+@responses.activate
+def test_get_raises_http_error_on_500() -> None:
+ client = _make_client()
+ url = f'{client.api_url}/error-endpoint'
+ responses.add(responses.GET, url, json={'error': 'server error'}, status=500)
+
+ with pytest.raises(RequestHttpError) as exc_info:
+ client.get('error-endpoint')
+ assert exc_info.value.status_code == 500
+
+
+@responses.activate
+def test_get_raises_http_error_on_403() -> None:
+ client = _make_client()
+ url = f'{client.api_url}/forbidden-endpoint'
+ responses.add(responses.GET, url, json={'error': 'forbidden'}, status=403)
+
+ with pytest.raises(RequestHttpError) as exc_info:
+ client.get('forbidden-endpoint')
+ assert exc_info.value.status_code == 403
diff --git a/tests/test_models_deserialization.py b/tests/test_models_deserialization.py
new file mode 100644
index 00000000..4c7dcd72
--- /dev/null
+++ b/tests/test_models_deserialization.py
@@ -0,0 +1,451 @@
+from cycode.cyclient.models import (
+ ApiToken,
+ ApiTokenGenerationPollingResponse,
+ ApiTokenGenerationPollingResponseSchema,
+ ApiTokenSchema,
+ AuthenticationSession,
+ AuthenticationSessionSchema,
+ ClassificationData,
+ ClassificationDataSchema,
+ Detection,
+ DetectionRule,
+ DetectionRuleSchema,
+ DetectionSchema,
+ Member,
+ MemberDetails,
+ MemberSchema,
+ ReportExecution,
+ ReportExecutionSchema,
+ RequestedMemberDetailsResultSchema,
+ RequestedSbomReportResultSchema,
+ SbomReport,
+ SbomReportStorageDetails,
+ SbomReportStorageDetailsSchema,
+ ScanConfiguration,
+ ScanConfigurationSchema,
+ ScanInitializationResponse,
+ ScanInitializationResponseSchema,
+ ScanResult,
+ ScanResultSchema,
+ ScanResultsSyncFlow,
+ ScanResultsSyncFlowSchema,
+ SupportedModulesPreferences,
+ SupportedModulesPreferencesSchema,
+ UserAgentOption,
+ UserAgentOptionScheme,
+)
+
+# --- DetectionSchema ---
+
+
+def test_detection_schema_load() -> None:
+ raw = {
+ 'id': 'det-123',
+ 'message': 'API key exposed',
+ 'type': 'secret',
+ 'severity': 'critical',
+ 'detection_type_id': 'secret-1',
+ 'detection_details': {'alert': True, 'value': 'sk_live_xxx'},
+ 'detection_rule_id': 'rule-456',
+ }
+ result = DetectionSchema().load(raw)
+ assert isinstance(result, Detection)
+ assert result.id == 'det-123'
+ assert result.message == 'API key exposed'
+ assert result.type == 'secret'
+ assert result.severity == 'critical'
+ assert result.detection_type_id == 'secret-1'
+ assert result.detection_details == {'alert': True, 'value': 'sk_live_xxx'}
+ assert result.detection_rule_id == 'rule-456'
+
+
+def test_detection_schema_load_defaults() -> None:
+ raw = {
+ 'message': 'Vulnerability found',
+ 'type': 'sca',
+ 'detection_type_id': 'vuln-1',
+ 'detection_details': {},
+ 'detection_rule_id': 'rule-789',
+ }
+ result = DetectionSchema().load(raw)
+ assert result.id is None
+ assert result.severity is None
+
+
+def test_detection_schema_excludes_unknown_fields() -> None:
+ raw = {
+ 'message': 'Test',
+ 'type': 'test',
+ 'detection_type_id': 'test-1',
+ 'detection_details': {},
+ 'detection_rule_id': 'test-rule',
+ 'unknown_field': 'should_be_ignored',
+ 'another_unknown': 123,
+ }
+ result = DetectionSchema().load(raw)
+ assert isinstance(result, Detection)
+ assert not hasattr(result, 'unknown_field')
+
+
+def test_detection_has_alert_true() -> None:
+ detection = Detection(
+ detection_type_id='secret-1',
+ type='secret',
+ message='Key found',
+ detection_details={'alert': {'severity': 'high'}},
+ detection_rule_id='rule-1',
+ )
+ assert detection.has_alert is True
+
+
+def test_detection_has_alert_false() -> None:
+ detection = Detection(
+ detection_type_id='license-1',
+ type='sca',
+ message='License issue',
+ detection_details={'license': 'GPL'},
+ detection_rule_id='rule-2',
+ )
+ assert detection.has_alert is False
+
+
+def test_detection_repr() -> None:
+ detection = Detection(
+ detection_type_id='secret-1',
+ type='secret',
+ message='API key exposed',
+ detection_details={'value': 'sk_live_xxx'},
+ detection_rule_id='rule-1',
+ severity='critical',
+ )
+ repr_str = repr(detection)
+ assert 'secret' in repr_str
+ assert 'critical' in repr_str
+ assert 'API key exposed' in repr_str
+ assert 'rule-1' in repr_str
+
+
+# --- ScanResultSchema ---
+
+
+def test_scan_result_schema_load_with_detections() -> None:
+ raw = {
+ 'did_detect': True,
+ 'scan_id': 'scan-abc',
+ 'detections': [
+ {
+ 'id': 'det-1',
+ 'message': 'Secret found',
+ 'type': 'secret',
+ 'detection_type_id': 'secret-1',
+ 'detection_details': {'alert': {}},
+ 'detection_rule_id': 'rule-1',
+ }
+ ],
+ 'err': '',
+ }
+ result = ScanResultSchema().load(raw)
+ assert isinstance(result, ScanResult)
+ assert result.did_detect is True
+ assert result.scan_id == 'scan-abc'
+ assert len(result.detections) == 1
+ assert isinstance(result.detections[0], Detection)
+ assert result.detections[0].id == 'det-1'
+
+
+def test_scan_result_schema_load_no_detections() -> None:
+ raw = {
+ 'did_detect': False,
+ 'scan_id': 'scan-def',
+ 'detections': None,
+ 'err': 'No files to scan',
+ }
+ result = ScanResultSchema().load(raw)
+ assert result.did_detect is False
+ assert result.detections is None
+ assert result.err == 'No files to scan'
+
+
+def test_scan_result_schema_excludes_unknown_fields() -> None:
+ raw = {
+ 'did_detect': False,
+ 'scan_id': 'scan-1',
+ 'detections': None,
+ 'err': '',
+ 'extra_field': 'ignored',
+ }
+ result = ScanResultSchema().load(raw)
+ assert isinstance(result, ScanResult)
+
+
+# --- ScanInitializationResponseSchema ---
+
+
+def test_scan_initialization_response_schema_load() -> None:
+ raw = {'scan_id': 'scan-init-123', 'err': ''}
+ result = ScanInitializationResponseSchema().load(raw)
+ assert isinstance(result, ScanInitializationResponse)
+ assert result.scan_id == 'scan-init-123'
+
+
+# --- AuthenticationSessionSchema ---
+
+
+def test_authentication_session_schema_load() -> None:
+ raw = {'session_id': 'sess-123'}
+ result = AuthenticationSessionSchema().load(raw)
+ assert isinstance(result, AuthenticationSession)
+ assert result.session_id == 'sess-123'
+
+
+# --- ApiTokenSchema (tests data_key mapping) ---
+
+
+def test_api_token_schema_load_data_key() -> None:
+ raw = {
+ 'clientId': 'client-123',
+ 'secret': 'secret-456',
+ 'description': 'My API Token',
+ }
+ result = ApiTokenSchema().load(raw)
+ assert isinstance(result, ApiToken)
+ assert result.client_id == 'client-123'
+ assert result.secret == 'secret-456'
+ assert result.description == 'My API Token'
+
+
+# --- ApiTokenGenerationPollingResponseSchema (nested) ---
+
+
+def test_api_token_generation_polling_schema_load() -> None:
+ raw = {
+ 'status': 'completed',
+ 'api_token': {
+ 'clientId': 'client-abc',
+ 'secret': 'secret-xyz',
+ 'description': 'Generated token',
+ },
+ }
+ result = ApiTokenGenerationPollingResponseSchema().load(raw)
+ assert isinstance(result, ApiTokenGenerationPollingResponse)
+ assert result.status == 'completed'
+ assert isinstance(result.api_token, ApiToken)
+ assert result.api_token.client_id == 'client-abc'
+
+
+def test_api_token_generation_polling_schema_load_null_token() -> None:
+ raw = {
+ 'status': 'pending',
+ 'api_token': None,
+ }
+ result = ApiTokenGenerationPollingResponseSchema().load(raw)
+ assert result.status == 'pending'
+ assert result.api_token is None
+
+
+# --- SbomReportStorageDetailsSchema / ReportExecutionSchema / RequestedSbomReportResultSchema ---
+
+
+def test_sbom_report_storage_details_schema_load() -> None:
+ raw = {'path': '/reports/sbom.json', 'folder': '/reports', 'size': 4096}
+ result = SbomReportStorageDetailsSchema().load(raw)
+ assert isinstance(result, SbomReportStorageDetails)
+ assert result.path == '/reports/sbom.json'
+ assert result.size == 4096
+
+
+def test_report_execution_schema_load() -> None:
+ raw = {
+ 'id': 1,
+ 'status': 'completed',
+ 'error_message': None,
+ 'status_message': 'Success',
+ 'storage_details': {'path': '/reports/sbom.json', 'folder': '/reports', 'size': 4096},
+ }
+ result = ReportExecutionSchema().load(raw)
+ assert isinstance(result, ReportExecution)
+ assert result.id == 1
+ assert result.status == 'completed'
+ assert isinstance(result.storage_details, SbomReportStorageDetails)
+
+
+def test_requested_sbom_report_result_schema_load() -> None:
+ raw = {
+ 'report_executions': [
+ {
+ 'id': 1,
+ 'status': 'completed',
+ 'error_message': None,
+ 'status_message': 'Done',
+ 'storage_details': {'path': '/r/sbom.json', 'folder': '/r', 'size': 1024},
+ },
+ {
+ 'id': 2,
+ 'status': 'failed',
+ 'error_message': 'Timeout',
+ 'status_message': None,
+ 'storage_details': None,
+ },
+ ]
+ }
+ result = RequestedSbomReportResultSchema().load(raw)
+ assert isinstance(result, SbomReport)
+ assert len(result.report_executions) == 2
+ assert result.report_executions[0].storage_details.path == '/r/sbom.json'
+ assert result.report_executions[1].error_message == 'Timeout'
+ assert result.report_executions[1].storage_details is None
+
+
+# --- UserAgentOptionScheme ---
+
+
+def test_user_agent_option_schema_load() -> None:
+ raw = {
+ 'app_name': 'vscode_extension',
+ 'app_version': '0.2.3',
+ 'env_name': 'Visual Studio Code',
+ 'env_version': '1.78.2',
+ }
+ result = UserAgentOptionScheme().load(raw)
+ assert isinstance(result, UserAgentOption)
+ assert result.app_name == 'vscode_extension'
+ assert 'vscode_extension' in result.user_agent_suffix
+ assert 'AppVersion: 0.2.3' in result.user_agent_suffix
+
+
+# --- MemberSchema / RequestedMemberDetailsResultSchema ---
+
+
+def test_member_schema_load() -> None:
+ raw = {'external_id': 'user-ext-123'}
+ result = MemberSchema().load(raw)
+ assert isinstance(result, Member)
+ assert result.external_id == 'user-ext-123'
+
+
+def test_requested_member_details_schema_load() -> None:
+ raw = {
+ 'items': [{'external_id': 'u1'}, {'external_id': 'u2'}],
+ 'page_size': 50,
+ 'next_page_token': 'token-abc',
+ }
+ result = RequestedMemberDetailsResultSchema().load(raw)
+ assert isinstance(result, MemberDetails)
+ assert len(result.items) == 2
+ assert result.page_size == 50
+ assert result.next_page_token == 'token-abc'
+
+
+def test_requested_member_details_schema_load_null_token() -> None:
+ raw = {
+ 'items': [],
+ 'page_size': 50,
+ 'next_page_token': None,
+ }
+ result = RequestedMemberDetailsResultSchema().load(raw)
+ assert result.next_page_token is None
+
+
+# --- ClassificationDataSchema / DetectionRuleSchema ---
+
+
+def test_classification_data_schema_load() -> None:
+ raw = {'severity': 'high'}
+ result = ClassificationDataSchema().load(raw)
+ assert isinstance(result, ClassificationData)
+ assert result.severity == 'high'
+
+
+def test_detection_rule_schema_load() -> None:
+ raw = {
+ 'classification_data': [{'severity': 'high'}, {'severity': 'medium'}],
+ 'detection_rule_id': 'rule-123',
+ 'custom_remediation_guidelines': 'Rotate the key',
+ 'remediation_guidelines': 'See docs',
+ 'description': 'Exposed API key',
+ 'policy_name': 'secrets-policy',
+ 'display_name': 'API Key Exposure',
+ }
+ result = DetectionRuleSchema().load(raw)
+ assert isinstance(result, DetectionRule)
+ assert len(result.classification_data) == 2
+ assert result.classification_data[0].severity == 'high'
+ assert result.detection_rule_id == 'rule-123'
+ assert result.custom_remediation_guidelines == 'Rotate the key'
+
+
+def test_detection_rule_schema_load_optional_nulls() -> None:
+ raw = {
+ 'classification_data': [{'severity': 'low'}],
+ 'detection_rule_id': 'rule-456',
+ 'custom_remediation_guidelines': None,
+ 'remediation_guidelines': None,
+ 'description': None,
+ 'policy_name': None,
+ 'display_name': None,
+ }
+ result = DetectionRuleSchema().load(raw)
+ assert result.custom_remediation_guidelines is None
+ assert result.display_name is None
+
+
+# --- ScanResultsSyncFlowSchema ---
+
+
+def test_scan_results_sync_flow_schema_load() -> None:
+ raw = {
+ 'id': 'sync-123',
+ 'detection_messages': [{'msg': 'found secret'}, {'msg': 'found vuln'}],
+ }
+ result = ScanResultsSyncFlowSchema().load(raw)
+ assert isinstance(result, ScanResultsSyncFlow)
+ assert result.id == 'sync-123'
+ assert len(result.detection_messages) == 2
+
+
+# --- SupportedModulesPreferencesSchema ---
+
+
+def test_supported_modules_preferences_schema_load() -> None:
+ raw = {
+ 'secret_scanning': True,
+ 'leak_scanning': True,
+ 'iac_scanning': False,
+ 'sca_scanning': True,
+ 'ci_cd_scanning': False,
+ 'sast_scanning': True,
+ 'container_scanning': False,
+ 'access_review': True,
+ 'asoc': False,
+ 'cimon': True,
+ 'ai_machine_learning': True,
+ 'ai_large_language_model': False,
+ }
+ result = SupportedModulesPreferencesSchema().load(raw)
+ assert isinstance(result, SupportedModulesPreferences)
+ assert result.secret_scanning is True
+ assert result.iac_scanning is False
+ assert result.ai_large_language_model is False
+
+
+# --- ScanConfigurationSchema ---
+
+
+def test_scan_configuration_schema_load() -> None:
+ raw = {
+ 'scannable_extensions': ['.py', '.js', '.ts'],
+ 'is_cycode_ignore_allowed': True,
+ }
+ result = ScanConfigurationSchema().load(raw)
+ assert isinstance(result, ScanConfiguration)
+ assert result.scannable_extensions == ['.py', '.js', '.ts']
+ assert result.is_cycode_ignore_allowed is True
+
+
+def test_scan_configuration_schema_load_defaults() -> None:
+ raw = {
+ 'scannable_extensions': None,
+ }
+ result = ScanConfigurationSchema().load(raw)
+ assert result.scannable_extensions is None
+ assert result.is_cycode_ignore_allowed is True # load_default=True