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