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

72 KiB

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 в функции генерации.
      • Проверить проверку токена.