diff --git a/post_parser.py b/post_parser.py index 57a5ea3..7b9556f 100644 --- a/post_parser.py +++ b/post_parser.py @@ -291,7 +291,6 @@ class PostParser: return None def _format_reply_info(self, message: Message) -> Union[str, None]: - logger.error(f"_format_reply_info message: {str(message)}") if getattr(message, "service", None) and 'PINNED_MESSAGE' in str(message.service) and (reply_to := getattr(message, "reply_to_message", None)): reply_text = reply_to.text or reply_to.caption or '' if len(reply_text) > 100: @@ -473,6 +472,7 @@ class PostParser: return '' def process_message(self, message: Message) -> Dict[Any, Any]: + logger.error(f"process_message message: {str(message)}") result = { 'channel': self.get_channel_username(message), 'message_id': message.id, @@ -546,7 +546,6 @@ class PostParser: if reply_html := self._format_reply_info(message): content_body.append(reply_html) - logger.error(f"_generate_html_body message: {str(message)}") if message.text: text = message.text.html elif message.caption: text = message.caption.html diff --git a/tests/plan.md b/tests/plan.md deleted file mode 100644 index 195d390..0000000 --- a/tests/plan.md +++ /dev/null @@ -1,756 +0,0 @@ -# Test Plan - -## `config.py` - -### `get_settings()` - -* **Цель:** Проверить корректность чтения настроек из переменных окружения и использование значений по умолчанию. -* **Тест-кейсы:** - * **Все переменные установлены:** - * Установить все переменные окружения (`TG_API_ID`, `TG_API_HASH`, `SESSION_PATH`, `API_HOST`, `API_PORT`, `PYROGRAM_BRIDGE_URL`, `LOG_LEVEL`, `DEBUG`, `TOKEN`, `TIME_BASED_MERGE`, `SHOW_BRIDGE_LINK`, `SHOW_POST_FLAGS`). - * Вызвать `get_settings()`. - * Проверить, что возвращенный словарь содержит правильные значения и типы данных (int для `tg_api_id`, `api_port`; bool для `debug`, `time_based_merge`, `show_bridge_link`, `show_post_flags`). - * **Использование значений по умолчанию:** - * Установить только обязательные `TG_API_ID` и `TG_API_HASH`. - * Не устанавливать остальные переменные. - * Вызвать `get_settings()`. - * Проверить, что для необязательных переменных используются значения по умолчанию (`data`, `0.0.0.0`, `8000`, `""`, `INFO`, `False`, `""`, `False`, `False`, `False`). - * **Частично установленные переменные:** - * Установить `TG_API_ID`, `TG_API_HASH` и некоторые необязательные переменные (например, `API_PORT`, `DEBUG="True"`). - * Вызвать `get_settings()`. - * Проверить, что установленные переменные считаны корректно, а для остальных используются значения по умолчанию. - * **Проверка булевых флагов:** - * Проверить различные варианты для булевых флагов (`DEBUG`, `TIME_BASED_MERGE`, `SHOW_BRIDGE_LINK`, `SHOW_POST_FLAGS`): - * `"True"` -> `True` - * `"true"` -> `True` (для `TIME_BASED_MERGE`, `SHOW_BRIDGE_LINK`, `SHOW_POST_FLAGS`) - * `"False"` -> `False` - * `"anything else"` -> `False` - * Переменная не установлена -> `False` - * **Проверка обязательных переменных (негативный тест):** - * Не устанавливать `TG_API_ID`. - * Мокнуть `os._exit` и `print`. - * Вызвать `get_settings()`. - * Проверить, что `print` был вызван с сообщением об ошибке "TG_API_ID and TG_API_HASH must be set". - * Проверить, что `os._exit(1)` был вызван. - * Повторить тест, не устанавливая `TG_API_HASH`. - * Повторить тест, не устанавливая обе переменные. - * **Проверка типов:** - * Убедиться, что `tg_api_id` и `api_port` всегда возвращаются как `int`. - * Передать нечисловое значение в `API_PORT` и проверить, что используется значение по умолчанию `8000`. - * Передать нечисловое значение в `TG_API_ID` и проверить, что возникает ошибка (или как это обрабатывается). *Примечание: Текущий код вызовет `ValueError` при `int(tg_api_id)`, это тоже можно проверить.* - -## `url_signer.py` - -### `KeyManager` - -* **Цель:** Проверить управление секретным ключом (генерация, чтение из файла, кеширование в памяти). -* **Замечание:** Для тестов необходимо активно использовать моки (`unittest.mock`) для изоляции от файловой системы и недетерминированной генерации ключей (`os.path.exists`, `open`, `secrets.token_hex`). Создать временную директорию `data` для тестов или полностью мокать файловые операции. -* **Тест-кейсы:** - * **Генерация нового ключа:** - * Убедиться, что `SECRET_FILE` (`data/media_digest.key`) не существует (используя мок `os.path.exists`). - * Мокнуть `secrets.token_hex` для возврата предопределенного ключа. - * Мокнуть `open` для проверки записи в файл. - * Вызвать `KeyManager.get_or_create_signing_key()`. - * Проверить, что `secrets.token_hex(32)` был вызван. - * Проверить, что `open` был вызван для записи (`'w'`) в `KeyManager.SECRET_FILE`. - * Проверить, что в файл был записан сгенерированный ключ. - * Проверить, что метод вернул сгенерированный ключ. - * Сбросить состояние моков `os.path.exists` и `open`. - * Вызвать `KeyManager.get_or_create_signing_key()` снова. - * Проверить, что `secrets.token_hex` и `open` для записи *не* были вызваны (ключ взят из памяти `cls.signing_key`). - * Проверить, что метод вернул тот же ключ. - * **Чтение ключа из файла:** - * Установить `KeyManager.signing_key = None` для сброса кеша в памяти. - * Мокнуть `os.path.exists` чтобы он возвращал `True`. - * Мокнуть `open` для чтения (`'r'`) из `KeyManager.SECRET_FILE`, возвращая предопределенный ключ. - * Вызвать `KeyManager.get_or_create_signing_key()`. - * Проверить, что `os.path.exists` был вызван. - * Проверить, что `open` был вызван для чтения. - * Проверить, что `secrets.token_hex` *не* был вызван. - * Проверить, что метод вернул ключ, прочитанный из файла. - * Вызвать `KeyManager.get_or_create_signing_key()` снова. - * Проверить, что `os.path.exists` и `open` для чтения *не* были вызваны (ключ взят из памяти `cls.signing_key`). - * Проверить, что метод вернул тот же ключ. - * **Использование ключа из памяти:** - * Установить `KeyManager.signing_key` в предопределенное значение. - * Вызвать `KeyManager.get_or_create_signing_key()`. - * Проверить, что `os.path.exists`, `open` и `secrets.token_hex` *не* были вызваны. - * Проверить, что метод вернул установленное значение `KeyManager.signing_key`. - -### `generate_media_digest(url)` - -* **Цель:** Проверить корректность генерации HMAC-SHA1 дайджеста для URL. -* **Замечание:** Мокнуть `KeyManager.get_or_create_signing_key`, чтобы возвращать фиксированный ключ для предсказуемости тестов. -* **Тест-кейсы:** - * **Успешная генерация:** - * Мокнуть `KeyManager.get_or_create_signing_key` для возврата известного ключа (например, `'test_key'`). - * Вызвать `generate_media_digest` с тестовым URL (например, `'http://example.com/media.jpg'`). - * Рассчитать ожидаемый HMAC-SHA1 дайджест для `http://example.com/media.jpg` с ключом `test_key` и взять первые 8 символов. - * Проверить, что функция вернула ожидаемый 8-символьный дайджест. - * **Разные URL:** - * Повторить тест с другим URL, убедиться, что дайджест отличается. - * **Пустой URL:** - * Вызвать `generate_media_digest('')`. - * Проверить, что результат является корректным 8-символьным дайджестом для пустой строки и тестового ключа. - * **URL с разными символами:** - * Протестировать URL с кириллицей, пробелами, спецсимволами, чтобы убедиться в корректной обработке `url.encode('utf-8')`. - -### `verify_media_digest(url, digest)` - -* **Цель:** Проверить корректность верификации дайджеста. -* **Замечание:** Можно мокнуть `generate_media_digest`, чтобы изолировать тесты верификации от тестов генерации, либо использовать общий мок для `KeyManager.get_or_create_signing_key`. Использование `hmac.compare_digest` важно для безопасности, тесты должны это учитывать. -* **Тест-кейсы:** - * **Успешная верификация:** - * Сгенерировать дайджест `d = generate_media_digest(url)` (или использовать предрассчитанный). - * Вызвать `verify_media_digest(url, d)`. - * Проверить, что результат `True`. - * **Неверный дайджест:** - * Вызвать `verify_media_digest(url, 'invalid_digest')`. - * Проверить, что результат `False`. - * **Неверный URL:** - * Сгенерировать дайджест `d = generate_media_digest(url1)`. - * Вызвать `verify_media_digest(url2, d)` где `url1 != url2`. - * Проверить, что результат `False`. - * **Пустой дайджест:** - * Вызвать `verify_media_digest(url, '')`. - * Проверить, что результат `False`. - * **Дайджест `None`:** - * Вызвать `verify_media_digest(url, None)`. - * Проверить, что результат `False`. - * **Дайджест неверной длины:** - * Вызвать `verify_media_digest(url, 'short')`. - * Проверить, что результат `False` (из-за несовпадения с ожидаемым 8-символьным дайджестом при `hmac.compare_digest`). - * Вызвать `verify_media_digest(url, 'toolongdigest')`. - * Проверить, что результат `False`. - -## `telegram_client.py` - -### `TelegramClient` - -* **Цель:** Проверить инициализацию, запуск и остановку клиента Pyrogram. -* **Замечание:** Основное внимание уделяется мокированию зависимостей (`pyrogram.Client`, `config.get_settings`, `os.makedirs`) и проверке правильности вызовов методов. -* **Тест-кейсы:** - * **Инициализация (`__init__`)**: - * Мокнуть `config.get_settings` для возврата тестовых настроек. - * Мокнуть `os.makedirs`. - * Мокнуть `pyrogram.Client`. - * Создать экземпляр `TelegramClient`. - * Проверить, что `get_settings` был вызван. - * Проверить, что `os.makedirs` был вызван с правильным `session_path` из настроек и `exist_ok=True`. - * Проверить, что `pyrogram.Client` был вызван с правильными `name`, `api_id`, `api_hash`, `workdir` из настроек. - * **Ошибка создания директории:** - * Настроить мок `os.makedirs` так, чтобы он вызывал исключение. - * Проверить, что при создании экземпляра `TelegramClient` возникает исключение. - * **Запуск (`start`)**: - * Мокнуть `self.client` (экземпляр `pyrogram.Client`) с атрибутом `is_connected` и методом `start`. - * **Клиент не подключен:** - * Установить `self.client.is_connected = False`. - * Вызвать `await client.start()`. - * Проверить, что `self.client.start()` был вызван. - * **Клиент уже подключен:** - * Установить `self.client.is_connected = True`. - * Вызвать `await client.start()`. - * Проверить, что `self.client.start()` *не* был вызван. - * **Ошибка при запуске:** - * Настроить мок `self.client.start()` так, чтобы он вызывал исключение. - * Установить `self.client.is_connected = False`. - * Проверить, что при вызове `await client.start()` возникает исключение. - * **Остановка (`stop`)**: - * Мокнуть `self.client` с атрибутом `is_connected` и методом `stop`. - * **Клиент подключен:** - * Установить `self.client.is_connected = True`. - * Вызвать `await client.stop()`. - * Проверить, что `self.client.stop()` был вызван. - * **Клиент не подключен:** - * Установить `self.client.is_connected = False`. - * Вызвать `await client.stop()`. - * Проверить, что `self.client.stop()` *не* был вызван. - * **Ошибка при остановке (хотя `stop` обычно не вызывает исключений, но можно проверить):** - * Настроить мок `self.client.stop()` так, чтобы он вызывал исключение (если это возможно). - * Установить `self.client.is_connected = True`. - * Вызвать `await client.stop()` и обработать потенциальное исключение (или убедиться, что оно не мешает). - -## `post_parser.py` - -### `PostParser` - -* **Цель:** Проверить парсинг и форматирование сообщений Telegram в JSON и HTML. -* **Замечание:** Это ключевой модуль. Тесты должны покрывать все типы сообщений, медиа, форварды, ответы и другие особенности. Потребуется обширное использование моков для `pyrogram.Client.get_messages`, `config.get_settings`, `url_signer.generate_media_digest`, `os.path`, `open`, `json`, `bleach`, `datetime`. Рекомендуется создать фабрику или набор фикстур для генерации тестовых объектов `Message` с различными атрибутами. - -#### Инициализация и Основные методы - -* **`__init__(self, client)`**: - * **Цель:** Проверить сохранение клиента. - * **Тест-кейс:** Создать мок клиента, инициализировать `PostParser`, проверить, что `self.client` равен моку. -* **`channel_name_prepare(self, channel)`**: - * **Цель:** Проверить корректное преобразование ID канала. - * **Тест-кейсы:** - * Передать строковый ID вида `"-100..."`. Проверить, что возвращается `int`. - * Передать строковый username (`"mychannel"`). Проверить, что возвращается строка без изменений. - * Передать числовой ID (если возможно). Проверить поведение (вероятно, вернет как есть). - * Передать пустую строку. Проверить поведение. -* **`get_post(self, channel, post_id, output_type, debug)`**: - * **Цель:** Проверить основную логику получения и форматирования поста. - * **Замечание:** Мокнуть `self.client.get_messages`, `self.channel_name_prepare`, `self._format_html`, `self.process_message`. - * **Тест-кейсы:** - * **Успешный вызов (HTML):** - * Мокнуть `get_messages` для возврата тестового `Message`. - * Вызвать `get_post` с `output_type='html'`. - * Проверить, что `channel_name_prepare` был вызван с `channel`. - * Проверить, что `get_messages` был вызван с подготовленным каналом и `post_id`. - * Проверить, что `_format_html` был вызван с полученным сообщением и `debug`. - * Проверить, что `process_message` *не* был вызван напрямую (вызывается внутри `_format_html`). - * Проверить, что результат равен значению, возвращенному `_format_html`. - * **Успешный вызов (JSON):** - * Мокнуть `get_messages` для возврата тестового `Message`. - * Вызвать `get_post` с `output_type='json'`. - * Проверить, что `channel_name_prepare` и `get_messages` были вызваны. - * Проверить, что `process_message` был вызван с полученным сообщением. - * Проверить, что `_format_html` *не* был вызван. - * Проверить, что результат равен значению, возвращенному `process_message`. - * Проверить, что результат равен значению, возвращенному `process_message`. - * **Пост не найден:** - * Мокнуть `get_messages` для возврата `None` или пустого списка. - * Вызвать `get_post`. - * Проверить, что возвращается `None`. - * **Неверный `output_type`:** - * Вызвать `get_post` с `output_type='invalid'`. - * Проверить, что возвращается `None` и логируется ошибка. - * **Исключение при `get_messages`:** - * Настроить мок `get_messages` так, чтобы он вызывал исключение. - * Проверить, что `get_post` перехватывает исключение, логирует его и вызывает `raise`. - * **Включен Debug:** - * Мокнуть `print`. - * Установить `Config["debug"] = True`. - * Вызвать `get_post`. - * Проверить, что `print(message)` был вызван после `get_messages`. - -#### Вспомогательные функции извлечения информации - -* **`_get_author_info(self, message)`**: - * **Цель:** Проверить извлечение имени автора. - * **Тест-кейсы:** - * Сообщение от канала (есть `sender_chat` с `title` и `username`). - * Сообщение от канала (есть `sender_chat` только с `title`). - * Сообщение от пользователя (есть `from_user` с `first_name`, `last_name`, `username`). - * Сообщение от пользователя (есть `from_user` только с `first_name`). - * Сообщение от пользователя (есть `from_user` только с `username`). - * Сообщение без `sender_chat` и `from_user`. Проверить возврат `"Unknown author"`. - * Поля с `None` или пустыми строками. -* **`_generate_title(self, message)`**: - * **Цель:** Проверить генерацию заголовка поста. - * **Тест-кейсы:** - * Сообщение "Channel created" (`channel_chat_created=True`). - * Текст содержит только URL YouTube. - * Текст содержит только другой URL. - * Текст содержит только URL и пробельные символы. - * Текст пустой, медиа - фото/видео/GIF/аудио/голос/кружок/стикер/опрос/PDF/документ/webpage. Проверить соответствующие заголовки. - * Текст пустой, медиа нет. - * Текст короткий (до 100 символов), без URL и тегов. - * Текст длинный (больше 100 символов). Проверить обрезку по последнему пробелу и добавление "...". - * Текст содержит URL и HTML-теги. Проверить их удаление перед генерацией заголовка. - * Текст после удаления URL/тегов пустой, но есть `web_page`. Проверить заголовок "🔗 Web link". - * Текст содержит несколько строк. Проверить, что берется только первая. -* **`_format_forward_info(self, message)`**: - * **Цель:** Проверить форматирование информации о пересылке. - * **Тест-кейсы:** - * Переслано из чата с `username`. - * Переслано из чата без `username`. - * Переслано от пользователя с `username`. - * Переслано от пользователя без `username`. - * Переслано от скрытого пользователя (`forward_sender_name`). - * Сообщение не переслано (все соответствующие атрибуты `None`). Проверить возврат `None`. - * Поля `title`, `first_name`, `last_name` пустые или `None`. -* **`_format_reply_info(self, message)`**: - * **Цель:** Проверить форматирование информации об ответе/закрепе. - * **Тест-кейсы:** - * Закрепленное сообщение (`message.service`, `reply_to_message`). - * Ответ на сообщение в чате с `username`. - * Ответ на сообщение в чате без `username`. - * Текст ответа длинный (> 100 символов). Проверить обрезку. - * Текст ответа пустой (`text` и `caption` - `None`). - * Сообщение не является ответом/закрепом. Проверить возврат `None`. -* **`_extract_reactions(self, message)`**: - * **Цель:** Проверить извлечение реакций. - * **Тест-кейсы:** - * Сообщение с реакциями (`message.reactions.reactions`). - * Сообщение без реакций (`message.reactions` - `None`). - * Реакции разных типов (эмодзи). -* **`_extract_flags(self, message)`**: - * **Цель:** Проверить извлечение флагов поста. - * **Замечание:** Мокнуть `self._generate_html_body`, `self.get_channel_username`. - * **Тест-кейсы:** - * Пересланное сообщение (`fwd`). - * Видео/анимация с коротким текстом (`video`). - * Пост без медиа или опрос (`no_image`). - * Стикер (`sticker`). - * Текст содержит "стрим", "вебинар" и т.д. (`stream`). - * Текст содержит "донат" (`donat`). - * Реакции с 30+ 🤡 (`clown`). - * Реакции с 30+ 💩 (`poo`). - * Текст содержит "#реклама", "erid" и т.д. (`advert`). - * Текст/атрибуты содержат http/https ссылки (`link`). - * Текст содержит упоминания `@channel` (`mention`). - * Текст содержит скрытые ссылки `t.me/+...` (`hid_channel`). - * Текст содержит ссылки на *другие* открытые каналы `t.me/otherchannel` (`foreign_channel`). Проверить, что ссылка на *текущий* канал не добавляет флаг. - * Комбинации флагов. - * Сообщение без атрибутов, вызывающих флаги. -* **`get_channel_username(self, message)`**: - * **Цель:** Проверить извлечение username или ID канала. - * **Тест-кейсы:** - * Чат с активным `username` в `usernames`. - * Чат с неактивным `username` в `usernames`, но есть `username`. - * Чат только с `username`. - * Чат без `username`, но с ID вида `-100...`. Проверить возврат `str(ID)`. - * Чат без `username` и с другим ID. Проверить возврат `None`. - * Объект `message` не содержит `chat` или `chat` равен `None`. - -#### Функции форматирования HTML - -* **`_format_html(self, message, debug)`**: - * **Цель:** Проверить сборку HTML из частей. - * **Замечание:** Мокнуть `self.process_message` и `json.dumps`. - * **Тест-кейсы:** - * Обычный вызов (`debug=False`). Проверить вызов `process_message`, сборку строк из `['html']['header']`, `['html']['media']`, `['html']['body']`, `['html']['footer']`. - * Вызов с `debug=True`. Проверить добавление блока `
` с `json.dumps(message)`. -* **`_format_flags(self, message)`**: - * **Цель:** Проверить форматирование флагов в HTML. - * **Замечание:** Мокнуть `self._extract_flags` и `Config['show_post_flags']`. - * **Тест-кейсы:** - * `show_post_flags = False`. Проверить возврат пустой строки. - * `show_post_flags = True`, `_extract_flags` возвращает пустой список. Проверить возврат пустой строки. - * `show_post_flags = True`, `_extract_flags` возвращает список флагов. Проверить генерацию `div.message-flags` с флагами. -* **`_generate_html_header(self, message)`**: - * **Цель:** Проверить генерацию HTML-заголовка. - * **Замечание:** Мокнуть `self._format_forward_info`, `self._format_reply_info`, `self._sanitize_html`. - * **Тест-кейсы:** - * Есть информация о форварде. Проверить включение результата `_format_forward_info`. - * Есть информация об ответе (но нет форварда). Проверить включение результата `_format_reply_info`. - * Нет ни форварда, ни ответа. Проверить пустой результат (до санации). - * Проверить вызов `_sanitize_html`. -* **`_generate_html_body(self, message)`**: - * **Цель:** Проверить генерацию основного тела HTML. - * **Замечание:** Мокнуть `self._add_hyperlinks_to_raw_urls`, `self._format_poll`, `self._sanitize_html`. - * **Тест-кейсы:** - * Есть `message.text`. Проверить использование `message.text.html`, замену `\n` на `
`, вызов `_add_hyperlinks_to_raw_urls`, оборачивание в `div.message-text`. - * Есть `message.caption` (текста нет). Аналогично `message.text`. - * Есть `message.poll`. Проверить вызов `_format_poll` и добавление результата. - * Есть `message.channel_chat_created`. Проверить добавление `div.message-service`. - * Комбинация текста и опроса. - * Нет текста, подписи, опроса, сервис-сообщения. Проверить пустой результат (до санации). - * Проверить вызов `_sanitize_html`. -* **`_generate_html_media(self, message)`**: - * **Цель:** Проверить генерацию HTML для медиа. - * **Замечание:** Мокнуть `self._save_media_file_ids`, `self._get_file_unique_id`, `self.get_channel_username`, `Config['pyrogram_bridge_url']`, `generate_media_digest`, `self._format_webpage`, `self._sanitize_html`. - * **Тест-кейсы:** - * Разные типы медиа (`PHOTO`, `VIDEO`, `ANIMATION`, `VIDEO_NOTE`, `AUDIO`, `VOICE`, `STICKER`, `DOCUMENT` (обычный), `DOCUMENT` (PDF)). Проверить генерацию соответствующих тегов (`img`, `video`, `audio`) с правильными `src` (включая подпись URL), `style`, `controls`. - * Медиа без `file_unique_id`. - * Сообщение без медиа. - * Сообщение с `web_page`, короткий текст/подпись. Проверить вызов `_format_webpage` и добавление результата. - * Сообщение с `web_page`, длинный текст/подпись. Проверить, что `_format_webpage` *не* добавляется. - * Проверить вызов `_save_media_file_ids` в начале. - * Проверить вызов `_sanitize_html` в конце. -* **`_format_webpage(self, webpage, message)`**: - * **Цель:** Проверить форматирование превью веб-страницы. - * **Замечание:** Мокнуть `Config['pyrogram_bridge_url']`, `self.get_channel_username`, `generate_media_digest`, `self._add_hyperlinks_to_raw_urls`. - * **Тест-кейсы:** - * Обычная веб-страница (с `site_name`, `title`, `description`, `display_url`, `photo`). - * Ссылка на сообщение Telegram (`type="telegram_message"`). Проверить особое форматирование. - * Отсутствие некоторых атрибутов (`site_name`, `title`, `description`, `display_url`, `photo`, `url`). - * Описание (`description`) с переносами строк и URL. Проверить обработку. - * Фото в превью (`webpage.photo`). Проверить генерацию `img src` с подписью. - * Исключение во время обработки. Проверить логирование и возврат `None`. -* **`_generate_html_footer(self, message)`**: - * **Цель:** Проверить генерацию HTML-подвала. - * **Замечание:** Мокнуть `self._reactions_views_links`, `self._format_flags`, `self._sanitize_html`. - * **Тест-кейсы:** - * Есть реакции/просмотры/ссылки и флаги. Проверить включение результатов обеих функций с `
`. - * Есть только реакции/просмотры/ссылки. - * Есть только флаги. - * Нет ни того, ни другого. Проверить возврат `
` (до санации). - * Проверить вызов `_sanitize_html`. -* **`_add_hyperlinks_to_raw_urls(self, text)`**: - * **Цель:** Проверить добавление тегов `` к "сырым" URL. - * **Тест-кейсы:** - * Текст без URL. - * Текст с одним URL. - * Текст с несколькими URL. - * Текст с URL, который уже внутри тега ``. Проверить, что он не оборачивается еще раз. - * Текст с URL внутри других тегов (``, ``). Проверить, что они оборачиваются. - * Сложный текст с HTML и URL. - * Исключение во время обработки. Проверить логирование и возврат оригинального текста. -* **`_reactions_views_links(self, message)`**: - * **Цель:** Проверить форматирование реакций, просмотров, даты и ссылок. - * **Замечание:** Мокнуть `self.get_channel_username`, `Config['pyrogram_bridge_url']`, `Config['show_bridge_link']`, `Config['token']`, `self._sanitize_html`. - * **Тест-кейсы:** - * Есть реакции (обычные, платные `is_paid`, кастомные `custom_emoji_id`), просмотры, дата. Проверить корректное форматирование первой строки. - * Отсутствуют реакции/просмотры. - * Канал с `username`. Проверить генерацию ссылок `tg://`, `https://t.me/`. - * Канал с ID (`-100...`). Проверить генерацию ссылок `tg://.../c/...`, `https://t.me/c/...`. - * `Config['show_bridge_link'] = True`. Проверить добавление ссылки на бридж с токеном. - * `Config['show_bridge_link'] = False`. - * Нет ни реакций, ни просмотров, ни даты, ни ссылок. Проверить возврат `None`. - * Исключение во время обработки. Проверить логирование и возврат `None`. - * Проверить вызов `_sanitize_html`. -* **`_format_poll(self, poll)`**: - * **Цель:** Проверить форматирование опроса. - * **Тест-кейсы:** - * Обычный опрос с вопросом и вариантами ответа. - * Опрос без вариантов ответа. - * Атрибут `poll.options` отсутствует или `None`. - * Исключение во время обработки. Проверить логирование и возврат строки с ошибкой. -* **`_get_file_unique_id(self, message)`**: - * **Цель:** Проверить извлечение `file_unique_id` для разных типов медиа. - * **Тест-кейсы:** - * Все поддерживаемые `MessageMediaType` (photo, video, document и т.д.). - * `MessageMediaType.WEB_PAGE` с фото. - * `MessageMediaType.WEB_PAGE` без фото. - * Неподдерживаемый тип медиа. - * `message.media` = `None`. - * Отсутствие нужного атрибута (например, `message.photo` есть, но у него нет `file_unique_id`). Проверить обработку исключения и возврат `None`. -* **`_save_media_file_ids(self, message)`**: - * **Цель:** Проверить сохранение `file_unique_id` в JSON-файл. - * **Замечание:** Мокнуть `self.get_channel_username`, `os.path.abspath`, `os.path.exists`, `open`, `json.load`, `json.dump`, `datetime.now`. - * **Тест-кейсы:** - * Сообщение с медиа (не большим видео). - * Файл `media_file_ids.json` не существует. Проверить создание файла и запись данных. - * Файл существует, но записи для этого `file_unique_id` нет. Проверить добавление записи. - * Файл существует, и запись для этого `file_unique_id` уже есть. Проверить обновление времени `added`. - * Сообщение с большим видео (> 100MB). Проверить, что запись *не* происходит. - * Сообщение без медиа. Проверить, что запись *не* происходит. - * Не удалось получить `channel_username`. Проверить логирование и выход. - * Ошибка при чтении/записи JSON-файла. Проверить логирование. - * Ошибка при получении `file_unique_id`. Проверить логирование. -* **`_sanitize_html(self, html_raw)`**: - * **Цель:** Проверить санацию HTML. - * **Замечание:** Мокнуть `HTMLSanitizer` и `CSSSanitizer`. - * **Тест-кейсы:** - * Передать валидный HTML с разрешенными тегами и атрибутами. Проверить, что он не изменился. - * Передать HTML с запрещенными тегами (`