diff --git a/tests/auxil/monkeypatch.py b/tests/auxil/monkeypatch.py new file mode 100644 index 00000000000..087ea80232d --- /dev/null +++ b/tests/auxil/monkeypatch.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import asyncio + + +async def return_true(*args, **kwargs): + return True + + +async def empty_get_updates(*args, **kwargs): + # The `await` gives the event loop a chance to run other tasks + await asyncio.sleep(0) + return [] diff --git a/tests/auxil/networking.py b/tests/auxil/networking.py index 7c20da7ac94..a695eb232f7 100644 --- a/tests/auxil/networking.py +++ b/tests/auxil/networking.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. from pathlib import Path -from typing import Optional +from typing import Optional, Tuple import pytest from httpx import AsyncClient, AsyncHTTPTransport, Response @@ -26,7 +26,7 @@ from telegram._utils.strings import TextEncoding from telegram._utils.types import ODVInput from telegram.error import BadRequest, RetryAfter, TimedOut -from telegram.request import HTTPXRequest, RequestData +from telegram.request import BaseRequest, HTTPXRequest, RequestData class NonchalantHttpxRequest(HTTPXRequest): @@ -60,6 +60,33 @@ async def _request_wrapper( pytest.xfail(f"Ignoring TimedOut error: {e}") +class OfflineRequest(BaseRequest): + """This Request class disallows making requests to Telegram's servers. + Use this in tests that should not depend on the network. + """ + + async def initialize(self) -> None: + pass + + async def shutdown(self) -> None: + pass + + def __init__(self, *args, **kwargs): + pass + + async def do_request( + self, + url: str, + method: str, + request_data: Optional[RequestData] = None, + read_timeout: ODVInput[float] = BaseRequest.DEFAULT_NONE, + write_timeout: ODVInput[float] = BaseRequest.DEFAULT_NONE, + connect_timeout: ODVInput[float] = BaseRequest.DEFAULT_NONE, + pool_timeout: ODVInput[float] = BaseRequest.DEFAULT_NONE, + ) -> Tuple[int, bytes]: + pytest.fail("OfflineRequest: Network access disallowed in this test") + + async def expect_bad_request(func, message, reason): """ Wrapper for testing bot functions expected to result in an :class:`telegram.error.BadRequest`. diff --git a/tests/auxil/pytest_classes.py b/tests/auxil/pytest_classes.py index 1b976b02e6c..b80945b6704 100644 --- a/tests/auxil/pytest_classes.py +++ b/tests/auxil/pytest_classes.py @@ -25,7 +25,7 @@ from tests.auxil.ci_bots import BOT_INFO_PROVIDER from tests.auxil.constants import PRIVATE_KEY from tests.auxil.envvars import TEST_WITH_OPT_DEPS -from tests.auxil.networking import NonchalantHttpxRequest +from tests.auxil.networking import NonchalantHttpxRequest, OfflineRequest def _get_bot_user(token: str) -> User: @@ -93,17 +93,20 @@ class PytestUpdater(Updater): pass -def make_bot(bot_info=None, **kwargs): +def make_bot(bot_info=None, offline: bool = False, **kwargs): """ 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")) private_key = kwargs.pop("private_key", PRIVATE_KEY) kwargs.pop("token", None) + + request_class = OfflineRequest if offline else NonchalantHttpxRequest + return PytestExtBot( token=token, private_key=private_key if TEST_WITH_OPT_DEPS else None, - request=NonchalantHttpxRequest(8), - get_updates_request=NonchalantHttpxRequest(1), + request=request_class(8), + get_updates_request=request_class(1), **kwargs, ) diff --git a/tests/conftest.py b/tests/conftest.py index c721605bdb5..e8ef01cabac 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -37,15 +37,14 @@ Update, User, ) -from telegram.ext import ApplicationBuilder, Defaults, Updater -from telegram.ext.filters import MessageFilter, UpdateFilter +from telegram.ext import Defaults from tests.auxil.build_messages import DATE from tests.auxil.ci_bots import BOT_INFO_PROVIDER from tests.auxil.constants import PRIVATE_KEY from tests.auxil.envvars import RUN_TEST_OFFICIAL, TEST_WITH_OPT_DEPS from tests.auxil.files import data_file from tests.auxil.networking import NonchalantHttpxRequest -from tests.auxil.pytest_classes import PytestApplication, PytestBot, make_bot +from tests.auxil.pytest_classes import PytestBot, make_bot from tests.auxil.timezones import BasicTimezone if TEST_WITH_OPT_DEPS: @@ -124,6 +123,15 @@ async def bot(bot_info): yield _bot +@pytest.fixture(scope="session") +async def offline_bot(bot_info): + """Makes an offline Bot instance with the given bot_info + Note that in tests/ext we also override the `bot` fixture to return the offline bot instead. + """ + async with make_bot(bot_info, offline=True) as _bot: + yield _bot + + @pytest.fixture def one_time_bot(bot_info): """A function scoped bot since the session bot would shutdown when `async with app` finishes""" @@ -211,28 +219,6 @@ def subscription_channel_id(bot_info): return bot_info["subscription_channel_id"] -@pytest.fixture -async def app(bot_info): - # 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.fixture -async def updater(bot_info): - # We build a new bot each time so that we use `updater` in a context manager without problems - up = Updater(bot=make_bot(bot_info), update_queue=asyncio.Queue()) - yield up - if up.running: - await up.stop() - await up.shutdown() - - @pytest.fixture def thumb_file(): with data_file("thumb.jpg").open("rb") as f: @@ -245,23 +231,6 @@ def class_thumb_file(): yield f -@pytest.fixture( - scope="class", - params=[{"class": MessageFilter}, {"class": UpdateFilter}], - ids=["MessageFilter", "UpdateFilter"], -) -def mock_filter(request): - class MockFilter(request.param["class"]): - def __init__(self): - super().__init__() - self.tested = False - - def filter(self, _): - self.tested = True - - return MockFilter() - - def _get_false_update_fixture_decorator_params(): message = Message(1, DATE, Chat(1, ""), from_user=User(1, "", False), text="test") params = [ diff --git a/tests/ext/conftest.py b/tests/ext/conftest.py new file mode 100644 index 00000000000..f1a877b6e27 --- /dev/null +++ b/tests/ext/conftest.py @@ -0,0 +1,102 @@ +import asyncio + +import pytest + +from telegram.ext import ApplicationBuilder, Updater +from telegram.ext.filters import MessageFilter, UpdateFilter +from tests.auxil.constants import PRIVATE_KEY +from tests.auxil.envvars import TEST_WITH_OPT_DEPS +from tests.auxil.monkeypatch import return_true +from tests.auxil.networking import OfflineRequest +from tests.auxil.pytest_classes import PytestApplication, PytestBot, make_bot + +# This module overrides the bot fixtures defined in the global conftest.py to use the offline bot. +# We don't want the tests on telegram.ext to depend on the network, so we use the offline bot +# instead. + + +@pytest.fixture(scope="session") +async def bot(bot_info, offline_bot): + return offline_bot + + +@pytest.fixture +async def app(bot_info, monkeypatch): + # 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, offline=True)) + .application_class(PytestApplication) + .build() + ) + monkeypatch.setattr(application.bot, "delete_webhook", return_true) + monkeypatch.setattr(application.bot, "set_webhook", return_true) + yield application + if application.running: + await application.stop() + await application.shutdown() + + +@pytest.fixture +async def updater(bot_info, monkeypatch): + # We build a new bot each time so that we use `updater` in a context manager without problems + up = Updater(bot=make_bot(bot_info, offline=True), update_queue=asyncio.Queue()) + monkeypatch.setattr(up.bot, "delete_webhook", return_true) + monkeypatch.setattr(up.bot, "set_webhook", return_true) + yield up + if up.running: + await up.stop() + await up.shutdown() + + +@pytest.fixture +def one_time_bot(bot_info): + """A function scoped bot since the session bot would shutdown when `async with app` finishes""" + return make_bot(bot_info, offline=True) + + +@pytest.fixture(scope="session") +async def cdc_bot(bot_info): + """Makes an ExtBot instance with the given bot_info that uses arbitrary callback_data""" + async with make_bot(bot_info, arbitrary_callback_data=True, offline=True) as _bot: + yield _bot + + +@pytest.fixture(scope="session") +async def raw_bot(bot_info): + """Makes an regular Bot instance with the given bot_info""" + async with PytestBot( + bot_info["token"], + private_key=PRIVATE_KEY if TEST_WITH_OPT_DEPS else None, + request=OfflineRequest(), + get_updates_request=OfflineRequest(), + ) as _bot: + yield _bot + + +@pytest.fixture +async def one_time_raw_bot(bot_info): + """Makes an regular Bot instance with the given bot_info""" + return PytestBot( + bot_info["token"], + private_key=PRIVATE_KEY if TEST_WITH_OPT_DEPS else None, + request=OfflineRequest(), + get_updates_request=OfflineRequest(), + ) + + +@pytest.fixture( + scope="class", + params=[{"class": MessageFilter}, {"class": UpdateFilter}], + ids=["MessageFilter", "UpdateFilter"], +) +def mock_filter(request): + class MockFilter(request.param["class"]): + def __init__(self): + super().__init__() + self.tested = False + + def filter(self, _): + self.tested = True + + return MockFilter() diff --git a/tests/ext/test_application.py b/tests/ext/test_application.py index c423c5d5fbf..2826f4cad99 100644 --- a/tests/ext/test_application.py +++ b/tests/ext/test_application.py @@ -60,6 +60,7 @@ from tests.auxil.asyncio_helpers import call_after from tests.auxil.build_messages import make_message_update from tests.auxil.files import PROJECT_ROOT_PATH +from tests.auxil.monkeypatch import empty_get_updates, return_true from tests.auxil.networking import send_webhook_message from tests.auxil.pytest_classes import PytestApplication, PytestUpdater, make_bot from tests.auxil.slots import mro_slots @@ -432,7 +433,7 @@ def test_builder(self, app): @pytest.mark.parametrize("job_queue", [True, False]) @pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning") - async def test_start_stop_processing_updates(self, one_time_bot, job_queue): + async def test_start_stop_processing_updates(self, one_time_bot, job_queue, monkeypatch): # TODO: repeat a similar test for create_task, persistence processing and job queue if job_queue: app = ApplicationBuilder().bot(one_time_bot).build() @@ -442,6 +443,9 @@ async def test_start_stop_processing_updates(self, one_time_bot, job_queue): async def callback(u, c): self.received = u + monkeypatch.setattr(app.bot, "get_updates", empty_get_updates) + monkeypatch.setattr(app.bot, "delete_webhook", return_true) + assert not app.running assert not app.updater.running if job_queue: @@ -1519,11 +1523,6 @@ def thread_target(): def test_run_polling_timeout_deprecation_warnings( self, timeout_name, monkeypatch, recwarn, app ): - async def get_updates(*args, **kwargs): - # This makes sure that other coroutines have a chance of running as well - await asyncio.sleep(0) - return [] - def thread_target(): waited = 0 while not app.running: @@ -1536,7 +1535,7 @@ def thread_target(): os.kill(os.getpid(), signal.SIGINT) - monkeypatch.setattr(app.bot, "get_updates", get_updates) + monkeypatch.setattr(app.bot, "get_updates", empty_get_updates) thread = Thread(target=thread_target) thread.start() @@ -1563,11 +1562,6 @@ def thread_target(): def test_run_polling_post_init(self, one_time_bot, monkeypatch): events = [] - async def get_updates(*args, **kwargs): - # This makes sure that other coroutines have a chance of running as well - await asyncio.sleep(0) - return [] - def thread_target(): waited = 0 while not app.running: @@ -1589,7 +1583,7 @@ async def post_init(app: Application) -> None: .build() ) app.bot._unfreeze() - monkeypatch.setattr(app.bot, "get_updates", get_updates) + monkeypatch.setattr(app.bot, "get_updates", empty_get_updates) monkeypatch.setattr( app, "initialize", call_after(app.initialize, lambda _: events.append("init")) ) @@ -1598,6 +1592,7 @@ async def post_init(app: Application) -> None: "start_polling", call_after(app.updater.start_polling, lambda _: events.append("start_polling")), ) + monkeypatch.setattr(app.bot, "delete_webhook", return_true) thread = Thread(target=thread_target) thread.start() @@ -1612,11 +1607,6 @@ async def post_init(app: Application) -> None: def test_run_polling_post_shutdown(self, one_time_bot, monkeypatch): events = [] - async def get_updates(*args, **kwargs): - # This makes sure that other coroutines have a chance of running as well - await asyncio.sleep(0) - return [] - def thread_target(): waited = 0 while not app.running: @@ -1638,7 +1628,7 @@ async def post_shutdown(app: Application) -> None: .build() ) app.bot._unfreeze() - monkeypatch.setattr(app.bot, "get_updates", get_updates) + monkeypatch.setattr(app.bot, "get_updates", empty_get_updates) monkeypatch.setattr( app, "shutdown", call_after(app.shutdown, lambda _: events.append("shutdown")) ) @@ -1647,6 +1637,7 @@ async def post_shutdown(app: Application) -> None: "shutdown", call_after(app.updater.shutdown, lambda _: events.append("updater.shutdown")), ) + monkeypatch.setattr(app.bot, "delete_webhook", return_true) thread = Thread(target=thread_target) thread.start() @@ -1665,11 +1656,6 @@ async def post_shutdown(app: Application) -> None: def test_run_polling_post_stop(self, one_time_bot, monkeypatch): events = [] - async def get_updates(*args, **kwargs): - # This makes sure that other coroutines have a chance of running as well - await asyncio.sleep(0) - return [] - def thread_target(): waited = 0 while not app.running: @@ -1691,7 +1677,7 @@ async def post_stop(app: Application) -> None: .build() ) app.bot._unfreeze() - monkeypatch.setattr(app.bot, "get_updates", get_updates) + monkeypatch.setattr(app.bot, "get_updates", empty_get_updates) monkeypatch.setattr(app, "stop", call_after(app.stop, lambda _: events.append("stop"))) monkeypatch.setattr( app.updater, @@ -1703,6 +1689,7 @@ async def post_stop(app: Application) -> None: "shutdown", call_after(app.updater.shutdown, lambda _: events.append("updater.shutdown")), ) + monkeypatch.setattr(app.bot, "delete_webhook", return_true) thread = Thread(target=thread_target) thread.start() @@ -1784,12 +1771,6 @@ def thread_target(): def test_run_webhook_basic(self, app, monkeypatch, caplog): assertions = {} - async def delete_webhook(*args, **kwargs): - return True - - async def set_webhook(*args, **kwargs): - return True - def thread_target(): waited = 0 while not app.running: @@ -1820,8 +1801,6 @@ def thread_target(): assertions["updater_not_running"] = not app.updater.running assertions["job_queue_not_running"] = not app.job_queue.scheduler.running - monkeypatch.setattr(app.bot, "set_webhook", set_webhook) - monkeypatch.setattr(app.bot, "delete_webhook", delete_webhook) app.add_handler(TypeHandler(object, self.callback_set_count(42))) thread = Thread(target=thread_target) @@ -1857,17 +1836,6 @@ def thread_target(): def test_run_webhook_post_init(self, one_time_bot, monkeypatch): events = [] - async def delete_webhook(*args, **kwargs): - return True - - async def set_webhook(*args, **kwargs): - return True - - async def get_updates(*args, **kwargs): - # This makes sure that other coroutines have a chance of running as well - await asyncio.sleep(0) - return [] - def thread_target(): waited = 0 while not app.running: @@ -1889,8 +1857,7 @@ async def post_init(app: Application) -> None: .build() ) app.bot._unfreeze() - monkeypatch.setattr(app.bot, "set_webhook", set_webhook) - monkeypatch.setattr(app.bot, "delete_webhook", delete_webhook) + monkeypatch.setattr( app, "initialize", call_after(app.initialize, lambda _: events.append("init")) ) @@ -1899,6 +1866,8 @@ async def post_init(app: Application) -> None: "start_webhook", call_after(app.updater.start_webhook, lambda _: events.append("start_webhook")), ) + monkeypatch.setattr(app.bot, "delete_webhook", return_true) + monkeypatch.setattr(app.bot, "set_webhook", return_true) thread = Thread(target=thread_target) thread.start() @@ -1923,17 +1892,6 @@ async def post_init(app: Application) -> None: def test_run_webhook_post_shutdown(self, one_time_bot, monkeypatch): events = [] - async def delete_webhook(*args, **kwargs): - return True - - async def set_webhook(*args, **kwargs): - return True - - async def get_updates(*args, **kwargs): - # This makes sure that other coroutines have a chance of running as well - await asyncio.sleep(0) - return [] - def thread_target(): waited = 0 while not app.running: @@ -1955,8 +1913,7 @@ async def post_shutdown(app: Application) -> None: .build() ) app.bot._unfreeze() - monkeypatch.setattr(app.bot, "set_webhook", set_webhook) - monkeypatch.setattr(app.bot, "delete_webhook", delete_webhook) + monkeypatch.setattr( app, "shutdown", call_after(app.shutdown, lambda _: events.append("shutdown")) ) @@ -1965,6 +1922,8 @@ async def post_shutdown(app: Application) -> None: "shutdown", call_after(app.updater.shutdown, lambda _: events.append("updater.shutdown")), ) + monkeypatch.setattr(app.bot, "delete_webhook", return_true) + monkeypatch.setattr(app.bot, "set_webhook", return_true) thread = Thread(target=thread_target) thread.start() @@ -1993,17 +1952,6 @@ async def post_shutdown(app: Application) -> None: def test_run_webhook_post_stop(self, one_time_bot, monkeypatch): events = [] - async def delete_webhook(*args, **kwargs): - return True - - async def set_webhook(*args, **kwargs): - return True - - async def get_updates(*args, **kwargs): - # This makes sure that other coroutines have a chance of running as well - await asyncio.sleep(0) - return [] - def thread_target(): waited = 0 while not app.running: @@ -2025,8 +1973,7 @@ async def post_stop(app: Application) -> None: .build() ) app.bot._unfreeze() - monkeypatch.setattr(app.bot, "set_webhook", set_webhook) - monkeypatch.setattr(app.bot, "delete_webhook", delete_webhook) + monkeypatch.setattr(app, "stop", call_after(app.stop, lambda _: events.append("stop"))) monkeypatch.setattr( app.updater, @@ -2038,6 +1985,8 @@ async def post_stop(app: Application) -> None: "shutdown", call_after(app.updater.shutdown, lambda _: events.append("updater.shutdown")), ) + monkeypatch.setattr(app.bot, "delete_webhook", return_true) + monkeypatch.setattr(app.bot, "set_webhook", return_true) thread = Thread(target=thread_target) thread.start() @@ -2217,10 +2166,6 @@ async def update_logger_callback(update, context): async def callback(*args, **kwargs): called_callbacks.add(kwargs["name"]) - async def get_updates(*args, **kwargs): - await asyncio.sleep(0) - return [] - for cls, method, entry in [ (Application, "initialize", "app_initialize"), (Application, "start", "app_start"), @@ -2249,7 +2194,8 @@ def after(_, name): .post_shutdown(functools.partial(callback, name="post_shutdown")) .build() ) - monkeypatch.setattr(app.bot, "get_updates", get_updates) + monkeypatch.setattr(app.bot, "get_updates", empty_get_updates) + monkeypatch.setattr(app.bot, "delete_webhook", return_true) app.add_handler(TypeHandler(object, update_logger_callback), group=-10) app.add_handler(TypeHandler(object, handler_callback)) @@ -2322,6 +2268,9 @@ def _after_shutdown(*args, **kwargs): Updater, "shutdown", call_after(Updater.shutdown, after_shutdown("updater")) ) app = ApplicationBuilder().bot(one_time_bot).build() + monkeypatch.setattr(app.bot, "delete_webhook", return_true) + monkeypatch.setattr(app.bot, "get_updates", empty_get_updates) + with pytest.raises(RuntimeError, match="Test Exception"): app.run_polling(close_loop=False) @@ -2418,7 +2367,9 @@ def signal_handler_test(*args, **kwargs): received_signals.append(args[0]) loop = asyncio.get_event_loop() + monkeypatch.setattr(loop, "add_signal_handler", signal_handler_test) + monkeypatch.setattr(app.bot, "get_updates", empty_get_updates) def abort_app(): raise SystemExit @@ -2496,16 +2447,6 @@ def test_stop_running(self, one_time_bot, monkeypatch, method): called_stop_running = threading.Event() assertions = {} - async def get_updates(*args, **kwargs): - await asyncio.sleep(0) - return [] - - async def delete_webhook(*args, **kwargs): - return True - - async def set_webhook(*args, **kwargs): - return True - async def post_init(app): # Simply calling app.update_queue.put_nowait(method) in the thread_target doesn't work # for some reason (probably threading magic), so we use an event from the thread_target @@ -2523,9 +2464,7 @@ async def task(app): .post_init(post_init) .build() ) - monkeypatch.setattr(app.bot, "get_updates", get_updates) - monkeypatch.setattr(app.bot, "set_webhook", set_webhook) - monkeypatch.setattr(app.bot, "delete_webhook", delete_webhook) + monkeypatch.setattr(app.bot, "get_updates", empty_get_updates) events = [] monkeypatch.setattr( @@ -2543,6 +2482,8 @@ async def task(app): "shutdown", call_after(app.shutdown, lambda _: events.append("app.shutdown")), ) + monkeypatch.setattr(app.bot, "set_webhook", return_true) + monkeypatch.setattr(app.bot, "delete_webhook", return_true) def thread_target(): waited = 0 diff --git a/tests/ext/test_callbackcontext.py b/tests/ext/test_callbackcontext.py index 0a099f64f15..9a5f64e6f21 100644 --- a/tests/ext/test_callbackcontext.py +++ b/tests/ext/test_callbackcontext.py @@ -20,7 +20,6 @@ import pytest from telegram import ( - Bot, CallbackQuery, Chat, InlineKeyboardButton, @@ -194,8 +193,7 @@ def test_application_attribute(self, app): callback_context = CallbackContext(app) assert callback_context.application is app - def test_drop_callback_data_exception(self, bot, app): - non_ext_bot = Bot(bot.token) + def test_drop_callback_data_exception(self, bot, app, raw_bot): update = Update( 0, message=Message(0, None, Chat(1, "chat"), from_user=User(1, "user", False)) ) @@ -206,7 +204,7 @@ def test_drop_callback_data_exception(self, bot, app): callback_context.drop_callback_data(None) try: - app.bot = non_ext_bot + app.bot = raw_bot with pytest.raises(RuntimeError, match="telegram.Bot does not allow for"): callback_context.drop_callback_data(None) finally: diff --git a/tests/_inline/test_inlinequeryhandler.py b/tests/ext/test_inlinequeryhandler.py similarity index 100% rename from tests/_inline/test_inlinequeryhandler.py rename to tests/ext/test_inlinequeryhandler.py diff --git a/tests/test_pollhandler.py b/tests/ext/test_pollhandler.py similarity index 100% rename from tests/test_pollhandler.py rename to tests/ext/test_pollhandler.py diff --git a/tests/ext/test_updater.py b/tests/ext/test_updater.py index 84a86c988da..b2b218cc137 100644 --- a/tests/ext/test_updater.py +++ b/tests/ext/test_updater.py @@ -30,12 +30,12 @@ from telegram._utils.defaultvalue import DEFAULT_NONE from telegram.error import InvalidToken, RetryAfter, TelegramError, TimedOut from telegram.ext import ExtBot, InvalidCallbackData, Updater -from telegram.request import HTTPXRequest from tests.auxil.build_messages import make_message, make_message_update from tests.auxil.envvars import TEST_WITH_OPT_DEPS from tests.auxil.files import TEST_DATA_PATH, data_file +from tests.auxil.monkeypatch import empty_get_updates, return_true from tests.auxil.networking import send_webhook_message -from tests.auxil.pytest_classes import PytestBot, make_bot +from tests.auxil.pytest_classes import make_bot from tests.auxil.slots import mro_slots UNIX_AVAILABLE = False @@ -179,14 +179,11 @@ async def test_start_without_initialize(self, updater, method): @pytest.mark.parametrize("method", ["start_polling", "start_webhook"]) async def test_shutdown_while_running(self, updater, method, monkeypatch): - async def set_webhook(*args, **kwargs): - return True - - monkeypatch.setattr(updater.bot, "set_webhook", set_webhook) - ip = "127.0.0.1" port = randrange(1024, 49152) # Select random port + monkeypatch.setattr(updater.bot, "get_updates", empty_get_updates) + async with updater: if "webhook" in method: await getattr(updater, method)( @@ -246,13 +243,12 @@ async def get_updates(*args, **kwargs): await asyncio.sleep(0.1) return [] - orig_del_webhook = updater.bot.delete_webhook - async def delete_webhook(*args, **kwargs): # Dropping pending updates is done by passing the parameter to delete_webhook if kwargs.get("drop_pending_updates"): self.message_count += 1 - return await orig_del_webhook(*args, **kwargs) + await asyncio.sleep(0) + return True monkeypatch.setattr(updater.bot, "get_updates", get_updates) monkeypatch.setattr(updater.bot, "delete_webhook", delete_webhook) @@ -264,7 +260,6 @@ async def delete_webhook(*args, **kwargs): await updates.join() await updater.stop() assert not updater.running - assert not (await updater.bot.get_webhook_info()).url if drop_pending_updates: assert self.message_count == 1 else: @@ -281,7 +276,6 @@ async def delete_webhook(*args, **kwargs): await updates.join() await updater.stop() assert not updater.running - assert not (await updater.bot.get_webhook_info()).url self.received = [] self.message_count = 0 @@ -384,11 +378,8 @@ async def get_updates(*args, **kwargs): assert log_found async def test_polling_mark_updates_as_read_failure(self, monkeypatch, updater, caplog): - async def get_updates(*args, **kwargs): - await asyncio.sleep(0) - return [] - monkeypatch.setattr(updater.bot, "get_updates", get_updates) + monkeypatch.setattr(updater.bot, "get_updates", empty_get_updates) async with updater: await updater.start_polling() @@ -411,7 +402,10 @@ async def get_updates(*args, **kwargs): assert log_found - async def test_start_polling_already_running(self, updater): + async def test_start_polling_already_running(self, updater, monkeypatch): + + monkeypatch.setattr(updater.bot, "get_updates", empty_get_updates) + async with updater: await updater.start_polling() task = asyncio.create_task(updater.start_polling()) @@ -498,15 +492,13 @@ async def get_updates(*args, **kwargs): async def test_start_polling_bootstrap_retries( self, updater, monkeypatch, exception_class, retries ): - async def do_request(*args, **kwargs): + async def delete_webhook(*args, **kwargs): self.message_count += 1 raise exception_class(str(self.message_count)) - async with updater: - # Patch within the context so that updater.bot.initialize can still be called - # by the context manager - monkeypatch.setattr(HTTPXRequest, "do_request", do_request) + monkeypatch.setattr(updater.bot, "delete_webhook", delete_webhook) + async with updater: if exception_class == InvalidToken: with pytest.raises(InvalidToken, match="1"): await updater.start_polling(bootstrap_retries=retries) @@ -705,14 +697,23 @@ async def delete_webhook(*args, **kwargs): "unix", [None, "file_path", "socket_object"] if UNIX_AVAILABLE else [None] ) async def test_webhook_basic( - self, monkeypatch, updater, drop_pending_updates, ext_bot, secret_token, unix, file_path + self, + monkeypatch, + updater, + drop_pending_updates, + ext_bot, + secret_token, + unix, + file_path, + one_time_bot, + one_time_raw_bot, ): # Testing with both ExtBot and Bot to make sure any logic in WebhookHandler # that depends on this distinction works if ext_bot and not isinstance(updater.bot, ExtBot): - updater.bot = ExtBot(updater.bot.token) + updater.bot = one_time_bot if not ext_bot and type(updater.bot) is not Bot: - updater.bot = PytestBot(updater.bot.token) + updater.bot = one_time_raw_bot async def delete_webhook(*args, **kwargs): # Dropping pending updates is done by passing the parameter to delete_webhook @@ -720,10 +721,7 @@ async def delete_webhook(*args, **kwargs): self.message_count += 1 return True - async def set_webhook(*args, **kwargs): - return True - - monkeypatch.setattr(updater.bot, "set_webhook", set_webhook) + monkeypatch.setattr(updater.bot, "set_webhook", return_true) monkeypatch.setattr(updater.bot, "delete_webhook", delete_webhook) ip = "127.0.0.1" @@ -873,12 +871,6 @@ async def test_no_unix(self, updater): await updater.start_webhook(unix="DoesntMatter", webhook_url="TOKEN") async def test_start_webhook_already_running(self, updater, monkeypatch): - async def return_true(*args, **kwargs): - return True - - monkeypatch.setattr(updater.bot, "set_webhook", return_true) - monkeypatch.setattr(updater.bot, "delete_webhook", return_true) - ip = "127.0.0.1" port = randrange(1024, 49152) # Select random port async with updater: @@ -977,12 +969,9 @@ async def test_webhook_arbitrary_callback_data( extensively in test_bot.py in conjunction with get_updates.""" updater = Updater(bot=cdc_bot, update_queue=asyncio.Queue()) - async def return_true(*args, **kwargs): - return True + monkeypatch.setattr(updater.bot, "set_webhook", return_true) try: - monkeypatch.setattr(updater.bot, "set_webhook", return_true) - monkeypatch.setattr(updater.bot, "delete_webhook", return_true) ip = "127.0.0.1" port = randrange(1024, 49152) # Select random port @@ -1025,11 +1014,6 @@ async def return_true(*args, **kwargs): updater.bot.callback_data_cache.clear_callback_queries() async def test_webhook_invalid_ssl(self, monkeypatch, updater): - async def return_true(*args, **kwargs): - return True - - monkeypatch.setattr(updater.bot, "set_webhook", return_true) - monkeypatch.setattr(updater.bot, "delete_webhook", return_true) ip = "127.0.0.1" port = randrange(1024, 49152) # Select random port @@ -1056,9 +1040,6 @@ async def set_webhook(**kwargs): self.test_flag.append(bool(kwargs.get("certificate"))) return True - async def return_true(*args, **kwargs): - return True - orig_wh_server_init = WebhookServer.__init__ def webhook_server_init(*args, **kwargs): @@ -1066,7 +1047,7 @@ def webhook_server_init(*args, **kwargs): orig_wh_server_init(*args, **kwargs) monkeypatch.setattr(updater.bot, "set_webhook", set_webhook) - monkeypatch.setattr(updater.bot, "delete_webhook", return_true) + monkeypatch.setattr( "telegram.ext._utils.webhookhandler.WebhookServer.__init__", webhook_server_init ) @@ -1088,15 +1069,13 @@ def webhook_server_init(*args, **kwargs): async def test_start_webhook_bootstrap_retries( self, updater, monkeypatch, exception_class, retries ): - async def do_request(*args, **kwargs): + async def set_webhook(*args, **kwargs): self.message_count += 1 raise exception_class(str(self.message_count)) - async with updater: - # Patch within the context so that updater.bot.initialize can still be called - # by the context manager - monkeypatch.setattr(HTTPXRequest, "do_request", do_request) + monkeypatch.setattr(updater.bot, "set_webhook", set_webhook) + async with updater: if exception_class == InvalidToken: with pytest.raises(InvalidToken, match="1"): await updater.start_webhook(bootstrap_retries=retries) @@ -1107,11 +1086,6 @@ async def do_request(*args, **kwargs): ) async def test_webhook_invalid_posts(self, updater, monkeypatch): - async def return_true(*args, **kwargs): - return True - - monkeypatch.setattr(updater.bot, "set_webhook", return_true) - monkeypatch.setattr(updater.bot, "delete_webhook", return_true) ip = "127.0.0.1" port = randrange(1024, 49152) @@ -1146,17 +1120,9 @@ async def return_true(*args, **kwargs): await updater.stop() async def test_webhook_update_de_json_fails(self, monkeypatch, updater, caplog): - async def delete_webhook(*args, **kwargs): - return True - - async def set_webhook(*args, **kwargs): - return True - def de_json_fails(*args, **kwargs): raise TypeError("Invalid input") - monkeypatch.setattr(updater.bot, "set_webhook", set_webhook) - monkeypatch.setattr(updater.bot, "delete_webhook", delete_webhook) orig_de_json = Update.de_json monkeypatch.setattr(Update, "de_json", de_json_fails)