Example #1
0
class TestUpdater(object):
    message_count = 0
    received = None
    attempts = 0

    @pytest.fixture(autouse=True)
    def reset(self):
        self.message_count = 0
        self.received = None
        self.attempts = 0

    def error_handler(self, bot, update, error):
        self.received = error.message

    def callback(self, bot, update):
        self.received = update.message.text

    # TODO: test clean= argument

    def test_error_on_get_updates(self, monkeypatch, updater):
        def test(*args, **kwargs):
            raise TelegramError('Test Error 2')

        monkeypatch.setattr('telegram.Bot.get_updates', test)
        monkeypatch.setattr('telegram.Bot.set_webhook',
                            lambda *args, **kwargs: True)
        updater.dispatcher.add_error_handler(self.error_handler)
        updater.start_polling(0.01)
        sleep(.1)
        assert self.received == 'Test Error 2'

    def test_webhook(self, monkeypatch, updater):
        q = Queue()
        monkeypatch.setattr('telegram.Bot.set_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr('telegram.Bot.delete_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr('telegram.ext.Dispatcher.process_update',
                            lambda _, u: q.put(u))

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port for travis
        updater.start_webhook(
            ip,
            port,
            url_path='TOKEN',
            cert='./tests/test_updater.py',
            key='./tests/test_updater.py',
        )
        sleep(.2)
        # SSL-Wrapping will fail, so we start the server without SSL
        thr = Thread(target=updater.httpd.serve_forever)
        thr.start()

        try:
            # Now, we send an update to the server via urlopen
            update = Update(1,
                            message=Message(1,
                                            User(1, '', False),
                                            None,
                                            Chat(1, ''),
                                            text='Webhook'))
            self._send_webhook_msg(ip, port, update.to_json(), 'TOKEN')
            sleep(.2)
            assert q.get(False) == update

            response = self._send_webhook_msg(ip, port, None,
                                              'webookhandler.py')
            assert b'' == response.read()
            assert 200 == response.code

            response = self._send_webhook_msg(ip,
                                              port,
                                              None,
                                              'webookhandler.py',
                                              get_method=lambda: 'HEAD')

            assert b'' == response.read()
            assert 200 == response.code

            # Test multiple shutdown() calls
            updater.httpd.shutdown()
        finally:
            updater.httpd.shutdown()
            thr.join()

    def test_webhook_no_ssl(self, monkeypatch, updater):
        q = Queue()
        monkeypatch.setattr('telegram.Bot.set_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr('telegram.Bot.delete_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr('telegram.ext.Dispatcher.process_update',
                            lambda _, u: q.put(u))

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port for travis
        updater.start_webhook(ip, port, webhook_url=None)
        sleep(.2)

        # Now, we send an update to the server via urlopen
        update = Update(1,
                        message=Message(1,
                                        User(1, '', False),
                                        None,
                                        Chat(1, ''),
                                        text='Webhook 2'))
        self._send_webhook_msg(ip, port, update.to_json())
        sleep(.2)
        assert q.get(False) == update

    def test_bootstrap_retries_success(self, monkeypatch, updater):
        retries = 2

        def attempt(_, *args, **kwargs):
            if self.attempts < retries:
                self.attempts += 1
                raise TelegramError('')

        monkeypatch.setattr('telegram.Bot.set_webhook', attempt)

        updater._bootstrap(retries, False, 'path', None)
        assert self.attempts == retries

    @pytest.mark.parametrize(
        ('error', 'attempts'),
        argvalues=[(TelegramError(''), 2), (Unauthorized(''), 1),
                   (InvalidToken(), 1)],
        ids=('TelegramError', 'Unauthorized', 'InvalidToken'))
    def test_bootstrap_retries_error(self, monkeypatch, updater, error,
                                     attempts):
        retries = 1

        def attempt(_, *args, **kwargs):
            self.attempts += 1
            raise error

        monkeypatch.setattr('telegram.Bot.set_webhook', attempt)

        with pytest.raises(type(error)):
            updater._bootstrap(retries, False, 'path', None)
        assert self.attempts == attempts

    def test_webhook_invalid_posts(self, updater):
        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # select random port for travis
        thr = Thread(target=updater._start_webhook,
                     args=(ip, port, '', None, None, 0, False, None, None))
        thr.start()

        sleep(.2)

        try:
            with pytest.raises(HTTPError) as excinfo:
                self._send_webhook_msg(ip,
                                       port,
                                       '<root><bla>data</bla></root>',
                                       content_type='application/xml')
            assert excinfo.value.code == 403

            with pytest.raises(HTTPError) as excinfo:
                self._send_webhook_msg(ip,
                                       port,
                                       'dummy-payload',
                                       content_len=-2)
            assert excinfo.value.code == 403

            # TODO: prevent urllib or the underlying from adding content-length
            # with pytest.raises(HTTPError) as excinfo:
            #     self._send_webhook_msg(ip, port, 'dummy-payload', content_len=None)
            # assert excinfo.value.code == 411

            with pytest.raises(HTTPError):
                self._send_webhook_msg(ip,
                                       port,
                                       'dummy-payload',
                                       content_len='not-a-number')
            assert excinfo.value.code == 403

        finally:
            updater.httpd.shutdown()
            thr.join()

    def _send_webhook_msg(self,
                          ip,
                          port,
                          payload_str,
                          url_path='',
                          content_len=-1,
                          content_type='application/json',
                          get_method=None):
        headers = {
            'content-type': content_type,
        }

        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 content_len is not None:
            headers['content-length'] = str(content_len)

        url = 'http://{ip}:{port}/{path}'.format(ip=ip,
                                                 port=port,
                                                 path=url_path)

        req = Request(url, data=payload, headers=headers)

        if get_method is not None:
            req.get_method = get_method

        return urlopen(req)

    def signal_sender(self, updater):
        sleep(0.2)
        while not updater.running:
            sleep(0.2)

        os.kill(os.getpid(), signal.SIGTERM)

    @signalskip
    def test_idle(self, updater, caplog):
        updater.start_polling(0.01)
        Thread(target=partial(self.signal_sender, updater=updater)).start()

        with caplog.at_level(logging.INFO):
            updater.idle()

        rec = caplog.records[-1]
        assert rec.msg.startswith('Received signal {}'.format(signal.SIGTERM))
        assert rec.levelname == 'INFO'

        # If we get this far, idle() ran through
        sleep(.5)
        assert updater.running is False

    @signalskip
    def test_user_signal(self, updater):
        temp_var = {'a': 0}

        def user_signal_inc(signum, frame):
            temp_var['a'] = 1

        updater.user_sig_handler = user_signal_inc
        updater.start_polling(0.01)
        Thread(target=partial(self.signal_sender, updater=updater)).start()
        updater.idle()
        # If we get this far, idle() ran through
        sleep(.5)
        assert updater.running is False
        assert temp_var['a'] != 0

    def test_create_bot(self):
        updater = Updater('123:abcd')
        assert updater.bot is not None

    def test_mutual_exclude_token_bot(self):
        bot = Bot('123:zyxw')
        with pytest.raises(ValueError):
            Updater(token='123:abcd', bot=bot)

    def test_no_token_or_bot(self):
        with pytest.raises(ValueError):
            Updater()
Example #2
0
class TestErrors:
    def test_telegram_error(self):
        with pytest.raises(TelegramError, match="^test message$"):
            raise TelegramError("test message")
        with pytest.raises(TelegramError, match="^Test message$"):
            raise TelegramError("Error: test message")
        with pytest.raises(TelegramError, match="^Test message$"):
            raise TelegramError("[Error]: test message")
        with pytest.raises(TelegramError, match="^Test message$"):
            raise TelegramError("Bad Request: test message")

    def test_unauthorized(self):
        with pytest.raises(Forbidden, match="test message"):
            raise Forbidden("test message")
        with pytest.raises(Forbidden, match="^Test message$"):
            raise Forbidden("Error: test message")
        with pytest.raises(Forbidden, match="^Test message$"):
            raise Forbidden("[Error]: test message")
        with pytest.raises(Forbidden, match="^Test message$"):
            raise Forbidden("Bad Request: test message")

    def test_invalid_token(self):
        with pytest.raises(InvalidToken, match="Invalid token"):
            raise InvalidToken

    def test_network_error(self):
        with pytest.raises(NetworkError, match="test message"):
            raise NetworkError("test message")
        with pytest.raises(NetworkError, match="^Test message$"):
            raise NetworkError("Error: test message")
        with pytest.raises(NetworkError, match="^Test message$"):
            raise NetworkError("[Error]: test message")
        with pytest.raises(NetworkError, match="^Test message$"):
            raise NetworkError("Bad Request: test message")

    def test_bad_request(self):
        with pytest.raises(BadRequest, match="test message"):
            raise BadRequest("test message")
        with pytest.raises(BadRequest, match="^Test message$"):
            raise BadRequest("Error: test message")
        with pytest.raises(BadRequest, match="^Test message$"):
            raise BadRequest("[Error]: test message")
        with pytest.raises(BadRequest, match="^Test message$"):
            raise BadRequest("Bad Request: test message")

    def test_timed_out(self):
        with pytest.raises(TimedOut, match="^Timed out$"):
            raise TimedOut

    def test_chat_migrated(self):
        with pytest.raises(
                ChatMigrated,
                match="Group migrated to supergroup. New chat id: 1234"):
            raise ChatMigrated(1234)
        try:
            raise ChatMigrated(1234)
        except ChatMigrated as e:
            assert e.new_chat_id == 1234

    def test_retry_after(self):
        with pytest.raises(
                RetryAfter,
                match="Flood control exceeded. Retry in 12 seconds"):
            raise RetryAfter(12)

    def test_conflict(self):
        with pytest.raises(Conflict, match="Something something."):
            raise Conflict("Something something.")

    @pytest.mark.parametrize(
        "exception, attributes",
        [
            (TelegramError("test message"), ["message"]),
            (Forbidden("test message"), ["message"]),
            (InvalidToken(), ["message"]),
            (NetworkError("test message"), ["message"]),
            (BadRequest("test message"), ["message"]),
            (TimedOut(), ["message"]),
            (ChatMigrated(1234), ["message", "new_chat_id"]),
            (RetryAfter(12), ["message", "retry_after"]),
            (Conflict("test message"), ["message"]),
            (PassportDecryptionError("test message"), ["message"]),
            (InvalidCallbackData("test data"), ["callback_data"]),
        ],
    )
    def test_errors_pickling(self, exception, attributes):
        pickled = pickle.dumps(exception)
        unpickled = pickle.loads(pickled)
        assert type(unpickled) is type(exception)
        assert str(unpickled) == str(exception)

        for attribute in attributes:
            assert getattr(unpickled,
                           attribute) == getattr(exception, attribute)

    @pytest.mark.parametrize(
        "inst",
        [
            (TelegramError("test message")),
            (Forbidden("test message")),
            (InvalidToken()),
            (NetworkError("test message")),
            (BadRequest("test message")),
            (TimedOut()),
            (ChatMigrated(1234)),
            (RetryAfter(12)),
            (Conflict("test message")),
            (PassportDecryptionError("test message")),
            (InvalidCallbackData("test data")),
        ],
    )
    def test_slot_behaviour(self, inst, mro_slots):
        for attr in inst.__slots__:
            assert getattr(inst, attr,
                           "err") != "err", f"got extra slot '{attr}'"
        assert len(mro_slots(inst)) == len(set(
            mro_slots(inst))), "duplicate slot"

    def test_coverage(self):
        """
        This test is only here to make sure that new errors will override __reduce__ and set
        __slots__ properly.
        Add the new error class to the below covered_subclasses dict, if it's covered in the above
        test_errors_pickling and test_slots_behavior tests.
        """
        def make_assertion(cls):
            assert set(cls.__subclasses__()) == covered_subclasses[cls]
            for subcls in cls.__subclasses__():
                make_assertion(subcls)

        covered_subclasses = defaultdict(set)
        covered_subclasses.update({
            TelegramError: {
                Forbidden,
                InvalidToken,
                NetworkError,
                ChatMigrated,
                RetryAfter,
                Conflict,
                PassportDecryptionError,
                InvalidCallbackData,
            },
            NetworkError: {BadRequest, TimedOut},
        })

        make_assertion(TelegramError)

    def test_string_representations(self):
        """We just randomly test a few of the subclasses - should suffice"""
        e = TelegramError("This is a message")
        assert repr(e) == "TelegramError('This is a message')"
        assert str(e) == "This is a message"

        e = RetryAfter(42)
        assert repr(
            e) == "RetryAfter('Flood control exceeded. Retry in 42 seconds')"
        assert str(e) == "Flood control exceeded. Retry in 42 seconds"

        e = BadRequest("This is a message")
        assert repr(e) == "BadRequest('This is a message')"
        assert str(e) == "This is a message"
Example #3
0
class TestUpdater:
    message_count = 0
    received = None
    attempts = 0
    err_handler_called = Event()
    cb_handler_called = Event()
    offset = 0
    test_flag = False

    def test_slot_behaviour(self, updater, mro_slots, recwarn):
        for at in updater.__slots__:
            at = f"_Updater{at}" if at.startswith(
                '__') and not at.endswith('__') else at
            assert getattr(updater, at,
                           'err') != 'err', f"got extra slot '{at}'"
        assert not updater.__dict__, f"got missing slot(s): {updater.__dict__}"
        assert len(mro_slots(updater)) == len(set(
            mro_slots(updater))), "duplicate slot"
        updater.custom, updater.running = 'should give warning', updater.running
        assert len(recwarn) == 1 and 'custom' in str(
            recwarn[0].message), recwarn.list

        class CustomUpdater(Updater):
            pass  # Tests that setting custom attributes of Updater subclass doesn't raise warning

        a = CustomUpdater(updater.bot.token)
        a.my_custom = 'no error!'
        assert len(recwarn) == 1

        updater.__setattr__('__test', 'mangled success')
        assert getattr(updater, '_Updater__test',
                       'e') == 'mangled success', "mangling failed"

    @pytest.fixture(autouse=True)
    def reset(self):
        self.message_count = 0
        self.received = None
        self.attempts = 0
        self.err_handler_called.clear()
        self.cb_handler_called.clear()
        self.test_flag = False

    def error_handler(self, bot, update, error):
        self.received = error.message
        self.err_handler_called.set()

    def callback(self, bot, update):
        self.received = update.message.text
        self.cb_handler_called.set()

    def test_warn_arbitrary_callback_data(self, bot, recwarn):
        Updater(bot=bot, arbitrary_callback_data=True)
        assert len(recwarn) == 1
        assert 'Passing arbitrary_callback_data to an Updater' in str(
            recwarn[0].message)

    @pytest.mark.parametrize(
        ('error', ),
        argvalues=[(TelegramError('Test Error 2'), ),
                   (Unauthorized('Test Unauthorized'), )],
        ids=('TelegramError', 'Unauthorized'),
    )
    def test_get_updates_normal_err(self, monkeypatch, updater, error):
        def test(*args, **kwargs):
            raise error

        monkeypatch.setattr(updater.bot, 'get_updates', test)
        monkeypatch.setattr(updater.bot, 'set_webhook',
                            lambda *args, **kwargs: True)
        updater.dispatcher.add_error_handler(self.error_handler)
        updater.start_polling(0.01)

        # Make sure that the error handler was called
        self.err_handler_called.wait()
        assert self.received == error.message

        # Make sure that Updater polling thread keeps running
        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()

        def test(*args, **kwargs):
            raise error

        with caplog.at_level(logging.DEBUG):
            monkeypatch.setattr(updater.bot, 'get_updates', test)
            monkeypatch.setattr(updater.bot, 'set_webhook',
                                lambda *args, **kwargs: True)
            updater.dispatcher.add_error_handler(self.error_handler)
            updater.start_polling(0.01)
            assert self.err_handler_called.wait(1) is not True

        sleep(1)
        # NOTE: This test might hit a race condition and fail (though the 1 seconds delay above
        #       should work around it).
        # NOTE: Checking Updater.running is problematic because it is not set to False when there's
        #       an unhandled exception.
        # TODO: We should have a way to poll Updater status and decide if it's running or not.
        import pprint

        pprint.pprint([rec.getMessage() for rec in caplog.get_records('call')])
        assert any(f'unhandled exception in Bot:{updater.bot.id}:updater' in
                   rec.getMessage() for rec in caplog.get_records('call'))

    @pytest.mark.parametrize(('error', ),
                             argvalues=[(RetryAfter(0.01), ), (TimedOut(), )],
                             ids=('RetryAfter', 'TimedOut'))
    def test_get_updates_retries(self, monkeypatch, updater, error):
        event = Event()

        def test(*args, **kwargs):
            event.set()
            raise error

        monkeypatch.setattr(updater.bot, 'get_updates', test)
        monkeypatch.setattr(updater.bot, 'set_webhook',
                            lambda *args, **kwargs: True)
        updater.dispatcher.add_error_handler(self.error_handler)
        updater.start_polling(0.01)

        # Make sure that get_updates was called, but not the error handler
        event.wait()
        assert self.err_handler_called.wait(0.5) is not True
        assert self.received != error.message

        # Make sure that Updater polling thread keeps running
        event.clear()
        event.wait()
        assert self.err_handler_called.wait(0.5) is not True

    @pytest.mark.parametrize('ext_bot', [True, False])
    def test_webhook(self, monkeypatch, updater, ext_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)
        if not ext_bot and not type(updater.bot) is Bot:
            updater.bot = Bot(updater.bot.token)

        q = Queue()
        monkeypatch.setattr(updater.bot, 'set_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr('telegram.ext.Dispatcher.process_update',
                            lambda _, u: q.put(u))

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port
        updater.start_webhook(ip, port, url_path='TOKEN')
        sleep(0.2)
        try:
            # Now, we send an update to the server via urlopen
            update = Update(
                1,
                message=Message(1,
                                None,
                                Chat(1, ''),
                                from_user=User(1, '', False),
                                text='Webhook'),
            )
            self._send_webhook_msg(ip, port, update.to_json(), 'TOKEN')
            sleep(0.2)
            assert q.get(False) == update

            # Returns 404 if path is incorrect
            with pytest.raises(HTTPError) as excinfo:
                self._send_webhook_msg(ip, port, None, 'webookhandler.py')
            assert excinfo.value.code == 404

            with pytest.raises(HTTPError) as excinfo:
                self._send_webhook_msg(ip,
                                       port,
                                       None,
                                       'webookhandler.py',
                                       get_method=lambda: 'HEAD')
            assert excinfo.value.code == 404

            # Test multiple shutdown() calls
            updater.httpd.shutdown()
        finally:
            updater.httpd.shutdown()
            sleep(0.2)
            assert not updater.httpd.is_running
            updater.stop()

    @pytest.mark.parametrize('invalid_data', [True, False])
    def test_webhook_arbitrary_callback_data(self, monkeypatch, updater,
                                             invalid_data):
        """Here we only test one simple setup. telegram.ext.ExtBot.insert_callback_data is tested
        extensively in test_bot.py in conjunction with get_updates."""
        updater.bot.arbitrary_callback_data = True
        try:
            q = Queue()
            monkeypatch.setattr(updater.bot, 'set_webhook',
                                lambda *args, **kwargs: True)
            monkeypatch.setattr(updater.bot, 'delete_webhook',
                                lambda *args, **kwargs: True)
            monkeypatch.setattr('telegram.ext.Dispatcher.process_update',
                                lambda _, u: q.put(u))

            ip = '127.0.0.1'
            port = randrange(1024, 49152)  # Select random port
            updater.start_webhook(ip, port, url_path='TOKEN')
            sleep(0.2)
            try:
                # Now, we send an update to the server via urlopen
                reply_markup = InlineKeyboardMarkup.from_button(
                    InlineKeyboardButton(text='text',
                                         callback_data='callback_data'))
                if not invalid_data:
                    reply_markup = updater.bot.callback_data_cache.process_keyboard(
                        reply_markup)

                message = Message(
                    1,
                    None,
                    None,
                    reply_markup=reply_markup,
                )
                update = Update(1, message=message)
                self._send_webhook_msg(ip, port, update.to_json(), 'TOKEN')
                sleep(0.2)
                received_update = q.get(False)
                assert received_update == update

                button = received_update.message.reply_markup.inline_keyboard[
                    0][0]
                if invalid_data:
                    assert isinstance(button.callback_data,
                                      InvalidCallbackData)
                else:
                    assert button.callback_data == 'callback_data'

                # Test multiple shutdown() calls
                updater.httpd.shutdown()
            finally:
                updater.httpd.shutdown()
                sleep(0.2)
                assert not updater.httpd.is_running
                updater.stop()
        finally:
            updater.bot.arbitrary_callback_data = False
            updater.bot.callback_data_cache.clear_callback_data()
            updater.bot.callback_data_cache.clear_callback_queries()

    def test_start_webhook_no_warning_or_error_logs(self, caplog, updater,
                                                    monkeypatch):
        monkeypatch.setattr(updater.bot, 'set_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook',
                            lambda *args, **kwargs: True)
        # prevent api calls from @info decorator when updater.bot.id is used in thread names
        monkeypatch.setattr(updater.bot, '_bot',
                            User(id=123, first_name='bot', is_bot=True))
        monkeypatch.setattr(updater.bot, '_commands', [])

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port
        with caplog.at_level(logging.WARNING):
            updater.start_webhook(ip, port)
            updater.stop()
        assert not caplog.records

    def test_webhook_ssl(self, monkeypatch, updater):
        monkeypatch.setattr(updater.bot, 'set_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook',
                            lambda *args, **kwargs: True)
        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port
        tg_err = False
        try:
            updater._start_webhook(
                ip,
                port,
                url_path='TOKEN',
                cert='./tests/test_updater.py',
                key='./tests/test_updater.py',
                bootstrap_retries=0,
                drop_pending_updates=False,
                webhook_url=None,
                allowed_updates=None,
            )
        except TelegramError:
            tg_err = True
        assert tg_err

    def test_webhook_no_ssl(self, monkeypatch, updater):
        q = Queue()
        monkeypatch.setattr(updater.bot, 'set_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr('telegram.ext.Dispatcher.process_update',
                            lambda _, u: q.put(u))

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port
        updater.start_webhook(ip, port, webhook_url=None)
        sleep(0.2)

        # Now, we send an update to the server via urlopen
        update = Update(
            1,
            message=Message(1,
                            None,
                            Chat(1, ''),
                            from_user=User(1, '', False),
                            text='Webhook 2'),
        )
        self._send_webhook_msg(ip, port, update.to_json())
        sleep(0.2)
        assert q.get(False) == update
        updater.stop()

    def test_webhook_ssl_just_for_telegram(self, monkeypatch, updater):
        q = Queue()

        def set_webhook(**kwargs):
            self.test_flag.append(bool(kwargs.get('certificate')))
            return True

        orig_wh_server_init = WebhookServer.__init__

        def webhook_server_init(*args):
            self.test_flag = [args[-1] is None]
            orig_wh_server_init(*args)

        monkeypatch.setattr(updater.bot, 'set_webhook', set_webhook)
        monkeypatch.setattr(updater.bot, 'delete_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr('telegram.ext.Dispatcher.process_update',
                            lambda _, u: q.put(u))
        monkeypatch.setattr(
            'telegram.ext.utils.webhookhandler.WebhookServer.__init__',
            webhook_server_init)

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port
        updater.start_webhook(ip,
                              port,
                              webhook_url=None,
                              cert='./tests/test_updater.py')
        sleep(0.2)

        # Now, we send an update to the server via urlopen
        update = Update(
            1,
            message=Message(1,
                            None,
                            Chat(1, ''),
                            from_user=User(1, '', False),
                            text='Webhook 2'),
        )
        self._send_webhook_msg(ip, port, update.to_json())
        sleep(0.2)
        assert q.get(False) == update
        updater.stop()
        assert self.test_flag == [True, True]

    @pytest.mark.parametrize('pass_max_connections', [True, False])
    def test_webhook_max_connections(self, monkeypatch, updater,
                                     pass_max_connections):
        q = Queue()
        max_connections = 42

        def set_webhook(**kwargs):
            print(kwargs)
            self.test_flag = kwargs.get('max_connections') == (
                max_connections if pass_max_connections else 40)
            return True

        monkeypatch.setattr(updater.bot, 'set_webhook', set_webhook)
        monkeypatch.setattr(updater.bot, 'delete_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr('telegram.ext.Dispatcher.process_update',
                            lambda _, u: q.put(u))

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port
        if pass_max_connections:
            updater.start_webhook(ip,
                                  port,
                                  webhook_url=None,
                                  max_connections=max_connections)
        else:
            updater.start_webhook(ip, port, webhook_url=None)

        sleep(0.2)

        # Now, we send an update to the server via urlopen
        update = Update(
            1,
            message=Message(1,
                            None,
                            Chat(1, ''),
                            from_user=User(1, '', False),
                            text='Webhook 2'),
        )
        self._send_webhook_msg(ip, port, update.to_json())
        sleep(0.2)
        assert q.get(False) == update
        updater.stop()
        assert self.test_flag is True

    @pytest.mark.parametrize(('error', ),
                             argvalues=[(TelegramError(''), )],
                             ids=('TelegramError', ))
    def test_bootstrap_retries_success(self, monkeypatch, updater, error):
        retries = 2

        def attempt(*args, **kwargs):
            if self.attempts < retries:
                self.attempts += 1
                raise error

        monkeypatch.setattr(updater.bot, 'set_webhook', attempt)

        updater.running = True
        updater._bootstrap(retries, False, 'path', None, bootstrap_interval=0)
        assert self.attempts == retries

    @pytest.mark.parametrize(
        ('error', 'attempts'),
        argvalues=[(TelegramError(''), 2), (Unauthorized(''), 1),
                   (InvalidToken(), 1)],
        ids=('TelegramError', 'Unauthorized', 'InvalidToken'),
    )
    def test_bootstrap_retries_error(self, monkeypatch, updater, error,
                                     attempts):
        retries = 1

        def attempt(*args, **kwargs):
            self.attempts += 1
            raise error

        monkeypatch.setattr(updater.bot, 'set_webhook', attempt)

        updater.running = True
        with pytest.raises(type(error)):
            updater._bootstrap(retries,
                               False,
                               'path',
                               None,
                               bootstrap_interval=0)
        assert self.attempts == attempts

    @pytest.mark.parametrize('drop_pending_updates', (True, False))
    def test_bootstrap_clean_updates(self, monkeypatch, updater,
                                     drop_pending_updates):
        # As dropping pending updates is done by passing `drop_pending_updates` to
        # set_webhook, we just check that we pass the correct value
        self.test_flag = False

        def delete_webhook(**kwargs):
            self.test_flag = kwargs.get(
                'drop_pending_updates') == drop_pending_updates

        monkeypatch.setattr(updater.bot, 'delete_webhook', delete_webhook)

        updater.running = True
        updater._bootstrap(
            1,
            drop_pending_updates=drop_pending_updates,
            webhook_url=None,
            allowed_updates=None,
            bootstrap_interval=0,
        )
        assert self.test_flag is True

    def test_deprecation_warnings_start_webhook(self, recwarn, updater,
                                                monkeypatch):
        monkeypatch.setattr(updater.bot, 'set_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook',
                            lambda *args, **kwargs: True)
        # prevent api calls from @info decorator when updater.bot.id is used in thread names
        monkeypatch.setattr(updater.bot, '_bot',
                            User(id=123, first_name='bot', is_bot=True))
        monkeypatch.setattr(updater.bot, '_commands', [])

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port
        updater.start_webhook(ip, port, clean=True, force_event_loop=False)
        updater.stop()

        for warning in recwarn:
            print(warning)

        try:  # This is for flaky tests (there's an unclosed socket sometimes)
            recwarn.pop(
                ResourceWarning
            )  # internally iterates through recwarn.list and deletes it
        except AssertionError:
            pass

        assert len(recwarn) == 3
        assert str(recwarn[0].message).startswith('Old Handler API')
        assert str(recwarn[1].message).startswith('The argument `clean` of')
        assert str(recwarn[2].message).startswith(
            'The argument `force_event_loop` of')

    def test_clean_deprecation_warning_polling(self, recwarn, updater,
                                               monkeypatch):
        monkeypatch.setattr(updater.bot, 'set_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook',
                            lambda *args, **kwargs: True)
        # prevent api calls from @info decorator when updater.bot.id is used in thread names
        monkeypatch.setattr(updater.bot, '_bot',
                            User(id=123, first_name='bot', is_bot=True))
        monkeypatch.setattr(updater.bot, '_commands', [])

        updater.start_polling(clean=True)
        updater.stop()
        for msg in recwarn:
            print(msg)

        try:  # This is for flaky tests (there's an unclosed socket sometimes)
            recwarn.pop(
                ResourceWarning
            )  # internally iterates through recwarn.list and deletes it
        except AssertionError:
            pass

        assert len(recwarn) == 2
        assert str(recwarn[0].message).startswith('Old Handler API')
        assert str(recwarn[1].message).startswith('The argument `clean` of')

    def test_clean_drop_pending_mutually_exclusive(self, updater):
        with pytest.raises(
                TypeError,
                match='`clean` and `drop_pending_updates` are mutually'):
            updater.start_polling(clean=True, drop_pending_updates=False)

        with pytest.raises(
                TypeError,
                match='`clean` and `drop_pending_updates` are mutually'):
            updater.start_webhook(clean=True, drop_pending_updates=False)

    @flaky(3, 1)
    def test_webhook_invalid_posts(self, updater):
        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # select random port for travis
        thr = Thread(target=updater._start_webhook,
                     args=(ip, port, '', None, None, 0, False, None, None))
        thr.start()

        sleep(0.2)

        try:
            with pytest.raises(HTTPError) as excinfo:
                self._send_webhook_msg(ip,
                                       port,
                                       '<root><bla>data</bla></root>',
                                       content_type='application/xml')
            assert excinfo.value.code == 403

            with pytest.raises(HTTPError) as excinfo:
                self._send_webhook_msg(ip,
                                       port,
                                       'dummy-payload',
                                       content_len=-2)
            assert excinfo.value.code == 500

            # TODO: prevent urllib or the underlying from adding content-length
            # with pytest.raises(HTTPError) as excinfo:
            #     self._send_webhook_msg(ip, port, 'dummy-payload', content_len=None)
            # assert excinfo.value.code == 411

            with pytest.raises(HTTPError):
                self._send_webhook_msg(ip,
                                       port,
                                       'dummy-payload',
                                       content_len='not-a-number')
            assert excinfo.value.code == 500

        finally:
            updater.httpd.shutdown()
            thr.join()

    def _send_webhook_msg(
        self,
        ip,
        port,
        payload_str,
        url_path='',
        content_len=-1,
        content_type='application/json',
        get_method=None,
    ):
        headers = {
            'content-type': content_type,
        }

        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 content_len is not None:
            headers['content-length'] = str(content_len)

        url = f'http://{ip}:{port}/{url_path}'

        req = Request(url, data=payload, headers=headers)

        if get_method is not None:
            req.get_method = get_method

        return urlopen(req)

    def signal_sender(self, updater):
        sleep(0.2)
        while not updater.running:
            sleep(0.2)

        os.kill(os.getpid(), signal.SIGTERM)

    @signalskip
    def test_idle(self, updater, caplog):
        updater.start_polling(0.01)
        Thread(target=partial(self.signal_sender, updater=updater)).start()

        with caplog.at_level(logging.INFO):
            updater.idle()

        # There is a chance of a conflict when getting updates since there can be many tests
        # (bots) running simultaneously while testing in github actions.
        records = caplog.records.copy(
        )  # To avoid iterating and removing at same time
        for idx, log in enumerate(records):
            print(log)
            msg = log.getMessage()
            if msg.startswith('Error while getting Updates: Conflict'):
                caplog.records.pop(idx)  # For stability

            if msg.startswith('No error handlers are registered'):
                caplog.records.pop(idx)

        assert len(caplog.records) == 2, caplog.records

        rec = caplog.records[-2]
        assert rec.getMessage().startswith(f'Received signal {signal.SIGTERM}')
        assert rec.levelname == 'INFO'

        rec = caplog.records[-1]
        assert rec.getMessage().startswith('Scheduler has been shut down')
        assert rec.levelname == 'INFO'

        # If we get this far, idle() ran through
        sleep(0.5)
        assert updater.running is False

    @signalskip
    def test_user_signal(self, updater):
        temp_var = {'a': 0}

        def user_signal_inc(signum, frame):
            temp_var['a'] = 1

        updater.user_sig_handler = user_signal_inc
        updater.start_polling(0.01)
        Thread(target=partial(self.signal_sender, updater=updater)).start()
        updater.idle()
        # If we get this far, idle() ran through
        sleep(0.5)
        assert updater.running is False
        assert temp_var['a'] != 0

    def test_create_bot(self):
        updater = Updater('123:abcd')
        assert updater.bot is not None

    def test_mutual_exclude_token_bot(self):
        bot = Bot('123:zyxw')
        with pytest.raises(ValueError):
            Updater(token='123:abcd', bot=bot)

    def test_no_token_or_bot_or_dispatcher(self):
        with pytest.raises(ValueError):
            Updater()

    def test_mutual_exclude_bot_private_key(self):
        bot = Bot('123:zyxw')
        with pytest.raises(ValueError):
            Updater(bot=bot, private_key=b'key')

    def test_mutual_exclude_bot_dispatcher(self, bot):
        dispatcher = Dispatcher(bot, None)
        bot = Bot('123:zyxw')
        with pytest.raises(ValueError):
            Updater(bot=bot, dispatcher=dispatcher)

    def test_mutual_exclude_persistence_dispatcher(self, bot):
        dispatcher = Dispatcher(bot, None)
        persistence = DictPersistence()
        with pytest.raises(ValueError):
            Updater(dispatcher=dispatcher, persistence=persistence)

    def test_mutual_exclude_workers_dispatcher(self, bot):
        dispatcher = Dispatcher(bot, None)
        with pytest.raises(ValueError):
            Updater(dispatcher=dispatcher, workers=8)

    def test_mutual_exclude_use_context_dispatcher(self, bot):
        dispatcher = Dispatcher(bot, None)
        use_context = not dispatcher.use_context
        with pytest.raises(ValueError):
            Updater(dispatcher=dispatcher, use_context=use_context)

    def test_mutual_exclude_custom_context_dispatcher(self):
        dispatcher = Dispatcher(None, None)
        with pytest.raises(ValueError):
            Updater(dispatcher=dispatcher, context_types=True)

    def test_defaults_warning(self, bot):
        with pytest.warns(TelegramDeprecationWarning,
                          match='no effect when a Bot is passed'):
            Updater(bot=bot, defaults=Defaults())
class TestUpdater:
    message_count = 0
    received = None
    attempts = 0
    err_handler_called = Event()
    cb_handler_called = Event()
    offset = 0

    @pytest.fixture(autouse=True)
    def reset(self):
        self.message_count = 0
        self.received = None
        self.attempts = 0
        self.err_handler_called.clear()
        self.cb_handler_called.clear()

    def error_handler(self, bot, update, error):
        self.received = error.message
        self.err_handler_called.set()

    def callback(self, bot, update):
        self.received = update.message.text
        self.cb_handler_called.set()

    @pytest.mark.parametrize(('error', ),
                             argvalues=[(TelegramError('Test Error 2'), ),
                                        (Unauthorized('Test Unauthorized'), )],
                             ids=('TelegramError', 'Unauthorized'))
    def test_get_updates_normal_err(self, monkeypatch, updater, error):
        def test(*args, **kwargs):
            raise error

        monkeypatch.setattr(updater.bot, 'get_updates', test)
        monkeypatch.setattr(updater.bot, 'set_webhook',
                            lambda *args, **kwargs: True)
        updater.dispatcher.add_error_handler(self.error_handler)
        updater.start_polling(0.01)

        # Make sure that the error handler was called
        self.err_handler_called.wait()
        assert self.received == error.message

        # Make sure that Updater polling thread keeps running
        self.err_handler_called.clear()
        self.err_handler_called.wait()

    def test_get_updates_bailout_err(self, monkeypatch, updater, caplog):
        error = InvalidToken()

        def test(*args, **kwargs):
            raise error

        with caplog.at_level(logging.DEBUG):
            monkeypatch.setattr(updater.bot, 'get_updates', test)
            monkeypatch.setattr(updater.bot, 'set_webhook',
                                lambda *args, **kwargs: True)
            updater.dispatcher.add_error_handler(self.error_handler)
            updater.start_polling(0.01)
            assert self.err_handler_called.wait(1) is not True

        sleep(1)
        # NOTE: This test might hit a race condition and fail (though the 1 seconds delay above
        #       should work around it).
        # NOTE: Checking Updater.running is problematic because it is not set to False when there's
        #       an unhandled exception.
        # TODO: We should have a way to poll Updater status and decide if it's running or not.
        import pprint
        pprint.pprint([rec.getMessage() for rec in caplog.get_records('call')])
        assert any('unhandled exception in Bot:{}:updater'.format(
            updater.bot.id) in rec.getMessage()
                   for rec in caplog.get_records('call'))

    @pytest.mark.parametrize(('error', ),
                             argvalues=[(RetryAfter(0.01), ), (TimedOut(), )],
                             ids=('RetryAfter', 'TimedOut'))
    def test_get_updates_retries(self, monkeypatch, updater, error):
        event = Event()

        def test(*args, **kwargs):
            event.set()
            raise error

        monkeypatch.setattr(updater.bot, 'get_updates', test)
        monkeypatch.setattr(updater.bot, 'set_webhook',
                            lambda *args, **kwargs: True)
        updater.dispatcher.add_error_handler(self.error_handler)
        updater.start_polling(0.01)

        # Make sure that get_updates was called, but not the error handler
        event.wait()
        assert self.err_handler_called.wait(0.5) is not True
        assert self.received != error.message

        # Make sure that Updater polling thread keeps running
        event.clear()
        event.wait()
        assert self.err_handler_called.wait(0.5) is not True

    def test_webhook(self, monkeypatch, updater):
        q = Queue()
        monkeypatch.setattr(updater.bot, 'set_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr('telegram.ext.Dispatcher.process_update',
                            lambda _, u: q.put(u))

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port
        updater.start_webhook(ip, port, url_path='TOKEN')
        sleep(.2)
        try:
            # Now, we send an update to the server via urlopen
            update = Update(1,
                            message=Message(1,
                                            None,
                                            Chat(1, ''),
                                            from_user=User(1, '', False),
                                            text='Webhook'))
            self._send_webhook_msg(ip, port, update.to_json(), 'TOKEN')
            sleep(.2)
            assert q.get(False) == update

            # Returns 404 if path is incorrect
            with pytest.raises(HTTPError) as excinfo:
                self._send_webhook_msg(ip, port, None, 'webookhandler.py')
            assert excinfo.value.code == 404

            with pytest.raises(HTTPError) as excinfo:
                self._send_webhook_msg(ip,
                                       port,
                                       None,
                                       'webookhandler.py',
                                       get_method=lambda: 'HEAD')
            assert excinfo.value.code == 404

            # Test multiple shutdown() calls
            updater.httpd.shutdown()
        finally:
            updater.httpd.shutdown()
            sleep(.2)
            assert not updater.httpd.is_running
            updater.stop()

    def test_start_webhook_no_warning_or_error_logs(self, caplog, updater,
                                                    monkeypatch):
        monkeypatch.setattr(updater.bot, 'set_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook',
                            lambda *args, **kwargs: True)
        # prevent api calls from @info decorator when updater.bot.id is used in thread names
        monkeypatch.setattr(updater.bot, 'bot',
                            User(id=123, first_name='bot', is_bot=True))
        monkeypatch.setattr(updater.bot, '_commands', [])

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port
        with caplog.at_level(logging.WARNING):
            updater.start_webhook(ip, port)
            updater.stop()
        assert not caplog.records

    @pytest.mark.skipif(
        os.name != 'nt' or sys.version_info < (3, 8),
        reason='Workaround only relevant on windows with py3.8+')
    def test_start_webhook_ensure_event_loop(self, updater, monkeypatch):
        def serve_forever(self, force_event_loop=False, ready=None):
            with self.server_lock:
                self.is_running = True
                self._ensure_event_loop(force_event_loop=force_event_loop)

                if ready is not None:
                    ready.set()

        monkeypatch.setattr(WebhookServer, 'serve_forever', serve_forever)
        monkeypatch.setattr(updater.bot, 'set_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook',
                            lambda *args, **kwargs: True)

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port

        with set_asyncio_event_loop(None):
            updater._start_webhook(ip,
                                   port,
                                   url_path='TOKEN',
                                   cert=None,
                                   key=None,
                                   bootstrap_retries=0,
                                   clean=False,
                                   webhook_url=None,
                                   allowed_updates=None)

            assert isinstance(asyncio.get_event_loop(),
                              asyncio.SelectorEventLoop)

    @pytest.mark.skipif(
        os.name != 'nt' or sys.version_info < (3, 8),
        reason='Workaround only relevant on windows with py3.8+')
    def test_start_webhook_force_event_loop_false(self, updater, monkeypatch):
        monkeypatch.setattr(updater.bot, 'set_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook',
                            lambda *args, **kwargs: True)

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port

        with set_asyncio_event_loop(asyncio.ProactorEventLoop()):
            with pytest.raises(TypeError,
                               match='`ProactorEventLoop` is incompatible'):
                updater._start_webhook(ip,
                                       port,
                                       url_path='TOKEN',
                                       cert=None,
                                       key=None,
                                       bootstrap_retries=0,
                                       clean=False,
                                       webhook_url=None,
                                       allowed_updates=None)

    @pytest.mark.skipif(
        os.name != 'nt' or sys.version_info < (3, 8),
        reason='Workaround only relevant on windows with py3.8+')
    def test_start_webhook_force_event_loop_true(self, updater, monkeypatch):
        def serve_forever(self, force_event_loop=False, ready=None):
            with self.server_lock:
                self.is_running = True
                self._ensure_event_loop(force_event_loop=force_event_loop)

                if ready is not None:
                    ready.set()

        monkeypatch.setattr(WebhookServer, 'serve_forever', serve_forever)
        monkeypatch.setattr(updater.bot, 'set_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook',
                            lambda *args, **kwargs: True)

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port

        with set_asyncio_event_loop(asyncio.ProactorEventLoop()):
            updater._start_webhook(ip,
                                   port,
                                   url_path='TOKEN',
                                   cert=None,
                                   key=None,
                                   bootstrap_retries=0,
                                   clean=False,
                                   webhook_url=None,
                                   allowed_updates=None,
                                   force_event_loop=True)
            assert isinstance(asyncio.get_event_loop(),
                              asyncio.ProactorEventLoop)

    def test_webhook_ssl(self, monkeypatch, updater):
        monkeypatch.setattr(updater.bot, 'set_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook',
                            lambda *args, **kwargs: True)
        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port
        tg_err = False
        try:
            updater._start_webhook(ip,
                                   port,
                                   url_path='TOKEN',
                                   cert='./tests/test_updater.py',
                                   key='./tests/test_updater.py',
                                   bootstrap_retries=0,
                                   clean=False,
                                   webhook_url=None,
                                   allowed_updates=None)
        except TelegramError:
            tg_err = True
        assert tg_err

    def test_webhook_no_ssl(self, monkeypatch, updater):
        q = Queue()
        monkeypatch.setattr(updater.bot, 'set_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook',
                            lambda *args, **kwargs: True)
        monkeypatch.setattr('telegram.ext.Dispatcher.process_update',
                            lambda _, u: q.put(u))

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port
        updater.start_webhook(ip, port, webhook_url=None)
        sleep(.2)

        # Now, we send an update to the server via urlopen
        update = Update(1,
                        message=Message(1,
                                        None,
                                        Chat(1, ''),
                                        from_user=User(1, '', False),
                                        text='Webhook 2'))
        self._send_webhook_msg(ip, port, update.to_json())
        sleep(.2)
        assert q.get(False) == update
        updater.stop()

    @pytest.mark.parametrize(('error', ),
                             argvalues=[(TelegramError(''), )],
                             ids=('TelegramError', ))
    def test_bootstrap_retries_success(self, monkeypatch, updater, error):
        retries = 2

        def attempt(*args, **kwargs):
            if self.attempts < retries:
                self.attempts += 1
                raise error

        monkeypatch.setattr(updater.bot, 'set_webhook', attempt)

        updater.running = True
        updater._bootstrap(retries, False, 'path', None, bootstrap_interval=0)
        assert self.attempts == retries

    @pytest.mark.parametrize(
        ('error', 'attempts'),
        argvalues=[(TelegramError(''), 2), (Unauthorized(''), 1),
                   (InvalidToken(), 1)],
        ids=('TelegramError', 'Unauthorized', 'InvalidToken'))
    def test_bootstrap_retries_error(self, monkeypatch, updater, error,
                                     attempts):
        retries = 1

        def attempt(*args, **kwargs):
            self.attempts += 1
            raise error

        monkeypatch.setattr(updater.bot, 'set_webhook', attempt)

        updater.running = True
        with pytest.raises(type(error)):
            updater._bootstrap(retries,
                               False,
                               'path',
                               None,
                               bootstrap_interval=0)
        assert self.attempts == attempts

    def test_bootstrap_clean_updates(self, monkeypatch, updater):
        clean = True
        expected_id = 4
        self.offset = 0

        def get_updates(*args, **kwargs):
            # we're hitting this func twice
            # 1. no args, return list of updates
            # 2. with 1 arg, int => if int == expected_id => test successful

            # case 2
            # 2nd call from bootstrap____clean
            # we should be called with offset = 4
            # save value passed in self.offset for assert down below
            if len(args) > 0:
                self.offset = int(args[0])
                return []

            class FakeUpdate():
                def __init__(self, update_id):
                    self.update_id = update_id

            # case 1
            # return list of obj's

            # build list of fake updates
            # returns list of 4 objects with
            # update_id's 0, 1, 2 and 3
            return [FakeUpdate(i) for i in range(0, expected_id)]

        monkeypatch.setattr(updater.bot, 'get_updates', get_updates)

        updater.running = True
        updater._bootstrap(1, clean, None, None, bootstrap_interval=0)
        assert self.offset == expected_id

    @flaky(3, 1)
    def test_webhook_invalid_posts(self, updater):
        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # select random port for travis
        thr = Thread(target=updater._start_webhook,
                     args=(ip, port, '', None, None, 0, False, None, None))
        thr.start()

        sleep(.2)

        try:
            with pytest.raises(HTTPError) as excinfo:
                self._send_webhook_msg(ip,
                                       port,
                                       '<root><bla>data</bla></root>',
                                       content_type='application/xml')
            assert excinfo.value.code == 403

            with pytest.raises(HTTPError) as excinfo:
                self._send_webhook_msg(ip,
                                       port,
                                       'dummy-payload',
                                       content_len=-2)
            assert excinfo.value.code == 500

            # TODO: prevent urllib or the underlying from adding content-length
            # with pytest.raises(HTTPError) as excinfo:
            #     self._send_webhook_msg(ip, port, 'dummy-payload', content_len=None)
            # assert excinfo.value.code == 411

            with pytest.raises(HTTPError):
                self._send_webhook_msg(ip,
                                       port,
                                       'dummy-payload',
                                       content_len='not-a-number')
            assert excinfo.value.code == 500

        finally:
            updater.httpd.shutdown()
            thr.join()

    def _send_webhook_msg(self,
                          ip,
                          port,
                          payload_str,
                          url_path='',
                          content_len=-1,
                          content_type='application/json',
                          get_method=None):
        headers = {
            'content-type': content_type,
        }

        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 content_len is not None:
            headers['content-length'] = str(content_len)

        url = 'http://{ip}:{port}/{path}'.format(ip=ip,
                                                 port=port,
                                                 path=url_path)

        req = Request(url, data=payload, headers=headers)

        if get_method is not None:
            req.get_method = get_method

        return urlopen(req)

    def signal_sender(self, updater):
        sleep(0.2)
        while not updater.running:
            sleep(0.2)

        os.kill(os.getpid(), signal.SIGTERM)

    @signalskip
    def test_idle(self, updater, caplog):
        updater.start_polling(0.01)
        Thread(target=partial(self.signal_sender, updater=updater)).start()

        with caplog.at_level(logging.INFO):
            updater.idle()

        rec = caplog.records[-2]
        assert rec.msg.startswith('Received signal {}'.format(signal.SIGTERM))
        assert rec.levelname == 'INFO'

        rec = caplog.records[-1]
        assert rec.msg.startswith('Scheduler has been shut down')
        assert rec.levelname == 'INFO'

        # If we get this far, idle() ran through
        sleep(.5)
        assert updater.running is False

    @signalskip
    def test_user_signal(self, updater):
        temp_var = {'a': 0}

        def user_signal_inc(signum, frame):
            temp_var['a'] = 1

        updater.user_sig_handler = user_signal_inc
        updater.start_polling(0.01)
        Thread(target=partial(self.signal_sender, updater=updater)).start()
        updater.idle()
        # If we get this far, idle() ran through
        sleep(.5)
        assert updater.running is False
        assert temp_var['a'] != 0

    def test_create_bot(self):
        updater = Updater('123:abcd')
        assert updater.bot is not None

    def test_mutual_exclude_token_bot(self):
        bot = Bot('123:zyxw')
        with pytest.raises(ValueError):
            Updater(token='123:abcd', bot=bot)

    def test_no_token_or_bot_or_dispatcher(self):
        with pytest.raises(ValueError):
            Updater()

    def test_mutual_exclude_bot_private_key(self):
        bot = Bot('123:zyxw')
        with pytest.raises(ValueError):
            Updater(bot=bot, private_key=b'key')

    def test_mutual_exclude_bot_dispatcher(self):
        dispatcher = Dispatcher(None, None)
        bot = Bot('123:zyxw')
        with pytest.raises(ValueError):
            Updater(bot=bot, dispatcher=dispatcher)

    def test_mutual_exclude_persistence_dispatcher(self):
        dispatcher = Dispatcher(None, None)
        persistence = DictPersistence()
        with pytest.raises(ValueError):
            Updater(dispatcher=dispatcher, persistence=persistence)

    def test_mutual_exclude_workers_dispatcher(self):
        dispatcher = Dispatcher(None, None)
        with pytest.raises(ValueError):
            Updater(dispatcher=dispatcher, workers=8)

    def test_mutual_exclude_use_context_dispatcher(self):
        dispatcher = Dispatcher(None, None)
        use_context = not dispatcher.use_context
        with pytest.raises(ValueError):
            Updater(dispatcher=dispatcher, use_context=use_context)

    def test_defaults_warning(self, bot):
        with pytest.warns(TelegramDeprecationWarning,
                          match='no effect when a Bot is passed'):
            Updater(bot=bot, defaults=Defaults())
Example #5
0
class TestUpdater(object):
    message_count = 0
    received = None
    attempts = 0
    err_handler_called = Event()
    cb_handler_called = Event()

    @pytest.fixture(autouse=True)
    def reset(self):
        self.message_count = 0
        self.received = None
        self.attempts = 0
        self.err_handler_called.clear()
        self.cb_handler_called.clear()

    def error_handler(self, bot, update, error):
        self.received = error.message
        self.err_handler_called.set()

    def callback(self, bot, update):
        self.received = update.message.text
        self.cb_handler_called.set()

    # TODO: test clean= argument of Updater._bootstrap

    @pytest.mark.parametrize(('error',),
                             argvalues=[(TelegramError('Test Error 2'),),
                                        (Unauthorized('Test Unauthorized'),)],
                             ids=('TelegramError', 'Unauthorized'))
    def test_get_updates_normal_err(self, monkeypatch, updater, error):
        def test(*args, **kwargs):
            raise error

        monkeypatch.setattr(updater.bot, 'get_updates', test)
        monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True)
        updater.dispatcher.add_error_handler(self.error_handler)
        updater.start_polling(0.01)

        # Make sure that the error handler was called
        self.err_handler_called.wait()
        assert self.received == error.message

        # Make sure that Updater polling thread keeps running
        self.err_handler_called.clear()
        self.err_handler_called.wait()

    def test_get_updates_bailout_err(self, monkeypatch, updater, caplog):
        error = InvalidToken()

        def test(*args, **kwargs):
            raise error

        with caplog.at_level(logging.DEBUG):
            monkeypatch.setattr(updater.bot, 'get_updates', test)
            monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True)
            updater.dispatcher.add_error_handler(self.error_handler)
            updater.start_polling(0.01)
            assert self.err_handler_called.wait(1) is not True

        sleep(1)
        # NOTE: This test might hit a race condition and fail (though the 1 seconds delay above
        #       should work around it).
        # NOTE: Checking Updater.running is problematic because it is not set to False when there's
        #       an unhandled exception.
        # TODO: We should have a way to poll Updater status and decide if it's running or not.
        import pprint
        pprint.pprint([rec.getMessage() for rec in caplog.get_records('call')])
        assert any('unhandled exception in Bot:{}:updater'.format(updater.bot.id) in
                   rec.getMessage() for rec in caplog.get_records('call'))

    @pytest.mark.parametrize(('error',),
                             argvalues=[(RetryAfter(0.01),),
                                        (TimedOut(),)],
                             ids=('RetryAfter', 'TimedOut'))
    def test_get_updates_retries(self, monkeypatch, updater, error):
        event = Event()

        def test(*args, **kwargs):
            event.set()
            raise error

        monkeypatch.setattr(updater.bot, 'get_updates', test)
        monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True)
        updater.dispatcher.add_error_handler(self.error_handler)
        updater.start_polling(0.01)

        # Make sure that get_updates was called, but not the error handler
        event.wait()
        assert self.err_handler_called.wait(0.5) is not True
        assert self.received != error.message

        # Make sure that Updater polling thread keeps running
        event.clear()
        event.wait()
        assert self.err_handler_called.wait(0.5) is not True

    def test_webhook(self, monkeypatch, updater):
        q = Queue()
        monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True)
        monkeypatch.setattr('telegram.ext.Dispatcher.process_update', lambda _, u: q.put(u))

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port
        updater.start_webhook(
            ip,
            port,
            url_path='TOKEN')
        sleep(.2)
        try:
            # Now, we send an update to the server via urlopen
            update = Update(1, message=Message(1, User(1, '', False), None, Chat(1, ''),
                                               text='Webhook'))
            self._send_webhook_msg(ip, port, update.to_json(), 'TOKEN')
            sleep(.2)
            assert q.get(False) == update

            # Returns 404 if path is incorrect
            with pytest.raises(HTTPError) as excinfo:
                self._send_webhook_msg(ip, port, None, 'webookhandler.py')
            assert excinfo.value.code == 404

            with pytest.raises(HTTPError) as excinfo:
                self._send_webhook_msg(ip, port, None, 'webookhandler.py',
                                       get_method=lambda: 'HEAD')
            assert excinfo.value.code == 404

            # Test multiple shutdown() calls
            updater.httpd.shutdown()
        finally:
            updater.httpd.shutdown()
            sleep(.2)
            assert not updater.httpd.is_running
            updater.stop()

    def test_webhook_ssl(self, monkeypatch, updater):
        monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True)
        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port
        tg_err = False
        try:
            updater._start_webhook(
                ip,
                port,
                url_path='TOKEN',
                cert='./tests/test_updater.py',
                key='./tests/test_updater.py',
                bootstrap_retries=0,
                clean=False,
                webhook_url=None,
                allowed_updates=None)
        except TelegramError:
            tg_err = True
        assert tg_err

    def test_webhook_no_ssl(self, monkeypatch, updater):
        q = Queue()
        monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True)
        monkeypatch.setattr('telegram.ext.Dispatcher.process_update', lambda _, u: q.put(u))

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port
        updater.start_webhook(ip, port, webhook_url=None)
        sleep(.2)

        # Now, we send an update to the server via urlopen
        update = Update(1, message=Message(1, User(1, '', False), None, Chat(1, ''),
                                           text='Webhook 2'))
        self._send_webhook_msg(ip, port, update.to_json())
        sleep(.2)
        assert q.get(False) == update
        updater.stop()

    def test_webhook_default_quote(self, monkeypatch, updater):
        updater._default_quote = True
        q = Queue()
        monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True)
        monkeypatch.setattr('telegram.ext.Dispatcher.process_update', lambda _, u: q.put(u))

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port
        updater.start_webhook(
            ip,
            port,
            url_path='TOKEN')
        sleep(.2)

        # Now, we send an update to the server via urlopen
        update = Update(1, message=Message(1, User(1, '', False), None, Chat(1, ''),
                                           text='Webhook'))
        self._send_webhook_msg(ip, port, update.to_json(), 'TOKEN')
        sleep(.2)
        # assert q.get(False) == update
        assert q.get(False).message.default_quote is True
        updater.stop()

    @pytest.mark.skipif(not (sys.platform.startswith("win") and sys.version_info >= (3, 8)),
                        reason="only relevant on win with py>=3.8")
    def test_webhook_tornado_win_py38_workaround(self, updater, monkeypatch):
        updater._default_quote = True
        q = Queue()
        monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True)
        monkeypatch.setattr('telegram.ext.Dispatcher.process_update', lambda _, u: q.put(u))

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port
        updater.start_webhook(
            ip,
            port,
            url_path='TOKEN')
        sleep(.2)

        try:
            from asyncio import (WindowsSelectorEventLoopPolicy)
        except ImportError:
            pass
            # not affected
        else:
            assert isinstance(asyncio.get_event_loop_policy(), WindowsSelectorEventLoopPolicy)

        updater.stop()

    @pytest.mark.parametrize(('error',),
                             argvalues=[(TelegramError(''),)],
                             ids=('TelegramError',))
    def test_bootstrap_retries_success(self, monkeypatch, updater, error):
        retries = 2

        def attempt(*args, **kwargs):
            if self.attempts < retries:
                self.attempts += 1
                raise error

        monkeypatch.setattr(updater.bot, 'set_webhook', attempt)

        updater.running = True
        updater._bootstrap(retries, False, 'path', None, bootstrap_interval=0)
        assert self.attempts == retries

    @pytest.mark.parametrize(('error', 'attempts'),
                             argvalues=[(TelegramError(''), 2),
                                        (Unauthorized(''), 1),
                                        (InvalidToken(), 1)],
                             ids=('TelegramError', 'Unauthorized', 'InvalidToken'))
    def test_bootstrap_retries_error(self, monkeypatch, updater, error, attempts):
        retries = 1

        def attempt(*args, **kwargs):
            self.attempts += 1
            raise error

        monkeypatch.setattr(updater.bot, 'set_webhook', attempt)

        updater.running = True
        with pytest.raises(type(error)):
            updater._bootstrap(retries, False, 'path', None, bootstrap_interval=0)
        assert self.attempts == attempts

    @flaky(3, 1)
    def test_webhook_invalid_posts(self, updater):
        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # select random port for travis
        thr = Thread(
            target=updater._start_webhook,
            args=(ip, port, '', None, None, 0, False, None, None))
        thr.start()

        sleep(.2)

        try:
            with pytest.raises(HTTPError) as excinfo:
                self._send_webhook_msg(ip, port, '<root><bla>data</bla></root>',
                                       content_type='application/xml')
            assert excinfo.value.code == 403

            with pytest.raises(HTTPError) as excinfo:
                self._send_webhook_msg(ip, port, 'dummy-payload', content_len=-2)
            assert excinfo.value.code == 500

            # TODO: prevent urllib or the underlying from adding content-length
            # with pytest.raises(HTTPError) as excinfo:
            #     self._send_webhook_msg(ip, port, 'dummy-payload', content_len=None)
            # assert excinfo.value.code == 411

            with pytest.raises(HTTPError):
                self._send_webhook_msg(ip, port, 'dummy-payload', content_len='not-a-number')
            assert excinfo.value.code == 500

        finally:
            updater.httpd.shutdown()
            thr.join()

    def _send_webhook_msg(self,
                          ip,
                          port,
                          payload_str,
                          url_path='',
                          content_len=-1,
                          content_type='application/json',
                          get_method=None):
        headers = {'content-type': content_type, }

        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 content_len is not None:
            headers['content-length'] = str(content_len)

        url = 'http://{ip}:{port}/{path}'.format(ip=ip, port=port, path=url_path)

        req = Request(url, data=payload, headers=headers)

        if get_method is not None:
            req.get_method = get_method

        return urlopen(req)

    def signal_sender(self, updater):
        sleep(0.2)
        while not updater.running:
            sleep(0.2)

        os.kill(os.getpid(), signal.SIGTERM)

    @signalskip
    def test_idle(self, updater, caplog):
        updater.start_polling(0.01)
        Thread(target=partial(self.signal_sender, updater=updater)).start()

        with caplog.at_level(logging.INFO):
            updater.idle()

        rec = caplog.records[-1]
        assert rec.msg.startswith('Received signal {}'.format(signal.SIGTERM))
        assert rec.levelname == 'INFO'

        # If we get this far, idle() ran through
        sleep(.5)
        assert updater.running is False

    @signalskip
    def test_user_signal(self, updater):
        temp_var = {'a': 0}

        def user_signal_inc(signum, frame):
            temp_var['a'] = 1

        updater.user_sig_handler = user_signal_inc
        updater.start_polling(0.01)
        Thread(target=partial(self.signal_sender, updater=updater)).start()
        updater.idle()
        # If we get this far, idle() ran through
        sleep(.5)
        assert updater.running is False
        assert temp_var['a'] != 0

    def test_create_bot(self):
        updater = Updater('123:abcd')
        assert updater.bot is not None

    def test_mutual_exclude_token_bot(self):
        bot = Bot('123:zyxw')
        with pytest.raises(ValueError):
            Updater(token='123:abcd', bot=bot)

    def test_no_token_or_bot_or_dispatcher(self):
        with pytest.raises(ValueError):
            Updater()

    def test_mutual_exclude_bot_private_key(self):
        bot = Bot('123:zyxw')
        with pytest.raises(ValueError):
            Updater(bot=bot, private_key=b'key')

    def test_mutual_exclude_bot_dispatcher(self):
        dispatcher = Dispatcher(None, None)
        bot = Bot('123:zyxw')
        with pytest.raises(ValueError):
            Updater(bot=bot, dispatcher=dispatcher)

    def test_mutual_exclude_persistence_dispatcher(self):
        dispatcher = Dispatcher(None, None)
        persistence = BasePersistence()
        with pytest.raises(ValueError):
            Updater(dispatcher=dispatcher, persistence=persistence)

    def test_mutual_exclude_workers_dispatcher(self):
        dispatcher = Dispatcher(None, None)
        with pytest.raises(ValueError):
            Updater(dispatcher=dispatcher, workers=8)

    def test_mutual_exclude_use_context_dispatcher(self):
        dispatcher = Dispatcher(None, None)
        use_context = not dispatcher.use_context
        with pytest.raises(ValueError):
            Updater(dispatcher=dispatcher, use_context=use_context)
    async def _request_wrapper(
        self,
        url: str,
        method: str,
        request_data: 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:
        """Wraps the real implementation request method.

        Performs the following tasks:
        * Handle the various HTTP response codes.
        * Parse the Telegram server response.

        Args:
            url (:obj:`str`): The URL to request.
            method (:obj:`str`): HTTP method (i.e. 'POST', 'GET', etc.).
            request_data (:class:`telegram.request.RequestData`, optional): An object containing
                information about parameters and files to upload for the request.
            read_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum
                amount of time (in seconds) to wait for a response from Telegram's server instead
                of the time specified during creating of this object. Defaults to
                :attr:`DEFAULT_NONE`.
            write_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum
                amount of time (in seconds) to wait for a write operation to complete (in terms of
                a network socket; i.e. POSTing a request or uploading a file) instead of the time
                specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`.
            connect_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the
                maximum amount of time (in seconds) to wait for a connection attempt to a server
                to succeed instead of the time specified during creating of this object. Defaults
                to :attr:`DEFAULT_NONE`.
            pool_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum
                amount of time (in seconds) to wait for a connection to become available instead
                of the time specified during creating of this object. Defaults to
                :attr:`DEFAULT_NONE`.

        Returns:
            bytes: The payload part of the HTTP server response.

        Raises:
            TelegramError

        """
        # TGs response also has the fields 'ok' and 'error_code'.
        # However, we rather rely on the HTTP status code for now.

        try:
            code, payload = await self.do_request(
                url=url,
                method=method,
                request_data=request_data,
                read_timeout=read_timeout,
                write_timeout=write_timeout,
                connect_timeout=connect_timeout,
                pool_timeout=pool_timeout,
            )
        except asyncio.CancelledError as exc:
            # TODO: in py3.8+, CancelledError is a subclass of BaseException, so we can drop this
            #  clause when we drop py3.7
            raise exc
        except TelegramError as exc:
            raise exc
        except Exception as exc:
            raise NetworkError(
                f"Unknown error in HTTP implementation: {repr(exc)}") from exc

        if HTTPStatus.OK <= code <= 299:
            # 200-299 range are HTTP success statuses
            return payload

        response_data = self.parse_json_payload(payload)

        description = response_data.get("description")
        if description:
            message = description
        else:
            message = "Unknown HTTPError"

        # In some special cases, we can raise more informative exceptions:
        # see https://core.telegram.org/bots/api#responseparameters and
        # https://core.telegram.org/bots/api#making-requests
        parameters = response_data.get("parameters")
        if parameters:
            migrate_to_chat_id = parameters.get("migrate_to_chat_id")
            if migrate_to_chat_id:
                raise ChatMigrated(migrate_to_chat_id)
            retry_after = parameters.get("retry_after")
            if retry_after:
                raise RetryAfter(retry_after)

            message += f"\nThe server response contained unknown parameters: {parameters}"

        if code == HTTPStatus.FORBIDDEN:
            raise Forbidden(message)
        if code in (HTTPStatus.NOT_FOUND, HTTPStatus.UNAUTHORIZED):
            # TG returns 404 Not found for
            #   1) malformed tokens
            #   2) correct tokens but non-existing method, e.g. api.tg.org/botTOKEN/unkonwnMethod
            # We can basically rule out 2) since we don't let users make requests manually
            # TG returns 401 Unauthorized for correctly formatted tokens that are not valid
            raise InvalidToken(message)
        if code == HTTPStatus.BAD_REQUEST:
            raise BadRequest(message)
        if code == HTTPStatus.CONFLICT:
            raise Conflict(message)
        if code == HTTPStatus.BAD_GATEWAY:
            raise NetworkError(description or "Bad Gateway")
        raise NetworkError(f"{message} ({code})")
Example #7
0
 def _valid_token(token):
     """a very basic validation on token"""
     left, sep, _right = token.partition(':')
     if (not sep) or (not left.isdigit()) or (len(left) < 3):
         raise InvalidToken()
     return token
Example #8
0
    def _request_wrapper(self, *args: Any, **kwargs: Any) -> bytes:
        """Wraps urllib3 request for handling known exceptions.

        Args:
            args: unnamed arguments, passed to urllib3 request.
            kwargs: keyword arguments, passed tp urllib3 request.

        Returns:
            bytes: A non-parsed JSON text.

        Raises:
            TelegramError

        """
        # Make sure to hint Telegram servers that we reuse connections by sending
        # "Connection: keep-alive" in the HTTP headers.
        if 'headers' not in kwargs:
            kwargs['headers'] = {}
        kwargs['headers']['connection'] = 'keep-alive'
        # Also set our user agent
        kwargs['headers']['user-agent'] = USER_AGENT
        if args[1].startswith("file://"):
            path = args[1][len("file://"):]
            if os.path.exists(path):
                with open(path, 'rb') as f:
                    return f.read()
            else:
                raise FileNotFoundError("Can't find file at {}".format(path))
        try:
            resp = self._con_pool.request(*args, **kwargs)
        except urllib3.exceptions.TimeoutError as error:
            raise TimedOut() from error
        except urllib3.exceptions.HTTPError as error:
            # HTTPError must come last as its the base urllib3 exception class
            # TODO: do something smart here; for now just raise NetworkError
            raise NetworkError('urllib3 HTTPError {}'.format(error)) from error

        if 200 <= resp.status <= 299:
            # 200-299 range are HTTP success statuses
            return resp.data

        try:
            message = str(self._parse(resp.data))
        except ValueError:
            message = 'Unknown HTTPError'

        if resp.status in (401, 403):
            raise Unauthorized(message)
        if resp.status == 400:
            raise BadRequest(message)
        if resp.status == 404:
            raise InvalidToken()
        if resp.status == 409:
            raise Conflict(message)
        if resp.status == 413:
            raise NetworkError(
                'File too large. Check telegram api limits '
                'https://core.telegram.org/bots/api#senddocument')
        if resp.status == 502:
            raise NetworkError('Bad Gateway')
        raise NetworkError('{} ({})'.format(message, resp.status))
class TestErrors:
    def test_telegram_error(self):
        with pytest.raises(TelegramError, match="^test message$"):
            raise TelegramError("test message")
        with pytest.raises(TelegramError, match="^Test message$"):
            raise TelegramError("Error: test message")
        with pytest.raises(TelegramError, match="^Test message$"):
            raise TelegramError("[Error]: test message")
        with pytest.raises(TelegramError, match="^Test message$"):
            raise TelegramError("Bad Request: test message")

    def test_unauthorized(self):
        with pytest.raises(Unauthorized, match="test message"):
            raise Unauthorized("test message")
        with pytest.raises(Unauthorized, match="^Test message$"):
            raise Unauthorized("Error: test message")
        with pytest.raises(Unauthorized, match="^Test message$"):
            raise Unauthorized("[Error]: test message")
        with pytest.raises(Unauthorized, match="^Test message$"):
            raise Unauthorized("Bad Request: test message")

    def test_invalid_token(self):
        with pytest.raises(InvalidToken, match="Invalid token"):
            raise InvalidToken

    def test_network_error(self):
        with pytest.raises(NetworkError, match="test message"):
            raise NetworkError("test message")
        with pytest.raises(NetworkError, match="^Test message$"):
            raise NetworkError("Error: test message")
        with pytest.raises(NetworkError, match="^Test message$"):
            raise NetworkError("[Error]: test message")
        with pytest.raises(NetworkError, match="^Test message$"):
            raise NetworkError("Bad Request: test message")

    def test_bad_request(self):
        with pytest.raises(BadRequest, match="test message"):
            raise BadRequest("test message")
        with pytest.raises(BadRequest, match="^Test message$"):
            raise BadRequest("Error: test message")
        with pytest.raises(BadRequest, match="^Test message$"):
            raise BadRequest("[Error]: test message")
        with pytest.raises(BadRequest, match="^Test message$"):
            raise BadRequest("Bad Request: test message")

    def test_timed_out(self):
        with pytest.raises(TimedOut, match="^Timed out$"):
            raise TimedOut

    def test_chat_migrated(self):
        with pytest.raises(
                ChatMigrated,
                match="Group migrated to supergroup. New chat id: 1234"):
            raise ChatMigrated(1234)
        try:
            raise ChatMigrated(1234)
        except ChatMigrated as e:
            assert e.new_chat_id == 1234

    def test_retry_after(self):
        with pytest.raises(
                RetryAfter,
                match="Flood control exceeded. Retry in 12.0 seconds"):
            raise RetryAfter(12)

    def test_conflict(self):
        with pytest.raises(Conflict, match='Something something.'):
            raise Conflict('Something something.')

    @pytest.mark.parametrize(
        "exception, attributes",
        [(TelegramError("test message"), ["message"]),
         (Unauthorized("test message"), ["message"]),
         (InvalidToken(), ["message"]),
         (NetworkError("test message"), ["message"]),
         (BadRequest("test message"), ["message"]), (TimedOut(), ["message"]),
         (ChatMigrated(1234), ["message", "new_chat_id"]),
         (RetryAfter(12), ["message", "retry_after"]),
         (Conflict("test message"), ["message"]),
         (TelegramDecryptionError("test message"), ["message"])],
    )
    def test_errors_pickling(self, exception, attributes):
        pickled = pickle.dumps(exception)
        unpickled = pickle.loads(pickled)
        assert type(unpickled) is type(exception)
        assert str(unpickled) == str(exception)

        for attribute in attributes:
            assert getattr(unpickled,
                           attribute) == getattr(exception, attribute)

    def test_pickling_test_coverage(self):
        """
        This test is only here to make sure that new errors will override __reduce__ properly.
        Add the new error class to the below covered_subclasses dict, if it's covered in the above
        test_errors_pickling test.
        """
        def make_assertion(cls):
            assert {sc
                    for sc in cls.__subclasses__()} == covered_subclasses[cls]
            for subcls in cls.__subclasses__():
                make_assertion(subcls)

        covered_subclasses = defaultdict(set)
        covered_subclasses.update({
            TelegramError: {
                Unauthorized, InvalidToken, NetworkError, ChatMigrated,
                RetryAfter, Conflict, TelegramDecryptionError
            },
            NetworkError: {BadRequest, TimedOut}
        })

        make_assertion(TelegramError)
class TestUpdater:
    message_count = 0
    received = None
    attempts = 0
    err_handler_called = Event()
    cb_handler_called = Event()
    offset = 0
    test_flag = False

    @pytest.fixture(autouse=True)
    def reset(self):
        self.message_count = 0
        self.received = None
        self.attempts = 0
        self.err_handler_called.clear()
        self.cb_handler_called.clear()
        self.test_flag = False

    def error_handler(self, bot, update, error):
        self.received = error.message
        self.err_handler_called.set()

    def callback(self, bot, update):
        self.received = update.message.text
        self.cb_handler_called.set()

    @pytest.mark.parametrize(
        ('error',),
        argvalues=[(TelegramError('Test Error 2'),), (Unauthorized('Test Unauthorized'),)],
        ids=('TelegramError', 'Unauthorized'),
    )
    def test_get_updates_normal_err(self, monkeypatch, updater, error):
        def test(*args, **kwargs):
            raise error

        monkeypatch.setattr(updater.bot, 'get_updates', test)
        monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True)
        updater.dispatcher.add_error_handler(self.error_handler)
        updater.start_polling(0.01)

        # Make sure that the error handler was called
        self.err_handler_called.wait()
        assert self.received == error.message

        # Make sure that Updater polling thread keeps running
        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()

        def test(*args, **kwargs):
            raise error

        with caplog.at_level(logging.DEBUG):
            monkeypatch.setattr(updater.bot, 'get_updates', test)
            monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True)
            updater.dispatcher.add_error_handler(self.error_handler)
            updater.start_polling(0.01)
            assert self.err_handler_called.wait(1) is not True

        sleep(1)
        # NOTE: This test might hit a race condition and fail (though the 1 seconds delay above
        #       should work around it).
        # NOTE: Checking Updater.running is problematic because it is not set to False when there's
        #       an unhandled exception.
        # TODO: We should have a way to poll Updater status and decide if it's running or not.
        import pprint

        pprint.pprint([rec.getMessage() for rec in caplog.get_records('call')])
        assert any(
            f'unhandled exception in Bot:{updater.bot.id}:updater' in rec.getMessage()
            for rec in caplog.get_records('call')
        )

    @pytest.mark.parametrize(
        ('error',), argvalues=[(RetryAfter(0.01),), (TimedOut(),)], ids=('RetryAfter', 'TimedOut')
    )
    def test_get_updates_retries(self, monkeypatch, updater, error):
        event = Event()

        def test(*args, **kwargs):
            event.set()
            raise error

        monkeypatch.setattr(updater.bot, 'get_updates', test)
        monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True)
        updater.dispatcher.add_error_handler(self.error_handler)
        updater.start_polling(0.01)

        # Make sure that get_updates was called, but not the error handler
        event.wait()
        assert self.err_handler_called.wait(0.5) is not True
        assert self.received != error.message

        # Make sure that Updater polling thread keeps running
        event.clear()
        event.wait()
        assert self.err_handler_called.wait(0.5) is not True

    def test_webhook(self, monkeypatch, updater):
        q = Queue()
        monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True)
        monkeypatch.setattr('telegram.ext.Dispatcher.process_update', lambda _, u: q.put(u))

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port
        updater.start_webhook(ip, port, url_path='TOKEN')
        sleep(0.2)
        try:
            # Now, we send an update to the server via urlopen
            update = Update(
                1,
                message=Message(
                    1, None, Chat(1, ''), from_user=User(1, '', False), text='Webhook'
                ),
            )
            self._send_webhook_msg(ip, port, update.to_json(), 'TOKEN')
            sleep(0.2)
            assert q.get(False) == update

            # Returns 404 if path is incorrect
            with pytest.raises(HTTPError) as excinfo:
                self._send_webhook_msg(ip, port, None, 'webookhandler.py')
            assert excinfo.value.code == 404

            with pytest.raises(HTTPError) as excinfo:
                self._send_webhook_msg(
                    ip, port, None, 'webookhandler.py', get_method=lambda: 'HEAD'
                )
            assert excinfo.value.code == 404

            # Test multiple shutdown() calls
            updater.httpd.shutdown()
        finally:
            updater.httpd.shutdown()
            sleep(0.2)
            assert not updater.httpd.is_running
            updater.stop()

    def test_start_webhook_no_warning_or_error_logs(self, caplog, updater, monkeypatch):
        monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True)
        # prevent api calls from @info decorator when updater.bot.id is used in thread names
        monkeypatch.setattr(updater.bot, '_bot', User(id=123, first_name='bot', is_bot=True))
        monkeypatch.setattr(updater.bot, '_commands', [])

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port
        with caplog.at_level(logging.WARNING):
            updater.start_webhook(ip, port)
            updater.stop()
        assert not caplog.records

    def test_webhook_ssl(self, monkeypatch, updater):
        monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True)
        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port
        tg_err = False
        try:
            updater._start_webhook(
                ip,
                port,
                url_path='TOKEN',
                cert='./tests/test_updater.py',
                key='./tests/test_updater.py',
                bootstrap_retries=0,
                drop_pending_updates=False,
                webhook_url=None,
                allowed_updates=None,
            )
        except TelegramError:
            tg_err = True
        assert tg_err

    def test_webhook_no_ssl(self, monkeypatch, updater):
        q = Queue()
        monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True)
        monkeypatch.setattr('telegram.ext.Dispatcher.process_update', lambda _, u: q.put(u))

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port
        updater.start_webhook(ip, port, webhook_url=None)
        sleep(0.2)

        # Now, we send an update to the server via urlopen
        update = Update(
            1,
            message=Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Webhook 2'),
        )
        self._send_webhook_msg(ip, port, update.to_json())
        sleep(0.2)
        assert q.get(False) == update
        updater.stop()

    def test_webhook_ssl_just_for_telegram(self, monkeypatch, updater):
        q = Queue()

        def set_webhook(**kwargs):
            self.test_flag.append(bool(kwargs.get('certificate')))
            return True

        orig_wh_server_init = WebhookServer.__init__

        def webhook_server_init(*args):
            self.test_flag = [args[-1] is None]
            orig_wh_server_init(*args)

        monkeypatch.setattr(updater.bot, 'set_webhook', set_webhook)
        monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True)
        monkeypatch.setattr('telegram.ext.Dispatcher.process_update', lambda _, u: q.put(u))
        monkeypatch.setattr(
            'telegram.ext.utils.webhookhandler.WebhookServer.__init__', webhook_server_init
        )

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port
        updater.start_webhook(ip, port, webhook_url=None, cert='./tests/test_updater.py')
        sleep(0.2)

        # Now, we send an update to the server via urlopen
        update = Update(
            1,
            message=Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Webhook 2'),
        )
        self._send_webhook_msg(ip, port, update.to_json())
        sleep(0.2)
        assert q.get(False) == update
        updater.stop()
        assert self.test_flag == [True, True]

    @pytest.mark.parametrize(('error',), argvalues=[(TelegramError(''),)], ids=('TelegramError',))
    def test_bootstrap_retries_success(self, monkeypatch, updater, error):
        retries = 2

        def attempt(*args, **kwargs):
            if self.attempts < retries:
                self.attempts += 1
                raise error

        monkeypatch.setattr(updater.bot, 'set_webhook', attempt)

        updater.running = True
        updater._bootstrap(retries, False, 'path', None, bootstrap_interval=0)
        assert self.attempts == retries

    @pytest.mark.parametrize(
        ('error', 'attempts'),
        argvalues=[(TelegramError(''), 2), (Unauthorized(''), 1), (InvalidToken(), 1)],
        ids=('TelegramError', 'Unauthorized', 'InvalidToken'),
    )
    def test_bootstrap_retries_error(self, monkeypatch, updater, error, attempts):
        retries = 1

        def attempt(*args, **kwargs):
            self.attempts += 1
            raise error

        monkeypatch.setattr(updater.bot, 'set_webhook', attempt)

        updater.running = True
        with pytest.raises(type(error)):
            updater._bootstrap(retries, False, 'path', None, bootstrap_interval=0)
        assert self.attempts == attempts

    @pytest.mark.parametrize('drop_pending_updates', (True, False))
    def test_bootstrap_clean_updates(self, monkeypatch, updater, drop_pending_updates):
        # As dropping pending updates is done by passing `drop_pending_updates` to
        # set_webhook, we just check that we pass the correct value
        self.test_flag = False

        def delete_webhook(**kwargs):
            self.test_flag = kwargs.get('drop_pending_updates') == drop_pending_updates

        monkeypatch.setattr(updater.bot, 'delete_webhook', delete_webhook)

        updater.running = True
        updater._bootstrap(
            1,
            drop_pending_updates=drop_pending_updates,
            webhook_url=None,
            allowed_updates=None,
            bootstrap_interval=0,
        )
        assert self.test_flag is True

    def test_deprecation_warnings_start_webhook(self, recwarn, updater, monkeypatch):
        monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True)
        # prevent api calls from @info decorator when updater.bot.id is used in thread names
        monkeypatch.setattr(updater.bot, '_bot', User(id=123, first_name='bot', is_bot=True))
        monkeypatch.setattr(updater.bot, '_commands', [])

        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # Select random port
        updater.start_webhook(ip, port, clean=True, force_event_loop=False)
        updater.stop()
        assert len(recwarn) == 3
        assert str(recwarn[0].message).startswith('Old Handler API')
        assert str(recwarn[1].message).startswith('The argument `clean` of')
        assert str(recwarn[2].message).startswith('The argument `force_event_loop` of')

    def test_clean_deprecation_warning_polling(self, recwarn, updater, monkeypatch):
        monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True)
        monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True)
        # prevent api calls from @info decorator when updater.bot.id is used in thread names
        monkeypatch.setattr(updater.bot, '_bot', User(id=123, first_name='bot', is_bot=True))
        monkeypatch.setattr(updater.bot, '_commands', [])

        updater.start_polling(clean=True)
        updater.stop()
        assert len(recwarn) == 2
        for msg in recwarn:
            print(msg)
        assert str(recwarn[0].message).startswith('Old Handler API')
        assert str(recwarn[1].message).startswith('The argument `clean` of')

    def test_clean_drop_pending_mutually_exclusive(self, updater):
        with pytest.raises(TypeError, match='`clean` and `drop_pending_updates` are mutually'):
            updater.start_polling(clean=True, drop_pending_updates=False)

        with pytest.raises(TypeError, match='`clean` and `drop_pending_updates` are mutually'):
            updater.start_webhook(clean=True, drop_pending_updates=False)

    @flaky(3, 1)
    def test_webhook_invalid_posts(self, updater):
        ip = '127.0.0.1'
        port = randrange(1024, 49152)  # select random port for travis
        thr = Thread(
            target=updater._start_webhook, args=(ip, port, '', None, None, 0, False, None, None)
        )
        thr.start()

        sleep(0.2)

        try:
            with pytest.raises(HTTPError) as excinfo:
                self._send_webhook_msg(
                    ip, port, '<root><bla>data</bla></root>', content_type='application/xml'
                )
            assert excinfo.value.code == 403

            with pytest.raises(HTTPError) as excinfo:
                self._send_webhook_msg(ip, port, 'dummy-payload', content_len=-2)
            assert excinfo.value.code == 500

            # TODO: prevent urllib or the underlying from adding content-length
            # with pytest.raises(HTTPError) as excinfo:
            #     self._send_webhook_msg(ip, port, 'dummy-payload', content_len=None)
            # assert excinfo.value.code == 411

            with pytest.raises(HTTPError):
                self._send_webhook_msg(ip, port, 'dummy-payload', content_len='not-a-number')
            assert excinfo.value.code == 500

        finally:
            updater.httpd.shutdown()
            thr.join()

    def _send_webhook_msg(
        self,
        ip,
        port,
        payload_str,
        url_path='',
        content_len=-1,
        content_type='application/json',
        get_method=None,
    ):
        headers = {
            'content-type': content_type,
        }

        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 content_len is not None:
            headers['content-length'] = str(content_len)

        url = f'http://{ip}:{port}/{url_path}'

        req = Request(url, data=payload, headers=headers)

        if get_method is not None:
            req.get_method = get_method

        return urlopen(req)

    def signal_sender(self, updater):
        sleep(0.2)
        while not updater.running:
            sleep(0.2)

        os.kill(os.getpid(), signal.SIGTERM)

    @signalskip
    def test_idle(self, updater, caplog):
        updater.start_polling(0.01)
        Thread(target=partial(self.signal_sender, updater=updater)).start()

        with caplog.at_level(logging.INFO):
            updater.idle()

        rec = caplog.records[-2]
        assert rec.getMessage().startswith(f'Received signal {signal.SIGTERM}')
        assert rec.levelname == 'INFO'

        rec = caplog.records[-1]
        assert rec.getMessage().startswith('Scheduler has been shut down')
        assert rec.levelname == 'INFO'

        # If we get this far, idle() ran through
        sleep(0.5)
        assert updater.running is False

    @signalskip
    def test_user_signal(self, updater):
        temp_var = {'a': 0}

        def user_signal_inc(signum, frame):
            temp_var['a'] = 1

        updater.user_sig_handler = user_signal_inc
        updater.start_polling(0.01)
        Thread(target=partial(self.signal_sender, updater=updater)).start()
        updater.idle()
        # If we get this far, idle() ran through
        sleep(0.5)
        assert updater.running is False
        assert temp_var['a'] != 0

    def test_create_bot(self):
        updater = Updater('123:abcd')
        assert updater.bot is not None

    def test_mutual_exclude_token_bot(self):
        bot = Bot('123:zyxw')
        with pytest.raises(ValueError):
            Updater(token='123:abcd', bot=bot)

    def test_no_token_or_bot_or_dispatcher(self):
        with pytest.raises(ValueError):
            Updater()

    def test_mutual_exclude_bot_private_key(self):
        bot = Bot('123:zyxw')
        with pytest.raises(ValueError):
            Updater(bot=bot, private_key=b'key')

    def test_mutual_exclude_bot_dispatcher(self):
        dispatcher = Dispatcher(None, None)
        bot = Bot('123:zyxw')
        with pytest.raises(ValueError):
            Updater(bot=bot, dispatcher=dispatcher)

    def test_mutual_exclude_persistence_dispatcher(self):
        dispatcher = Dispatcher(None, None)
        persistence = DictPersistence()
        with pytest.raises(ValueError):
            Updater(dispatcher=dispatcher, persistence=persistence)

    def test_mutual_exclude_workers_dispatcher(self):
        dispatcher = Dispatcher(None, None)
        with pytest.raises(ValueError):
            Updater(dispatcher=dispatcher, workers=8)

    def test_mutual_exclude_use_context_dispatcher(self):
        dispatcher = Dispatcher(None, None)
        use_context = not dispatcher.use_context
        with pytest.raises(ValueError):
            Updater(dispatcher=dispatcher, use_context=use_context)

    def test_defaults_warning(self, bot):
        with pytest.warns(TelegramDeprecationWarning, match='no effect when a Bot is passed'):
            Updater(bot=bot, defaults=Defaults())