#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2024 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. import asyncio import datetime import pytest from telegram import ( ForumTopic, ForumTopicClosed, ForumTopicCreated, ForumTopicEdited, ForumTopicReopened, GeneralForumTopicHidden, GeneralForumTopicUnhidden, Sticker, ) from telegram.error import BadRequest from tests.auxil.slots import mro_slots TEST_MSG_TEXT = "Topics are forever" TEST_TOPIC_ICON_COLOR = 0x6FB9F0 TEST_TOPIC_NAME = "Sad bot true: real stories" @pytest.fixture(scope="module") async def emoji_id(bot): emoji_sticker_list = await bot.get_forum_topic_icon_stickers() first_sticker = emoji_sticker_list[0] return first_sticker.custom_emoji_id @pytest.fixture(scope="module") async def forum_topic_object(forum_group_id, emoji_id): return ForumTopic( message_thread_id=forum_group_id, name=TEST_TOPIC_NAME, icon_color=TEST_TOPIC_ICON_COLOR, icon_custom_emoji_id=emoji_id, ) @pytest.fixture async def real_topic(bot, emoji_id, forum_group_id): result = await bot.create_forum_topic( chat_id=forum_group_id, name=TEST_TOPIC_NAME, icon_color=TEST_TOPIC_ICON_COLOR, icon_custom_emoji_id=emoji_id, ) yield result result = await bot.delete_forum_topic( chat_id=forum_group_id, message_thread_id=result.message_thread_id ) assert result is True, "Topic was not deleted" class TestForumTopicWithoutRequest: def test_slot_behaviour(self, forum_topic_object): inst = forum_topic_object 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" async def test_expected_values(self, emoji_id, forum_group_id, forum_topic_object): assert forum_topic_object.message_thread_id == forum_group_id assert forum_topic_object.icon_color == TEST_TOPIC_ICON_COLOR assert forum_topic_object.name == TEST_TOPIC_NAME assert forum_topic_object.icon_custom_emoji_id == emoji_id def test_de_json(self, offline_bot, emoji_id, forum_group_id): assert ForumTopic.de_json(None, bot=offline_bot) is None json_dict = { "message_thread_id": forum_group_id, "name": TEST_TOPIC_NAME, "icon_color": TEST_TOPIC_ICON_COLOR, "icon_custom_emoji_id": emoji_id, } topic = ForumTopic.de_json(json_dict, offline_bot) assert topic.api_kwargs == {} assert topic.message_thread_id == forum_group_id assert topic.icon_color == TEST_TOPIC_ICON_COLOR assert topic.name == TEST_TOPIC_NAME assert topic.icon_custom_emoji_id == emoji_id def test_to_dict(self, emoji_id, forum_group_id, forum_topic_object): topic_dict = forum_topic_object.to_dict() assert isinstance(topic_dict, dict) assert topic_dict["message_thread_id"] == forum_group_id assert topic_dict["name"] == TEST_TOPIC_NAME assert topic_dict["icon_color"] == TEST_TOPIC_ICON_COLOR assert topic_dict["icon_custom_emoji_id"] == emoji_id def test_equality(self, emoji_id, forum_group_id): a = ForumTopic( message_thread_id=forum_group_id, name=TEST_TOPIC_NAME, icon_color=TEST_TOPIC_ICON_COLOR, ) b = ForumTopic( message_thread_id=forum_group_id, name=TEST_TOPIC_NAME, icon_color=TEST_TOPIC_ICON_COLOR, icon_custom_emoji_id=emoji_id, ) c = ForumTopic( message_thread_id=forum_group_id, name=f"{TEST_TOPIC_NAME}!", icon_color=TEST_TOPIC_ICON_COLOR, ) d = ForumTopic( message_thread_id=forum_group_id + 1, name=TEST_TOPIC_NAME, icon_color=TEST_TOPIC_ICON_COLOR, ) e = ForumTopic( message_thread_id=forum_group_id, name=TEST_TOPIC_NAME, icon_color=0xFFD67E, ) assert a == b assert hash(a) == hash(b) assert a != c assert hash(a) != hash(c) assert a != d assert hash(a) != hash(d) assert a != e assert hash(a) != hash(e) class TestForumMethodsWithRequest: async def test_create_forum_topic(self, real_topic): result = real_topic assert isinstance(result, ForumTopic) assert result.name == TEST_TOPIC_NAME assert result.message_thread_id assert isinstance(result.icon_color, int) assert isinstance(result.icon_custom_emoji_id, str) async def test_create_forum_topic_with_only_required_args(self, bot, forum_group_id): result = await bot.create_forum_topic(chat_id=forum_group_id, name=TEST_TOPIC_NAME) assert isinstance(result, ForumTopic) assert result.name == TEST_TOPIC_NAME assert result.message_thread_id assert isinstance(result.icon_color, int) # color is still there though it was not passed assert result.icon_custom_emoji_id is None result = await bot.delete_forum_topic( chat_id=forum_group_id, message_thread_id=result.message_thread_id ) assert result is True, "Failed to delete forum topic" async def test_get_forum_topic_icon_stickers(self, bot): emoji_sticker_list = await bot.get_forum_topic_icon_stickers() first_sticker = emoji_sticker_list[0] assert first_sticker.emoji == "📰" assert first_sticker.height == 512 assert first_sticker.width == 512 assert first_sticker.is_animated assert not first_sticker.is_video assert first_sticker.set_name == "Topics" assert first_sticker.type == Sticker.CUSTOM_EMOJI assert first_sticker.thumbnail.width == 128 assert first_sticker.thumbnail.height == 128 # The following data of first item returned has changed in the past already, # so check sizes loosely and ID's only by length of string assert first_sticker.thumbnail.file_size in range(2000, 7000) assert first_sticker.file_size in range(20000, 70000) assert len(first_sticker.custom_emoji_id) == 19 assert len(first_sticker.thumbnail.file_unique_id) == 16 assert len(first_sticker.file_unique_id) == 15 async def test_edit_forum_topic(self, emoji_id, forum_group_id, bot, real_topic): result = await bot.edit_forum_topic( chat_id=forum_group_id, message_thread_id=real_topic.message_thread_id, name=f"{TEST_TOPIC_NAME}_EDITED", icon_custom_emoji_id=emoji_id, ) assert result is True, "Failed to edit forum topic" # no way of checking the edited name, just the boolean result async def test_send_message_to_topic(self, bot, forum_group_id, real_topic): message_thread_id = real_topic.message_thread_id message = await bot.send_message( chat_id=forum_group_id, text=TEST_MSG_TEXT, message_thread_id=message_thread_id ) assert message.text == TEST_MSG_TEXT assert message.is_topic_message is True assert message.message_thread_id == message_thread_id async def test_close_and_reopen_forum_topic(self, bot, forum_group_id, real_topic): message_thread_id = real_topic.message_thread_id result = await bot.close_forum_topic( chat_id=forum_group_id, message_thread_id=message_thread_id, ) assert result is True, "Failed to close forum topic" # bot will still be able to send a message to a closed topic, so can't test anything like # the inability to post to the topic result = await bot.reopen_forum_topic( chat_id=forum_group_id, message_thread_id=message_thread_id, ) assert result is True, "Failed to reopen forum topic" async def test_unpin_all_forum_topic_messages(self, bot, forum_group_id, real_topic): # We need 2 or more pinned msgs for this to work, else we get Chat_not_modified error message_thread_id = real_topic.message_thread_id pin_msg_tasks = set() awaitables = { bot.send_message(forum_group_id, TEST_MSG_TEXT, message_thread_id=message_thread_id) for _ in range(2) } for coro in asyncio.as_completed(awaitables): msg = await coro pin_msg_tasks.add(asyncio.create_task(msg.pin())) assert all([await task for task in pin_msg_tasks]) is True, "Message(s) were not pinned" result = await bot.unpin_all_forum_topic_messages(forum_group_id, message_thread_id) assert result is True, "Failed to unpin all the messages in forum topic" async def test_unpin_all_general_forum_topic_messages(self, bot, forum_group_id): # We need 2 or more pinned msgs for this to work, else we get Chat_not_modified error pin_msg_tasks = set() awaitables = {bot.send_message(forum_group_id, TEST_MSG_TEXT) for _ in range(2)} for coro in asyncio.as_completed(awaitables): msg = await coro pin_msg_tasks.add(asyncio.create_task(msg.pin())) assert all([await task for task in pin_msg_tasks]) is True, "Message(s) were not pinned" result = await bot.unpin_all_general_forum_topic_messages(forum_group_id) assert result is True, "Failed to unpin all the messages in forum topic" async def test_edit_general_forum_topic(self, bot, forum_group_id): result = await bot.edit_general_forum_topic( chat_id=forum_group_id, name=f"GENERAL_{datetime.datetime.now().timestamp()}", ) assert result is True, "Failed to edit general forum topic" # no way of checking the edited name, just the boolean result async def test_close_reopen_hide_unhide_general_forum_topic(self, bot, forum_group_id): """Since reopening also unhides and hiding also closes, testing (un)hiding and closing/reopening in different tests would mean that the tests have to be executed in a specific order. For stability, we instead test all of them in one test.""" # We first ensure that the topic is open and visible # Otherwise the tests below will fail try: await bot.reopen_general_forum_topic(chat_id=forum_group_id) except BadRequest as exc: # If the topic is already open, we get BadRequest: Topic_not_modified if "Topic_not_modified" not in exc.message: raise exc # first just close, bot don't hide result = await bot.close_general_forum_topic( chat_id=forum_group_id, ) assert result is True, "Failed to close general forum topic" # then hide result = await bot.hide_general_forum_topic( chat_id=forum_group_id, ) assert result is True, "Failed to hide general forum topic" # then unhide, but don't reopen result = await bot.unhide_general_forum_topic( chat_id=forum_group_id, ) assert result is True, "Failed to unhide general forum topic" # finally, reopen # as this also unhides, this should ensure that the topic is open and visible # for the next test run result = await bot.reopen_general_forum_topic( chat_id=forum_group_id, ) assert result is True, "Failed to reopen general forum topic" @pytest.fixture(scope="module") def topic_created(): return ForumTopicCreated(name=TEST_TOPIC_NAME, icon_color=TEST_TOPIC_ICON_COLOR) class TestForumTopicCreatedWithoutRequest: def test_slot_behaviour(self, topic_created): for attr in topic_created.__slots__: assert getattr(topic_created, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(topic_created)) == len( set(mro_slots(topic_created)) ), "duplicate slot" def test_expected_values(self, topic_created): assert topic_created.icon_color == TEST_TOPIC_ICON_COLOR assert topic_created.name == TEST_TOPIC_NAME def test_de_json(self, offline_bot): assert ForumTopicCreated.de_json(None, bot=offline_bot) is None json_dict = {"icon_color": TEST_TOPIC_ICON_COLOR, "name": TEST_TOPIC_NAME} action = ForumTopicCreated.de_json(json_dict, offline_bot) assert action.api_kwargs == {} assert action.icon_color == TEST_TOPIC_ICON_COLOR assert action.name == TEST_TOPIC_NAME def test_to_dict(self, topic_created): action_dict = topic_created.to_dict() assert isinstance(action_dict, dict) assert action_dict["name"] == TEST_TOPIC_NAME assert action_dict["icon_color"] == TEST_TOPIC_ICON_COLOR def test_equality(self, emoji_id): a = ForumTopicCreated(name=TEST_TOPIC_NAME, icon_color=TEST_TOPIC_ICON_COLOR) b = ForumTopicCreated( name=TEST_TOPIC_NAME, icon_color=TEST_TOPIC_ICON_COLOR, icon_custom_emoji_id=emoji_id, ) c = ForumTopicCreated(name=f"{TEST_TOPIC_NAME}!", icon_color=TEST_TOPIC_ICON_COLOR) d = ForumTopicCreated(name=TEST_TOPIC_NAME, icon_color=0xFFD67E) assert a == b assert hash(a) == hash(b) assert a != c assert hash(a) != hash(c) assert a != d assert hash(a) != hash(d) class TestForumTopicClosedWithoutRequest: def test_slot_behaviour(self): action = ForumTopicClosed() for attr in action.__slots__: assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" def test_de_json(self): action = ForumTopicClosed.de_json({}, None) assert action.api_kwargs == {} assert isinstance(action, ForumTopicClosed) def test_to_dict(self): action = ForumTopicClosed() action_dict = action.to_dict() assert action_dict == {} class TestForumTopicReopenedWithoutRequest: def test_slot_behaviour(self): action = ForumTopicReopened() for attr in action.__slots__: assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" def test_de_json(self): action = ForumTopicReopened.de_json({}, None) assert action.api_kwargs == {} assert isinstance(action, ForumTopicReopened) def test_to_dict(self): action = ForumTopicReopened() action_dict = action.to_dict() assert action_dict == {} @pytest.fixture(scope="module") def topic_edited(emoji_id): return ForumTopicEdited(name=TEST_TOPIC_NAME, icon_custom_emoji_id=emoji_id) class TestForumTopicEdited: def test_slot_behaviour(self, topic_edited): for attr in topic_edited.__slots__: assert getattr(topic_edited, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(topic_edited)) == len(set(mro_slots(topic_edited))), "duplicate slot" def test_expected_values(self, topic_edited, emoji_id): assert topic_edited.name == TEST_TOPIC_NAME assert topic_edited.icon_custom_emoji_id == emoji_id def test_de_json(self, bot, emoji_id): assert ForumTopicEdited.de_json(None, bot=bot) is None json_dict = {"name": TEST_TOPIC_NAME, "icon_custom_emoji_id": emoji_id} action = ForumTopicEdited.de_json(json_dict, bot) assert action.api_kwargs == {} assert action.name == TEST_TOPIC_NAME assert action.icon_custom_emoji_id == emoji_id # special test since it is mentioned in the docs that icon_custom_emoji_id can be an # empty string json_dict = {"icon_custom_emoji_id": ""} action = ForumTopicEdited.de_json(json_dict, bot) assert not action.icon_custom_emoji_id def test_to_dict(self, topic_edited, emoji_id): action_dict = topic_edited.to_dict() assert isinstance(action_dict, dict) assert action_dict["name"] == TEST_TOPIC_NAME assert action_dict["icon_custom_emoji_id"] == emoji_id def test_equality(self, emoji_id): a = ForumTopicEdited(name=TEST_TOPIC_NAME, icon_custom_emoji_id="") b = ForumTopicEdited( name=TEST_TOPIC_NAME, icon_custom_emoji_id="", ) c = ForumTopicEdited(name=f"{TEST_TOPIC_NAME}!", icon_custom_emoji_id=emoji_id) d = ForumTopicEdited(icon_custom_emoji_id="") assert a == b assert hash(a) == hash(b) assert a != c assert hash(a) != hash(c) assert a != d assert hash(a) != hash(d) class TestGeneralForumTopicHidden: def test_slot_behaviour(self): action = GeneralForumTopicHidden() for attr in action.__slots__: assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" def test_de_json(self): action = GeneralForumTopicHidden.de_json({}, None) assert action.api_kwargs == {} assert isinstance(action, GeneralForumTopicHidden) def test_to_dict(self): action = GeneralForumTopicHidden() action_dict = action.to_dict() assert action_dict == {} class TestGeneralForumTopicUnhidden: def test_slot_behaviour(self): action = GeneralForumTopicUnhidden() for attr in action.__slots__: assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" def test_de_json(self): action = GeneralForumTopicUnhidden.de_json({}, None) assert action.api_kwargs == {} assert isinstance(action, GeneralForumTopicUnhidden) def test_to_dict(self): action = GeneralForumTopicUnhidden() action_dict = action.to_dict() assert action_dict == {}