Files
pyrogram-bridge/tests/plan.md
T
2025-04-04 22:09:52 +03:00

756 lines
72 KiB
Markdown

# 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`. Проверить добавление блока `<pre>` с `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` на `<br>`, вызов `_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`.
* **Тест-кейсы:**
* Есть реакции/просмотры/ссылки и флаги. Проверить включение результатов обеих функций с `<br>`.
* Есть только реакции/просмотры/ссылки.
* Есть только флаги.
* Нет ни того, ни другого. Проверить возврат `<br>` (до санации).
* Проверить вызов `_sanitize_html`.
* **`_add_hyperlinks_to_raw_urls(self, text)`**:
* **Цель:** Проверить добавление тегов `<a>` к "сырым" URL.
* **Тест-кейсы:**
* Текст без URL.
* Текст с одним URL.
* Текст с несколькими URL.
* Текст с URL, который уже внутри тега `<a>`. Проверить, что он не оборачивается еще раз.
* Текст с URL внутри других тегов (`<b>`, `<i>`). Проверить, что они оборачиваются.
* Сложный текст с 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 с запрещенными тегами (`<script>`). Проверить их удаление.
* Передать HTML с запрещенными атрибутами (`onclick`). Проверить их удаление.
* Передать HTML с разрешенными CSS-стилями. Проверить, что они остались.
* Передать HTML с запрещенными CSS-стилями (`position: absolute`). Проверить их удаление.
* Передать невалидный HTML. Проверить поведение `bleach`.
* Исключение во время санации. Проверить логирование и возврат исходного `html_raw`.
#### `process_message(self, message)`
* **Цель:** Проверить сборку итогового словаря JSON.
* **Замечание:** Мокнуть все внутренние вызовы (`get_channel_username`, `_generate_title`, `_generate_html_header`, `_generate_html_body`, `_generate_html_media`, `_generate_html_footer`, `_extract_flags`, `_get_author_info`, `_extract_reactions`, `datetime.timestamp`).
* **Тест-кейсы:**
* Сообщение со всеми возможными полями (текст, дата, просмотры, реакции, медиа, `media_group_id`, `web_page` и т.д.). Проверить, что все поля в итоговом словаре заполнены корректно результатами вызовов моков.
* Сообщение с минимальным набором полей.
* Сообщение без `web_page`. Проверить, что ключ `webpage` отсутствует в результате.
* Проверить корректность форматов (`timestamp` для даты, строка для `date_formatted`, список для `flags`, словарь для `reactions`, словарь для `webpage`).
## `rss_generator.py`
* **Цель:** Проверить генерацию RSS и HTML лент, включая группировку сообщений, фильтрацию и обработку ошибок.
* **Замечание:** Много асинхронных функций. Использовать `pytest-asyncio`. Мокировать `PostParser`, `pyrogram.Client`, `feedgen.feed.FeedGenerator`, `datetime`, `re`, `config.get_settings`. Понадобятся тестовые объекты `Message` и результаты `PostParser.process_message`.
### Вспомогательные функции группировки и рендеринга
* **`_create_time_based_media_groups(messages, merge_seconds)`**:
* **Цель:** Проверить объединение сообщений в группы по времени.
* **Тест-кейсы:**
* Сообщения без `media_group_id`, временной интервал < `merge_seconds`. Проверить, что они объединяются в одну группу с `media_group_id` вида `"time_..."`.
* Сообщения без `media_group_id`, временной интервал > `merge_seconds`. Проверить, что они остаются в разных группах (без `media_group_id` или каждая в своей `"time_..."` группе, если их >1).
* Сообщения с существующими `media_group_id`. Проверить, что они не объединяются по времени с другими группами или одиночными сообщениями.
* Смешанные сообщения: с `media_group_id` и без, с разными интервалами.
* Группа сообщений без `media_group_id` в начале/конце списка.
* Пустой список `messages`.
* Список с одним сообщением.
* Проверить корректность установки `media_group_id` для всех сообщений в созданной группе.
* **`_create_messages_groups(messages)`**:
* **Цель:** Проверить группировку сообщений по `media_group_id`.
* **Тест-кейсы:**
* Список сообщений с несколькими `media_group_id`. Проверить, что создаются правильные группы.
* Список сообщений только с `media_group_id`.
* Список сообщений без `media_group_id`. Проверить, что каждое сообщение становится отдельной группой.
* Смешанные сообщения.
* Сообщения с сервисными событиями (`PINNED_MESSAGE`, `NEW_CHAT_PHOTO`, `NEW_CHAT_TITLE`). Проверить, что они пропускаются.
* Пустой список `messages`.
* Проверить, что сообщения внутри каждой медиа-группы отсортированы по `id` (возрастание).
* Проверить, что итоговый список `processing_groups` отсортирован по дате первого сообщения в группе (убывание).
* **`_trim_messages_groups(messages_groups, limit)`**:
* **Цель:** Проверить обрезку списка групп до лимита.
* **Тест-кейсы:**
* Количество групп <= `limit`. Проверить, что удаляется самая старая группа (последняя в списке после сортировки в `_create_messages_groups`).
* Количество групп > `limit`. Проверить, что удаляется самая старая и список обрезается до `limit` элементов.
* Пустой список `messages_groups`.
* `limit = 0` (или меньше, хотя проверка есть в вызывающих функциях).
* **`_render_messages_groups(messages_groups, post_parser, exclude_flags, exclude_text)`**:
* **Цель:** Проверить рендеринг групп сообщений в посты и применение фильтров.
* **Замечание:** Мокнуть `post_parser.process_message` и `re.compile`/`search`.
* **Тест-кейсы:**
* **Рендеринг одиночной группы:**
* Мокнуть `process_message` для возврата тестовых данных.
* Передать группу из одного сообщения.
* Проверить, что `process_message` вызван один раз.
* Проверить правильность сборки HTML из `header`, `media`, `body`, `footer`.
* Проверить корректность полей `date`, `message_id`, `title`, `text`, `author`, `flags` в итоговом словаре.
* **Рендеринг медиа-группы:**
* Мокнуть `process_message`.
* Передать группу из нескольких сообщений.
* Проверить, что `process_message` вызван для каждого сообщения.
* Проверить логику выбора `main_message` (первое с текстом или первое в группе).
* Проверить объединение `html.media` через `<br>`.
* Проверить объединение `text` через `\\n`.
* Проверить объединение `html.body` через `<br><br>`.
* Проверить использование `header`, `footer`, `title`, `date`, `message_id`, `author`, `flags` от `main_message`.
* **Фильтрация по `exclude_flags`:**
* Задать `exclude_flags="advert,spam"`.
* Передать посты с флагами `advert`, `spam`, `other`, без флагов.
* Проверить, что посты с `advert` и `spam` отфильтрованы.
* Задать `exclude_flags="all"`. Проверить, что все посты с *любыми* флагами отфильтрованы.
* `exclude_flags = None`. Проверить, что фильтрация не применяется.
* **Фильтрация по `exclude_text`:**
* Задать `exclude_text="купить|продать"`.
* Передать посты с текстом "купить слона", "продам гараж", "просто текст".
* Проверить, что первые два поста отфильтрованы (регистронезависимо).
* Проверить корректность работы с кириллицей (флаг `re.UNICODE`).
* `exclude_text = None`. Проверить, что фильтрация не применяется.
* **Итоговая сортировка:** Проверить, что результат отсортирован по `date` (убывание).
* **Обработка ошибок:** Мокнуть `process_message` чтобы вызвать исключение для одной из групп. Проверить, что ошибка логируется, группа пропускается, остальные обрабатываются.
### Основные функции генерации
* **`generate_channel_rss(channel, post_parser, client, limit, exclude_flags, exclude_text, merge_seconds)`**:
* **Цель:** Проверить генерацию полной RSS ленты.
* **Замечание:** Мокнуть `PostParser` (и его методы `channel_name_prepare`, `get_channel_username`, `client.get_chat`, `client.get_chat_history`), `FeedGenerator`, `datetime`, вспомогательные функции (`_create_time_based_media_groups`, `_create_messages_groups`, `_trim_messages_groups`, `_render_messages_groups`), `Config`, `create_error_feed`.
* **Тест-кейсы:**
* **Успешная генерация:**
* Мокнуть `get_chat` для возврата информации о канале (`title`, `username`).
* Мокнуть `get_chat_history` для возврата списка тестовых `Message`.
* Мокнуть `_render_messages_groups` для возврата списка отрендеренных постов.
* Проверить вызов `PostParser` (если не передан).
* Проверить вызов `channel_name_prepare`.
* Проверить вызов `get_chat`.
* Проверить вызов `get_channel_username`.
* Проверить установку метаданных `FeedGenerator` (`title`, `link`, `description`, `language`, `id`).
* Проверить вызов `get_chat_history` с `limit*2`.
* Проверить вызов `_create_time_based_media_groups` (если `Config['time_based_merge']` True).
* Проверить вызов `_create_messages_groups`.
* Проверить вызов `_trim_messages_groups`.
* Проверить вызов `_render_messages_groups` с правильными параметрами (`exclude_flags`, `exclude_text`).
* Проверить добавление записей (`add_entry`) в `FeedGenerator` для каждого поста с правильными полями (`title`, `link`, `description`, `content`, `pubDate`, `guid`, `author`).
* Проверить вызов `fg.rss_str(pretty=True)`.
* Проверить декодирование байтов (если `rss_str` вернул `bytes`).
* **Канал не найден:**
* Мокнуть `get_chat` чтобы вызвать исключение `USERNAME_INVALID` или `USERNAME_NOT_OCCUPIED`.
* Проверить, что вызывается `create_error_feed` и возвращается его результат.
* **Ошибка получения `channel_username`:**
* Мокнуть `get_channel_username` чтобы вернуть `None`.
* Проверить, что вызывается `create_error_feed`.
* **Другая ошибка при `get_chat`:**
* Мокнуть `get_chat` чтобы вызвать другое исключение.
* Проверить, что исключение пробрасывается дальше.
* **Невалидный `limit`:**
* Передать `limit=0` или `limit=201`.
* Проверить, что вызывается `ValueError`.
* **Исключение на других этапах:**
* Мокнуть `get_chat_history` или другие функции для вызова исключения.
* Проверить, что исключение логируется и пробрасывается.
* **`generate_channel_html(channel, post_parser, client, limit, exclude_flags, exclude_text, merge_seconds)`**:
* **Цель:** Проверить генерацию HTML-страницы с постами.
* **Замечание:** Аналогично `generate_channel_rss`, но без `FeedGenerator`. Мокировать `PostParser`, `client`, `datetime`, вспомогательные функции, `Config`, `create_error_feed`.
* **Тест-кейсы:**
* **Успешная генерация:**
* Мокнуть `get_chat`, `get_chat_history`, `_render_messages_groups`.
* Проверить шаги получения и обработки сообщений (аналогично RSS).
* Проверить, что из результатов `_render_messages_groups` извлекается поле `html`.
* Проверить, что HTML-части постов объединяются через `<hr class="post-divider">`.
* Проверить, что возвращается строка HTML.
* **Канал не найден / Ошибка `channel_username`:**
* Проверить вызов `create_error_feed`.
* **Невалидный `limit`:**
* Проверить `ValueError`.
* **Исключение на других этапах:**
* Проверить логирование и проброс исключения.
* **`create_error_feed(channel, base_url)`**:
* **Цель:** Проверить генерацию RSS-ленты об ошибке.
* **Замечание:** Мокнуть `FeedGenerator`, `datetime`.
* **Тест-кейсы:**
* Проверить установку метаданных `FeedGenerator` (`title`, `link`, `description`, `language`, `id`), указывающих на ошибку.
* Проверить добавление одной записи (`add_entry`) с информацией об ошибке.
* Проверить вызов `fg.rss_str(pretty=True)`.
* Проверить декодирование байтов.
## `api_server.py`
* **Цель:** Протестировать API эндпоинты, логику кеширования, запуск/остановку приложения и обработку ошибок.
* **Замечание:** Использовать `FastAPI.TestClient` для тестирования эндпоинтов. Мокировать `TelegramClient`, `PostParser`, `generate_channel_rss`, `generate_channel_html`, `verify_media_digest`, `os`, `magic`, `mimetypes`, `time`, `asyncio`, `json`, `json_repair`, `uvicorn`.
### Middleware
* **`RequestLoggingMiddleware`**:
* **Цель:** Проверить логирование запросов и ответов.
* **Замечание:** Тестируется косвенно через `TestClient`. Нужно настроить `logging` для перехвата сообщений.
* **Тест-кейсы:**
* Сделать запрос к любому эндпоинту.
* Проверить, что в логах появились записи о запросе (`method`, `url`, `headers`, `query params`).
* Проверить, что в логах появилась запись о статусе ответа.
* Вызвать эндпоинт, который генерирует исключение. Проверить, что в логах появилась запись об ошибке.
### Lifespan Manager (`lifespan`)
* **Цель:** Проверить корректный запуск и остановку фоновых задач и клиента.
* **Замечание:** Сложно тестировать напрямую. Можно выделить логику в отдельные функции или тестировать интеграционно.
* **Тест-кейсы (идеи):**
* Мокнуть `client.start`, `client.stop`, `asyncio.create_task`, `task.cancel`.
* Запустить приложение с `TestClient`.
* Проверить, что `client.start` и `asyncio.create_task(cache_media_files)` были вызваны при старте.
* Завершить работу приложения (если `TestClient` это позволяет симулировать).
* Проверить, что `task.cancel` и `client.stop` были вызваны при остановке.
* Проверить создание директории `data/cache`.
### Вспомогательные функции
* **`mask_sensitive_value(input_str)`**:
* **Цель:** Проверить маскирование чувствительных данных.
* **Тест-кейсы:**
* Строка длиннее 8 символов.
* Строка короче 8 символов.
* Пустая строка.
* `None`.
* **`find_file_id_in_message(message, file_unique_id)`**:
* **Цель:** Проверить поиск `file_id` по `file_unique_id` в сообщении.
* **Тест-кейсы:**
* Сообщение с `photo` и совпадающим `file_unique_id`.
* Сообщение с `video` и совпадающим `file_unique_id`.
* Аналогично для `animation`, `video_note`, `audio`, `voice`, `sticker`, `document`, `web_page.photo`.
* Сообщение с медиа, но `file_unique_id` не совпадает.
* Сообщение без медиа или с `POLL`.
* Сообщение, где медиа-объект (`message.photo`) есть, но у него нет `file_id`.
* **`delayed_delete_file(file_path, delay)`**:
* **Цель:** Проверить отложенное удаление файла.
* **Замечание:** Мокнуть `time.sleep` и `os.remove`.
* **Тест-кейсы:**
* Проверить, что `time.sleep` вызывается с правильным `delay`.
* Проверить, что `os.remove` вызывается с правильным `file_path`.
* Мокнуть `os.remove` для вызова исключения. Проверить логирование ошибки.
* **`prepare_file_response(file_path, delete_after)`**:
* **Цель:** Проверить подготовку `FileResponse` с правильными заголовками и MIME-типом.
* **Замечание:** Мокнуть `os.path.exists`, `magic.Magic`, `mimetypes.guess_type`, `FileResponse`, `BackgroundTask`, `delayed_delete_file`.
* **Тест-кейсы:**
* Файл существует.
* `magic` успешно определяет MIME-тип.
* `magic` падает, `mimetypes` определяет тип.
* Оба падают, используется `application/octet-stream`.
* Проверить правильность `media_type` и заголовка `Content-Disposition`.
* `delete_after = True`. Проверить, что `FileResponse` создается с `BackgroundTask(delayed_delete_file)`.
* `delete_after = False`. Проверить, что `FileResponse` создается без `BackgroundTask`.
* Файл не существует (`os.path.exists` -> `False`). Проверить, что вызывается `HTTPException(404)`.
* **`download_media_file(channel, post_id, file_unique_id)`**:
* **Цель:** Проверить логику скачивания и кеширования медиа.
* **Замечание:** Мокнуть `client.client.get_messages`, `find_file_id_in_message`, `client.client.download_media`, `os.path`, `os.makedirs`, `json.load`, `json.dump`, `datetime.now`.
* **Тест-кейсы:**
* **Обычное видео/файл:**
* Файл уже в кеше (`os.path.exists(cache_path)` -> `True`). Проверить возврат пути к кешу, обновление `media_file_ids.json`.
* Файла нет в кеше. Проверить вызов `get_messages`, `find_file_id_in_message`, `download_media` с правильным путем `cache_path`. Проверить возврат пути.
* **Большое видео (>100MB):**
* Временный файл (`temp_...`) уже существует. Проверить возврат пути к временному файлу.
* Временного файла нет. Проверить вызов `get_messages`, `find_file_id_in_message`, `download_media` с временным путем (`temp_...`). Проверить возврат пути.
* **Сообщение - опрос (`POLL`).** Проверить возврат `None`.
* **`file_id` не найден:** Мокнуть `find_file_id_in_message` -> `None`. Проверить `HTTPException(404)`.
* **Ошибка `get_messages` или `download_media`.** Проверить проброс исключения.
* **Ошибка обновления `media_file_ids.json`.** Проверить логирование ошибки.
* Канал задан как ID (`-100...`). Проверить преобразование в `int` перед `get_messages`.
* **`remove_old_cached_files(media_files, cache_dir)`**:
* **Цель:** Проверить удаление старых файлов из кеша и временных файлов.
* **Замечание:** Мокнуть `datetime.now`, `os.path`, `os.remove`, `os.listdir`, `os.rmdir`, `os.walk`, `os.path.getmtime`, `time.time`.
* **Тест-кейсы:**
* Файл старше 20 дней. Проверить удаление файла, удаление пустых родительских директорий, обновление счетчика `files_removed`, исключение файла из возвращаемого списка `updated_media_files`.
* Файл младше 20 дней. Проверить, что он остается.
* Временный файл (`temp_...`) старше 1 часа. Проверить удаление файла, обновление счетчика, исключение из `updated_media_files`.
* Временный файл младше 1 часа. Проверить, что он остается.
* Ошибка при удалении файла/директории. Проверить логирование.
* Некорректная запись в `media_files`. Проверить пропуск записи.
* **`download_new_files(media_files, cache_dir)`**:
* **Цель:** Проверить фоновую загрузку отсутствующих файлов.
* **Замечание:** Мокнуть `os.path`, `os.makedirs`, `download_media_file`, `asyncio.sleep`.
* **Тест-кейсы:**
* Есть файлы для скачивания (нет в кеше). Проверить вызов `download_media_file` для каждого, вызов `asyncio.sleep(1)`.
* Все файлы уже в кеше. Проверить, что `download_media_file` не вызывается.
* Есть временные файлы (`temp_...`). Проверить, что они пропускаются.
* Некорректная запись в `media_files`. Проверить логирование и пропуск.
* Ошибка при `download_media_file`. Проверить логирование и продолжение работы.
* **`fix_corrupted_json(file_path)`**:
* **Цель:** Проверить восстановление поврежденного JSON.
* **Замечание:** Мокнуть `open`, `json_repair.loads`.
* **Тест-кейсы:**
* Успешное восстановление. Проверить возврат списка валидных словарей (с нужными ключами).
* Неудачное восстановление (`json_repair` падает). Проверить логирование и возврат пустого списка.
* Ошибка чтения файла. Проверить логирование и возврат пустого списка.
* **`cache_media_files()`**:
* **Цель:** Проверить основной цикл управления кешем.
* **Замечание:** Сложно тестировать напрямую. Мокнуть `os.path`, `open`, `json.load`, `fix_corrupted_json`, `remove_old_cached_files`, `download_new_files`, `asyncio.sleep`.
* **Тест-кейсы (идеи):**
* Проверить основной цикл: чтение `media_file_ids.json`, вызов `remove_old_cached_files`, вызов `download_new_files`, `asyncio.sleep`.
* Файл `media_file_ids.json` не существует. Проверить ожидание `delay`.
* Файл `media_file_ids.json` поврежден. Проверить вызов `fix_corrupted_json`, запись исправленного файла (если успешно).
* Ошибка при `remove_old_cached_files` или `download_new_files`. Проверить логирование и продолжение цикла.
* **`calculate_cache_stats()`**:
* **Цель:** Проверить расчет статистики кеша.
* **Замечание:** Мокнуть `os.path`, `os.walk`, `os.path.getsize`, `json.load`, `datetime.now`.
* **Тест-кейсы:**
* Пустой кеш.
* Кеш с файлами в разных поддиректориях (каналах). Проверить правильность подсчета общего количества, размера, статистики по каналам.
* Есть `media_file_ids.json` с записями `added`. Проверить расчет `cache_time_diff_days`.
* Нет `media_file_ids.json` или он пустой/без `added`. Проверить `cache_time_diff_days = 0`.
* Ошибка чтения `media_file_ids.json`. Проверить логирование.
### API Эндпоинты (с использованием `TestClient`)
* **Общая проверка токена:**
* Для всех защищенных эндпоинтов проверить:
* `Config["token"]` установлен, запрос без токена (или с неверным) от внешнего IP -> 403.
* `Config["token"]` установлен, запрос с верным токеном -> 2xx/4xx/5xx (успех или другая ошибка).
* `Config["token"]` установлен, запрос от localhost -> 2xx/4xx/5xx (токен не требуется).
* `Config["token"]` не установлен -> 2xx/4xx/5xx (токен не требуется).
* **`/html/{channel}/{post_id}` и `/post/html/{channel}/{post_id}`**:
* **Цель:** Проверить получение HTML поста.
* **Замечание:** Мокнуть `PostParser`.
* **Тест-кейсы:**
* Успешный запрос. Мокнуть `parser.get_post` -> `html_string`. Проверить статус 200, `Content-Type: text/html`, тело ответа.
* Пост не найден. Мокнуть `parser.get_post` -> `None`. Проверить статус 404.
* Ошибка парсера. Мокнуть `parser.get_post` -> Exception. Проверить статус 500.
* Проверить проверку токена.
* **`/json/{channel}/{post_id}` и `/post/json/{channel}/{post_id}`**:
* **Цель:** Проверить получение JSON поста.
* **Замечание:** Мокнуть `PostParser`.
* **Тест-кейсы:**
* Успешный запрос. Мокнуть `parser.get_post` -> `dict`. Проверить статус 200, `Content-Type: application/json`, тело ответа.
* Пост не найден. Мокнуть `parser.get_post` -> `None`. Проверить статус 404.
* Ошибка парсера. Мокнуть `parser.get_post` -> Exception. Проверить статус 500.
* Проверить проверку токена.
* **`/health`**:
* **Цель:** Проверить эндпоинт состояния.
* **Замечание:** Мокнуть `client.client.get_me`, `calculate_cache_stats`, `Config`.
* **Тест-кейсы:**
* Успешный запрос. Мокнуть `get_me` для возврата объекта пользователя. Проверить статус 200, все поля в ответе. Проверить маскирование чувствительных данных в `config`.
* Ошибка `get_me`. Проверить статус 500.
* Проверить проверку токена.
* **`/media/{channel}/{post_id}/{file_unique_id}/{digest}`**:
* **Цель:** Проверить получение медиафайла.
* **Замечание:** Мокнуть `verify_media_digest`, `download_media_file`, `prepare_file_response`.
* **Тест-кейсы:**
* Успешный запрос (валидный `digest`). Мокнуть `verify_media_digest` -> `True`, `download_media_file` -> `(file_path, delete_after)`, `prepare_file_response` -> `FileResponse`. Проверить статус 200, тип ответа `FileResponse`.
* Невалидный `digest`. Мокнуть `verify_media_digest` -> `False`. Проверить статус 403.
* Ошибка `download_media_file` (например, `HTTPException(404)`). Проверить проброс исключения (404).
* Ошибка `download_media_file` (другая). Проверить статус 500.
* Ошибка `prepare_file_response`. Проверить статус 500.
* Ошибка `pyrogram.errors.RPCError` при скачивании. Проверить статус 404.
* **`/rss/{channel}`**:
* **Цель:** Проверить генерацию RSS/HTML ленты.
* **Замечание:** Мокнуть `generate_channel_rss`, `generate_channel_html`.
* **Тест-кейсы:**
* Успешный запрос (`output_type='rss'`, по умолчанию). Мокнуть `generate_channel_rss`. Проверить статус 200, `Content-Type: application/xml`.
* Успешный запрос (`output_type='html'`). Мокнуть `generate_channel_html`. Проверить статус 200, `Content-Type: text/html`.
* Невалидный `limit` (`generate_...` вызывает `ValueError`). Проверить статус 400.
* Ошибка `FloodWait`. Мокнуть `generate_...` для вызова `errors.FloodWait`. Проверить статус 429, заголовок `Retry-After`.
* Другая ошибка генерации. Мокнуть `generate_...` для вызова `Exception`. Проверить статус 500.
* Проверить передачу параметров `limit`, `exclude_flags`, `exclude_text`, `merge_seconds` в функции генерации.
* Проверить проверку токена.
---