Esempio n. 1
0
 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")
Esempio n. 2
0
    def add_item(self, update, context):
        args = update.message.text.split(' ')
        if len(args) <= 1:
            raise TelegramError("Ich konnte nichts hinzufügen")
        added_items = []
        with session_scope(
                TelegramError("Hups, keine Items hinzugefügt:")) as session:
            for item in args[1:]:
                item = self._get_or_increase_item(session, item, added_items)
                session.add(item)

        return f"Added {added_items} to shopping list"
Esempio n. 3
0
    def __init__(self):
        if config.LINKS_REPO_DEPLOY_KEY:
            self.ssh_cmd = 'ssh -i ' + config.LINKS_REPO_DEPLOY_KEY

        repo_path_local_base = ''
        if not config.LINKS_REPO_PATH_LOCAL_ABS:
            repo_path_local_base = os.path.expanduser('~')
        self.repo_path_local = os.path.join(repo_path_local_base,
                                            config.LINKS_REPO_PATH_LOCAL)
        self.repo = Repo.init(self.repo_path_local)
        try:
            self.repo.remotes.origin.exists()
            if self.repo.remotes.origin.url != config.LINKS_REPO_URL:
                raise TelegramError(
                    'Links repository path seems to be conflicting with another repo'
                )
        except AttributeError:
            self.repo.create_remote('origin', config.LINKS_REPO_URL)
        with self.repo.git.custom_environment(GIT_SSH_COMMAND=self.ssh_cmd):
            self.repo.remotes.origin.pull(config.LINKS_REPO_BRANCH)
        self.repo.git.checkout(config.LINKS_REPO_BRANCH)

        # Init Shortener
        raw_path = os.path.join(self.repo_path_local, 'raw')
        output_path = os.path.join(self.repo_path_local, 'output/docs')
        self.shortener = Shortener(raw_path, output_path)
    def test_from_error(self, app):
        error = TelegramError("test")
        update = Update(0,
                        message=Message(0,
                                        None,
                                        Chat(1, "chat"),
                                        from_user=User(1, "user", False)))
        job = object()
        coroutine = object()

        callback_context = CallbackContext.from_error(update=update,
                                                      error=error,
                                                      application=app,
                                                      job=job,
                                                      coroutine=coroutine)

        assert callback_context.error is error
        assert callback_context.chat_data == {}
        assert callback_context.user_data == {}
        assert callback_context.bot_data is app.bot_data
        assert callback_context.bot is app.bot
        assert callback_context.job_queue is app.job_queue
        assert callback_context.update_queue is app.update_queue
        assert callback_context.coroutine is coroutine
        assert callback_context.job is job
Esempio n. 5
0
    def editMessageCaption(self,
                           chat_id=None,
                           message_id=None,
                           inline_message_id=None,
                           caption=None,
                           reply_markup=None,
                           timeout=None,
                           **kwargs):
        if inline_message_id is None and (chat_id is None
                                          or message_id is None):
            raise TelegramError(
                'editMessageCaption: Both chat_id and message_id are required when '
                'inline_message_id is not specified')

        data = {}

        if caption:
            data['caption'] = caption
        if chat_id:
            data['chat_id'] = chat_id
        if message_id:
            data['message_id'] = message_id
        if inline_message_id:
            data['inline_message_id'] = inline_message_id

        return data
Esempio n. 6
0
 def reset_shopping_list(self, update, context):
     with session_scope(
             TelegramError("Konnte die shopping list nicht zurücksetzen:(")
     ) as session:
         for item in session.query(Item):
             item.on_list = False
     return "keine Sachen mehr auf der Einkaufsliste"
Esempio n. 7
0
 def add_item_from_items_dialog(self, update, context):
     with session_scope(
             TelegramError("Kann keine Liste erstellen.")) as session:
         items = [
             str(it) for it in session.query(Item).filter(
                 Item.on_list == False).order_by(asc(Item.name))
         ]
     return build_menu(items, 2, 'add')
Esempio n. 8
0
 def delete_from_larder_dialog(self, update, context):
     with session_scope(
             TelegramError(
                 "Kann leider nicht aus der Vorratskammer löschen.")
     ) as session:
         items = [
             str(it) for it in session.query(Item).order_by(asc(Item.name))
         ]
     return build_menu(items, 2, 'rmLarder')
Esempio n. 9
0
 def get_list_dialog(self, update, context):
     with session_scope(
             TelegramError(
                 "hups! Die Liste konnte ich nicht kriegen...")) as session:
         items = [
             str(it)
             for it in session.query(Item).filter(Item.on_list == True)
         ]
     return build_menu(items, 2, 'rmList')
Esempio n. 10
0
 def delete_from_larder_button(self, update, context, callback_dict):
     with session_scope(
             TelegramError("Dieses Element konnte ich leider nicht löschen")
     ) as session:
         session.query(Item).filter(
             Item.name == callback_dict['name']).delete()
         items = [str(it) for it in session.query(Item).all()]
         context.bot.edit_message_reply_markup(
             chat_id=update.callback_query.message.chat_id,
             message_id=update.callback_query.message.message_id,
             reply_markup=InlineKeyboardMarkup(
                 build_menu(items, 2, 'rmLarder')))
Esempio n. 11
0
 def add_item_from_items_button(self, update, context, callback_dict):
     with session_scope(
             TelegramError("Kann keine Liste erstellen.")) as session:
         item = session.query(Item).filter(
             Item.name == callback_dict['name']).first()
         item.on_list = True
         items = [
             str(it) for it in session.query(Item).filter(
                 Item.on_list == False).order_by(asc(Item.name))
         ]
         context.bot.edit_message_reply_markup(
             chat_id=update.callback_query.message.chat_id,
             message_id=update.callback_query.message.message_id,
             reply_markup=InlineKeyboardMarkup(build_menu(items, 2, 'add')))
Esempio n. 12
0
    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"
Esempio n. 13
0
def kick_user(context: CallbackContext, chat_id: int, kick_id: int, reason: str = '') -> bool:
    bot: Bot = context.bot
    try:
        if bot.kick_chat_member(chat_id=chat_id, user_id=kick_id, until_date=datetime.utcnow()+timedelta(days=367)):
            logger.info(f"Kicked {kick_id} in the group {chat_id}{', reason: ' if reason else ''}{reason}")
        else:
            raise TelegramError('kick_chat_member returned bad status')
    except TelegramError as err:
        logger.error(f"Cannot kick {kick_id} in the group {chat_id}, {err}")
    except Exception:
        print_traceback(DEBUG)
    else:
        return True
    return False
Esempio n. 14
0
def delete_message(context: CallbackContext, chat_id: int, message_id: int) -> bool:
    try:
        if context.bot.delete_message(chat_id=chat_id, message_id=message_id):
            logger.debug(f"Deleted message {message_id} in the group {chat_id}")
        else:
            raise TelegramError('delete_message returned bad status')
    except NetworkError:
        raise
    except TelegramError as err:
        logger.error(f"Cannot delete message {message_id} in the group {chat_id}, {err}")
    except Exception:
        print_traceback(DEBUG)
    else:
        return True
    return False
