diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e516f8ea1..711e00dd6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,13 +15,6 @@ jobs: matrix: python-version: [3.6, 3.7, 3.8, 3.9] os: [ubuntu-latest, windows-latest, macos-latest] - include: - - os: ubuntu-latest - python-version: 3.7 - test-build: True - - os: windows-latest - python-version: 3.7 - test-build: True fail-fast: False steps: - uses: actions/checkout@v2 @@ -40,18 +33,24 @@ jobs: python -W ignore -m pip install -r requirements-dev.txt - name: Test with pytest + # We run three different suites here + # 1. Test just the build process + # 2. Test just test_no_passport.py without passport dependencies being installed + # 3. Test everything else + # The second one is achieved by mocking the imports, see test_no_passport.py for details run: | - pytest -v -m nocoverage - nocov_exit=$? - pytest -v -m "not nocoverage" --cov - cov_exit=$? - global_exit=$(( nocov_exit > cov_exit ? nocov_exit : cov_exit )) + pytest -v -s --cov -k test_no_passport.py + no_passport_exit=$? + export TEST_NO_PASSPORT='false' + pytest -v -s --cov --cov-append + passport_exit=$? + global_exit=$(( passport_exit > no_passport_exit ? passport_exit : no_passport_exit )) exit ${global_exit} env: JOB_INDEX: ${{ strategy.job-index }} BOTS: W3sidG9rZW4iOiAiNjk2MTg4NzMyOkFBR1Z3RUtmSEhsTmpzY3hFRE5LQXdraEdzdFpfa28xbUMwIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WldGaU1UUmxNbVF5TnpNeSIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMi43IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMzkwOTgzOTk3IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzI3X2JvdCJ9LCB7InRva2VuIjogIjY3MTQ2ODg4NjpBQUdQR2ZjaVJJQlVORmU4MjR1SVZkcTdKZTNfWW5BVE5HdyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpHWXdPVGxrTXpNeE4yWTIiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTQ0NjAyMjUyMiIsICJib3RfdXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zNF9ib3QifSwgeyJ0b2tlbiI6ICI2MjkzMjY1Mzg6QUFGUnJaSnJCN29CM211ekdzR0pYVXZHRTVDUXpNNUNVNG8iLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpNbU01WVdKaFl6a3hNMlUxIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgQ1B5dGhvbiAzLjUiLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDE0OTY5MTc3NTAiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfdHJhdmlzX2NweXRob25fMzVfYm90In0sIHsidG9rZW4iOiAiNjQwMjA4OTQzOkFBRmhCalFwOXFtM1JUeFN6VXBZekJRakNsZS1Kano1aGNrIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WXpoa1pUZzFOamMxWXpWbCIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMy42IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMzMzODcxNDYxIiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzM2X2JvdCJ9LCB7InRva2VuIjogIjY5NTEwNDA4ODpBQUhmenlsSU9qU0lJUy1lT25JMjB5MkUyMEhvZEhzZnotMCIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk9HUTFNRGd3WmpJd1pqRmwiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNyIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTQ3ODI5MzcxNCIsICJib3RfdXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zN19ib3QifSwgeyJ0b2tlbiI6ICI2OTE0MjM1NTQ6QUFGOFdrakNaYm5IcVBfaTZHaFRZaXJGRWxackdhWU9oWDAiLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpZamM1TlRoaU1tUXlNV1ZoIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgUHlQeSAyLjciLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDEzNjM5MzI1NzMiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfdHJhdmlzX3B5cHlfMjdfYm90In0sIHsidG9rZW4iOiAiNjg0MzM5OTg0OkFBRk1nRUVqcDAxcjVyQjAwN3lDZFZOc2c4QWxOc2FVLWNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TVRBek1UWTNNR1V5TmpnMCIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIFB5UHkgMy41IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDA3ODM2NjA1IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19weXB5XzM1X2JvdCJ9LCB7InRva2VuIjogIjY5MDA5MTM0NzpBQUZMbVI1cEFCNVljcGVfbU9oN3pNNEpGQk9oMHozVDBUbyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpEaGxOekU1TURrd1lXSmkiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIEFwcFZleW9yIHVzaW5nIENQeXRob24gMy40IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMjc5NjAwMDI2IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX2FwcHZleW9yX2NweXRob25fMzRfYm90In0sIHsidG9rZW4iOiAiNjk0MzA4MDUyOkFBRUIyX3NvbkNrNTVMWTlCRzlBTy1IOGp4aVBTNTVvb0JBIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WW1aaVlXWm1NakpoWkdNeSIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gQXBwVmV5b3IgdXNpbmcgQ1B5dGhvbiAyLjciLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDEyOTMwNzkxNjUiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfYXBwdmV5b3JfY3B5dGhvbl8yN19ib3QifSwgeyJ0b2tlbiI6ICIxMDU1Mzk3NDcxOkFBRzE4bkJfUzJXQXd1SjNnN29oS0JWZ1hYY2VNbklPeVNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpBd056QXpZalZpTkdOayIsICJuYW1lIjogIlBUQiB0ZXN0cyBbMF0iLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDExODU1MDk2MzYiLCAidXNlcm5hbWUiOiAicHRiXzBfYm90In0sIHsidG9rZW4iOiAiMTA0NzMyNjc3MTpBQUY4bk90ODFGcFg4bGJidno4VWV3UVF2UmZUYkZmQnZ1SSIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOllUVTFOVEk0WkdSallqbGkiLCAibmFtZSI6ICJQVEIgdGVzdHMgWzFdIiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDg0Nzk3NjEyIiwgInVzZXJuYW1lIjogInB0Yl8xX2JvdCJ9LCB7InRva2VuIjogIjk3MTk5Mjc0NTpBQUdPa09hVzBOSGpnSXY1LTlqUWJPajR2R3FkaFNGLVV1cyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk5XWmtNV1ZoWWpsallqVTUiLCAibmFtZSI6ICJQVEIgdGVzdHMgWzJdIiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDAyMjU1MDcwIiwgInVzZXJuYW1lIjogInB0Yl8yX2JvdCJ9XQ== - TEST_BUILD: ${{ matrix.test-build }} - TEST_PRE_COMMIT: ${{ matrix.test-pre-commit }} + TEST_BUILD: "true" + TEST_NO_PASSPORT: "true" shell: bash --noprofile --norc {0} - name: Submit coverage diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9bf9e0311..f5a22a65d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,12 +21,12 @@ repos: args: - --rcfile=setup.cfg - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.790 + rev: v0.800 hooks: - id: mypy files: ^(telegram|examples)/.*\.py$ - repo: https://github.com/asottile/pyupgrade - rev: v2.7.4 + rev: v2.10.0 hooks: - id: pyupgrade files: ^(telegram|examples|tests)/.*\.py$ diff --git a/README.rst b/README.rst index 9ae16124d..54545e923 100644 --- a/README.rst +++ b/README.rst @@ -137,6 +137,16 @@ In case you have a previously cloned local repository already, you should initia $ git submodule update --init --recursive +--------------------- +Optional Dependencies +--------------------- + +PTB can be installed with optional dependencies: + +* ``pip install python-telegram-bot[passport]`` installs the `cryptography `_ library. Use this, if you want to use Telegram Passport related functionality. +* ``pip install python-telegram-bot[ujson]`` installs the `ujson `_ library. It will then be used for JSON de- & encoding, which can bring speed up compared to the standard `json `_ library. +* ``pip install python-telegram-bot[socks]`` installs the `PySocks `_ library. Use this, if you want to work behind a Socks5 server. + =============== Getting started =============== diff --git a/README_RAW.rst b/README_RAW.rst index 483c77ce7..4670bf7ac 100644 --- a/README_RAW.rst +++ b/README_RAW.rst @@ -137,6 +137,15 @@ Note Installing the `.tar.gz` archive available on PyPi directly via `pip` will *not* work as expected, as `pip` does not recognize that it should use `setup-raw.py` instead of `setup.py`. +--------------------- +Optional Dependencies +--------------------- + +PTB can be installed with optional dependencies: + +* ``pip install python-telegram-bot-raw[passport]`` installs the `cryptography `_ library. Use this, if you want to use Telegram Passport related functionality. +* ``pip install python-telegram-bot-raw[ujson]`` installs the `ujson `_ library. It will then be used for JSON de- & encoding, which can bring speed up compared to the standard `json `_ library. + =============== Getting started =============== diff --git a/requirements-dev.txt b/requirements-dev.txt index bf34254ae..90af297b3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,14 +1,15 @@ +# cryptography is an optional dependency, but running the tests properly requires it +cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3 + pre-commit # Make sure that the versions specified here match the pre-commit settings black==20.8b1 flake8==3.8.4 pylint==2.6.0 -mypy==0.790 -pyupgrade==2.7.4 +mypy==0.800 +pyupgrade==2.10.0 -pytest==4.2.0 -# Need older attrs version for pytest 4.2.0 -attrs==19.1.0 +pytest==6.2.2 flaky beautifulsoup4 diff --git a/requirements.txt b/requirements.txt index 13b262c50..69828590f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ certifi -cryptography!=3.4 # only telegram.ext: # Keep this line here; used in setup(-raw).py tornado>=5.1 APScheduler==3.6.3 diff --git a/setup.cfg b/setup.cfg index e0dbe7a74..240159cc4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,9 @@ addopts = --no-success-flaky-report -rsxX filterwarnings = error ignore::DeprecationWarning - ignore::telegram.utils.deprecate.TelegramDeprecationWarning +; Unfortunately due to https://github.com/pytest-dev/pytest/issues/8343 we can't have this here +; and instead do a trick directly in tests/conftest.py +; ignore::telegram.utils.deprecate.TelegramDeprecationWarning [coverage:run] branch = True diff --git a/setup.py b/setup.py index 7de97893f..762288fd0 100644 --- a/setup.py +++ b/setup.py @@ -84,7 +84,9 @@ def get_setup_kwargs(raw=False): install_requires=requirements, extras_require={ 'json': 'ujson', - 'socks': 'PySocks' + 'socks': 'PySocks', + # 3.4-3.4.3 contained some cyclical import bugs + 'passport': 'cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3', }, include_package_data=True, classifiers=[ diff --git a/telegram/bot.py b/telegram/bot.py index c59eddf0c..94ea76af2 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -41,8 +41,15 @@ try: except ImportError: import json # type: ignore[no-redef] # noqa: F723 -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization +try: + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import serialization + + CRYPTO_INSTALLED = True +except ImportError: + default_backend = None # type: ignore[assignment] + serialization = None # type: ignore[assignment] + CRYPTO_INSTALLED = False from telegram import ( Animation, @@ -212,6 +219,11 @@ class Bot(TelegramObject): self.logger = logging.getLogger(__name__) if private_key: + if not CRYPTO_INSTALLED: + raise RuntimeError( + 'To use Telegram Passports, PTB must be installed via `pip install ' + 'python-telegram-bot[passport]`.' + ) self.private_key = serialization.load_pem_private_key( private_key, password=private_key_password, backend=default_backend() ) diff --git a/telegram/passport/credentials.py b/telegram/passport/credentials.py index 83ea97ab7..8f7944270 100644 --- a/telegram/passport/credentials.py +++ b/telegram/passport/credentials.py @@ -25,12 +25,21 @@ except ImportError: from base64 import b64decode from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union, no_type_check -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP -from cryptography.hazmat.primitives.ciphers import Cipher -from cryptography.hazmat.primitives.ciphers.algorithms import AES -from cryptography.hazmat.primitives.ciphers.modes import CBC -from cryptography.hazmat.primitives.hashes import SHA1, SHA256, SHA512, Hash +try: + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP + from cryptography.hazmat.primitives.ciphers import Cipher + from cryptography.hazmat.primitives.ciphers.algorithms import AES + from cryptography.hazmat.primitives.ciphers.modes import CBC + from cryptography.hazmat.primitives.hashes import SHA1, SHA256, SHA512, Hash + + CRYPTO_INSTALLED = True +except ImportError: + default_backend = None + MGF1, OAEP, Cipher, AES, CBC = (None, None, None, None, None) # type: ignore[misc] + SHA1, SHA256, SHA512, Hash = (None, None, None, None) # type: ignore[misc] + + CRYPTO_INSTALLED = False from telegram import TelegramError, TelegramObject from telegram.utils.types import JSONDict @@ -74,6 +83,11 @@ def decrypt(secret, hash, data): :obj:`bytes`: The decrypted data as bytes. """ + if not CRYPTO_INSTALLED: + raise RuntimeError( + 'To use Telegram Passports, PTB must be installed via `pip install ' + 'python-telegram-bot[passport]`.' + ) # Make a SHA512 hash of secret + update digest = Hash(SHA512(), backend=default_backend()) digest.update(secret + hash) @@ -153,6 +167,11 @@ class EncryptedCredentials(TelegramObject): private/public key but can also suggest malformed/tampered data. """ if self._decrypted_secret is None: + if not CRYPTO_INSTALLED: + raise RuntimeError( + 'To use Telegram Passports, PTB must be installed via `pip install ' + 'python-telegram-bot[passport]`.' + ) # Try decrypting according to step 1 at # https://core.telegram.org/passport#decrypting-data # We make sure to base64 decode the secret first. diff --git a/tests/conftest.py b/tests/conftest.py index fa6f97b62..f7d58c06b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -46,6 +46,14 @@ from telegram.ext import Dispatcher, JobQueue, Updater, MessageFilter, Defaults, from telegram.error import BadRequest from tests.bots import get_bot + +# This is here instead of in setup.cfg due to https://github.com/pytest-dev/pytest/issues/8343 +def pytest_runtestloop(session): + session.add_marker( + pytest.mark.filterwarnings('ignore::telegram.utils.deprecate.TelegramDeprecationWarning') + ) + + GITHUB_ACTION = os.getenv('GITHUB_ACTION', False) if GITHUB_ACTION: @@ -56,6 +64,14 @@ if GITHUB_ACTION: PRIVATE_KEY = b"-----BEGIN RSA PRIVATE KEY-----\r\nMIIEowIBAAKCAQEA0AvEbNaOnfIL3GjB8VI4M5IaWe+GcK8eSPHkLkXREIsaddum\r\nwPBm/+w8lFYdnY+O06OEJrsaDtwGdU//8cbGJ/H/9cJH3dh0tNbfszP7nTrQD+88\r\nydlcYHzClaG8G+oTe9uEZSVdDXj5IUqR0y6rDXXb9tC9l+oSz+ShYg6+C4grAb3E\r\nSTv5khZ9Zsi/JEPWStqNdpoNuRh7qEYc3t4B/a5BH7bsQENyJSc8AWrfv+drPAEe\r\njQ8xm1ygzWvJp8yZPwOIYuL+obtANcoVT2G2150Wy6qLC0bD88Bm40GqLbSazueC\r\nRHZRug0B9rMUKvKc4FhG4AlNzBCaKgIcCWEqKwIDAQABAoIBACcIjin9d3Sa3S7V\r\nWM32JyVF3DvTfN3XfU8iUzV7U+ZOswA53eeFM04A/Ly4C4ZsUNfUbg72O8Vd8rg/\r\n8j1ilfsYpHVvphwxaHQlfIMa1bKCPlc/A6C7b2GLBtccKTbzjARJA2YWxIaqk9Nz\r\nMjj1IJK98i80qt29xRnMQ5sqOO3gn2SxTErvNchtBiwOH8NirqERXig8VCY6fr3n\r\nz7ZImPU3G/4qpD0+9ULrt9x/VkjqVvNdK1l7CyAuve3D7ha3jPMfVHFtVH5gqbyp\r\nKotyIHAyD+Ex3FQ1JV+H7DkP0cPctQiss7OiO9Zd9C1G2OrfQz9el7ewAPqOmZtC\r\nKjB3hUECgYEA/4MfKa1cvaCqzd3yUprp1JhvssVkhM1HyucIxB5xmBcVLX2/Kdhn\r\nhiDApZXARK0O9IRpFF6QVeMEX7TzFwB6dfkyIePsGxputA5SPbtBlHOvjZa8omMl\r\nEYfNa8x/mJkvSEpzvkWPascuHJWv1cEypqphu/70DxubWB5UKo/8o6cCgYEA0HFy\r\ncgwPMB//nltHGrmaQZPFT7/Qgl9ErZT3G9S8teWY4o4CXnkdU75tBoKAaJnpSfX3\r\nq8VuRerF45AFhqCKhlG4l51oW7TUH50qE3GM+4ivaH5YZB3biwQ9Wqw+QyNLAh/Q\r\nnS4/Wwb8qC9QuyEgcCju5lsCaPEXZiZqtPVxZd0CgYEAshBG31yZjO0zG1TZUwfy\r\nfN3euc8mRgZpSdXIHiS5NSyg7Zr8ZcUSID8jAkJiQ3n3OiAsuq1MGQ6kNa582kLT\r\nFPQdI9Ea8ahyDbkNR0gAY9xbM2kg/Gnro1PorH9PTKE0ekSodKk1UUyNrg4DBAwn\r\nqE6E3ebHXt/2WmqIbUD653ECgYBQCC8EAQNX3AFegPd1GGxU33Lz4tchJ4kMCNU0\r\nN2NZh9VCr3nTYjdTbxsXU8YP44CCKFG2/zAO4kymyiaFAWEOn5P7irGF/JExrjt4\r\nibGy5lFLEq/HiPtBjhgsl1O0nXlwUFzd7OLghXc+8CPUJaz5w42unqT3PBJa40c3\r\nQcIPdQKBgBnSb7BcDAAQ/Qx9juo/RKpvhyeqlnp0GzPSQjvtWi9dQRIu9Pe7luHc\r\nm1Img1EO1OyE3dis/rLaDsAa2AKu1Yx6h85EmNjavBqP9wqmFa0NIQQH8fvzKY3/\r\nP8IHY6009aoamLqYaexvrkHVq7fFKiI6k8myMJ6qblVNFv14+KXU\r\n-----END RSA PRIVATE KEY-----" # noqa: E501 +def env_var_2_bool(env_var: object) -> bool: + if isinstance(env_var, bool): + return env_var + if not isinstance(env_var, str): + return False + return env_var.lower().strip() == 'true' + + @pytest.fixture(scope='session') def bot_info(): return get_bot() diff --git a/tests/test_bot.py b/tests/test_bot.py index 21f8890a4..d8cb9618d 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -354,7 +354,7 @@ class TestBot: assert message_quiz.poll.explanation_entities == explanation_entities @flaky(3, 1) - @pytest.mark.timeout(10) + @pytest.mark.timeout(15) @pytest.mark.parametrize(['open_period', 'close_date'], [(5, None), (None, True)]) def test_send_open_period(self, bot, super_group_id, open_period, close_date): question = 'Is this a test?' diff --git a/tests/test_constants.py b/tests/test_constants.py index fab086ff7..d3becb494 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -32,7 +32,6 @@ class TestConstants: with pytest.raises( BadRequest, match='Message is too long', - message='MAX_MESSAGE_LENGTH is no longer valid', ): bot.send_message(chat_id=chat_id, text='a' * (constants.MAX_MESSAGE_LENGTH + 1)) @@ -48,7 +47,6 @@ class TestConstants: with pytest.raises( BadRequest, match="Media_caption_too_long", - message='MAX_CAPTION_LENGTH is no longer valid', ): with open('tests/data/telegram.png', 'rb') as f: bot.send_photo(photo=f, caption=bad_caption, chat_id=chat_id) diff --git a/tests/test_meta.py b/tests/test_meta.py index ea330cd47..35b1f80a2 100644 --- a/tests/test_meta.py +++ b/tests/test_meta.py @@ -20,26 +20,18 @@ import os import pytest - -def call_pre_commit_hook(hook_id): - __tracebackhide__ = True - return os.system(' '.join(['pre-commit', 'run', hook_id, '--all-files'])) # pragma: no cover +from tests.conftest import env_var_2_bool -@pytest.mark.nocoverage -@pytest.mark.parametrize('hook_id', ('black', 'flake8', 'pylint', 'mypy')) -@pytest.mark.skipif(not os.getenv('TEST_PRE_COMMIT', False), reason='TEST_PRE_COMMIT not enabled') -def test_pre_commit_hook(hook_id): - assert call_pre_commit_hook(hook_id) == 0 # pragma: no cover - - -@pytest.mark.nocoverage -@pytest.mark.skipif(not os.getenv('TEST_BUILD', False), reason='TEST_BUILD not enabled') +@pytest.mark.skipif( + not env_var_2_bool(os.getenv('TEST_BUILD', False)), reason='TEST_BUILD not enabled' +) def test_build(): assert os.system('python setup.py bdist_dumb') == 0 # pragma: no cover -@pytest.mark.nocoverage -@pytest.mark.skipif(not os.getenv('TEST_BUILD', False), reason='TEST_BUILD not enabled') +@pytest.mark.skipif( + not env_var_2_bool(os.getenv('TEST_BUILD', False)), reason='TEST_BUILD not enabled' +) def test_build_raw(): assert os.system('python setup-raw.py bdist_dumb') == 0 # pragma: no cover diff --git a/tests/test_no_passport.py b/tests/test_no_passport.py new file mode 100644 index 000000000..8345f6ced --- /dev/null +++ b/tests/test_no_passport.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2021 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. + +""" +This file tests the case that PTB was installed *without* the optional dependency `passport`. +Currently this only means that cryptography is not installed. + +Because imports in pytest are intricate, we just run + pytest -k test_no_passport.py + +with the TEST_NO_PASSPORT environment variable set in addition to the regular test suite. +Because actually uninstalling the optional dependencies would lead to errors in the test suite we +just mock the import to raise the expected exception. + +Note that a fixture that just does this for every test that needs it is a nice idea, but for some +reason makes test_updater.py hang indefinitely on GitHub Actions (at least when Hinrich tried that) +""" +import os +from importlib import reload +from unittest import mock + +import pytest + +from telegram import bot +from telegram.passport import credentials +from tests.conftest import env_var_2_bool + +TEST_NO_PASSPORT = env_var_2_bool(os.getenv('TEST_NO_PASSPORT', False)) + +if TEST_NO_PASSPORT: + orig_import = __import__ + + def import_mock(module_name, *args, **kwargs): + if module_name.startswith('cryptography'): + raise ModuleNotFoundError('We are testing without cryptography here') + return orig_import(module_name, *args, **kwargs) + + with mock.patch('builtins.__import__', side_effect=import_mock): + reload(bot) + reload(credentials) + + +class TestNoPassport: + """ + The monkeypatches simulate cryptography not being installed even when TEST_NO_PASSPORT is + False, though that doesn't test the actual imports + """ + + def test_bot_init(self, bot_info, monkeypatch): + if not TEST_NO_PASSPORT: + monkeypatch.setattr(bot, 'CRYPTO_INSTALLED', False) + with pytest.raises(RuntimeError, match='passport'): + bot.Bot(bot_info['token'], private_key=1, private_key_password=2) + + def test_credentials_decrypt(self, monkeypatch): + if not TEST_NO_PASSPORT: + monkeypatch.setattr(credentials, 'CRYPTO_INSTALLED', False) + with pytest.raises(RuntimeError, match='passport'): + credentials.decrypt(1, 1, 1) + + def test_encrypted_credentials_decrypted_secret(self, monkeypatch): + if not TEST_NO_PASSPORT: + monkeypatch.setattr(credentials, 'CRYPTO_INSTALLED', False) + ec = credentials.EncryptedCredentials('data', 'hash', 'secret') + with pytest.raises(RuntimeError, match='passport'): + ec.decrypted_secret diff --git a/tests/test_official.py b/tests/test_official.py index 94c71bc42..996ac624d 100644 --- a/tests/test_official.py +++ b/tests/test_official.py @@ -25,6 +25,7 @@ from bs4 import BeautifulSoup from telegram.vendor.ptb_urllib3 import urllib3 import telegram +from tests.conftest import env_var_2_bool IGNORED_OBJECTS = ('ResponseParameters', 'CallbackGame') IGNORED_PARAMETERS = { @@ -170,6 +171,8 @@ for thing in soup.select('h4 > a.anchor'): @pytest.mark.parametrize(('method', 'data'), argvalues=argvalues, ids=names) -@pytest.mark.skipif(os.getenv('TEST_OFFICIAL') != 'true', reason='test_official is not enabled') +@pytest.mark.skipif( + not env_var_2_bool(os.getenv('TEST_OFFICIAL')), reason='test_official is not enabled' +) def test_official(method, data): method(data) diff --git a/tests/test_updater.py b/tests/test_updater.py index 3fdf7036e..9431df6a4 100644 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -111,6 +111,7 @@ class TestUpdater: self.err_handler_called.clear() self.err_handler_called.wait() + @pytest.mark.filterwarnings('ignore:.*:pytest.PytestUnhandledThreadExceptionWarning') def test_get_updates_bailout_err(self, monkeypatch, updater, caplog): error = InvalidToken()