From e7a9115fa77b450dcd8b0ca8bca6bf29aeebd5d1 Mon Sep 17 00:00:00 2001 From: Andrey Kataev Date: Sun, 19 Oct 2025 15:57:30 +0300 Subject: [PATCH 01/14] =?UTF-8?q?=D0=9D=D0=B0=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=B9=D0=BA=D0=B8=20=D0=BF=D0=BE=D0=B4=20=D1=81=D0=BE=D0=BD?= =?UTF-8?q?=D0=B0=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/lint-and-test.yml | 2 +- pdm.lock | 148 +++++++++++++++++++++++++++- pyproject.toml | 1 + tools/common.just | 2 +- tools/load_envs.sh | 2 +- 5 files changed, 151 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index d697eaa..e549c35 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -13,7 +13,7 @@ permissions: jobs: lint-and-test: - uses: esclient/tools/.github/workflows/lint-and-test-python.yml@v1.0.0 + uses: esclient/tools/.github/workflows/lint-and-test-python.yml@455a2700e3d2a08c371b072b1327689d76582758 with: python-version: '3.13.7' secrets: diff --git a/pdm.lock b/pdm.lock index 301df39..e7dedf0 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:d5bab202f6faef35a9f10ac6d61606aec20f4d5b719540d89425540741c7130c" +content_hash = "sha256:625b764548bfaa516dea0f4bec30ab352ff6de1464602178d0b60a17800138df" [[metadata.targets]] requires_python = "~=3.13" @@ -113,6 +113,137 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coverage" +version = "7.11.0" +requires_python = ">=3.10" +summary = "Code coverage measurement for Python" +groups = ["dev"] +files = [ + {file = "coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1"}, + {file = "coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be"}, + {file = "coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d"}, + {file = "coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82"}, + {file = "coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52"}, + {file = "coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b"}, + {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4"}, + {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd"}, + {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc"}, + {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48"}, + {file = "coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040"}, + {file = "coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05"}, + {file = "coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a"}, + {file = "coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b"}, + {file = "coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37"}, + {file = "coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de"}, + {file = "coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f"}, + {file = "coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c"}, + {file = "coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa"}, + {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740"}, + {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef"}, + {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0"}, + {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca"}, + {file = "coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2"}, + {file = "coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268"}, + {file = "coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836"}, + {file = "coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497"}, + {file = "coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e"}, + {file = "coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1"}, + {file = "coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca"}, + {file = "coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd"}, + {file = "coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43"}, + {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777"}, + {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2"}, + {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d"}, + {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4"}, + {file = "coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721"}, + {file = "coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad"}, + {file = "coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479"}, + {file = "coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f"}, + {file = "coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e"}, + {file = "coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44"}, + {file = "coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3"}, + {file = "coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b"}, + {file = "coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d"}, + {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2"}, + {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e"}, + {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996"}, + {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11"}, + {file = "coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73"}, + {file = "coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547"}, + {file = "coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3"}, + {file = "coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68"}, + {file = "coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050"}, +] + +[[package]] +name = "coverage" +version = "7.11.0" +extras = ["toml"] +requires_python = ">=3.10" +summary = "Code coverage measurement for Python" +groups = ["dev"] +dependencies = [ + "coverage==7.11.0", + "tomli; python_full_version <= \"3.11.0a6\"", +] +files = [ + {file = "coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1"}, + {file = "coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be"}, + {file = "coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d"}, + {file = "coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82"}, + {file = "coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52"}, + {file = "coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b"}, + {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4"}, + {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd"}, + {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc"}, + {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48"}, + {file = "coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040"}, + {file = "coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05"}, + {file = "coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a"}, + {file = "coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b"}, + {file = "coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37"}, + {file = "coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de"}, + {file = "coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f"}, + {file = "coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c"}, + {file = "coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa"}, + {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740"}, + {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef"}, + {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0"}, + {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca"}, + {file = "coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2"}, + {file = "coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268"}, + {file = "coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836"}, + {file = "coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497"}, + {file = "coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e"}, + {file = "coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1"}, + {file = "coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca"}, + {file = "coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd"}, + {file = "coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43"}, + {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777"}, + {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2"}, + {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d"}, + {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4"}, + {file = "coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721"}, + {file = "coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad"}, + {file = "coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479"}, + {file = "coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f"}, + {file = "coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e"}, + {file = "coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44"}, + {file = "coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3"}, + {file = "coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b"}, + {file = "coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d"}, + {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2"}, + {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e"}, + {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996"}, + {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11"}, + {file = "coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73"}, + {file = "coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547"}, + {file = "coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3"}, + {file = "coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68"}, + {file = "coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050"}, +] + [[package]] name = "flake8" version = "7.3.0" @@ -581,6 +712,21 @@ files = [ {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, ] +[[package]] +name = "pytest-cov" +version = "5.0.0" +requires_python = ">=3.8" +summary = "Pytest plugin for measuring coverage." +groups = ["dev"] +dependencies = [ + "coverage[toml]>=5.2.1", + "pytest>=4.6", +] +files = [ + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, +] + [[package]] name = "pytest-mock" version = "3.15.1" diff --git a/pyproject.toml b/pyproject.toml index 519ef09..d4bf8b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,6 +122,7 @@ dev = [ "grpc-stubs==1.53.0.6", "protobuf==6.32.1", "pytest==8.4.2", + "pytest-cov==5.0.0", "pytest-mock==3.15.1", "black==25.9.0", "isort==6.0.1", diff --git a/tools/common.just b/tools/common.just index 883b36c..a399cda 100644 --- a/tools/common.just +++ b/tools/common.just @@ -39,6 +39,6 @@ lint: mypy --strict . test: - pytest + pytest --cov=src --cov-report=xml:coverage.xml prepare: format lint test diff --git a/tools/load_envs.sh b/tools/load_envs.sh index e713b7b..cbc4a4b 100644 --- a/tools/load_envs.sh +++ b/tools/load_envs.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -if [ -f .env ]; then +if [ "$ENV" = "prod" ] && [ -f .env ]; then export $(grep -v '^#' .env | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | sed 's/"//g' | sed "s/'//g" | xargs) fi From 0ff03e39bfe263257ef543de03b8c0e39f8dde3d Mon Sep 17 00:00:00 2001 From: Andrey Kataev Date: Sun, 19 Oct 2025 16:30:28 +0300 Subject: [PATCH 02/14] =?UTF-8?q?=D0=A2=D0=B5=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/lint-and-test.yml | 2 +- pdm.lock | 42 +++++++++++++- pyproject.toml | 6 ++ tests/handler/test_create_comment.py | 32 ++++++++--- tests/handler/test_delete_comment.py | 49 ++++++++++++++++ tests/handler/test_edit_comment.py | 50 +++++++++++++++++ tests/handler/test_get_comments.py | 71 ++++++++++++++++++++++++ tests/repository/test_create_comment.py | 53 ++++++++++++++++++ tests/repository/test_delete_comment.py | 71 ++++++++++++++++++++++++ tests/repository/test_edit_comment.py | 74 +++++++++++++++++++++++++ tests/repository/test_get_comments.py | 66 ++++++++++++++++++++++ tests/service/test_create_comment.py | 34 ++++++++++++ tests/service/test_delete_comment.py | 29 ++++++++++ tests/service/test_edit_comment.py | 32 +++++++++++ tests/service/test_get_comments.py | 46 +++++++++++++++ 15 files changed, 647 insertions(+), 10 deletions(-) create mode 100644 tests/handler/test_delete_comment.py create mode 100644 tests/handler/test_edit_comment.py create mode 100644 tests/handler/test_get_comments.py create mode 100644 tests/repository/test_create_comment.py create mode 100644 tests/repository/test_delete_comment.py create mode 100644 tests/repository/test_edit_comment.py create mode 100644 tests/repository/test_get_comments.py create mode 100644 tests/service/test_create_comment.py create mode 100644 tests/service/test_delete_comment.py create mode 100644 tests/service/test_edit_comment.py create mode 100644 tests/service/test_get_comments.py diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index e549c35..01ed80a 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -15,6 +15,6 @@ jobs: lint-and-test: uses: esclient/tools/.github/workflows/lint-and-test-python.yml@455a2700e3d2a08c371b072b1327689d76582758 with: - python-version: '3.13.7' + python-version: "3.13.7" secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/pdm.lock b/pdm.lock index e7dedf0..4c6ddce 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:625b764548bfaa516dea0f4bec30ab352ff6de1464602178d0b60a17800138df" +content_hash = "sha256:0090072db697f590cd3382f502e00e6128089ba9eb266e78cc4f32f314f1c97b" [[metadata.targets]] requires_python = "~=3.13" @@ -244,6 +244,21 @@ files = [ {file = "coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050"}, ] +[[package]] +name = "faker" +version = "30.8.2" +requires_python = ">=3.8" +summary = "Faker is a Python package that generates fake data for you." +groups = ["dev"] +dependencies = [ + "python-dateutil>=2.4", + "typing-extensions", +] +files = [ + {file = "Faker-30.8.2-py3-none-any.whl", hash = "sha256:4a82b2908cd19f3bba1a4da2060cc4eb18a40410ccdf9350d071d79dc92fe3ce"}, + {file = "faker-30.8.2.tar.gz", hash = "sha256:aa31b52cdae3673d6a78b4857c7bcdc0e98f201a5cb77d7827fa9e6b5876da94"}, +] + [[package]] name = "flake8" version = "7.3.0" @@ -741,6 +756,20 @@ files = [ {file = "pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f"}, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Extensions to the standard Python datetime module" +groups = ["dev"] +dependencies = [ + "six>=1.5", +] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + [[package]] name = "python-dotenv" version = "1.1.1" @@ -802,6 +831,17 @@ files = [ {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, ] +[[package]] +name = "six" +version = "1.17.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Python 2 and 3 compatibility utilities" +groups = ["dev"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + [[package]] name = "sniffio" version = "1.3.1" diff --git a/pyproject.toml b/pyproject.toml index d4bf8b7..f43e310 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,6 +93,11 @@ plugins = ["pydantic.mypy"] module = "asyncpg.*" ignore_missing_imports = true +[tool.pytest.ini_options] +addopts = "--import-mode=importlib" +testpaths = ["tests"] +python_files = ["test_*.py"] + [project] name = "commentservice" version = "0.0.1" @@ -124,6 +129,7 @@ dev = [ "pytest==8.4.2", "pytest-cov==5.0.0", "pytest-mock==3.15.1", + "Faker==30.8.2", "black==25.9.0", "isort==6.0.1", "flake8==7.3.0", diff --git a/tests/handler/test_create_comment.py b/tests/handler/test_create_comment.py index db069ee..945fd63 100644 --- a/tests/handler/test_create_comment.py +++ b/tests/handler/test_create_comment.py @@ -1,4 +1,9 @@ +import asyncio +from unittest.mock import AsyncMock + import grpc +import pytest +from faker import Faker from pytest_mock import MockerFixture from commentservice.grpc.comment_pb2 import ( @@ -9,20 +14,31 @@ from commentservice.service.service import CommentService -def test_create_comment_success(mocker: MockerFixture) -> None: +@pytest.fixture() +def fake() -> Faker: + f = Faker() + f.seed_instance(20251019) + return f + + +def test_create_comment_success(mocker: MockerFixture, fake: Faker) -> None: ctx = mocker.Mock(spec=grpc.ServicerContext) fake_service = mocker.Mock(spec=CommentService) - fake_service.create_comment.return_value = 42 + new_id = fake.random_int(min=1, max=100000) + fake_service.create_comment = AsyncMock(return_value=new_id) + mod_id = fake.random_int(min=1, max=100000) + author_id = fake.random_int(min=1, max=100000) + text = fake.sentence() request = CreateCommentRequest( - mod_id=7, - author_id=13, - text="Test text", + mod_id=mod_id, author_id=author_id, text=text ) - response = CreateComment(fake_service, request, ctx) + response = asyncio.run(CreateComment(fake_service, request, ctx)) assert isinstance(response, CreateCommentResponse) - assert response.comment_id == 42 + assert response.comment_id == new_id - fake_service.create_comment.assert_called_once_with(7, 13, "Test text") + fake_service.create_comment.assert_awaited_once_with( + mod_id, author_id, text + ) diff --git a/tests/handler/test_delete_comment.py b/tests/handler/test_delete_comment.py new file mode 100644 index 0000000..1345338 --- /dev/null +++ b/tests/handler/test_delete_comment.py @@ -0,0 +1,49 @@ +import asyncio +from unittest.mock import AsyncMock + +import grpc +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +from commentservice.grpc.comment_pb2 import ( + DeleteCommentRequest, + DeleteCommentResponse, +) +from commentservice.handler.delete_comment import DeleteComment +from commentservice.service.service import CommentService + + +@pytest.fixture() +def fake() -> Faker: + f = Faker() + f.seed_instance(20251019) + return f + + +def test_delete_comment_success(mocker: MockerFixture, fake: Faker) -> None: + ctx = mocker.Mock(spec=grpc.ServicerContext) + fake_service = mocker.Mock(spec=CommentService) + fake_service.delete_comment = AsyncMock(return_value=True) + + comment_id = fake.random_int(min=1, max=100000) + request = DeleteCommentRequest(comment_id=comment_id) + + response = asyncio.run(DeleteComment(fake_service, request, ctx)) + + assert isinstance(response, DeleteCommentResponse) + assert response.success is True + fake_service.delete_comment.assert_awaited_once_with(comment_id) + + +def test_delete_comment_not_found(mocker: MockerFixture, fake: Faker) -> None: + ctx = mocker.Mock(spec=grpc.ServicerContext) + fake_service = mocker.Mock(spec=CommentService) + fake_service.delete_comment = AsyncMock(return_value=False) + + comment_id = fake.random_int(min=1, max=100000) + request = DeleteCommentRequest(comment_id=comment_id) + response = asyncio.run(DeleteComment(fake_service, request, ctx)) + + assert response.success is False + fake_service.delete_comment.assert_awaited_once_with(comment_id) diff --git a/tests/handler/test_edit_comment.py b/tests/handler/test_edit_comment.py new file mode 100644 index 0000000..060d667 --- /dev/null +++ b/tests/handler/test_edit_comment.py @@ -0,0 +1,50 @@ +import asyncio +from unittest.mock import AsyncMock + +import grpc +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +from commentservice.grpc.comment_pb2 import ( + EditCommentRequest, + EditCommentResponse, +) +from commentservice.handler.edit_comment import EditComment +from commentservice.service.service import CommentService + + +@pytest.fixture() +def fake() -> Faker: + f = Faker() + f.seed_instance(20251019) + return f + + +def test_edit_comment_success(mocker: MockerFixture, fake: Faker) -> None: + ctx = mocker.Mock(spec=grpc.ServicerContext) + fake_service = mocker.Mock(spec=CommentService) + fake_service.edit_comment = AsyncMock(return_value=True) + + comment_id = fake.random_int(min=1, max=100000) + new_text = fake.sentence() + request = EditCommentRequest(comment_id=comment_id, text=new_text) + response = asyncio.run(EditComment(fake_service, request, ctx)) + + assert isinstance(response, EditCommentResponse) + assert response.success is True + fake_service.edit_comment.assert_awaited_once_with(comment_id, new_text) + + +def test_edit_comment_not_found(mocker: MockerFixture, fake: Faker) -> None: + ctx = mocker.Mock(spec=grpc.ServicerContext) + fake_service = mocker.Mock(spec=CommentService) + fake_service.edit_comment = AsyncMock(return_value=False) + + comment_id = fake.random_int(min=1, max=100000) + new_text = fake.sentence() + request = EditCommentRequest(comment_id=comment_id, text=new_text) + response = asyncio.run(EditComment(fake_service, request, ctx)) + + assert response.success is False + fake_service.edit_comment.assert_awaited_once_with(comment_id, new_text) diff --git a/tests/handler/test_get_comments.py b/tests/handler/test_get_comments.py new file mode 100644 index 0000000..60857d2 --- /dev/null +++ b/tests/handler/test_get_comments.py @@ -0,0 +1,71 @@ +import asyncio +from datetime import UTC, timedelta + +import grpc +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +from commentservice.grpc.comment_pb2 import ( + GetCommentsRequest, + GetCommentsResponse, +) +from commentservice.handler.get_comments import GetComments, ts_to_dt +from commentservice.repository.model import Comment +from commentservice.service.service import CommentService + + +@pytest.fixture() +def fake() -> Faker: + f = Faker() + f.seed_instance(20251019) + return f + + +def test_get_comments_success(mocker: MockerFixture, fake: Faker) -> None: + ctx = mocker.Mock(spec=grpc.ServicerContext) + fake_service = mocker.Mock(spec=CommentService) + + now = fake.date_time(tzinfo=UTC) + earlier = now - timedelta(hours=fake.random_int(min=1, max=12)) + + comment1 = Comment( + id=fake.random_int(min=1, max=100000), + author_id=fake.random_int(min=1, max=100000), + text=fake.sentence(), + created_at=earlier, + edited_at=None, + ) + comment2 = Comment( + id=fake.random_int(min=1, max=100000), + author_id=fake.random_int(min=1, max=100000), + text=fake.sentence(), + created_at=now, + edited_at=now, + ) + comments = [comment1, comment2] + fake_service.get_comments.return_value = comments + + mod_id = fake.random_int(min=1, max=100000) + request = GetCommentsRequest(mod_id=mod_id) + response = asyncio.run(GetComments(fake_service, request, ctx)) + + assert isinstance(response, GetCommentsResponse) + assert response.mod_id == mod_id + assert len(response.comments) == 2 + + c1 = response.comments[0] + assert c1.id == comment1.id + assert c1.author_id == comment1.author_id + assert c1.text == comment1.text + assert not c1.HasField("edited_at") + assert ts_to_dt(c1.created_at, tz=UTC) == earlier + + c2 = response.comments[1] + assert c2.id == comment2.id + assert c2.author_id == comment2.author_id + assert c2.text == comment2.text + assert c2.HasField("edited_at") + assert ts_to_dt(c2.created_at, tz=UTC) == now + + fake_service.get_comments.assert_called_once_with(mod_id=mod_id) diff --git a/tests/repository/test_create_comment.py b/tests/repository/test_create_comment.py new file mode 100644 index 0000000..ddb6aaf --- /dev/null +++ b/tests/repository/test_create_comment.py @@ -0,0 +1,53 @@ +import asyncio +import textwrap +from unittest.mock import AsyncMock + +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +from commentservice.repository.repository import CommentRepository + + +@pytest.fixture() +def fake() -> Faker: + f = Faker() + f.seed_instance(20251019) + return f + + +def test_repo_create_comment_returns_id( + mocker: MockerFixture, fake: Faker +) -> None: + conn = mocker.Mock() + conn.fetchval = AsyncMock(return_value=42) + + acquire_cm = mocker.MagicMock() + acquire_cm.__aenter__ = AsyncMock(return_value=conn) + acquire_cm.__aexit__ = AsyncMock(return_value=None) + + pool = mocker.Mock() + pool.acquire.return_value = acquire_cm + + repo = CommentRepository(pool) # type: ignore[arg-type] + mod_id = fake.random_int(min=1, max=100000) + author_id = fake.random_int(min=1, max=100000) + text = fake.sentence() + new_id = asyncio.run( + repo.create_comment(mod_id=mod_id, author_id=author_id, text=text) + ) + + assert new_id == 42 + assert conn.fetchval.await_count == 1 + # check SQL and args + expected_sql = """ + INSERT INTO comments (mod_id, author_id, text) + VALUES ($1, $2, $3) + RETURNING id + """ + actual_sql = conn.fetchval.await_args.args[0] + assert ( + textwrap.dedent(actual_sql).strip() + == textwrap.dedent(expected_sql).strip() + ) + assert conn.fetchval.await_args.args[1:] == (mod_id, author_id, text) diff --git a/tests/repository/test_delete_comment.py b/tests/repository/test_delete_comment.py new file mode 100644 index 0000000..7a6c637 --- /dev/null +++ b/tests/repository/test_delete_comment.py @@ -0,0 +1,71 @@ +import asyncio +import textwrap +from unittest.mock import AsyncMock + +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +from commentservice.repository.repository import CommentRepository + + +@pytest.fixture() +def fake() -> Faker: + f = Faker() + f.seed_instance(20251019) + return f + + +def _make_pool(mocker: MockerFixture, conn: object): + acquire_cm = mocker.MagicMock() + acquire_cm.__aenter__ = AsyncMock(return_value=conn) + acquire_cm.__aexit__ = AsyncMock(return_value=None) + pool = mocker.Mock() + pool.acquire.return_value = acquire_cm + return pool + + +def test_repo_delete_comment_success(mocker: MockerFixture, fake) -> None: + conn = mocker.Mock() + conn.fetchval = AsyncMock(return_value=123) + pool = _make_pool(mocker, conn) + repo = CommentRepository(pool) # type: ignore[arg-type] + + cid = fake.random_int(min=1, max=100000) + conn.fetchval.return_value = cid + + ok = asyncio.run(repo.delete_comment(comment_id=cid)) + assert ok is True + expected_sql = """ + DELETE FROM comments + WHERE id = $1 + RETURNING id + """ + actual_sql = conn.fetchval.await_args.args[0] + assert ( + textwrap.dedent(actual_sql).strip() + == textwrap.dedent(expected_sql).strip() + ) + assert conn.fetchval.await_args.args[1:] == (cid,) + + +def test_repo_delete_comment_not_found(mocker: MockerFixture, fake) -> None: + conn = mocker.Mock() + conn.fetchval = AsyncMock(return_value=None) + pool = _make_pool(mocker, conn) + repo = CommentRepository(pool) # type: ignore[arg-type] + + cid = fake.random_int(min=1, max=100000) + ok = asyncio.run(repo.delete_comment(comment_id=cid)) + assert ok is False + expected_sql = """ + DELETE FROM comments + WHERE id = $1 + RETURNING id + """ + actual_sql = conn.fetchval.await_args.args[0] + assert ( + textwrap.dedent(actual_sql).strip() + == textwrap.dedent(expected_sql).strip() + ) + assert conn.fetchval.await_args.args[1:] == (cid,) diff --git a/tests/repository/test_edit_comment.py b/tests/repository/test_edit_comment.py new file mode 100644 index 0000000..d99ec12 --- /dev/null +++ b/tests/repository/test_edit_comment.py @@ -0,0 +1,74 @@ +import asyncio +import textwrap +from unittest.mock import AsyncMock + +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +from commentservice.repository.repository import CommentRepository + + +@pytest.fixture() +def fake() -> Faker: + f = Faker() + f.seed_instance(20251019) + return f + + +def _make_pool(mocker: MockerFixture, conn: object): + acquire_cm = mocker.MagicMock() + acquire_cm.__aenter__ = AsyncMock(return_value=conn) + acquire_cm.__aexit__ = AsyncMock(return_value=None) + pool = mocker.Mock() + pool.acquire.return_value = acquire_cm + return pool + + +def test_repo_edit_comment_success(mocker: MockerFixture, fake) -> None: + conn = mocker.Mock() + conn.fetchval = AsyncMock(return_value=5) + pool = _make_pool(mocker, conn) + repo = CommentRepository(pool) # type: ignore[arg-type] + + cid = fake.random_int(min=1, max=100000) + new_text = fake.sentence() + conn.fetchval.return_value = cid + ok = asyncio.run(repo.edit_comment(comment_id=cid, new_text=new_text)) + assert ok is True + expected_sql = """ + UPDATE comments + SET text = $1, edited_at = NOW() + WHERE id = $2 + RETURNING id + """ + actual_sql = conn.fetchval.await_args.args[0] + assert ( + textwrap.dedent(actual_sql).strip() + == textwrap.dedent(expected_sql).strip() + ) + assert conn.fetchval.await_args.args[1:] == (new_text, cid) + + +def test_repo_edit_comment_not_found(mocker: MockerFixture, fake) -> None: + conn = mocker.Mock() + conn.fetchval = AsyncMock(return_value=None) + pool = _make_pool(mocker, conn) + repo = CommentRepository(pool) # type: ignore[arg-type] + + cid = fake.random_int(min=1, max=100000) + new_text = fake.sentence() + ok = asyncio.run(repo.edit_comment(comment_id=cid, new_text=new_text)) + assert ok is False + expected_sql = """ + UPDATE comments + SET text = $1, edited_at = NOW() + WHERE id = $2 + RETURNING id + """ + actual_sql = conn.fetchval.await_args.args[0] + assert ( + textwrap.dedent(actual_sql).strip() + == textwrap.dedent(expected_sql).strip() + ) + assert conn.fetchval.await_args.args[1:] == (new_text, cid) diff --git a/tests/repository/test_get_comments.py b/tests/repository/test_get_comments.py new file mode 100644 index 0000000..7da37d9 --- /dev/null +++ b/tests/repository/test_get_comments.py @@ -0,0 +1,66 @@ +import asyncio +import textwrap +from datetime import UTC +from unittest.mock import AsyncMock + +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +from commentservice.repository.repository import CommentRepository + + +@pytest.fixture() +def fake() -> Faker: + f = Faker() + f.seed_instance(20251019) + return f + + +def test_repo_get_comments_maps_rows( + mocker: MockerFixture, fake: Faker +) -> None: + rows = [ + { + "id": fake.random_int(), + "author_id": fake.random_int(), + "text": fake.sentence(), + "created_at": fake.date_time(tzinfo=UTC), + "edited_at": None, + }, + { + "id": fake.random_int(), + "author_id": fake.random_int(), + "text": fake.sentence(), + "created_at": fake.date_time(tzinfo=UTC), + "edited_at": fake.date_time(tzinfo=UTC), + }, + ] + + conn = mocker.Mock() + conn.fetch = AsyncMock(return_value=rows) + acquire_cm = mocker.MagicMock() + acquire_cm.__aenter__ = AsyncMock(return_value=conn) + acquire_cm.__aexit__ = AsyncMock(return_value=None) + pool = mocker.Mock() + pool.acquire.return_value = acquire_cm + repo = CommentRepository(pool) # type: ignore[arg-type] + + out = asyncio.run(repo.get_comments(mod_id=77)) + + assert len(out) == 2 + assert out[0].id == rows[0]["id"] and out[0].text == rows[0]["text"] + assert out[0].edited_at is None + assert out[1].id == rows[1]["id"] and out[1].text == rows[1]["text"] + assert out[1].edited_at is not None + expected_sql = """ + SELECT id, author_id, text, created_at, edited_at + FROM comments + WHERE mod_id = $1 + """ + actual_sql = conn.fetch.await_args.args[0] + assert ( + textwrap.dedent(actual_sql).strip() + == textwrap.dedent(expected_sql).strip() + ) + assert conn.fetch.await_args.args[1:] == (77,) diff --git a/tests/service/test_create_comment.py b/tests/service/test_create_comment.py new file mode 100644 index 0000000..393e9bb --- /dev/null +++ b/tests/service/test_create_comment.py @@ -0,0 +1,34 @@ +import asyncio +from unittest.mock import AsyncMock + +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +from commentservice.repository.repository import CommentRepository +from commentservice.service.service import CommentService + + +@pytest.fixture() +def fake() -> Faker: + f = Faker() + f.seed_instance(20251019) + return f + + +def test_service_create_comment(mocker: MockerFixture, fake: Faker) -> None: + fake_repo = mocker.Mock(spec=CommentRepository) + new_id = fake.random_int(min=1, max=100000) + fake_repo.create_comment = AsyncMock(return_value=new_id) + + mod_id = fake.random_int(min=1, max=100000) + author_id = fake.random_int(min=1, max=100000) + text = fake.sentence() + + service = CommentService(fake_repo) + result = asyncio.run( + service.create_comment(mod_id=mod_id, author_id=author_id, text=text) + ) + + assert result == new_id + fake_repo.create_comment.assert_awaited_once_with(mod_id, author_id, text) diff --git a/tests/service/test_delete_comment.py b/tests/service/test_delete_comment.py new file mode 100644 index 0000000..d241388 --- /dev/null +++ b/tests/service/test_delete_comment.py @@ -0,0 +1,29 @@ +import asyncio +from unittest.mock import AsyncMock + +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +from commentservice.repository.repository import CommentRepository +from commentservice.service.service import CommentService + + +@pytest.fixture() +def fake() -> Faker: + f = Faker() + f.seed_instance(20251019) + return f + + +def test_service_delete_comment(mocker: MockerFixture, fake: Faker) -> None: + fake_repo = mocker.Mock(spec=CommentRepository) + fake_repo.delete_comment = AsyncMock(return_value=True) + + comment_id = fake.random_int(min=1, max=100000) + + service = CommentService(fake_repo) + result = asyncio.run(service.delete_comment(comment_id=comment_id)) + + assert result is True + fake_repo.delete_comment.assert_awaited_once_with(comment_id) diff --git a/tests/service/test_edit_comment.py b/tests/service/test_edit_comment.py new file mode 100644 index 0000000..bf40b8b --- /dev/null +++ b/tests/service/test_edit_comment.py @@ -0,0 +1,32 @@ +import asyncio +from unittest.mock import AsyncMock + +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +from commentservice.repository.repository import CommentRepository +from commentservice.service.service import CommentService + + +@pytest.fixture() +def fake() -> Faker: + f = Faker() + f.seed_instance(20251019) + return f + + +def test_service_edit_comment(mocker: MockerFixture, fake: Faker) -> None: + fake_repo = mocker.Mock(spec=CommentRepository) + fake_repo.edit_comment = AsyncMock(return_value=True) + + comment_id = fake.random_int(min=1, max=100000) + new_text = fake.sentence() + + service = CommentService(fake_repo) + result = asyncio.run( + service.edit_comment(comment_id=comment_id, new_text=new_text) + ) + + assert result is True + fake_repo.edit_comment.assert_awaited_once_with(comment_id, new_text) diff --git a/tests/service/test_get_comments.py b/tests/service/test_get_comments.py new file mode 100644 index 0000000..649a124 --- /dev/null +++ b/tests/service/test_get_comments.py @@ -0,0 +1,46 @@ +import asyncio +from datetime import UTC +from unittest.mock import AsyncMock + +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +from commentservice.repository.model import Comment +from commentservice.repository.repository import CommentRepository +from commentservice.service.service import CommentService + + +@pytest.fixture() +def fake() -> Faker: + f = Faker() + f.seed_instance(20251019) + return f + + +def test_service_get_comments(mocker: MockerFixture, fake: Faker) -> None: + fake_repo = mocker.Mock(spec=CommentRepository) + comments = [ + Comment( + id=fake.random_int(), + author_id=fake.random_int(), + text=fake.sentence(), + created_at=fake.date_time(tzinfo=UTC), + edited_at=None, + ), + Comment( + id=fake.random_int(), + author_id=fake.random_int(), + text=fake.sentence(), + created_at=fake.date_time(tzinfo=UTC), + edited_at=None, + ), + ] + fake_repo.get_comments = AsyncMock(return_value=comments) + + mod_id = fake.random_int(min=1, max=100000) + service = CommentService(fake_repo) + out = asyncio.run(service.get_comments(mod_id=mod_id)) + + assert out == comments + fake_repo.get_comments.assert_awaited_once_with(mod_id) From af8ae714b24a3ab4ec6886444e91280137194d1a Mon Sep 17 00:00:00 2001 From: Andrey Kataev Date: Sun, 19 Oct 2025 17:54:07 +0300 Subject: [PATCH 03/14] =?UTF-8?q?=D0=A2=D0=B5=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pdm.lock | 192 +++++++++--------------- pyproject.toml | 5 +- tests/handler/test_create_comment.py | 23 ++- tests/handler/test_delete_comment.py | 26 ++-- tests/handler/test_edit_comment.py | 30 ++-- tests/handler/test_get_comments.py | 33 ++-- tests/repository/test_create_comment.py | 42 +++--- tests/repository/test_delete_comment.py | 53 +++---- tests/repository/test_edit_comment.py | 57 ++++--- tests/repository/test_get_comments.py | 47 +++--- tests/service/test_create_comment.py | 25 ++- tests/service/test_delete_comment.py | 17 +-- tests/service/test_edit_comment.py | 21 +-- tests/service/test_get_comments.py | 33 ++-- tools/common.just | 6 +- 15 files changed, 258 insertions(+), 352 deletions(-) diff --git a/pdm.lock b/pdm.lock index 4c6ddce..1abebf0 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:0090072db697f590cd3382f502e00e6128089ba9eb266e78cc4f32f314f1c97b" +content_hash = "sha256:e3b96de7c4efceb61703b10262da72cbc0a735cbb324a8fe866247d8c513a297" [[metadata.targets]] requires_python = "~=3.13" @@ -26,9 +26,9 @@ files = [ [[package]] name = "anyio" -version = "4.9.0" +version = "4.11.0" requires_python = ">=3.9" -summary = "High level compatibility layer for multiple asynchronous event loop implementations" +summary = "High-level concurrency and networking framework on top of asyncio or Trio" groups = ["default"] dependencies = [ "exceptiongroup>=1.0.2; python_version < \"3.11\"", @@ -37,8 +37,8 @@ dependencies = [ "typing-extensions>=4.5; python_version < \"3.13\"", ] files = [ - {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, - {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, + {file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}, + {file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"}, ] [[package]] @@ -89,7 +89,7 @@ files = [ [[package]] name = "click" -version = "8.2.1" +version = "8.3.0" requires_python = ">=3.10" summary = "Composable command line interface toolkit" groups = ["dev"] @@ -97,8 +97,8 @@ dependencies = [ "colorama; platform_system == \"Windows\"", ] files = [ - {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, - {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, + {file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"}, + {file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"}, ] [[package]] @@ -246,17 +246,16 @@ files = [ [[package]] name = "faker" -version = "30.8.2" -requires_python = ">=3.8" +version = "37.11.0" +requires_python = ">=3.9" summary = "Faker is a Python package that generates fake data for you." groups = ["dev"] dependencies = [ - "python-dateutil>=2.4", - "typing-extensions", + "tzdata", ] files = [ - {file = "Faker-30.8.2-py3-none-any.whl", hash = "sha256:4a82b2908cd19f3bba1a4da2060cc4eb18a40410ccdf9350d071d79dc92fe3ce"}, - {file = "faker-30.8.2.tar.gz", hash = "sha256:aa31b52cdae3673d6a78b4857c7bcdc0e98f201a5cb77d7827fa9e6b5876da94"}, + {file = "faker-37.11.0-py3-none-any.whl", hash = "sha256:1508d2da94dfd1e0087b36f386126d84f8583b3de19ac18e392a2831a6676c57"}, + {file = "faker-37.11.0.tar.gz", hash = "sha256:22969803849ba0618be8eee2dd01d0d9e2cd3b75e6ff1a291fa9abcdb34da5e6"}, ] [[package]] @@ -389,24 +388,24 @@ files = [ [[package]] name = "idna" -version = "3.10" -requires_python = ">=3.6" +version = "3.11" +requires_python = ">=3.8" summary = "Internationalized Domain Names in Applications (IDNA)" groups = ["default"] files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, ] [[package]] name = "iniconfig" -version = "2.1.0" -requires_python = ">=3.8" +version = "2.3.0" +requires_python = ">=3.10" summary = "brain-dead simple config-ini parsing" groups = ["dev"] files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, ] [[package]] @@ -495,13 +494,13 @@ files = [ [[package]] name = "platformdirs" -version = "4.3.8" -requires_python = ">=3.9" +version = "4.5.0" +requires_python = ">=3.10" summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." groups = ["dev"] files = [ - {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, - {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, + {file = "platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3"}, + {file = "platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312"}, ] [[package]] @@ -590,47 +589,6 @@ dependencies = [ "typing-extensions!=4.7.0,>=4.6.0", ] files = [ - {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, - {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, - {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, - {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, - {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, - {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, - {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, - {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, @@ -648,24 +606,6 @@ files = [ {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, ] @@ -727,6 +667,20 @@ files = [ {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, ] +[[package]] +name = "pytest-asyncio" +version = "0.24.0" +requires_python = ">=3.8" +summary = "Pytest support for asyncio" +groups = ["dev"] +dependencies = [ + "pytest<9,>=8.2", +] +files = [ + {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, + {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, +] + [[package]] name = "pytest-cov" version = "5.0.0" @@ -743,31 +697,29 @@ files = [ ] [[package]] -name = "pytest-mock" -version = "3.15.1" -requires_python = ">=3.9" -summary = "Thin-wrapper around the mock package for easier use with pytest" +name = "pytest-faker" +version = "2.0.0" +summary = "Faker integration with the pytest framework." groups = ["dev"] dependencies = [ - "pytest>=6.2.5", + "Faker>=0.7.3", ] files = [ - {file = "pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d"}, - {file = "pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f"}, + {file = "pytest-faker-2.0.0.tar.gz", hash = "sha256:6b37bb89d94f96552bfa51f8e8b89d32addded8ddb58a331488299ef0137d9b6"}, ] [[package]] -name = "python-dateutil" -version = "2.9.0.post0" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -summary = "Extensions to the standard Python datetime module" +name = "pytest-mock" +version = "3.15.1" +requires_python = ">=3.9" +summary = "Thin-wrapper around the mock package for easier use with pytest" groups = ["dev"] dependencies = [ - "six>=1.5", + "pytest>=6.2.5", ] files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, + {file = "pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d"}, + {file = "pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f"}, ] [[package]] @@ -783,13 +735,13 @@ files = [ [[package]] name = "pytokens" -version = "0.1.10" +version = "0.2.0" requires_python = ">=3.8" -summary = "A Fast, spec compliant Python 3.12+ tokenizer that runs on older Pythons." +summary = "A Fast, spec compliant Python 3.13+ tokenizer that runs on older Pythons." groups = ["dev"] files = [ - {file = "pytokens-0.1.10-py3-none-any.whl", hash = "sha256:db7b72284e480e69fb085d9f251f66b3d2df8b7166059261258ff35f50fb711b"}, - {file = "pytokens-0.1.10.tar.gz", hash = "sha256:c9a4bfa0be1d26aebce03e6884ba454e842f186a59ea43a6d3b25af58223c044"}, + {file = "pytokens-0.2.0-py3-none-any.whl", hash = "sha256:74d4b318c67f4295c13782ddd9abcb7e297ec5630ad060eb90abf7ebbefe59f8"}, + {file = "pytokens-0.2.0.tar.gz", hash = "sha256:532d6421364e5869ea57a9523bf385f02586d4662acbcc0342afd69511b4dd43"}, ] [[package]] @@ -831,17 +783,6 @@ files = [ {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, ] -[[package]] -name = "six" -version = "1.17.0" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -summary = "Python 2 and 3 compatibility utilities" -groups = ["dev"] -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - [[package]] name = "sniffio" version = "1.3.1" @@ -877,18 +818,18 @@ files = [ [[package]] name = "typing-extensions" -version = "4.14.1" +version = "4.15.0" requires_python = ">=3.9" summary = "Backported and Experimental Type Hints for Python 3.9+" groups = ["default", "dev"] files = [ - {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, - {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] [[package]] name = "typing-inspection" -version = "0.4.1" +version = "0.4.2" requires_python = ">=3.9" summary = "Runtime typing introspection tools" groups = ["default"] @@ -896,8 +837,19 @@ dependencies = [ "typing-extensions>=4.12.0", ] files = [ - {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, - {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, + {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, + {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, +] + +[[package]] +name = "tzdata" +version = "2025.2" +requires_python = ">=2" +summary = "Provider of IANA time zone data" +groups = ["dev"] +files = [ + {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, + {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index f43e310..fa9ab01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,6 +95,7 @@ ignore_missing_imports = true [tool.pytest.ini_options] addopts = "--import-mode=importlib" +asyncio_mode = "strict" testpaths = ["tests"] python_files = ["test_*.py"] @@ -127,9 +128,11 @@ dev = [ "grpc-stubs==1.53.0.6", "protobuf==6.32.1", "pytest==8.4.2", + "pytest-asyncio==0.24.0", "pytest-cov==5.0.0", "pytest-mock==3.15.1", - "Faker==30.8.2", + "pytest-faker==2.0.0", + "Faker==37.11.0", "black==25.9.0", "isort==6.0.1", "flake8==7.3.0", diff --git a/tests/handler/test_create_comment.py b/tests/handler/test_create_comment.py index 945fd63..c0d901b 100644 --- a/tests/handler/test_create_comment.py +++ b/tests/handler/test_create_comment.py @@ -1,4 +1,3 @@ -import asyncio from unittest.mock import AsyncMock import grpc @@ -14,27 +13,23 @@ from commentservice.service.service import CommentService -@pytest.fixture() -def fake() -> Faker: - f = Faker() - f.seed_instance(20251019) - return f - - -def test_create_comment_success(mocker: MockerFixture, fake: Faker) -> None: +@pytest.mark.asyncio +async def test_create_comment_success( + mocker: MockerFixture, faker: Faker +) -> None: ctx = mocker.Mock(spec=grpc.ServicerContext) fake_service = mocker.Mock(spec=CommentService) - new_id = fake.random_int(min=1, max=100000) + new_id = faker.random_int(min=1, max=100000) fake_service.create_comment = AsyncMock(return_value=new_id) - mod_id = fake.random_int(min=1, max=100000) - author_id = fake.random_int(min=1, max=100000) - text = fake.sentence() + mod_id = faker.random_int(min=1, max=100000) + author_id = faker.random_int(min=1, max=100000) + text = faker.sentence() request = CreateCommentRequest( mod_id=mod_id, author_id=author_id, text=text ) - response = asyncio.run(CreateComment(fake_service, request, ctx)) + response = await CreateComment(fake_service, request, ctx) assert isinstance(response, CreateCommentResponse) assert response.comment_id == new_id diff --git a/tests/handler/test_delete_comment.py b/tests/handler/test_delete_comment.py index 1345338..ee87f94 100644 --- a/tests/handler/test_delete_comment.py +++ b/tests/handler/test_delete_comment.py @@ -1,4 +1,3 @@ -import asyncio from unittest.mock import AsyncMock import grpc @@ -14,36 +13,35 @@ from commentservice.service.service import CommentService -@pytest.fixture() -def fake() -> Faker: - f = Faker() - f.seed_instance(20251019) - return f - - -def test_delete_comment_success(mocker: MockerFixture, fake: Faker) -> None: +@pytest.mark.asyncio +async def test_delete_comment_success( + mocker: MockerFixture, faker: Faker +) -> None: ctx = mocker.Mock(spec=grpc.ServicerContext) fake_service = mocker.Mock(spec=CommentService) fake_service.delete_comment = AsyncMock(return_value=True) - comment_id = fake.random_int(min=1, max=100000) + comment_id = faker.random_int(min=1, max=100000) request = DeleteCommentRequest(comment_id=comment_id) - response = asyncio.run(DeleteComment(fake_service, request, ctx)) + response = await DeleteComment(fake_service, request, ctx) assert isinstance(response, DeleteCommentResponse) assert response.success is True fake_service.delete_comment.assert_awaited_once_with(comment_id) -def test_delete_comment_not_found(mocker: MockerFixture, fake: Faker) -> None: +@pytest.mark.asyncio +async def test_delete_comment_not_found( + mocker: MockerFixture, faker: Faker +) -> None: ctx = mocker.Mock(spec=grpc.ServicerContext) fake_service = mocker.Mock(spec=CommentService) fake_service.delete_comment = AsyncMock(return_value=False) - comment_id = fake.random_int(min=1, max=100000) + comment_id = faker.random_int(min=1, max=100000) request = DeleteCommentRequest(comment_id=comment_id) - response = asyncio.run(DeleteComment(fake_service, request, ctx)) + response = await DeleteComment(fake_service, request, ctx) assert response.success is False fake_service.delete_comment.assert_awaited_once_with(comment_id) diff --git a/tests/handler/test_edit_comment.py b/tests/handler/test_edit_comment.py index 060d667..24345d8 100644 --- a/tests/handler/test_edit_comment.py +++ b/tests/handler/test_edit_comment.py @@ -1,4 +1,3 @@ -import asyncio from unittest.mock import AsyncMock import grpc @@ -14,37 +13,36 @@ from commentservice.service.service import CommentService -@pytest.fixture() -def fake() -> Faker: - f = Faker() - f.seed_instance(20251019) - return f - - -def test_edit_comment_success(mocker: MockerFixture, fake: Faker) -> None: +@pytest.mark.asyncio +async def test_edit_comment_success( + mocker: MockerFixture, faker: Faker +) -> None: ctx = mocker.Mock(spec=grpc.ServicerContext) fake_service = mocker.Mock(spec=CommentService) fake_service.edit_comment = AsyncMock(return_value=True) - comment_id = fake.random_int(min=1, max=100000) - new_text = fake.sentence() + comment_id = faker.random_int(min=1, max=100000) + new_text = faker.sentence() request = EditCommentRequest(comment_id=comment_id, text=new_text) - response = asyncio.run(EditComment(fake_service, request, ctx)) + response = await EditComment(fake_service, request, ctx) assert isinstance(response, EditCommentResponse) assert response.success is True fake_service.edit_comment.assert_awaited_once_with(comment_id, new_text) -def test_edit_comment_not_found(mocker: MockerFixture, fake: Faker) -> None: +@pytest.mark.asyncio +async def test_edit_comment_not_found( + mocker: MockerFixture, faker: Faker +) -> None: ctx = mocker.Mock(spec=grpc.ServicerContext) fake_service = mocker.Mock(spec=CommentService) fake_service.edit_comment = AsyncMock(return_value=False) - comment_id = fake.random_int(min=1, max=100000) - new_text = fake.sentence() + comment_id = faker.random_int(min=1, max=100000) + new_text = faker.sentence() request = EditCommentRequest(comment_id=comment_id, text=new_text) - response = asyncio.run(EditComment(fake_service, request, ctx)) + response = await EditComment(fake_service, request, ctx) assert response.success is False fake_service.edit_comment.assert_awaited_once_with(comment_id, new_text) diff --git a/tests/handler/test_get_comments.py b/tests/handler/test_get_comments.py index 60857d2..4d4937b 100644 --- a/tests/handler/test_get_comments.py +++ b/tests/handler/test_get_comments.py @@ -1,4 +1,3 @@ -import asyncio from datetime import UTC, timedelta import grpc @@ -15,40 +14,36 @@ from commentservice.service.service import CommentService -@pytest.fixture() -def fake() -> Faker: - f = Faker() - f.seed_instance(20251019) - return f - - -def test_get_comments_success(mocker: MockerFixture, fake: Faker) -> None: +@pytest.mark.asyncio +async def test_get_comments_success( + mocker: MockerFixture, faker: Faker +) -> None: ctx = mocker.Mock(spec=grpc.ServicerContext) fake_service = mocker.Mock(spec=CommentService) - now = fake.date_time(tzinfo=UTC) - earlier = now - timedelta(hours=fake.random_int(min=1, max=12)) + now = faker.date_time(tzinfo=UTC) + earlier = now - timedelta(hours=faker.random_int(min=1, max=12)) comment1 = Comment( - id=fake.random_int(min=1, max=100000), - author_id=fake.random_int(min=1, max=100000), - text=fake.sentence(), + id=faker.random_int(min=1, max=100000), + author_id=faker.random_int(min=1, max=100000), + text=faker.sentence(), created_at=earlier, edited_at=None, ) comment2 = Comment( - id=fake.random_int(min=1, max=100000), - author_id=fake.random_int(min=1, max=100000), - text=fake.sentence(), + id=faker.random_int(min=1, max=100000), + author_id=faker.random_int(min=1, max=100000), + text=faker.sentence(), created_at=now, edited_at=now, ) comments = [comment1, comment2] fake_service.get_comments.return_value = comments - mod_id = fake.random_int(min=1, max=100000) + mod_id = faker.random_int(min=1, max=100000) request = GetCommentsRequest(mod_id=mod_id) - response = asyncio.run(GetComments(fake_service, request, ctx)) + response = await GetComments(fake_service, request, ctx) assert isinstance(response, GetCommentsResponse) assert response.mod_id == mod_id diff --git a/tests/repository/test_create_comment.py b/tests/repository/test_create_comment.py index ddb6aaf..564d5c5 100644 --- a/tests/repository/test_create_comment.py +++ b/tests/repository/test_create_comment.py @@ -1,4 +1,3 @@ -import asyncio import textwrap from unittest.mock import AsyncMock @@ -9,37 +8,30 @@ from commentservice.repository.repository import CommentRepository -@pytest.fixture() -def fake() -> Faker: - f = Faker() - f.seed_instance(20251019) - return f - - -def test_repo_create_comment_returns_id( - mocker: MockerFixture, fake: Faker +@pytest.mark.asyncio +async def test_repo_create_comment_returns_id( + mocker: MockerFixture, faker: Faker ) -> None: + comment_id = faker.random_int(min=1, max=100000) conn = mocker.Mock() - conn.fetchval = AsyncMock(return_value=42) - - acquire_cm = mocker.MagicMock() - acquire_cm.__aenter__ = AsyncMock(return_value=conn) - acquire_cm.__aexit__ = AsyncMock(return_value=None) + conn.fetchval = AsyncMock(return_value=comment_id) pool = mocker.Mock() - pool.acquire.return_value = acquire_cm - - repo = CommentRepository(pool) # type: ignore[arg-type] - mod_id = fake.random_int(min=1, max=100000) - author_id = fake.random_int(min=1, max=100000) - text = fake.sentence() - new_id = asyncio.run( - repo.create_comment(mod_id=mod_id, author_id=author_id, text=text) + pool.acquire = mocker.Mock() + pool.acquire.return_value = AsyncMock() + pool.acquire.return_value.__aenter__.return_value = conn + pool.acquire.return_value.__aexit__.return_value = None + + repo = CommentRepository(pool) + mod_id = faker.random_int(min=1, max=100000) + author_id = faker.random_int(min=1, max=100000) + text = faker.sentence() + new_id = await repo.create_comment( + mod_id=mod_id, author_id=author_id, text=text ) - assert new_id == 42 + assert new_id == comment_id assert conn.fetchval.await_count == 1 - # check SQL and args expected_sql = """ INSERT INTO comments (mod_id, author_id, text) VALUES ($1, $2, $3) diff --git a/tests/repository/test_delete_comment.py b/tests/repository/test_delete_comment.py index 7a6c637..b5f48c0 100644 --- a/tests/repository/test_delete_comment.py +++ b/tests/repository/test_delete_comment.py @@ -1,4 +1,3 @@ -import asyncio import textwrap from unittest.mock import AsyncMock @@ -9,32 +8,23 @@ from commentservice.repository.repository import CommentRepository -@pytest.fixture() -def fake() -> Faker: - f = Faker() - f.seed_instance(20251019) - return f - - -def _make_pool(mocker: MockerFixture, conn: object): - acquire_cm = mocker.MagicMock() - acquire_cm.__aenter__ = AsyncMock(return_value=conn) - acquire_cm.__aexit__ = AsyncMock(return_value=None) - pool = mocker.Mock() - pool.acquire.return_value = acquire_cm - return pool - - -def test_repo_delete_comment_success(mocker: MockerFixture, fake) -> None: +@pytest.mark.asyncio +async def test_repo_delete_comment_success( + mocker: MockerFixture, faker: Faker +) -> None: conn = mocker.Mock() - conn.fetchval = AsyncMock(return_value=123) - pool = _make_pool(mocker, conn) - repo = CommentRepository(pool) # type: ignore[arg-type] + conn.fetchval = AsyncMock() + pool = mocker.Mock() + pool.acquire = mocker.Mock() + pool.acquire.return_value = AsyncMock() + pool.acquire.return_value.__aenter__.return_value = conn + pool.acquire.return_value.__aexit__.return_value = None + repo = CommentRepository(pool) - cid = fake.random_int(min=1, max=100000) + cid = faker.random_int(min=1, max=100000) conn.fetchval.return_value = cid - ok = asyncio.run(repo.delete_comment(comment_id=cid)) + ok = await repo.delete_comment(comment_id=cid) assert ok is True expected_sql = """ DELETE FROM comments @@ -49,14 +39,21 @@ def test_repo_delete_comment_success(mocker: MockerFixture, fake) -> None: assert conn.fetchval.await_args.args[1:] == (cid,) -def test_repo_delete_comment_not_found(mocker: MockerFixture, fake) -> None: +@pytest.mark.asyncio +async def test_repo_delete_comment_not_found( + mocker: MockerFixture, faker: Faker +) -> None: conn = mocker.Mock() conn.fetchval = AsyncMock(return_value=None) - pool = _make_pool(mocker, conn) - repo = CommentRepository(pool) # type: ignore[arg-type] + pool = mocker.Mock() + pool.acquire = mocker.Mock() + pool.acquire.return_value = AsyncMock() + pool.acquire.return_value.__aenter__.return_value = conn + pool.acquire.return_value.__aexit__.return_value = None + repo = CommentRepository(pool) - cid = fake.random_int(min=1, max=100000) - ok = asyncio.run(repo.delete_comment(comment_id=cid)) + cid = faker.random_int(min=1, max=100000) + ok = await repo.delete_comment(comment_id=cid) assert ok is False expected_sql = """ DELETE FROM comments diff --git a/tests/repository/test_edit_comment.py b/tests/repository/test_edit_comment.py index d99ec12..198dea4 100644 --- a/tests/repository/test_edit_comment.py +++ b/tests/repository/test_edit_comment.py @@ -1,4 +1,3 @@ -import asyncio import textwrap from unittest.mock import AsyncMock @@ -9,32 +8,23 @@ from commentservice.repository.repository import CommentRepository -@pytest.fixture() -def fake() -> Faker: - f = Faker() - f.seed_instance(20251019) - return f - - -def _make_pool(mocker: MockerFixture, conn: object): - acquire_cm = mocker.MagicMock() - acquire_cm.__aenter__ = AsyncMock(return_value=conn) - acquire_cm.__aexit__ = AsyncMock(return_value=None) - pool = mocker.Mock() - pool.acquire.return_value = acquire_cm - return pool - - -def test_repo_edit_comment_success(mocker: MockerFixture, fake) -> None: +@pytest.mark.asyncio +async def test_repo_edit_comment_success( + mocker: MockerFixture, faker: Faker +) -> None: conn = mocker.Mock() - conn.fetchval = AsyncMock(return_value=5) - pool = _make_pool(mocker, conn) - repo = CommentRepository(pool) # type: ignore[arg-type] + conn.fetchval = AsyncMock() + pool = mocker.Mock() + pool.acquire = mocker.Mock() + pool.acquire.return_value = AsyncMock() + pool.acquire.return_value.__aenter__.return_value = conn + pool.acquire.return_value.__aexit__.return_value = None + repo = CommentRepository(pool) - cid = fake.random_int(min=1, max=100000) - new_text = fake.sentence() + cid = faker.random_int(min=1, max=100000) + new_text = faker.sentence() conn.fetchval.return_value = cid - ok = asyncio.run(repo.edit_comment(comment_id=cid, new_text=new_text)) + ok = await repo.edit_comment(comment_id=cid, new_text=new_text) assert ok is True expected_sql = """ UPDATE comments @@ -50,15 +40,22 @@ def test_repo_edit_comment_success(mocker: MockerFixture, fake) -> None: assert conn.fetchval.await_args.args[1:] == (new_text, cid) -def test_repo_edit_comment_not_found(mocker: MockerFixture, fake) -> None: +@pytest.mark.asyncio +async def test_repo_edit_comment_not_found( + mocker: MockerFixture, faker: Faker +) -> None: conn = mocker.Mock() conn.fetchval = AsyncMock(return_value=None) - pool = _make_pool(mocker, conn) - repo = CommentRepository(pool) # type: ignore[arg-type] + pool = mocker.Mock() + pool.acquire = mocker.Mock() + pool.acquire.return_value = AsyncMock() + pool.acquire.return_value.__aenter__.return_value = conn + pool.acquire.return_value.__aexit__.return_value = None + repo = CommentRepository(pool) - cid = fake.random_int(min=1, max=100000) - new_text = fake.sentence() - ok = asyncio.run(repo.edit_comment(comment_id=cid, new_text=new_text)) + cid = faker.random_int(min=1, max=100000) + new_text = faker.sentence() + ok = await repo.edit_comment(comment_id=cid, new_text=new_text) assert ok is False expected_sql = """ UPDATE comments diff --git a/tests/repository/test_get_comments.py b/tests/repository/test_get_comments.py index 7da37d9..03981b5 100644 --- a/tests/repository/test_get_comments.py +++ b/tests/repository/test_get_comments.py @@ -1,4 +1,3 @@ -import asyncio import textwrap from datetime import UTC from unittest.mock import AsyncMock @@ -10,43 +9,39 @@ from commentservice.repository.repository import CommentRepository -@pytest.fixture() -def fake() -> Faker: - f = Faker() - f.seed_instance(20251019) - return f - - -def test_repo_get_comments_maps_rows( - mocker: MockerFixture, fake: Faker +@pytest.mark.asyncio +async def test_repo_get_comments_maps_rows( + mocker: MockerFixture, faker: Faker ) -> None: rows = [ { - "id": fake.random_int(), - "author_id": fake.random_int(), - "text": fake.sentence(), - "created_at": fake.date_time(tzinfo=UTC), + "id": faker.random_int(), + "author_id": faker.random_int(), + "text": faker.sentence(), + "created_at": faker.date_time(tzinfo=UTC), "edited_at": None, }, { - "id": fake.random_int(), - "author_id": fake.random_int(), - "text": fake.sentence(), - "created_at": fake.date_time(tzinfo=UTC), - "edited_at": fake.date_time(tzinfo=UTC), + "id": faker.random_int(), + "author_id": faker.random_int(), + "text": faker.sentence(), + "created_at": faker.date_time(tzinfo=UTC), + "edited_at": faker.date_time(tzinfo=UTC), }, ] conn = mocker.Mock() conn.fetch = AsyncMock(return_value=rows) - acquire_cm = mocker.MagicMock() - acquire_cm.__aenter__ = AsyncMock(return_value=conn) - acquire_cm.__aexit__ = AsyncMock(return_value=None) pool = mocker.Mock() - pool.acquire.return_value = acquire_cm - repo = CommentRepository(pool) # type: ignore[arg-type] + pool.acquire = mocker.Mock() + pool.acquire.return_value = AsyncMock() + pool.acquire.return_value.__aenter__.return_value = conn + pool.acquire.return_value.__aexit__.return_value = None + repo = CommentRepository(pool) + + mod_id = faker.random_int(min=1, max=100000) - out = asyncio.run(repo.get_comments(mod_id=77)) + out = await repo.get_comments(mod_id=mod_id) assert len(out) == 2 assert out[0].id == rows[0]["id"] and out[0].text == rows[0]["text"] @@ -63,4 +58,4 @@ def test_repo_get_comments_maps_rows( textwrap.dedent(actual_sql).strip() == textwrap.dedent(expected_sql).strip() ) - assert conn.fetch.await_args.args[1:] == (77,) + assert conn.fetch.await_args.args[1:] == (mod_id,) diff --git a/tests/service/test_create_comment.py b/tests/service/test_create_comment.py index 393e9bb..4f6e216 100644 --- a/tests/service/test_create_comment.py +++ b/tests/service/test_create_comment.py @@ -1,4 +1,3 @@ -import asyncio from unittest.mock import AsyncMock import pytest @@ -9,25 +8,21 @@ from commentservice.service.service import CommentService -@pytest.fixture() -def fake() -> Faker: - f = Faker() - f.seed_instance(20251019) - return f - - -def test_service_create_comment(mocker: MockerFixture, fake: Faker) -> None: +@pytest.mark.asyncio +async def test_service_create_comment( + mocker: MockerFixture, faker: Faker +) -> None: fake_repo = mocker.Mock(spec=CommentRepository) - new_id = fake.random_int(min=1, max=100000) + new_id = faker.random_int(min=1, max=100000) fake_repo.create_comment = AsyncMock(return_value=new_id) - mod_id = fake.random_int(min=1, max=100000) - author_id = fake.random_int(min=1, max=100000) - text = fake.sentence() + mod_id = faker.random_int(min=1, max=100000) + author_id = faker.random_int(min=1, max=100000) + text = faker.sentence() service = CommentService(fake_repo) - result = asyncio.run( - service.create_comment(mod_id=mod_id, author_id=author_id, text=text) + result = await service.create_comment( + mod_id=mod_id, author_id=author_id, text=text ) assert result == new_id diff --git a/tests/service/test_delete_comment.py b/tests/service/test_delete_comment.py index d241388..5db3a93 100644 --- a/tests/service/test_delete_comment.py +++ b/tests/service/test_delete_comment.py @@ -1,4 +1,3 @@ -import asyncio from unittest.mock import AsyncMock import pytest @@ -9,21 +8,17 @@ from commentservice.service.service import CommentService -@pytest.fixture() -def fake() -> Faker: - f = Faker() - f.seed_instance(20251019) - return f - - -def test_service_delete_comment(mocker: MockerFixture, fake: Faker) -> None: +@pytest.mark.asyncio +async def test_service_delete_comment( + mocker: MockerFixture, faker: Faker +) -> None: fake_repo = mocker.Mock(spec=CommentRepository) fake_repo.delete_comment = AsyncMock(return_value=True) - comment_id = fake.random_int(min=1, max=100000) + comment_id = faker.random_int(min=1, max=100000) service = CommentService(fake_repo) - result = asyncio.run(service.delete_comment(comment_id=comment_id)) + result = await service.delete_comment(comment_id=comment_id) assert result is True fake_repo.delete_comment.assert_awaited_once_with(comment_id) diff --git a/tests/service/test_edit_comment.py b/tests/service/test_edit_comment.py index bf40b8b..c9d750d 100644 --- a/tests/service/test_edit_comment.py +++ b/tests/service/test_edit_comment.py @@ -1,4 +1,3 @@ -import asyncio from unittest.mock import AsyncMock import pytest @@ -9,23 +8,19 @@ from commentservice.service.service import CommentService -@pytest.fixture() -def fake() -> Faker: - f = Faker() - f.seed_instance(20251019) - return f - - -def test_service_edit_comment(mocker: MockerFixture, fake: Faker) -> None: +@pytest.mark.asyncio +async def test_service_edit_comment( + mocker: MockerFixture, faker: Faker +) -> None: fake_repo = mocker.Mock(spec=CommentRepository) fake_repo.edit_comment = AsyncMock(return_value=True) - comment_id = fake.random_int(min=1, max=100000) - new_text = fake.sentence() + comment_id = faker.random_int(min=1, max=100000) + new_text = faker.sentence() service = CommentService(fake_repo) - result = asyncio.run( - service.edit_comment(comment_id=comment_id, new_text=new_text) + result = await service.edit_comment( + comment_id=comment_id, new_text=new_text ) assert result is True diff --git a/tests/service/test_get_comments.py b/tests/service/test_get_comments.py index 649a124..6ad2343 100644 --- a/tests/service/test_get_comments.py +++ b/tests/service/test_get_comments.py @@ -1,4 +1,3 @@ -import asyncio from datetime import UTC from unittest.mock import AsyncMock @@ -11,36 +10,32 @@ from commentservice.service.service import CommentService -@pytest.fixture() -def fake() -> Faker: - f = Faker() - f.seed_instance(20251019) - return f - - -def test_service_get_comments(mocker: MockerFixture, fake: Faker) -> None: +@pytest.mark.asyncio +async def test_service_get_comments( + mocker: MockerFixture, faker: Faker +) -> None: fake_repo = mocker.Mock(spec=CommentRepository) comments = [ Comment( - id=fake.random_int(), - author_id=fake.random_int(), - text=fake.sentence(), - created_at=fake.date_time(tzinfo=UTC), + id=faker.random_int(), + author_id=faker.random_int(), + text=faker.sentence(), + created_at=faker.date_time(tzinfo=UTC), edited_at=None, ), Comment( - id=fake.random_int(), - author_id=fake.random_int(), - text=fake.sentence(), - created_at=fake.date_time(tzinfo=UTC), + id=faker.random_int(), + author_id=faker.random_int(), + text=faker.sentence(), + created_at=faker.date_time(tzinfo=UTC), edited_at=None, ), ] fake_repo.get_comments = AsyncMock(return_value=comments) - mod_id = fake.random_int(min=1, max=100000) + mod_id = faker.random_int(min=1, max=100000) service = CommentService(fake_repo) - out = asyncio.run(service.get_comments(mod_id=mod_id)) + out = await service.get_comments(mod_id=mod_id) assert out == comments fake_repo.get_comments.assert_awaited_once_with(mod_id) diff --git a/tools/common.just b/tools/common.just index a399cda..db8b233 100644 --- a/tools/common.just +++ b/tools/common.just @@ -39,6 +39,10 @@ lint: mypy --strict . test: - pytest --cov=src --cov-report=xml:coverage.xml + pytest \ + --cov=src/commentservice/handler \ + --cov=src/commentservice/service \ + --cov=src/commentservice/repository \ + --cov-report=xml:coverage.xml prepare: format lint test From 4e65fca92af70b241d0e2b8f5af43dee3448227a Mon Sep 17 00:00:00 2001 From: Andrey Kataev Date: Sun, 19 Oct 2025 17:57:51 +0300 Subject: [PATCH 04/14] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B2=D0=BE=D1=80=D0=BA=D1=84=D0=BB=D0=BE=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/lint-and-test.yml | 3 ++- justfile | 3 ++- tools/common.just | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index 01ed80a..c11b722 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -13,8 +13,9 @@ permissions: jobs: lint-and-test: - uses: esclient/tools/.github/workflows/lint-and-test-python.yml@455a2700e3d2a08c371b072b1327689d76582758 + uses: esclient/tools/.github/workflows/lint-and-test-python.yml@d3ed2b83a8b318be00d3c211560f62ac40efc2d2 with: python-version: "3.13.7" + source: "commentservice" secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/justfile b/justfile index 9de3706..59ae2d8 100644 --- a/justfile +++ b/justfile @@ -7,7 +7,8 @@ LOAD_ENVS_URL := 'https://raw.githubusercontent.com/esclient/tools/refs/heads/ma PROTO_TAG := 'v0.0.17' PROTO_NAME := 'comment.proto' TMP_DIR := '.proto' -OUT_DIR := 'src/commentservice/grpc' +SOURCE := 'commentservice' +OUT_DIR := 'src/' + SOURCE + '/grpc' MKDIR_TOOLS := 'mkdir -p tools' diff --git a/tools/common.just b/tools/common.just index db8b233..0ef3141 100644 --- a/tools/common.just +++ b/tools/common.just @@ -40,9 +40,9 @@ lint: test: pytest \ - --cov=src/commentservice/handler \ - --cov=src/commentservice/service \ - --cov=src/commentservice/repository \ + --cov=src/{{SOURCE}}/handler \ + --cov=src/{{SOURCE}}/service \ + --cov=src/{{SOURCE}}/repository \ --cov-report=xml:coverage.xml prepare: format lint test From a86e739696ad8648e99000585d61c8c018c8b252 Mon Sep 17 00:00:00 2001 From: Andrey Kataev Date: Sun, 19 Oct 2025 18:03:27 +0300 Subject: [PATCH 05/14] Update lint-and-test.yml --- .github/workflows/lint-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index c11b722..956390d 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -13,7 +13,7 @@ permissions: jobs: lint-and-test: - uses: esclient/tools/.github/workflows/lint-and-test-python.yml@d3ed2b83a8b318be00d3c211560f62ac40efc2d2 + uses: esclient/tools/.github/workflows/lint-and-test-python.yml@84db005db60d5ca2a357d1796b8b3648a2898077 with: python-version: "3.13.7" source: "commentservice" From 406f87d4bf13a71be69368f033289a4a26de21eb Mon Sep 17 00:00:00 2001 From: Andrey Kataev Date: Mon, 20 Oct 2025 18:47:24 +0300 Subject: [PATCH 06/14] =?UTF-8?q?=D0=94=D0=BE=D0=BF=20=D1=82=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D1=8B=20=D0=B8=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 11 ++- tests/handler/test_handler.py | 130 ++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 tests/handler/test_handler.py diff --git a/Dockerfile b/Dockerfile index 714c456..d23d1cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM python:3.13-slim -RUN apt-get update && apt-get install -y wget curl jq && \ +RUN apt-get update && apt-get install -y curl jq wget && \ wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 && \ chmod +x /usr/local/bin/yq && \ apt-get remove -y wget && apt-get autoremove -y && \ @@ -9,15 +9,14 @@ RUN apt-get update && apt-get install -y wget curl jq && \ WORKDIR /app COPY pyproject.toml pdm.lock ./ +COPY . . RUN pip install --no-cache-dir pdm && \ pdm export --without-hashes -f requirements > /tmp/req.txt && \ pip install --no-cache-dir -r /tmp/req.txt && \ pip uninstall -y pdm && \ - rm /tmp/req.txt - -COPY . . -RUN pip install --no-cache-dir -e . -RUN chmod +x tools/load_envs.sh + rm /tmp/req.txt && \ + pip install --no-cache-dir -e . && \ + chmod +x tools/load_envs.sh ENV ENV=prod ENV PYTHONPATH=src diff --git a/tests/handler/test_handler.py b/tests/handler/test_handler.py new file mode 100644 index 0000000..442df42 --- /dev/null +++ b/tests/handler/test_handler.py @@ -0,0 +1,130 @@ +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any +from unittest.mock import AsyncMock + +import grpc +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +import commentservice.handler.handler as handler_module +from commentservice.grpc import comment_pb2 +from commentservice.handler.handler import CommentHandler +from commentservice.service.service import CommentService + + +def _build_create_pair( + faker: Faker, +) -> tuple[ + comment_pb2.CreateCommentRequest, comment_pb2.CreateCommentResponse +]: + mod_id = faker.random_int(min=1, max=100000) + author_id = faker.random_int(min=1, max=100000) + text = faker.sentence() + response = comment_pb2.CreateCommentResponse( + comment_id=faker.random_int(min=1, max=100000) + ) + request = comment_pb2.CreateCommentRequest( + mod_id=mod_id, + author_id=author_id, + text=text, + ) + return request, response + + +def _build_edit_pair( + faker: Faker, +) -> tuple[comment_pb2.EditCommentRequest, comment_pb2.EditCommentResponse]: + comment_id = faker.random_int(min=1, max=100000) + new_text = faker.sentence() + response = comment_pb2.EditCommentResponse(success=True) + request = comment_pb2.EditCommentRequest( + comment_id=comment_id, text=new_text + ) + return request, response + + +def _build_delete_pair( + faker: Faker, +) -> tuple[ + comment_pb2.DeleteCommentRequest, comment_pb2.DeleteCommentResponse +]: + comment_id = faker.random_int(min=1, max=100000) + response = comment_pb2.DeleteCommentResponse(success=True) + request = comment_pb2.DeleteCommentRequest(comment_id=comment_id) + return request, response + + +def _build_get_pair( + faker: Faker, +) -> tuple[comment_pb2.GetCommentsRequest, comment_pb2.GetCommentsResponse]: + mod_id = faker.random_int(min=1, max=100000) + response = comment_pb2.GetCommentsResponse(mod_id=mod_id) + response.comments.add( + id=faker.random_int(min=1, max=100000), + author_id=faker.random_int(min=1, max=100000), + text=faker.sentence(), + ) + request = comment_pb2.GetCommentsRequest(mod_id=mod_id) + return request, response + + +@dataclass(frozen=True) +class HandlerCase: + method_name: str + helper_attr: str + builder: Callable[[Faker], tuple[Any, Any]] + + +CASES: tuple[HandlerCase, ...] = ( + HandlerCase( + "CreateComment", + "_create_comment", + _build_create_pair, + ), + HandlerCase( + "EditComment", + "_edit_comment", + _build_edit_pair, + ), + HandlerCase( + "DeleteComment", + "_delete_comment", + _build_delete_pair, + ), + HandlerCase( + "GetComments", + "_get_comments", + _build_get_pair, + ), +) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "case", + CASES, + ids=lambda case: case.method_name, +) +async def test_comment_handler_delegates_to_helper( + mocker: MockerFixture, + faker: Faker, + case: HandlerCase, +) -> None: + service = mocker.Mock(spec=CommentService) + handler = CommentHandler(service) + request, expected_response = case.builder(faker) + + helper = mocker.patch.object( + handler_module, + case.helper_attr, + new=AsyncMock(return_value=expected_response), + ) + context = mocker.Mock(spec=grpc.ServicerContext) + + method = getattr(handler, case.method_name) + result = await method(request, context) + + assert result is expected_response + helper.assert_awaited_once_with(service, request, context) From 53b0edbdbf23f6788fb469051056eb4e4e703365 Mon Sep 17 00:00:00 2001 From: Andrey Kataev Date: Mon, 20 Oct 2025 19:01:54 +0300 Subject: [PATCH 07/14] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D0=BF=D1=83=D1=82=D1=8F=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 11 +++ tests/handler/test_handler.py | 130 ---------------------------------- tools/common.just | 4 +- 3 files changed, 12 insertions(+), 133 deletions(-) delete mode 100644 tests/handler/test_handler.py diff --git a/pyproject.toml b/pyproject.toml index fa9ab01..d385cef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,6 +99,17 @@ asyncio_mode = "strict" testpaths = ["tests"] python_files = ["test_*.py"] +[tool.coverage.run] +source = ["src/commentservice"] +relative_files = true + +[tool.coverage.report] +include = [ + "src/commentservice/handler/*", + "src/commentservice/service/*", + "src/commentservice/repository/*", +] + [project] name = "commentservice" version = "0.0.1" diff --git a/tests/handler/test_handler.py b/tests/handler/test_handler.py deleted file mode 100644 index 442df42..0000000 --- a/tests/handler/test_handler.py +++ /dev/null @@ -1,130 +0,0 @@ -from collections.abc import Callable -from dataclasses import dataclass -from typing import Any -from unittest.mock import AsyncMock - -import grpc -import pytest -from faker import Faker -from pytest_mock import MockerFixture - -import commentservice.handler.handler as handler_module -from commentservice.grpc import comment_pb2 -from commentservice.handler.handler import CommentHandler -from commentservice.service.service import CommentService - - -def _build_create_pair( - faker: Faker, -) -> tuple[ - comment_pb2.CreateCommentRequest, comment_pb2.CreateCommentResponse -]: - mod_id = faker.random_int(min=1, max=100000) - author_id = faker.random_int(min=1, max=100000) - text = faker.sentence() - response = comment_pb2.CreateCommentResponse( - comment_id=faker.random_int(min=1, max=100000) - ) - request = comment_pb2.CreateCommentRequest( - mod_id=mod_id, - author_id=author_id, - text=text, - ) - return request, response - - -def _build_edit_pair( - faker: Faker, -) -> tuple[comment_pb2.EditCommentRequest, comment_pb2.EditCommentResponse]: - comment_id = faker.random_int(min=1, max=100000) - new_text = faker.sentence() - response = comment_pb2.EditCommentResponse(success=True) - request = comment_pb2.EditCommentRequest( - comment_id=comment_id, text=new_text - ) - return request, response - - -def _build_delete_pair( - faker: Faker, -) -> tuple[ - comment_pb2.DeleteCommentRequest, comment_pb2.DeleteCommentResponse -]: - comment_id = faker.random_int(min=1, max=100000) - response = comment_pb2.DeleteCommentResponse(success=True) - request = comment_pb2.DeleteCommentRequest(comment_id=comment_id) - return request, response - - -def _build_get_pair( - faker: Faker, -) -> tuple[comment_pb2.GetCommentsRequest, comment_pb2.GetCommentsResponse]: - mod_id = faker.random_int(min=1, max=100000) - response = comment_pb2.GetCommentsResponse(mod_id=mod_id) - response.comments.add( - id=faker.random_int(min=1, max=100000), - author_id=faker.random_int(min=1, max=100000), - text=faker.sentence(), - ) - request = comment_pb2.GetCommentsRequest(mod_id=mod_id) - return request, response - - -@dataclass(frozen=True) -class HandlerCase: - method_name: str - helper_attr: str - builder: Callable[[Faker], tuple[Any, Any]] - - -CASES: tuple[HandlerCase, ...] = ( - HandlerCase( - "CreateComment", - "_create_comment", - _build_create_pair, - ), - HandlerCase( - "EditComment", - "_edit_comment", - _build_edit_pair, - ), - HandlerCase( - "DeleteComment", - "_delete_comment", - _build_delete_pair, - ), - HandlerCase( - "GetComments", - "_get_comments", - _build_get_pair, - ), -) - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "case", - CASES, - ids=lambda case: case.method_name, -) -async def test_comment_handler_delegates_to_helper( - mocker: MockerFixture, - faker: Faker, - case: HandlerCase, -) -> None: - service = mocker.Mock(spec=CommentService) - handler = CommentHandler(service) - request, expected_response = case.builder(faker) - - helper = mocker.patch.object( - handler_module, - case.helper_attr, - new=AsyncMock(return_value=expected_response), - ) - context = mocker.Mock(spec=grpc.ServicerContext) - - method = getattr(handler, case.method_name) - result = await method(request, context) - - assert result is expected_response - helper.assert_awaited_once_with(service, request, context) diff --git a/tools/common.just b/tools/common.just index 0ef3141..b4cdc97 100644 --- a/tools/common.just +++ b/tools/common.just @@ -40,9 +40,7 @@ lint: test: pytest \ - --cov=src/{{SOURCE}}/handler \ - --cov=src/{{SOURCE}}/service \ - --cov=src/{{SOURCE}}/repository \ + --cov=src/{{SOURCE}} \ --cov-report=xml:coverage.xml prepare: format lint test From d0ab8b276d4341f1c82c5fbcf3a4202824ed488f Mon Sep 17 00:00:00 2001 From: Andrey Kataev Date: Mon, 20 Oct 2025 19:05:25 +0300 Subject: [PATCH 08/14] =?UTF-8?q?=D0=A2=D0=B5=D1=81=D1=82=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D1=85=D0=B5=D0=BD=D0=B4=D0=BB=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/handler/test_handler.py | 130 ++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 tests/handler/test_handler.py diff --git a/tests/handler/test_handler.py b/tests/handler/test_handler.py new file mode 100644 index 0000000..442df42 --- /dev/null +++ b/tests/handler/test_handler.py @@ -0,0 +1,130 @@ +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any +from unittest.mock import AsyncMock + +import grpc +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +import commentservice.handler.handler as handler_module +from commentservice.grpc import comment_pb2 +from commentservice.handler.handler import CommentHandler +from commentservice.service.service import CommentService + + +def _build_create_pair( + faker: Faker, +) -> tuple[ + comment_pb2.CreateCommentRequest, comment_pb2.CreateCommentResponse +]: + mod_id = faker.random_int(min=1, max=100000) + author_id = faker.random_int(min=1, max=100000) + text = faker.sentence() + response = comment_pb2.CreateCommentResponse( + comment_id=faker.random_int(min=1, max=100000) + ) + request = comment_pb2.CreateCommentRequest( + mod_id=mod_id, + author_id=author_id, + text=text, + ) + return request, response + + +def _build_edit_pair( + faker: Faker, +) -> tuple[comment_pb2.EditCommentRequest, comment_pb2.EditCommentResponse]: + comment_id = faker.random_int(min=1, max=100000) + new_text = faker.sentence() + response = comment_pb2.EditCommentResponse(success=True) + request = comment_pb2.EditCommentRequest( + comment_id=comment_id, text=new_text + ) + return request, response + + +def _build_delete_pair( + faker: Faker, +) -> tuple[ + comment_pb2.DeleteCommentRequest, comment_pb2.DeleteCommentResponse +]: + comment_id = faker.random_int(min=1, max=100000) + response = comment_pb2.DeleteCommentResponse(success=True) + request = comment_pb2.DeleteCommentRequest(comment_id=comment_id) + return request, response + + +def _build_get_pair( + faker: Faker, +) -> tuple[comment_pb2.GetCommentsRequest, comment_pb2.GetCommentsResponse]: + mod_id = faker.random_int(min=1, max=100000) + response = comment_pb2.GetCommentsResponse(mod_id=mod_id) + response.comments.add( + id=faker.random_int(min=1, max=100000), + author_id=faker.random_int(min=1, max=100000), + text=faker.sentence(), + ) + request = comment_pb2.GetCommentsRequest(mod_id=mod_id) + return request, response + + +@dataclass(frozen=True) +class HandlerCase: + method_name: str + helper_attr: str + builder: Callable[[Faker], tuple[Any, Any]] + + +CASES: tuple[HandlerCase, ...] = ( + HandlerCase( + "CreateComment", + "_create_comment", + _build_create_pair, + ), + HandlerCase( + "EditComment", + "_edit_comment", + _build_edit_pair, + ), + HandlerCase( + "DeleteComment", + "_delete_comment", + _build_delete_pair, + ), + HandlerCase( + "GetComments", + "_get_comments", + _build_get_pair, + ), +) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "case", + CASES, + ids=lambda case: case.method_name, +) +async def test_comment_handler_delegates_to_helper( + mocker: MockerFixture, + faker: Faker, + case: HandlerCase, +) -> None: + service = mocker.Mock(spec=CommentService) + handler = CommentHandler(service) + request, expected_response = case.builder(faker) + + helper = mocker.patch.object( + handler_module, + case.helper_attr, + new=AsyncMock(return_value=expected_response), + ) + context = mocker.Mock(spec=grpc.ServicerContext) + + method = getattr(handler, case.method_name) + result = await method(request, context) + + assert result is expected_response + helper.assert_awaited_once_with(service, request, context) From 6729279d0c6c06bbe661acc50c8e433df7a04286 Mon Sep 17 00:00:00 2001 From: Andrey Kataev Date: Mon, 20 Oct 2025 19:13:30 +0300 Subject: [PATCH 09/14] =?UTF-8?q?=D0=98=D1=81=D0=BA=D0=BB=D1=8E=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20server.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/lint-and-test.yml | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index 956390d..decec89 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -13,7 +13,7 @@ permissions: jobs: lint-and-test: - uses: esclient/tools/.github/workflows/lint-and-test-python.yml@84db005db60d5ca2a357d1796b8b3648a2898077 + uses: esclient/tools/.github/workflows/lint-and-test-python.yml@0f001c373b846dc27d20073437537483fadd39ff with: python-version: "3.13.7" source: "commentservice" diff --git a/pyproject.toml b/pyproject.toml index d385cef..f367c1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,6 +109,7 @@ include = [ "src/commentservice/service/*", "src/commentservice/repository/*", ] +omit = ["src/commentservice/server.py"] [project] name = "commentservice" From 118b2443dbae4ec29c8848de260d60fed950959f Mon Sep 17 00:00:00 2001 From: Andrey Kataev Date: Mon, 20 Oct 2025 19:21:21 +0300 Subject: [PATCH 10/14] Update lint-and-test.yml --- .github/workflows/lint-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index decec89..7b23704 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -13,7 +13,7 @@ permissions: jobs: lint-and-test: - uses: esclient/tools/.github/workflows/lint-and-test-python.yml@0f001c373b846dc27d20073437537483fadd39ff + uses: esclient/tools/.github/workflows/lint-and-test-python.yml@v1.0.8 with: python-version: "3.13.7" source: "commentservice" From 4a6443ba6c6111b4138f49e92f5d7e0cc3ea3655 Mon Sep 17 00:00:00 2001 From: Andrey Kataev Date: Mon, 20 Oct 2025 23:00:19 +0300 Subject: [PATCH 11/14] Update lint-and-test.yml --- .github/workflows/lint-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index 7b23704..51a40bc 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -13,7 +13,7 @@ permissions: jobs: lint-and-test: - uses: esclient/tools/.github/workflows/lint-and-test-python.yml@v1.0.8 + uses: esclient/tools/.github/workflows/lint-and-test-python.yml@c42231390bcf175f277712f0db1174c39257258a with: python-version: "3.13.7" source: "commentservice" From 4a4696bb8c15aad2bc1bc9800f994da58cf83a4e Mon Sep 17 00:00:00 2001 From: Andrey Kataev Date: Mon, 20 Oct 2025 23:05:33 +0300 Subject: [PATCH 12/14] Update lint-and-test.yml --- .github/workflows/lint-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index 51a40bc..9b59265 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -13,7 +13,7 @@ permissions: jobs: lint-and-test: - uses: esclient/tools/.github/workflows/lint-and-test-python.yml@c42231390bcf175f277712f0db1174c39257258a + uses: esclient/tools/.github/workflows/lint-and-test-python.yml@v1.0.9 with: python-version: "3.13.7" source: "commentservice" From 2f39b5848be55a7c90fe3b429aedddeafbac3a88 Mon Sep 17 00:00:00 2001 From: Andrey Kataev Date: Mon, 20 Oct 2025 23:16:56 +0300 Subject: [PATCH 13/14] Update lint-and-test.yml --- .github/workflows/lint-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index 9b59265..333ac17 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -13,7 +13,7 @@ permissions: jobs: lint-and-test: - uses: esclient/tools/.github/workflows/lint-and-test-python.yml@v1.0.9 + uses: esclient/tools/.github/workflows/lint-and-test-python.yml@6e3e64fee289eacd97170c58ba6b5fce2c8852eb with: python-version: "3.13.7" source: "commentservice" From 6531a6ecccbc5ccaec91ce296c4892040c078f57 Mon Sep 17 00:00:00 2001 From: Andrey Kataev Date: Mon, 20 Oct 2025 23:21:21 +0300 Subject: [PATCH 14/14] Update lint-and-test.yml --- .github/workflows/lint-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index 333ac17..734f605 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -13,7 +13,7 @@ permissions: jobs: lint-and-test: - uses: esclient/tools/.github/workflows/lint-and-test-python.yml@6e3e64fee289eacd97170c58ba6b5fce2c8852eb + uses: esclient/tools/.github/workflows/lint-and-test-python.yml@v1.0.0 with: python-version: "3.13.7" source: "commentservice"