Esempio n. 15
0
 def get_list_button(self, update, context, callback_dict):
     with session_scope(
             TelegramError(
                 "Grmph!! Das konnte ich nicht löschen!")) as session:
         item = session.query(Item).filter(
             Item.name == callback_dict['name']).first()
         item.on_list = False
         remaining_items = [
             str(it) for it in session.query(Item).filter(
                 Item.on_list == True).order_by(asc(Item.name))
         ]
         context.bot.edit_message_reply_markup(
             chat_id=update.callback_query.message.chat_id,
             message_id=update.callback_query.message.message_id,
             reply_markup=InlineKeyboardMarkup(
                 build_menu(remaining_items, 2, 'rmList')))
Esempio n. 16
0
    def start(self, ready=None):
        """Thread target of thread 'dispatcher'.

        Runs in background and processes the update queue.

        Args:
            ready (:obj:`threading.Event`, optional): If specified, the event will be set once the
                dispatcher is ready.

        """
        if self.running:
            self.logger.warning('already running')
            if ready is not None:
                ready.set()
            return

        if self.__exception_event.is_set():
            msg = 'reusing dispatcher after exception event is forbidden'
            self.logger.error(msg)
            raise TelegramError(msg)

        self._init_async_threads(uuid4(), self.workers)
        self.running = True
        self.logger.debug('Dispatcher started')

        if ready is not None:
            ready.set()

        while 1:
            try:
                # Pop update from update queue.
                update = self.update_queue.get(True, 1)
            except Empty:
                if self.__stop_event.is_set():
                    self.logger.debug('orderly stopping')
                    break
                elif self.__exception_event.is_set():
                    self.logger.critical('stopping due to exception in another thread')
                    break
                continue

            self.logger.debug('Processing Update: %s' % update)
            self.process_update(update)
            self.update_queue.task_done()

        self.running = False
        self.logger.debug('Dispatcher thread stopped')
Esempio n. 17
0
def unban_user(context: CallbackContext, chat_id: int, user_id: int, reason: str = '') -> bool:
    try:
        if context.bot.restrict_chat_member(chat_id=chat_id, user_id=user_id,
                                permissions = CHAT_PERMISSION_RW,
                                until_date=datetime.utcnow()+timedelta(days=367)):
            logger.info(f"Unbanned {user_id} in the group {chat_id}{', reason: ' if reason else ''}{reason}")
        else:
            raise TelegramError('restrict_chat_member returned bad status')
    except NetworkError:
        raise
    except TelegramError as err:
        logger.error(f"Cannot unban {user_id} in the group {chat_id}, {err}")
    except Exception:
        print_traceback(DEBUG)
    else:
        return True
    return False
Esempio n. 18
0
def buy(conn: TConn, entity_id: int, inventory_id: int, *, quantity: int = 1):
    """ Purchase an item for money
    Where the entity_id is the plyaer, and the inventory_id is the item bought.
    The shopkeeper is determinated from the inventory item's owner.
    """
    # validate there is stock
    inv_item = conn.execute("""
        SELECT item_id, quantity, price
        FROM inventory
        WHERE inventory_id = :inventory_id
    """, {'inventory_id': inventory_id}).fetchone()

    if inv_item['quantity'] < quantity:
        raise Exception("There isn't enough in stock")

    total_price = inv_item['price'] * quantity

    # validate the player can afford it
    player = status(conn, entity_id)
    if player.money < total_price:
        raise TelegramError("You don't have enough money")

    # subtract the item from the shopkeeper's inventory
    conn.execute("""
        UPDATE inventory
        SET quantity = quantity - :quantity_sold
        WHERE inventory_id = :inventory_id
    """, {
        'inventory_id': inventory_id,
        'quantity_sold': quantity
    })
    # subtract the price from the player
    conn.execute("""
        UPDATE entity
        SET money = money - :price
        WHERE entity_id = :entity_id
    """, {
        'entity_id': entity_id,
        'price': total_price
    })

    give_item(conn, entity_id, inv_item['item_id'], quantity)
    def parse_json_payload(payload: bytes) -> JSONDict:
        """Parse the JSON returned from Telegram.

        Tip:
            By default, this method uses the standard library's :func:`json.loads` and
            ``errors="replace"`` in :meth:`bytes.decode`.
            You can override it to customize either of these behaviors.

        Args:
            payload (:obj:`bytes`): The UTF-8 encoded JSON payload as returned by Telegram.

        Returns:
            dict: A JSON parsed as Python dict with results.

        Raises:
            TelegramError: If loading the JSON data failed
        """
        decoded_s = payload.decode("utf-8", "replace")
        try:
            return json.loads(decoded_s)
        except ValueError as exc:
            raise TelegramError("Invalid server response") from exc
Esempio n. 20
0
    def editMessageReplyMarkup(self,
                               chat_id=None,
                               message_id=None,
                               inline_message_id=None,
                               reply_markup=None,
                               timeout=None,
                               **kwargs):
        if inline_message_id is None and (chat_id is None
                                          or message_id is None):
            raise TelegramError(
                "editMessageCaption: Both chat_id and message_id are required when "
                "inline_message_id is not specified")

        data = {}

        if chat_id:
            data["chat_id"] = chat_id
        if message_id:
            data["message_id"] = message_id
        if inline_message_id:
            data["inline_message_id"] = inline_message_id

        return data
