From 010a228380d1aa6988b4978ef9121814579f90df Mon Sep 17 00:00:00 2001 From: Dmitry Afanasyev <71835315+Balshgit@users.noreply.github.com> Date: Wed, 20 Sep 2023 21:56:36 +0300 Subject: [PATCH] feat: add test and fix GitHub actions (#2) --- .github/workflows/check-lint.yml | 4 +- .github/workflows/poetry-test.yml | 4 +- lefthook.yml | 4 +- poetry.lock | 155 +++++++++--- pyproject.toml | 58 +++-- .../integration}/__init__.py | 0 tests/integration/bot/__init__.py | 0 tests/integration/bot/conftest.py | 237 ++++++++++++++++++ tests/integration/bot/networking.py | 101 ++++++++ tests/integration/bot/test_bot_updates.py | 12 + tests/integration/factories/__init__.py | 0 tests/integration/factories/bot.py | 23 +- 12 files changed, 536 insertions(+), 62 deletions(-) rename {app/chat-gpt => tests/integration}/__init__.py (100%) create mode 100644 tests/integration/bot/__init__.py create mode 100644 tests/integration/bot/conftest.py create mode 100644 tests/integration/bot/networking.py create mode 100644 tests/integration/bot/test_bot_updates.py create mode 100644 tests/integration/factories/__init__.py diff --git a/.github/workflows/check-lint.yml b/.github/workflows/check-lint.yml index 89a43cf..28728b3 100644 --- a/.github/workflows/check-lint.yml +++ b/.github/workflows/check-lint.yml @@ -23,13 +23,13 @@ jobs: id: setup-python uses: actions/setup-python@v3 with: - python-version: '3.11.3' + python-version: '3.11.5' #---------------------------------------------- # ----- install & configure poetry ----- #---------------------------------------------- - name: Install poetry env: # Keep in sync with `POETRY_VERSION` in `Dockerfile` - POETRY_VERSION: "1.4.2" + POETRY_VERSION: "1.6.1" run: | curl -sSL "https://install.python-poetry.org" | python - # Adding `poetry` to `$PATH`: diff --git a/.github/workflows/poetry-test.yml b/.github/workflows/poetry-test.yml index 47a4d33..2924f81 100644 --- a/.github/workflows/poetry-test.yml +++ b/.github/workflows/poetry-test.yml @@ -23,13 +23,13 @@ jobs: id: setup-python uses: actions/setup-python@v3 with: - python-version: '3.11.3' + python-version: '3.11.5' #---------------------------------------------- # ----- install & configure poetry ----- #---------------------------------------------- - name: Install poetry env: # Keep in sync with `POETRY_VERSION` in `Dockerfile` - POETRY_VERSION: "1.4.2" + POETRY_VERSION: "1.6.1" run: | curl -sSL "https://install.python-poetry.org" | python - # Adding `poetry` to `$PATH`: diff --git a/lefthook.yml b/lefthook.yml index 5517305..521db56 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -23,7 +23,7 @@ pre-push: run: black --check -S {all_files} mypy: glob: "*.py" - run: mypy {all_files} --namespace-packages + run: mypy app settings tests --config-file pyproject.toml flake8: glob: "*.py" run: flake8 {all_files} @@ -51,7 +51,7 @@ lint: commands: mypy: glob: "*.py" - run: mypy {staged_files} --namespace-packages + run: mypy {staged_files} --namespace-packages --config-file pyproject.toml flake8: glob: "*.py" run: flake8 {staged_files} diff --git a/poetry.lock b/poetry.lock index 14f74c5..8f867b6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -325,13 +325,13 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte [[package]] name = "autoflake" -version = "2.2.0" +version = "2.2.1" description = "Removes unused imports and unused variables" optional = false python-versions = ">=3.8" files = [ - {file = "autoflake-2.2.0-py3-none-any.whl", hash = "sha256:de409b009a34c1c2a7cc2aae84c4c05047f9773594317c6a6968bd497600d4a0"}, - {file = "autoflake-2.2.0.tar.gz", hash = "sha256:62e1f74a0fdad898a96fee6f99fe8241af90ad99c7110c884b35855778412251"}, + {file = "autoflake-2.2.1-py3-none-any.whl", hash = "sha256:265cde0a43c1f44ecfb4f30d95b0437796759d07be7706a2f70e4719234c0f79"}, + {file = "autoflake-2.2.1.tar.gz", hash = "sha256:62b7b6449a692c3c9b0c916919bbc21648da7281e8506bcf8d3f8280e431ebc1"}, ] [package.dependencies] @@ -906,13 +906,13 @@ files = [ [[package]] name = "faker" -version = "19.6.1" +version = "19.6.2" description = "Faker is a Python package that generates fake data for you." optional = false python-versions = ">=3.8" files = [ - {file = "Faker-19.6.1-py3-none-any.whl", hash = "sha256:64c8513c53c3a809075ee527b323a0ba61517814123f3137e4912f5d43350139"}, - {file = "Faker-19.6.1.tar.gz", hash = "sha256:5d6b7880b3bea708075ddf91938424453f07053a59f8fa0453c1870df6ff3292"}, + {file = "Faker-19.6.2-py3-none-any.whl", hash = "sha256:8fba91068dc26e3159c1ac9f22444a2338704b0991d86605322e454bda420092"}, + {file = "Faker-19.6.2.tar.gz", hash = "sha256:d5d5953556b0fb428a46019e03fc2d40eab2980135ddef5a9eb3d054947fdf83"}, ] [package.dependencies] @@ -1015,13 +1015,13 @@ flake8 = ">=5.0.0" [[package]] name = "flake8-bugbear" -version = "23.7.10" +version = "23.9.16" description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." optional = false python-versions = ">=3.8.1" files = [ - {file = "flake8-bugbear-23.7.10.tar.gz", hash = "sha256:0ebdc7d8ec1ca8bd49347694562381f099f4de2f8ec6bda7a7dca65555d9e0d4"}, - {file = "flake8_bugbear-23.7.10-py3-none-any.whl", hash = "sha256:d99d005114020fbef47ed5e4aebafd22f167f9a0fbd0d8bf3c9e90612cb25c34"}, + {file = "flake8-bugbear-23.9.16.tar.gz", hash = "sha256:90cf04b19ca02a682feb5aac67cae8de742af70538590509941ab10ae8351f71"}, + {file = "flake8_bugbear-23.9.16-py3-none-any.whl", hash = "sha256:b182cf96ea8f7a8595b2f87321d7d9b28728f4d9c3318012d896543d19742cb5"}, ] [package.dependencies] @@ -1459,6 +1459,79 @@ gitdb = ">=4.0.1,<5" [package.extras] test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-sugar", "virtualenv"] +[[package]] +name = "greenlet" +version = "2.0.2" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +files = [ + {file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"}, + {file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"}, + {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, + {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, + {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, + {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, + {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, + {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, + {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470"}, + {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a"}, + {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, + {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, + {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, + {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, + {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, + {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, + {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19"}, + {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3"}, + {file = "greenlet-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5"}, + {file = "greenlet-2.0.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6"}, + {file = "greenlet-2.0.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43"}, + {file = "greenlet-2.0.2-cp35-cp35m-win32.whl", hash = "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a"}, + {file = "greenlet-2.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394"}, + {file = "greenlet-2.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75"}, + {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf"}, + {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292"}, + {file = "greenlet-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9"}, + {file = "greenlet-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f"}, + {file = "greenlet-2.0.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73"}, + {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86"}, + {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33"}, + {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, + {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, + {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857"}, + {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a"}, + {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, + {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, + {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, + {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b"}, + {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8"}, + {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9"}, + {file = "greenlet-2.0.2-cp39-cp39-win32.whl", hash = "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5"}, + {file = "greenlet-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564"}, + {file = "greenlet-2.0.2.tar.gz", hash = "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0"}, +] + +[package.extras] +docs = ["Sphinx", "docutils (<0.18)"] +test = ["objgraph", "psutil"] + [[package]] name = "gunicorn" version = "21.2.0" @@ -1715,13 +1788,13 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "jsonschema" -version = "4.19.0" +version = "4.19.1" description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.8" files = [ - {file = "jsonschema-4.19.0-py3-none-any.whl", hash = "sha256:043dc26a3845ff09d20e4420d6012a9c91c9aa8999fa184e7efcfeccb41e32cb"}, - {file = "jsonschema-4.19.0.tar.gz", hash = "sha256:6e1e7569ac13be8139b2dd2c21a55d350066ee3f80df06c608b398cdc6f30e8f"}, + {file = "jsonschema-4.19.1-py3-none-any.whl", hash = "sha256:cd5f1f9ed9444e554b38ba003af06c0a8c2868131e56bfbef0550fb450c0330e"}, + {file = "jsonschema-4.19.1.tar.gz", hash = "sha256:ec84cc37cfa703ef7cd4928db24f9cb31428a5d0fa77747b8b51a847458e0bbf"}, ] [package.dependencies] @@ -2378,13 +2451,13 @@ files = [ [[package]] name = "nest-asyncio" -version = "1.5.7" +version = "1.5.8" description = "Patch asyncio to allow nested event loops" optional = false python-versions = ">=3.5" files = [ - {file = "nest_asyncio-1.5.7-py3-none-any.whl", hash = "sha256:5301c82941b550b3123a1ea772ba9a1c80bad3a182be8c1a5ae6ad3be57a9657"}, - {file = "nest_asyncio-1.5.7.tar.gz", hash = "sha256:6a80f7b98f24d9083ed24608977c09dd608d83f91cccc24c9d2cba6d10e01c10"}, + {file = "nest_asyncio-1.5.8-py3-none-any.whl", hash = "sha256:accda7a339a70599cb08f9dd09a67e0c2ef8d8d6f4c07f96ab203f2ae254e48d"}, + {file = "nest_asyncio-1.5.8.tar.gz", hash = "sha256:25aa2ca0d2a5b5531956b9e273b45cf664cae2b145101d73b86b199978d48fdb"}, ] [[package]] @@ -3737,13 +3810,13 @@ tzdata = {version = "*", markers = "python_version >= \"3.6\""} [[package]] name = "pyupgrade" -version = "3.11.0" +version = "3.11.1" description = "A tool to automatically upgrade syntax for newer versions." optional = false python-versions = ">=3.8.1" files = [ - {file = "pyupgrade-3.11.0-py2.py3-none-any.whl", hash = "sha256:7bd8b83bc1a61b3a4c8fea5e16313b7b29e5cdf1be6184f8c6c467557e9cfab3"}, - {file = "pyupgrade-3.11.0.tar.gz", hash = "sha256:1d0bf0dbadf179ff8952d92d52759a5984526597a055d1626884397c46f94003"}, + {file = "pyupgrade-3.11.1-py2.py3-none-any.whl", hash = "sha256:6e9dd362394b3068123e06ca268de5845d41e2bb29f387b38323cc1009fb3100"}, + {file = "pyupgrade-3.11.1.tar.gz", hash = "sha256:3e6c7689d2f3ae418c6a60ee981477fe9130eccaed3e33dac6c21274cf7d45f4"}, ] [package.dependencies] @@ -4002,13 +4075,13 @@ six = ">=1.7.0" [[package]] name = "rich" -version = "13.5.2" +version = "13.5.3" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.5.2-py3-none-any.whl", hash = "sha256:146a90b3b6b47cac4a73c12866a499e9817426423f57c5a66949c086191a8808"}, - {file = "rich-13.5.2.tar.gz", hash = "sha256:fb9d6c0a0f643c99eed3875b5377a184132ba9be4d61516a55273d3554d75a39"}, + {file = "rich-13.5.3-py3-none-any.whl", hash = "sha256:9257b468badc3d347e146a4faa268ff229039d4c2d176ab0cffb4c4fbc73d5d9"}, + {file = "rich-13.5.3.tar.gz", hash = "sha256:87b43e0543149efa1253f485cd845bb7ee54df16c9617b8a893650ab84b4acb6"}, ] [package.dependencies] @@ -4260,13 +4333,13 @@ files = [ [[package]] name = "smmap" -version = "5.0.0" +version = "5.0.1" description = "A pure Python implementation of a sliding window memory map manager" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, - {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, + {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, + {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, ] [[package]] @@ -4421,13 +4494,13 @@ tests = ["pytest", "pytest-cov"] [[package]] name = "tls-client" -version = "0.2.1" +version = "0.2.2" description = "Advanced Python HTTP Client." optional = false python-versions = "*" files = [ - {file = "tls_client-0.2.1-py3-none-any.whl", hash = "sha256:124a710952b979d5e20b4e2b7879b7958d6e48a259d0f5b83101055eb173f0bd"}, - {file = "tls_client-0.2.1.tar.gz", hash = "sha256:473fb4c671d9d4ca6b818548ab6e955640dd589767bfce520830c5618c2f2e2b"}, + {file = "tls_client-0.2.2-py3-none-any.whl", hash = "sha256:30934871397cdad6862e00b5634f382666314a452ddd3d774e18323a0ad9b765"}, + {file = "tls_client-0.2.2.tar.gz", hash = "sha256:78bc0e291e3aadc6c5e903b62bb26c01374577691f2a9e5e17899900a5927a13"}, ] [[package]] @@ -4581,13 +4654,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.7.1" -description = "Backported and Experimental Type Hints for Python 3.7+" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, - {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, ] [[package]] @@ -4621,13 +4694,13 @@ devenv = ["black", "check-manifest", "flake8", "pyroma", "pytest (>=4.3)", "pyte [[package]] name = "urllib3" -version = "2.0.4" +version = "2.0.5" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.7" files = [ - {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"}, - {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"}, + {file = "urllib3-2.0.5-py3-none-any.whl", hash = "sha256:ef16afa8ba34a1f989db38e1dbbe0c302e4289a47856990d0682e374563ce35e"}, + {file = "urllib3-2.0.5.tar.gz", hash = "sha256:13abf37382ea2ce6fb744d4dad67838eec857c9f4f57009891805e0b5e123594"}, ] [package.dependencies] @@ -5006,20 +5079,20 @@ multidict = ">=4.0" [[package]] name = "zipp" -version = "3.16.2" +version = "3.17.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"}, - {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"}, + {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, + {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "d6373ca5961a3c25ed89b26a5121750b66c9a785644ae04d4b9f7d76e44d0a47" +content-hash = "8410825bec6fb2d9470304c86ede01c6dff319548a471b643527832dc7114d5f" diff --git a/pyproject.toml b/pyproject.toml index 5233c03..f62a0ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ wheel = "^0.41" orjson = "^3.9" websocket-client = "^1.6" +greenlet = "^2.0.2" requests = "^2.31" selenium = "^4.11" tls-client = "^0.2" @@ -90,7 +91,7 @@ assertpy = "^1.1" coverage = "^7.3" -autoflake = "2.2" +autoflake = "^2.2" flake8 = "^6.1" flake8-logging-format = "^0.9" flake8-comprehensions = "^3.14" @@ -136,8 +137,8 @@ ignore = [ ] per-file-ignores = [ # too complex queries - "./app/tests/*: TAE001, S101, S311", - "app/tests/*/factories/*: S5720", + "tests/*: S101", + "tests/integration/bot/conftest.py: NEW100", "settings/config.py: S104" ] @@ -179,19 +180,48 @@ warn_unused_configs = true warn_unreachable = true warn_no_return = true exclude = [ - "app/chat-gpt/*" -] - - -[tool.coverage.run] -relative_files = true - -[tool.pytest.ini_options] -filterwarnings = [ - "error", - "ignore::DeprecationWarning", + "app/chat-gpt/" ] [tool.black] line-length = 120 target-version = ['py311'] + + +[tool.coverage.run] +relative_files = true +concurrency = ["greenlet", "thread"] + +[tool.coverage.report] +sort = "cover" +skip_covered = true + +[tool.pytest.ini_options] +minversion = "7.0" +testpaths = "tests" +python_files = [ + # tests declarations + "test_*.py", + # base test scenarios and helpers for tests + "support.py", +] +filterwarnings = [ + "error", + "ignore::DeprecationWarning", + "ignore:_SixMetaPathImporter.*not found:ImportWarning" +] +addopts = ''' + --disable-socket + --allow-unix-socket + --allow-hosts=::1,127.0.0.1 + --strict-markers + --tb=short + --cov=app + --cov=settings + --cov=tests + --cov-branch + --cov-fail-under=90 + --cov-config=.coveragerc + --cov-context=test + --no-cov +''' \ No newline at end of file diff --git a/app/chat-gpt/__init__.py b/tests/integration/__init__.py similarity index 100% rename from app/chat-gpt/__init__.py rename to tests/integration/__init__.py diff --git a/tests/integration/bot/__init__.py b/tests/integration/bot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/bot/conftest.py b/tests/integration/bot/conftest.py new file mode 100644 index 0000000..9753bde --- /dev/null +++ b/tests/integration/bot/conftest.py @@ -0,0 +1,237 @@ +"""This module contains subclasses of classes from the python-telegram-bot library that +modify behavior of the respective parent classes in order to make them easier to use in the +pytest framework. A common change is to allow monkeypatching of the class members by not +enforcing slots in the subclasses.""" +import asyncio +from asyncio import AbstractEventLoop +from datetime import tzinfo +from typing import Any, AsyncGenerator + +import pytest +import pytest_asyncio +from fastapi import FastAPI +from httpx import AsyncClient +from pytest_asyncio.plugin import SubRequest +from telegram import Bot, User +from telegram.ext import Application, ApplicationBuilder, Defaults, ExtBot + +from app.core.bot import BotApplication +from app.main import Application as AppApplication +from settings.config import get_settings +from tests.integration.bot.networking import NonchalantHttpxRequest +from tests.integration.factories.bot import BotInfoFactory + + +class PytestExtBot(ExtBot): # type: ignore + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + # Makes it easier to work with the bot in tests + self._unfreeze() + + # Here we override get_me for caching because we don't want to call the API repeatedly in tests + async def get_me(self, *args: Any, **kwargs: Any) -> User: + return await _mocked_get_me(self) + + +class PytestBot(Bot): + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + # Makes it easier to work with the bot in tests + self._unfreeze() + + # Here we override get_me for caching because we don't want to call the API repeatedly in tests + async def get_me(self, *args: Any, **kwargs: Any) -> User: + return await _mocked_get_me(self) + + +class PytestApplication(Application): # type: ignore + pass + + +def make_bot(bot_info: dict[str, Any] | None = None, **kwargs: Any) -> PytestExtBot: + """ + Tests are executed on tg.ext.ExtBot, as that class only extends the functionality of tg.bot + """ + token = kwargs.pop("token", (bot_info or {}).get("token")) + kwargs.pop("token", None) + return PytestExtBot( + token=token, + private_key=None, + request=NonchalantHttpxRequest(connection_pool_size=8), + get_updates_request=NonchalantHttpxRequest(connection_pool_size=1), + **kwargs, + ) + + +async def _mocked_get_me(bot: Bot) -> User: + if bot._bot_user is None: + bot._bot_user = _get_bot_user(bot.token) + return bot._bot_user + + +def _get_bot_user(token: str) -> User: + """Used to return a mock user in bot.get_me(). This saves API calls on every init.""" + bot_info = BotInfoFactory() + # We don't take token from bot_info, because we need to make a bot with a specific ID. So we + # generate the correct user_id from the token (token from bot_info is random each test run). + # This is important in e.g. bot equality tests. The other parameters like first_name don't + # matter as much. In the future we may provide a way to get all the correct info from the token + user_id = int(token.split(":")[0]) + first_name = bot_info.get( + "name", + ) + username = bot_info.get( + "username", + ).strip("@") + return User( + user_id, + first_name, + is_bot=True, + username=username, + can_join_groups=True, + can_read_all_group_messages=False, + supports_inline_queries=True, + ) + + +# Redefine the event_loop fixture to have a session scope. Otherwise `bot` fixture can't be +# session. See https://github.com/pytest-dev/pytest-asyncio/issues/68 for more details. +@pytest.fixture(scope="session") +def event_loop(request: SubRequest) -> AbstractEventLoop: + """ + Пересоздаем луп для изоляции тестов. В основном нужно для запуска юнит тестов + в связке с интеграционными, т.к. без этого pytest зависает. + Для интеграционных тестов фикстура определяется дополнительная фикстура на всю сессию. + """ + loop = asyncio.get_event_loop_policy().new_event_loop() + asyncio.set_event_loop(loop) + return loop + + +@pytest.fixture(scope="session") +def bot_info() -> dict[str, Any]: + return BotInfoFactory() + + +@pytest_asyncio.fixture(scope="session") +async def bot(bot_info: dict[str, Any]) -> AsyncGenerator[PytestExtBot, None]: + """Makes an ExtBot instance with the given bot_info""" + async with make_bot(bot_info) as _bot: + yield _bot + + +@pytest.fixture() +def one_time_bot(bot_info: dict[str, Any]) -> PytestExtBot: + """A function scoped bot since the session bot would shutdown when `async with app` finishes""" + return make_bot(bot_info) + + +@pytest_asyncio.fixture(scope="session") +async def cdc_bot(bot_info: dict[str, Any]) -> AsyncGenerator[PytestExtBot, None]: + """Makes an ExtBot instance with the given bot_info that uses arbitrary callback_data""" + async with make_bot(bot_info, arbitrary_callback_data=True) as _bot: + yield _bot + + +@pytest_asyncio.fixture(scope="session") +async def raw_bot(bot_info: dict[str, Any]) -> AsyncGenerator[PytestBot, None]: + """Makes an regular Bot instance with the given bot_info""" + async with PytestBot( + bot_info["token"], + private_key=None, + request=NonchalantHttpxRequest(8), + get_updates_request=NonchalantHttpxRequest(1), + ) as _bot: + yield _bot + + +# Here we store the default bots so that we don't have to create them again and again. +# They are initialized but not shutdown on pytest_sessionfinish because it is causing +# problems with the event loop (Event loop is closed). +_default_bots: dict[Defaults, PytestExtBot] = {} + + +@pytest_asyncio.fixture(scope="session") +async def default_bot(request: SubRequest, bot_info: dict[str, Any]) -> PytestExtBot: + param = request.param if hasattr(request, "param") else {} + defaults = Defaults(**param) + + # If the bot is already created, return it. Else make a new one. + default_bot = _default_bots.get(defaults) + if default_bot is None: + default_bot = make_bot(bot_info, defaults=defaults) + await default_bot.initialize() + _default_bots[defaults] = default_bot # Defaults object is hashable + return default_bot + + +@pytest_asyncio.fixture(scope="session") +async def tz_bot(timezone: tzinfo, bot_info: dict[str, Any]) -> PytestExtBot: + defaults = Defaults(tzinfo=timezone) + try: # If the bot is already created, return it. Saves time since get_me is not called again. + return _default_bots[defaults] + except KeyError: + default_bot = make_bot(bot_info, defaults=defaults) + await default_bot.initialize() + _default_bots[defaults] = default_bot + return default_bot + + +@pytest.fixture(scope="session") +def chat_id(bot_info: dict[str, Any]) -> int: + return bot_info["chat_id"] + + +@pytest.fixture(scope="session") +def super_group_id(bot_info: dict[str, Any]) -> int: + return bot_info["super_group_id"] + + +@pytest.fixture(scope="session") +def forum_group_id(bot_info: dict[str, Any]) -> int: + return int(bot_info["forum_group_id"]) + + +@pytest.fixture(scope="session") +def channel_id(bot_info: dict[str, Any]) -> int: + return bot_info["channel_id"] + + +@pytest.fixture(scope="session") +def provider_token(bot_info: dict[str, Any]) -> str: + return bot_info["payment_provider_token"] + + +@pytest_asyncio.fixture(scope="session") +async def bot_application(bot_info: dict[str, Any]) -> AsyncGenerator[Any, None]: + # We build a new bot each time so that we use `app` in a context manager without problems + application = ApplicationBuilder().bot(make_bot(bot_info)).application_class(PytestApplication).build() + yield application + if application.running: + await application.stop() + await application.shutdown() + + +@pytest_asyncio.fixture(scope="session") +async def main_application(bot_application: PytestApplication) -> FastAPI: + settings = get_settings() + bot_app = BotApplication(settings=settings) + bot_app.application = bot_application + fast_api_app = AppApplication(settings=settings, bot_app=bot_app).fastapi_app + return fast_api_app + + +@pytest_asyncio.fixture() +async def rest_client( + main_application: FastAPI, +) -> AsyncGenerator[AsyncClient, None]: + """ + Default http client. Use to test unauthorized requests, public endpoints + or special authorization methods. + """ + async with AsyncClient( + app=main_application, + base_url="http://test", + headers={"Content-Type": "application/json"}, + ) as client: + yield client diff --git a/tests/integration/bot/networking.py b/tests/integration/bot/networking.py new file mode 100644 index 0000000..af6b686 --- /dev/null +++ b/tests/integration/bot/networking.py @@ -0,0 +1,101 @@ +from typing import Any, Callable, Optional + +import pytest +from httpx import AsyncClient, Response +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import ODVInput +from telegram.error import BadRequest, RetryAfter, TimedOut +from telegram.request import HTTPXRequest, RequestData + + +class NonchalantHttpxRequest(HTTPXRequest): + """This Request class is used in the tests to suppress errors that we don't care about + in the test suite. + """ + + async def _request_wrapper( + self, + url: str, + method: str, + request_data: Optional[RequestData] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + ) -> bytes: + try: + return await super()._request_wrapper( + method=method, + url=url, + request_data=request_data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + ) + except RetryAfter as e: + pytest.xfail(f"Not waiting for flood control: {e}") + except TimedOut as e: + pytest.xfail(f"Ignoring TimedOut error: {e}") + + +async def expect_bad_request(func: Callable[..., Any], message: str, reason: str) -> Callable[..., Any]: + """ + Wrapper for testing bot functions expected to result in an :class:`telegram.error.BadRequest`. + Makes it XFAIL, if the specified error message is present. + + Args: + func: The awaitable to be executed. + message: The expected message of the bad request error. If another message is present, + the error will be reraised. + reason: Explanation for the XFAIL. + + Returns: + On success, returns the return value of :attr:`func` + """ + try: + return await func() + except BadRequest as e: + if message in str(e): + pytest.xfail(f"{reason}. {e}") + else: + raise e + + +async def send_webhook_message( + ip: str, + port: int, + payload_str: str | None, + url_path: str = "", + content_len: int | None = -1, + content_type: str = "application/json", + get_method: str | None = None, + secret_token: str | None = None, +) -> Response: + headers = { + "content-type": content_type, + } + if secret_token: + headers["X-Telegram-Bot-Api-Secret-Token"] = secret_token + + if not payload_str: + content_len = None + payload = None + else: + payload = bytes(payload_str, encoding="utf-8") + + if content_len == -1: + content_len = len(payload) if payload else None + + if content_len is not None: + headers["content-length"] = str(content_len) + + url = f"http://{ip}:{port}/{url_path}" + + async with AsyncClient() as client: + return await client.request( + url=url, + method=get_method or "POST", + data=payload, # type: ignore + headers=headers, + ) diff --git a/tests/integration/bot/test_bot_updates.py b/tests/integration/bot/test_bot_updates.py new file mode 100644 index 0000000..03d58ed --- /dev/null +++ b/tests/integration/bot/test_bot_updates.py @@ -0,0 +1,12 @@ +import pytest +from httpx import AsyncClient + +pytestmark = [ + pytest.mark.asyncio, +] + + +async def test_bot_updates(rest_client: AsyncClient) -> None: + response = await rest_client.get("/api/healthcheck") + + assert response.status_code == 200 diff --git a/tests/integration/factories/__init__.py b/tests/integration/factories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/factories/bot.py b/tests/integration/factories/bot.py index b157639..d408f7d 100644 --- a/tests/integration/factories/bot.py +++ b/tests/integration/factories/bot.py @@ -1,3 +1,4 @@ +import string import time import factory @@ -28,7 +29,7 @@ data = { faker = Faker("ru_RU") -class DeleteUserFactory(factory.Factory): +class UserFactory(factory.Factory): id = factory.Sequence(lambda n: 1000 + n) is_bot = False first_name = factory.Faker("first_name") @@ -49,3 +50,23 @@ class ChatFactory(factory.Factory): class Meta: model = Chat + + +class BotInfoFactory(factory.DictFactory): + token = factory.Faker( + "bothify", text="#########:??????????????????????????-#????????#?", letters=string.ascii_letters + ) # example: 579694714:AAFpK8w6zkkUrD4xSeYwF3MO8e-4Grmcy7c + payment_provider_token = factory.Faker( + "bothify", text="#########:TEST:????????????????", letters=string.ascii_letters + ) # example: 579694714:TEST:K8w6zkkUrD4xSeYw + chat_id = factory.Faker("random_int", min=10**8, max=10**9 - 1) + super_group_id = factory.Faker("random_int", min=-(10**12) - 10**9, max=-(10**12)) # -1001838004577 + forum_group_id = factory.Faker("random_int", min=-(10**12) - 10**9, max=-(10**12)) + channel_name = factory.Faker("name") + channel_id = factory.LazyAttribute(lambda obj: f"@{obj.channel_name}") + name = factory.Faker("name") + fake_username = factory.Faker("name") + username = factory.LazyAttribute(lambda obj: "_".join(f"@{obj.fake_username}".split(" "))) # @Peter_Parker + + class Meta: + exclude = ("channel_name", "fake_username")