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 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"
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
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
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"
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')
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')
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')
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')))
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')))
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"
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
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
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')))
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')
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
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
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
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:
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
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"
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)
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)