Esempio n. 21
0
def simple_challenge(context, chat_id, user, invite_user, join_msgid) -> None:
    bot: Bot = context.bot
    fldlock: Lock = FLD_LOCKS.setdefault(chat_id, Lock())
    u_mgr: UserManager = context.chat_data.setdefault('u_mgr',
                                                      UserManager(chat_id))
    settings = chatSettings(context.chat_data.get('chat_settings', dict()))
    MIN_CLG_TIME = settings.get('MIN_CLG_TIME')
    CLG_TIMEOUT = settings.get('CHALLENGE_TIMEOUT')
    try:
        RCLG_TIMEOUT = (lambda score: \
                        (userfilter.MAX_SCORE-score)/userfilter.MAX_SCORE*(CLG_TIMEOUT-MIN_CLG_TIME)+MIN_CLG_TIME) \
                       (userfilter.spam_score(user.full_name))
        RCLG_TIMEOUT = int(RCLG_TIMEOUT)
    except Exception:
        RCLG_TIMEOUT = CLG_TIMEOUT
        print_traceback(debug=DEBUG)
    (CLG_QUESTION, CLG_ACCEPT, CLG_DENY) = settings.get_clg_accecpt_deny()
    # flooding protection
    FLOOD_LIMIT = settings.get('FLOOD_LIMIT')
    if FLOOD_LIMIT == 0:
        flag_flooding = False
    elif FLOOD_LIMIT == 1:
        flag_flooding = True
    else:
        if len(u_mgr) + 1 >= FLOOD_LIMIT:
            flag_flooding = True
        else:
            flag_flooding = False
    flag_flooding = flag_flooding and not user.is_bot

    def organize_btns(
        buttons: List[InlineKeyboardButton]
    ) -> List[List[InlineKeyboardButton]]:
        '''
            Shuffle buttons and put them into a 2d array
        '''
        shuffle(buttons)
        output = [
            list(),
        ]
        LENGTH_PER_LINE = 20
        MAXIMUM_PER_LINE = 4
        clength = LENGTH_PER_LINE
        for btn in buttons:
            l = len(btn.text) + len(find_cjk_letters(
                btn.text))  # cjk letters has a length of 2
            clength -= l
            if clength < 0 or len(output[-1]) >= MAXIMUM_PER_LINE:
                clength = LENGTH_PER_LINE - l
                output.append([btn])
            else:
                output[-1].append(btn)
        return output

    try:
        if restrict_user(context, chat_id=chat_id, user_id=user.id, extra=((' [flooding]' if flag_flooding else '') + \
                                                                           (' [bot]' if user.is_bot else ''))):
            if flag_flooding:
                fldlock.acquire()
            try:
                if u_mgr.fldmsg_id and flag_flooding:
                    logger.debug(
                        f'Deleting flooding captcha {u_mgr.fldmsg_id} in {chat_id}'
                    )
                    delete_message(context, chat_id, u_mgr.fldmsg_id)
                buttons = [
                    InlineKeyboardButton(text=CLG_ACCEPT, callback_data = (\
                        f"clg {user.id} {challenge_gen_pw(user.id, join_msgid)}" + \
                    (f" {user.id}" if not flag_flooding else ''))),
                    *[InlineKeyboardButton(text=fake_btn_text, callback_data = (\
                        f"clg {user.id} {challenge_gen_pw(user.id, join_msgid, real=False)}" + \
                    (f" {user.id}" if not flag_flooding else '')))
                    for fake_btn_text in CLG_DENY]
                ]
                callback_datalist = [btn.callback_data for btn in buttons]
                buttons = organize_btns(buttons)
                for _ in range(3):
                    try:
                        msg: Message = bot.send_message(chat_id=chat_id,
                                        reply_to_message_id=join_msgid,
                                        text=('' if not flag_flooding else \
                                                    f'待验证用户: {len(u_mgr)+1}名\n') + \
                                                settings.choice('WELCOME_WORDS').replace(
                                                '%time%', f"{RCLG_TIMEOUT}") + \
                                                f"\n{CLG_QUESTION}",
                                        reply_markup=InlineKeyboardMarkup(buttons),
                                        disable_notification=True,
                                        isgroup=False) # These messages are essential and should not be delayed.
                    except TelegramError:
                        pass
                    else:
                        break
                else:
                    raise TelegramError(
                        f'Send challenge message failed 3 times for {user.id}')
                if flag_flooding:
                    u_mgr.fldmsg_id = msg.message_id
                    u_mgr.fldmsg_callbacks = callback_datalist
                bot_invite_uid = None if flag_flooding else invite_user.id
                u_mgr.add(
                    restUser(user.id,
                             join_msgid,
                             msg.message_id,
                             bot_invite_uid,
                             flooding=flag_flooding))
            finally:
                if flag_flooding:
                    fldlock.release()
            # User restricted and buttons sent, now search for this user's previous messages and delete them
            sto_msgs: List[Tuple[int, int, int]] = context.chat_data.get(
                'stored_messages', list())
            msgids_to_delete: Set[int] = set([
                u_m_t[1] for u_m_t in sto_msgs
                if u_m_t[0] == user.id and int(u_m_t[1]) > int(join_msgid)
            ])
            for _mid in msgids_to_delete:
                delete_message(context, chat_id, _mid)
            # kick them after timeout
            def kick_then_unban(_: CallbackContext) -> None:
                def then_unban(_: CallbackContext) -> None:
                    unban_user(context,
                               chat_id,
                               user.id,
                               reason='Unban timeout reached.')

                if kick_user(context,
                             chat_id,
                             user.id,
                             reason='Challange timeout.'):
                    if (UNBAN_TIMEOUT := settings.get('UNBAN_TIMEOUT')) > 0:
                        context.job_queue.run_once(then_unban,
                                                   UNBAN_TIMEOUT,
                                                   name='unban_job')
                u_mgr.pop(user.id)
                # delete messages
                if flag_flooding:
                    fldlock.acquire()
                    try:
                        if len(u_mgr) == 0 and u_mgr.fldmsg_id:
                            delete_message(context,
                                           chat_id=chat_id,
                                           message_id=u_mgr.fldmsg_id)
                            u_mgr.fldmsg_id = None
                    finally:
                        fldlock.release()
                else:
                    delete_message(context,
                                   chat_id=chat_id,
                                   message_id=msg.message_id)
                delete_message(context, chat_id=chat_id, message_id=join_msgid)

            context.job_queue.run_once(kick_then_unban,
                                       RCLG_TIMEOUT if RCLG_TIMEOUT > 0 else 0,
                                       name=challange_hash(
                                           user.id, chat_id, join_msgid))
        else:
Esempio n. 22
0
                                           message_id=u_mgr.fldmsg_id)
                            u_mgr.fldmsg_id = None
                    finally:
                        fldlock.release()
                else:
                    delete_message(context,
                                   chat_id=chat_id,
                                   message_id=msg.message_id)
                delete_message(context, chat_id=chat_id, message_id=join_msgid)

            context.job_queue.run_once(kick_then_unban,
                                       RCLG_TIMEOUT if RCLG_TIMEOUT > 0 else 0,
                                       name=challange_hash(
                                           user.id, chat_id, join_msgid))
        else:
            raise TelegramError('')
    except TelegramError:
        bot.send_message(chat_id=chat_id,
                         text="发现新加入的成员: {0} ,但机器人不是管理员导致无法实施有效行动。"
                         "请将机器人设为管理员并打开封禁权限。".format(fName(user,
                                                           markdown=True)),
                         parse_mode="Markdown")
        logger.error((f"Cannot restrict {user.id} and {invite_user.id} in "
                      f"the group {chat_id}{' [bot]' if user.is_bot else ''}"))


@run_async
@collect_error
@filter_old_updates
def at_admins(update: Update, context: CallbackContext) -> None:
    chat_type: str = update.message.chat.type
class TestUpdater:
    message_count = 0
    received = None
    attempts = 0
    err_handler_called = None
    cb_handler_called = None
    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 = None
        self.cb_handler_called = None
        self.test_flag = False

    def error_callback(self, error):
        self.received = error
        self.err_handler_called.set()

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

    async def test_slot_behaviour(self, updater, mro_slots):
        async with updater:
            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 len(mro_slots(updater)) == len(set(
                mro_slots(updater))), "duplicate slot"

    def test_init(self, bot):
        queue = asyncio.Queue()
        updater = Updater(bot=bot, update_queue=queue)
        assert updater.bot is bot
        assert updater.update_queue is queue

    async def test_initialize(self, bot, monkeypatch):
        async def initialize_bot(*args, **kwargs):
            self.test_flag = True

        async with make_bot(token=bot.token) as test_bot:
            monkeypatch.setattr(test_bot, "initialize", initialize_bot)

            updater = Updater(bot=test_bot, update_queue=asyncio.Queue())
            await updater.initialize()

        assert self.test_flag

    async def test_shutdown(self, bot, monkeypatch):
        async def shutdown_bot(*args, **kwargs):
            self.test_flag = True

        async with make_bot(token=bot.token) as test_bot:
            monkeypatch.setattr(test_bot, "shutdown", shutdown_bot)

            updater = Updater(bot=test_bot, update_queue=asyncio.Queue())
            await updater.initialize()
            await updater.shutdown()

        assert self.test_flag

    async def test_multiple_inits_and_shutdowns(self, updater, monkeypatch):
        self.test_flag = defaultdict(int)

        async def initialize(*args, **kargs):
            self.test_flag["init"] += 1

        async def shutdown(*args, **kwargs):
            self.test_flag["shutdown"] += 1

        monkeypatch.setattr(updater.bot, "initialize", initialize)
        monkeypatch.setattr(updater.bot, "shutdown", shutdown)

        await updater.initialize()
        await updater.initialize()
        await updater.initialize()
        await updater.shutdown()
        await updater.shutdown()
        await updater.shutdown()

        assert self.test_flag["init"] == 1
        assert self.test_flag["shutdown"] == 1

    async def test_multiple_init_cycles(self, updater):
        # nothing really to assert - this should just not fail
        async with updater:
            await updater.bot.get_me()
        async with updater:
            await updater.bot.get_me()

    @pytest.mark.parametrize("method", ["start_polling", "start_webhook"])
    async def test_start_without_initialize(self, updater, method):
        with pytest.raises(RuntimeError, match="not initialized"):
            await getattr(updater, method)()

    @pytest.mark.parametrize("method", ["start_polling", "start_webhook"])
    async def test_shutdown_while_running(self, updater, method, monkeypatch):
        async def set_webhook(*args, **kwargs):
            return True

        monkeypatch.setattr(updater.bot, "set_webhook", set_webhook)

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

        async with updater:
            if "webhook" in method:
                await getattr(updater, method)(
                    ip_address=ip,
                    port=port,
                )
            else:
                await getattr(updater, method)()

            with pytest.raises(RuntimeError, match="still running"):
                await updater.shutdown()
            await updater.stop()

    async def test_context_manager(self, monkeypatch, updater):
        async def initialize(*args, **kwargs):
            self.test_flag = ["initialize"]

        async def shutdown(*args, **kwargs):
            self.test_flag.append("stop")

        monkeypatch.setattr(Updater, "initialize", initialize)
        monkeypatch.setattr(Updater, "shutdown", shutdown)

        async with updater:
            pass

        assert self.test_flag == ["initialize", "stop"]

    async def test_context_manager_exception_on_init(self, monkeypatch,
                                                     updater):
        async def initialize(*args, **kwargs):
            raise RuntimeError("initialize")

        async def shutdown(*args):
            self.test_flag = "stop"

        monkeypatch.setattr(Updater, "initialize", initialize)
        monkeypatch.setattr(Updater, "shutdown", shutdown)

        with pytest.raises(RuntimeError, match="initialize"):
            async with updater:
                pass

        assert self.test_flag == "stop"

    @pytest.mark.parametrize("drop_pending_updates", (True, False))
    async def test_polling_basic(self, monkeypatch, updater,
                                 drop_pending_updates):
        updates = asyncio.Queue()
        await updates.put(Update(update_id=1))
        await updates.put(Update(update_id=2))

        async def get_updates(*args, **kwargs):
            next_update = await updates.get()
            updates.task_done()
            return [next_update]

        orig_del_webhook = updater.bot.delete_webhook

        async def delete_webhook(*args, **kwargs):
            # Dropping pending updates is done by passing the parameter to delete_webhook
            if kwargs.get("drop_pending_updates"):
                self.message_count += 1
            return await orig_del_webhook(*args, **kwargs)

        monkeypatch.setattr(updater.bot, "get_updates", get_updates)
        monkeypatch.setattr(updater.bot, "delete_webhook", delete_webhook)

        async with updater:
            return_value = await updater.start_polling(
                drop_pending_updates=drop_pending_updates)
            assert return_value is updater.update_queue
            assert updater.running
            await updates.join()
            await updater.stop()
            assert not updater.running
            assert not (await updater.bot.get_webhook_info()).url
            if drop_pending_updates:
                assert self.message_count == 1
            else:
                assert self.message_count == 0

            await updates.put(Update(update_id=3))
            await updates.put(Update(update_id=4))

            # We call the same logic twice to make sure that restarting the updater works as well
            await updater.start_polling(
                drop_pending_updates=drop_pending_updates)
            assert updater.running
            await updates.join()
            await updater.stop()
            assert not updater.running
            assert not (await updater.bot.get_webhook_info()).url

        self.received = []
        self.message_count = 0
        while not updater.update_queue.empty():
            update = updater.update_queue.get_nowait()
            self.message_count += 1
            self.received.append(update.update_id)

        assert self.message_count == 4
        assert self.received == [1, 2, 3, 4]

    async def test_start_polling_already_running(self, updater):
        async with updater:
            await updater.start_polling()
            task = asyncio.create_task(updater.start_polling())
            with pytest.raises(RuntimeError, match="already running"):
                await task
            await updater.stop()
            with pytest.raises(RuntimeError, match="not running"):
                await updater.stop()

    async def test_start_polling_get_updates_parameters(
            self, updater, monkeypatch):
        update_queue = asyncio.Queue()
        await update_queue.put(Update(update_id=1))

        expected = dict(
            timeout=10,
            read_timeout=2,
            write_timeout=DEFAULT_NONE,
            connect_timeout=DEFAULT_NONE,
            pool_timeout=DEFAULT_NONE,
            allowed_updates=None,
            api_kwargs=None,
        )

        async def get_updates(*args, **kwargs):
            for key, value in expected.items():
                assert kwargs.pop(key, None) == value

            offset = kwargs.pop("offset", None)
            # Check that we don't get any unexpected kwargs
            assert kwargs == {}

            if offset is not None and self.message_count != 0:
                assert offset == self.message_count + 1, "get_updates got wrong `offset` parameter"

            update = await update_queue.get()
            self.message_count = update.update_id
            update_queue.task_done()
            return [update]

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

        async with updater:
            await updater.start_polling()
            await update_queue.join()
            await updater.stop()

            expected = dict(
                timeout=42,
                read_timeout=43,
                write_timeout=44,
                connect_timeout=45,
                pool_timeout=46,
                allowed_updates=["message"],
                api_kwargs=None,
            )

            await update_queue.put(Update(update_id=2))
            await updater.start_polling(
                timeout=42,
                read_timeout=43,
                write_timeout=44,
                connect_timeout=45,
                pool_timeout=46,
                allowed_updates=["message"],
            )
            await update_queue.join()
            await updater.stop()

    @pytest.mark.parametrize("exception_class", (InvalidToken, TelegramError))
    @pytest.mark.parametrize("retries", (3, 0))
    async def test_start_polling_bootstrap_retries(self, updater, monkeypatch,
                                                   exception_class, retries):
        async def do_request(*args, **kwargs):
            self.message_count += 1
            raise exception_class(str(self.message_count))

        async with updater:
            # Patch within the context so that updater.bot.initialize can still be called
            # by the context manager
            monkeypatch.setattr(HTTPXRequest, "do_request", do_request)

            if exception_class == InvalidToken:
                with pytest.raises(InvalidToken, match="1"):
                    await updater.start_polling(bootstrap_retries=retries)
            else:
                with pytest.raises(TelegramError, match=str(retries + 1)):
                    await updater.start_polling(bootstrap_retries=retries)

    @pytest.mark.parametrize(
        "error,callback_should_be_called",
        argvalues=[
            (TelegramError("TestMessage"), True),
            (RetryAfter(1), False),
            (TimedOut("TestMessage"), False),
        ],
        ids=("TelegramError", "RetryAfter", "TimedOut"),
    )
    @pytest.mark.parametrize("custom_error_callback", [True, False])
    async def test_start_polling_exceptions_and_error_callback(
            self, monkeypatch, updater, error, callback_should_be_called,
            custom_error_callback, caplog):
        get_updates_event = asyncio.Event()

        async def get_updates(*args, **kwargs):
            # So that the main task has a chance to be called
            await asyncio.sleep(0)

            get_updates_event.set()
            raise error

        monkeypatch.setattr(updater.bot, "get_updates", get_updates)
        monkeypatch.setattr(updater.bot, "set_webhook",
                            lambda *args, **kwargs: True)

        with pytest.raises(
                TypeError,
                match="`error_callback` must not be a coroutine function"):
            await updater.start_polling(error_callback=get_updates)

        async with updater:
            self.err_handler_called = asyncio.Event()

            with caplog.at_level(logging.ERROR):
                if custom_error_callback:
                    await updater.start_polling(
                        error_callback=self.error_callback)
                else:
                    await updater.start_polling()

                # Also makes sure that the error handler was called
                await get_updates_event.wait()

                if callback_should_be_called:
                    # Make sure that the error handler was called
                    if custom_error_callback:
                        assert self.received == error
                    else:
                        assert len(caplog.records) > 0
                        records = (record.getMessage()
                                   for record in caplog.records)
                        assert "Error while getting Updates: TestMessage" in records

                # Make sure that get_updates was called
                assert get_updates_event.is_set()

            # Make sure that Updater polling keeps running
            self.err_handler_called.clear()
            get_updates_event.clear()
            caplog.clear()

            # Also makes sure that the error handler was called
            await get_updates_event.wait()

            if callback_should_be_called:
                if callback_should_be_called:
                    if custom_error_callback:
                        assert self.received == error
                    else:
                        assert len(caplog.records) > 0
                        records = (record.getMessage()
                                   for record in caplog.records)
                        assert "Error while getting Updates: TestMessage" in records
            await updater.stop()

    async def test_start_polling_unexpected_shutdown(self, updater,
                                                     monkeypatch, caplog):
        update_queue = asyncio.Queue()
        await update_queue.put(Update(update_id=1))
        await update_queue.put(Update(update_id=2))
        first_update_event = asyncio.Event()
        second_update_event = asyncio.Event()

        async def get_updates(*args, **kwargs):
            self.message_count = kwargs.get("offset")
            update = await update_queue.get()
            if update.update_id == 1:
                first_update_event.set()
            else:
                await second_update_event.wait()
            return [update]

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

        async with updater:
            with caplog.at_level(logging.ERROR):
                await updater.start_polling()

                await first_update_event.wait()
                # Unfortunately we need to use the private attribute here to produce the problem
                updater._running = False
                second_update_event.set()

                await asyncio.sleep(0.1)
                assert caplog.records
                records = (record.getMessage() for record in caplog.records)
                assert any("Updater stopped unexpectedly." in record
                           for record in records)

        # Make sure that the update_id offset wasn't increased
        assert self.message_count == 2

    async def test_start_polling_not_running_after_failure(
            self, updater, monkeypatch):
        # Unfortunately we have to use some internal logic to trigger an exception
        async def _start_polling(*args, **kwargs):
            raise Exception("Test Exception")

        monkeypatch.setattr(Updater, "_start_polling", _start_polling)

        async with updater:
            with pytest.raises(Exception, match="Test Exception"):
                await updater.start_polling()
            assert updater.running is False

    async def test_polling_update_de_json_fails(self, monkeypatch, updater,
                                                caplog):
        updates = asyncio.Queue()
        raise_exception = True
        await updates.put(Update(update_id=1))

        async def get_updates(*args, **kwargs):
            if raise_exception:
                await asyncio.sleep(0.01)
                raise TypeError("Invalid Data")

            next_update = await updates.get()
            updates.task_done()
            return [next_update]

        orig_del_webhook = updater.bot.delete_webhook

        async def delete_webhook(*args, **kwargs):
            # Dropping pending updates is done by passing the parameter to delete_webhook
            if kwargs.get("drop_pending_updates"):
                self.message_count += 1
            return await orig_del_webhook(*args, **kwargs)

        monkeypatch.setattr(updater.bot, "get_updates", get_updates)
        monkeypatch.setattr(updater.bot, "delete_webhook", delete_webhook)

        async with updater:
            with caplog.at_level(logging.CRITICAL):
                await updater.start_polling()
                assert updater.running
                await asyncio.sleep(1)

            assert len(caplog.records) > 0
            for record in caplog.records:
                assert record.getMessage().startswith(
                    "Something went wrong processing")

            # Make sure that everything works fine again when receiving proper updates
            raise_exception = False
            await asyncio.sleep(0.5)
            caplog.clear()
            with caplog.at_level(logging.CRITICAL):
                await updates.join()
            assert len(caplog.records) == 0

            await updater.stop()
            assert not updater.running

    @pytest.mark.parametrize("ext_bot", [True, False])
    @pytest.mark.parametrize("drop_pending_updates", (True, False))
    @pytest.mark.parametrize("secret_token", ["SecretToken", None])
    async def test_webhook_basic(self, monkeypatch, updater,
                                 drop_pending_updates, ext_bot, secret_token):
        # 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 = DictBot(updater.bot.token)

        async def delete_webhook(*args, **kwargs):
            # Dropping pending updates is done by passing the parameter to delete_webhook
            if kwargs.get("drop_pending_updates"):
                self.message_count += 1
            return True

        async def set_webhook(*args, **kwargs):
            return True

        monkeypatch.setattr(updater.bot, "set_webhook", set_webhook)
        monkeypatch.setattr(updater.bot, "delete_webhook", delete_webhook)

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

        async with updater:
            return_value = await updater.start_webhook(
                drop_pending_updates=drop_pending_updates,
                ip_address=ip,
                port=port,
                url_path="TOKEN",
                secret_token=secret_token,
            )
            assert return_value is updater.update_queue
            assert updater.running

            # Now, we send an update to the server
            update = make_message_update("Webhook")
            await send_webhook_message(ip,
                                       port,
                                       update.to_json(),
                                       "TOKEN",
                                       secret_token=secret_token)
            assert (await
                    updater.update_queue.get()).to_dict() == update.to_dict()

            # Returns Not Found if path is incorrect
            response = await send_webhook_message(ip, port, "123456",
                                                  "webhook_handler.py")
            assert response.status_code == HTTPStatus.NOT_FOUND

            # Returns METHOD_NOT_ALLOWED if method is not allowed
            response = await send_webhook_message(ip,
                                                  port,
                                                  None,
                                                  "TOKEN",
                                                  get_method="HEAD")
            assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED

            if secret_token:
                # Returns Forbidden if no secret token is set
                response_text = "<html><title>403: {0}</title><body>403: {0}</body></html>"
                response = await send_webhook_message(ip, port,
                                                      update.to_json(),
                                                      "TOKEN")
                assert response.status_code == HTTPStatus.FORBIDDEN
                assert response.text == response_text.format(
                    "Request did not include the secret token")

                # Returns Forbidden if the secret token is wrong
                response = await send_webhook_message(
                    ip,
                    port,
                    update.to_json(),
                    "TOKEN",
                    secret_token="NotTheSecretToken")
                assert response.status_code == HTTPStatus.FORBIDDEN
                assert response.text == response_text.format(
                    "Request had the wrong secret token")

            await updater.stop()
            assert not updater.running

            if drop_pending_updates:
                assert self.message_count == 1
            else:
                assert self.message_count == 0

            # We call the same logic twice to make sure that restarting the updater works as well
            await updater.start_webhook(
                drop_pending_updates=drop_pending_updates,
                ip_address=ip,
                port=port,
                url_path="TOKEN",
            )
            assert updater.running
            update = make_message_update("Webhook")
            await send_webhook_message(ip, port, update.to_json(), "TOKEN")
            assert (await
                    updater.update_queue.get()).to_dict() == update.to_dict()
            await updater.stop()
            assert not updater.running

    async def test_start_webhook_already_running(self, updater, monkeypatch):
        async def return_true(*args, **kwargs):
            return True

        monkeypatch.setattr(updater.bot, "set_webhook", return_true)
        monkeypatch.setattr(updater.bot, "delete_webhook", return_true)

        ip = "127.0.0.1"
        port = randrange(1024, 49152)  # Select random port
        async with updater:
            await updater.start_webhook(ip, port, url_path="TOKEN")
            task = asyncio.create_task(
                updater.start_webhook(ip, port, url_path="TOKEN"))
            with pytest.raises(RuntimeError, match="already running"):
                await task
            await updater.stop()
            with pytest.raises(RuntimeError, match="not running"):
                await updater.stop()

    async def test_start_webhook_parameters_passing(self, updater,
                                                    monkeypatch):
        expected_delete_webhook = dict(drop_pending_updates=None, )

        expected_set_webhook = dict(
            certificate=None,
            max_connections=40,
            allowed_updates=None,
            ip_address=None,
            secret_token=None,
            **expected_delete_webhook,
        )

        async def set_webhook(*args, **kwargs):
            for key, value in expected_set_webhook.items():
                assert kwargs.pop(key, None) == value, f"set, {key}, {value}"

            assert kwargs in (
                {
                    "url": "http://127.0.0.1:80/"
                },
                {
                    "url": "http://*****:*****@pytest.mark.parametrize("invalid_data", [True, False],
                             ids=("invalid data", "valid data"))
    async def test_webhook_arbitrary_callback_data(self, monkeypatch, updater,
                                                   invalid_data, chat_id):
        """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

        async def return_true(*args, **kwargs):
            return True

        try:
            monkeypatch.setattr(updater.bot, "set_webhook", return_true)
            monkeypatch.setattr(updater.bot, "delete_webhook", return_true)

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

            async with updater:
                await updater.start_webhook(ip, port, url_path="TOKEN")
                # Now, we send an update to the server
                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)

                update = make_message_update(
                    message="test_webhook_arbitrary_callback_data",
                    message_factory=make_message,
                    reply_markup=reply_markup,
                    user=updater.bot.bot,
                )

                await send_webhook_message(ip, port, update.to_json(), "TOKEN")
                received_update = await updater.update_queue.get()

                assert received_update.update_id == update.update_id
                message_dict = update.message.to_dict()
                received_dict = received_update.message.to_dict()
                message_dict.pop("reply_markup")
                received_dict.pop("reply_markup")
                assert message_dict == received_dict

                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"

                await 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()

    async def test_webhook_invalid_ssl(self, monkeypatch, updater):
        async def return_true(*args, **kwargs):
            return True

        monkeypatch.setattr(updater.bot, "set_webhook", return_true)
        monkeypatch.setattr(updater.bot, "delete_webhook", return_true)

        ip = "127.0.0.1"
        port = randrange(1024, 49152)  # Select random port
        async with updater:
            with pytest.raises(TelegramError, match="Invalid SSL"):
                await updater.start_webhook(
                    ip,
                    port,
                    url_path="TOKEN",
                    cert=Path(__file__).as_posix(),
                    key=Path(__file__).as_posix(),
                    bootstrap_retries=0,
                    drop_pending_updates=False,
                    webhook_url=None,
                    allowed_updates=None,
                )
            assert updater.running is False

    async def test_webhook_ssl_just_for_telegram(self, monkeypatch, updater):
        """Here we just test that the SSL info is pased to Telegram, but __not__ to the the
        webhook server"""
        async def set_webhook(**kwargs):
            self.test_flag.append(bool(kwargs.get("certificate")))
            return True

        async def return_true(*args, **kwargs):
            return True

        orig_wh_server_init = WebhookServer.__init__

        def webhook_server_init(*args, **kwargs):
            self.test_flag = [kwargs.get("ssl_ctx") is None]
            orig_wh_server_init(*args, **kwargs)

        monkeypatch.setattr(updater.bot, "set_webhook", set_webhook)
        monkeypatch.setattr(updater.bot, "delete_webhook", return_true)
        monkeypatch.setattr(
            "telegram.ext._utils.webhookhandler.WebhookServer.__init__",
            webhook_server_init)

        ip = "127.0.0.1"
        port = randrange(1024, 49152)  # Select random port
        async with updater:
            await updater.start_webhook(ip,
                                        port,
                                        webhook_url=None,
                                        cert=Path(__file__).as_posix())

            # Now, we send an update to the server
            update = make_message_update(message="test_message")
            await send_webhook_message(ip, port, update.to_json())
            assert (await
                    updater.update_queue.get()).to_dict() == update.to_dict()
            assert self.test_flag == [True, True]
            await updater.stop()

    @pytest.mark.parametrize("exception_class", (InvalidToken, TelegramError))
    @pytest.mark.parametrize("retries", (3, 0))
    async def test_start_webhook_bootstrap_retries(self, updater, monkeypatch,
                                                   exception_class, retries):
        async def do_request(*args, **kwargs):
            self.message_count += 1
            raise exception_class(str(self.message_count))

        async with updater:
            # Patch within the context so that updater.bot.initialize can still be called
            # by the context manager
            monkeypatch.setattr(HTTPXRequest, "do_request", do_request)

            if exception_class == InvalidToken:
                with pytest.raises(InvalidToken, match="1"):
                    await updater.start_webhook(bootstrap_retries=retries)
            else:
                with pytest.raises(TelegramError, match=str(retries + 1)):
                    await updater.start_webhook(bootstrap_retries=retries, )

    async def test_webhook_invalid_posts(self, updater, monkeypatch):
        async def return_true(*args, **kwargs):
            return True

        monkeypatch.setattr(updater.bot, "set_webhook", return_true)
        monkeypatch.setattr(updater.bot, "delete_webhook", return_true)

        ip = "127.0.0.1"
        port = randrange(1024, 49152)

        async with updater:
            await updater.start_webhook(listen=ip, port=port)

            response = await send_webhook_message(ip,
                                                  port,
                                                  None,
                                                  content_type="invalid")
            assert response.status_code == HTTPStatus.FORBIDDEN

            response = await send_webhook_message(
                ip,
                port,
                payload_str="<root><bla>data</bla></root>",
                content_type="application/xml",
            )
            assert response.status_code == HTTPStatus.FORBIDDEN

            response = await send_webhook_message(ip,
                                                  port,
                                                  "dummy-payload",
                                                  content_len=None)
            assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR

            # httpx already complains about bad content length in _send_webhook_message
            # before the requests below reach the webhook, but not testing this is probably
            # okay
            # response = await send_webhook_message(
            #   ip, port, 'dummy-payload', content_len=-2)
            # assert response.status_code == HTTPStatus.FORBIDDEN
            # response = await send_webhook_message(
            #   ip, port, 'dummy-payload', content_len='not-a-number')
            # assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR

            await updater.stop()

    async def test_webhook_update_de_json_fails(self, monkeypatch, updater,
                                                caplog):
        async def delete_webhook(*args, **kwargs):
            return True

        async def set_webhook(*args, **kwargs):
            return True

        def de_json_fails(*args, **kwargs):
            raise TypeError("Invalid input")

        monkeypatch.setattr(updater.bot, "set_webhook", set_webhook)
        monkeypatch.setattr(updater.bot, "delete_webhook", delete_webhook)
        orig_de_json = Update.de_json
        monkeypatch.setattr(Update, "de_json", de_json_fails)

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

        async with updater:
            return_value = await updater.start_webhook(
                ip_address=ip,
                port=port,
                url_path="TOKEN",
            )
            assert return_value is updater.update_queue
            assert updater.running

            # Now, we send an update to the server
            update = make_message_update("Webhook")
            with caplog.at_level(logging.CRITICAL):
                await send_webhook_message(ip, port, update.to_json(), "TOKEN")

            assert len(caplog.records) == 1
            assert caplog.records[-1].getMessage().startswith(
                "Something went wrong processing")

            # Make sure that everything works fine again when receiving proper updates
            caplog.clear()
            with caplog.at_level(logging.CRITICAL):
                monkeypatch.setattr(Update, "de_json", orig_de_json)
                await send_webhook_message(ip, port, update.to_json(), "TOKEN")
                assert (
                    await
                    updater.update_queue.get()).to_dict() == update.to_dict()
            assert len(caplog.records) == 0

            await updater.stop()
            assert not updater.running
Esempio n. 24
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"
Esempio n. 25
0
    async def _start_webhook(
        self,
        listen: str,
        port: int,
        url_path: str,
        bootstrap_retries: int,
        allowed_updates: Optional[List[str]],
        cert: Union[str, Path] = None,
        key: Union[str, Path] = None,
        drop_pending_updates: bool = None,
        webhook_url: str = None,
        ready: asyncio.Event = None,
        ip_address: str = None,
        max_connections: int = 40,
        secret_token: str = None,
    ) -> None:
        self._logger.debug("Updater thread started (webhook)")

        if not url_path.startswith("/"):
            url_path = f"/{url_path}"

        # Create Tornado app instance
        app = WebhookAppClass(url_path, self.bot, self.update_queue,
                              secret_token)

        # Form SSL Context
        # An SSLError is raised if the private key does not match with the certificate
        # Note that we only use the SSL certificate for the WebhookServer, if the key is also
        # present. This is because the WebhookServer may not actually be in charge of performing
        # the SSL handshake, e.g. in case a reverse proxy is used
        if cert is not None and key is not None:
            try:
                ssl_ctx: Optional[ssl.SSLContext] = ssl.create_default_context(
                    ssl.Purpose.CLIENT_AUTH)
                ssl_ctx.load_cert_chain(cert, key)  # type: ignore[union-attr]
            except ssl.SSLError as exc:
                raise TelegramError("Invalid SSL Certificate") from exc
        else:
            ssl_ctx = None

        # Create and start server
        self._httpd = WebhookServer(listen, port, app, ssl_ctx)

        if not webhook_url:
            webhook_url = self._gen_webhook_url(
                protocol="https" if ssl_ctx else "http",
                listen=listen,
                port=port,
                url_path=url_path,
            )

        # We pass along the cert to the webhook if present.
        await self._bootstrap(
            # Passing a Path or string only works if the bot is running against a local bot API
            # server, so let's read the contents
            cert=Path(cert).read_bytes() if cert else None,
            max_retries=bootstrap_retries,
            drop_pending_updates=drop_pending_updates,
            webhook_url=webhook_url,
            allowed_updates=allowed_updates,
            ip_address=ip_address,
            max_connections=max_connections,
            secret_token=secret_token,
        )

        await self._httpd.serve_forever(ready=ready)
Esempio n. 26
0
class TestRequest:
    test_flag = None

    @pytest.fixture(autouse=True)
    def reset(self):
        self.test_flag = None

    def test_slot_behaviour(self, mro_slots):
        inst = HTTPXRequest()
        for attr in inst.__slots__:
            if attr.startswith("__"):
                attr = f"_{inst.__class__.__name__}{attr}"
            assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
        assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"

    async def test_context_manager(self, monkeypatch):
        async def initialize():
            self.test_flag = ["initialize"]

        async def shutdown():
            self.test_flag.append("stop")

        httpx_request = HTTPXRequest()

        monkeypatch.setattr(httpx_request, "initialize", initialize)
        monkeypatch.setattr(httpx_request, "shutdown", shutdown)

        async with httpx_request:
            pass

        assert self.test_flag == ["initialize", "stop"]

    async def test_context_manager_exception_on_init(self, monkeypatch):
        async def initialize():
            raise RuntimeError("initialize")

        async def shutdown():
            self.test_flag = "stop"

        httpx_request = HTTPXRequest()

        monkeypatch.setattr(httpx_request, "initialize", initialize)
        monkeypatch.setattr(httpx_request, "shutdown", shutdown)

        with pytest.raises(RuntimeError, match="initialize"):
            async with httpx_request:
                pass

        assert self.test_flag == "stop"

    async def test_replaced_unprintable_char(self, monkeypatch, httpx_request):
        """Clients can send arbitrary bytes in callback data. Make sure that we just replace
        those
        """
        server_response = b'{"result": "test_string\x80"}'

        monkeypatch.setattr(httpx_request, "do_request", mocker_factory(response=server_response))

        assert await httpx_request.post(None, None, None) == "test_string�"
        # Explicitly call `parse_json_payload` here is well so that this public method is covered
        # not only implicitly.
        assert httpx_request.parse_json_payload(server_response) == {"result": "test_string�"}

    async def test_illegal_json_response(self, monkeypatch, httpx_request: HTTPXRequest):
        # for proper JSON it should be `"result":` instead of `result:`
        server_response = b'{result: "test_string"}'

        monkeypatch.setattr(httpx_request, "do_request", mocker_factory(response=server_response))

        with pytest.raises(TelegramError, match="Invalid server response"):
            await httpx_request.post(None, None, None)

    async def test_chat_migrated(self, monkeypatch, httpx_request: HTTPXRequest):
        server_response = b'{"ok": "False", "parameters": {"migrate_to_chat_id": 123}}'

        monkeypatch.setattr(
            httpx_request,
            "do_request",
            mocker_factory(response=server_response, return_code=HTTPStatus.BAD_REQUEST),
        )

        with pytest.raises(ChatMigrated, match="New chat id: 123") as exc_info:
            await httpx_request.post(None, None, None)

        assert exc_info.value.new_chat_id == 123

    async def test_retry_after(self, monkeypatch, httpx_request: HTTPXRequest):
        server_response = b'{"ok": "False", "parameters": {"retry_after": 42}}'

        monkeypatch.setattr(
            httpx_request,
            "do_request",
            mocker_factory(response=server_response, return_code=HTTPStatus.BAD_REQUEST),
        )

        with pytest.raises(RetryAfter, match="Retry in 42") as exc_info:
            await httpx_request.post(None, None, None)

        assert exc_info.value.retry_after == 42

    async def test_unknown_request_params(self, monkeypatch, httpx_request: HTTPXRequest):
        server_response = b'{"ok": "False", "parameters": {"unknown": "42"}}'

        monkeypatch.setattr(
            httpx_request,
            "do_request",
            mocker_factory(response=server_response, return_code=HTTPStatus.BAD_REQUEST),
        )

        with pytest.raises(
            BadRequest,
            match="{'unknown': '42'}",
        ):
            await httpx_request.post(None, None, None)

    @pytest.mark.parametrize("description", [True, False])
    async def test_error_description(self, monkeypatch, httpx_request: HTTPXRequest, description):
        response_data = {"ok": "False"}
        if description:
            match = "ErrorDescription"
            response_data["description"] = match
        else:
            match = "Unknown HTTPError"

        server_response = json.dumps(response_data).encode("utf-8")

        monkeypatch.setattr(
            httpx_request,
            "do_request",
            mocker_factory(response=server_response, return_code=-1),
        )

        with pytest.raises(NetworkError, match=match):
            await httpx_request.post(None, None, None)

        # Special casing for bad gateway
        if not description:
            monkeypatch.setattr(
                httpx_request,
                "do_request",
                mocker_factory(response=server_response, return_code=HTTPStatus.BAD_GATEWAY),
            )

            with pytest.raises(NetworkError, match="Bad Gateway"):
                await httpx_request.post(None, None, None)

    @pytest.mark.parametrize(
        "code, exception_class",
        [
            (HTTPStatus.FORBIDDEN, Forbidden),
            (HTTPStatus.NOT_FOUND, InvalidToken),
            (HTTPStatus.UNAUTHORIZED, InvalidToken),
            (HTTPStatus.BAD_REQUEST, BadRequest),
            (HTTPStatus.CONFLICT, Conflict),
            (HTTPStatus.BAD_GATEWAY, NetworkError),
            (-1, NetworkError),
        ],
    )
    async def test_special_errors(
        self, monkeypatch, httpx_request: HTTPXRequest, code, exception_class
    ):
        server_response = b'{"ok": "False", "description": "Test Message"}'

        monkeypatch.setattr(
            httpx_request,
            "do_request",
            mocker_factory(response=server_response, return_code=code),
        )

        with pytest.raises(exception_class, match="Test Message"):
            await httpx_request.post(None, None, None)

    @pytest.mark.parametrize(
        ["exception", "catch_class", "match"],
        [
            (TelegramError("TelegramError"), TelegramError, "TelegramError"),
            (
                RuntimeError("CustomError"),
                Exception,
                r"HTTP implementation: RuntimeError\('CustomError'\)",
            ),
        ],
    )
    async def test_exceptions_in_do_request(
        self, monkeypatch, httpx_request: HTTPXRequest, exception, catch_class, match
    ):
        async def do_request(*args, **kwargs):
            raise exception

        monkeypatch.setattr(
            httpx_request,
            "do_request",
            do_request,
        )

        with pytest.raises(catch_class, match=match):
            await httpx_request.post(None, None, None)

    async def test_retrieve(self, monkeypatch, httpx_request):
        """Here we just test that retrieve gives us the raw bytes instead of trying to parse them
        as json
        """
        server_response = b'{"result": "test_string\x80"}'

        monkeypatch.setattr(httpx_request, "do_request", mocker_factory(response=server_response))

        assert await httpx_request.retrieve(None, None) == server_response

    async def test_timeout_propagation(self, monkeypatch, httpx_request):
        async def make_assertion(*args, **kwargs):
            self.test_flag = (
                kwargs.get("read_timeout"),
                kwargs.get("connect_timeout"),
                kwargs.get("write_timeout"),
                kwargs.get("pool_timeout"),
            )
            return HTTPStatus.OK, b'{"ok": "True", "result": {}}'

        monkeypatch.setattr(httpx_request, "do_request", make_assertion)

        await httpx_request.post("url", "method")
        assert self.test_flag == (DEFAULT_NONE, DEFAULT_NONE, DEFAULT_NONE, DEFAULT_NONE)

        await httpx_request.post(
            "url", None, read_timeout=1, connect_timeout=2, write_timeout=3, pool_timeout=4
        )
        assert self.test_flag == (1, 2, 3, 4)