:Release Notes: - Added a new Telegram command `/get_hottest <number> [format]` to export the top `N` trends as a CSV or Markdown file. :Detailed Notes: - Created `ITrendExporter` interface and concrete `CsvTrendExporter` and `MarkdownTrendExporter` implementations for formatting DTOs. - Updated `src/bot/handlers.py` to include `command_get_hottest_handler` mapping to `/get_hottest`. - Used `BufferedInputFile` to stream generated files asynchronously directly to Telegram without disk I/O. - Fixed unrelated pipeline test failures regarding `EphemeralClient` usage with ChromaDB. :Testing Performed: - Implemented TDD with `pytest` for parsing parameters, exporting logic, and handling empty DB scenarios. - Ran the full test suite (90 tests) which completed successfully. :QA Notes: - Fully covered the new handler using `pytest-asyncio` and `aiogram` mocked objects. :Issues Addressed: - Resolves request to export high-relevance parsed entries. Change-Id: I25dd90f1e4491ba298682518d835259bffab4190
171 lines
5.7 KiB
Python
171 lines
5.7 KiB
Python
import pytest
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
from aiogram.types import Message, BufferedInputFile
|
|
from aiogram.filters import CommandObject
|
|
from datetime import datetime
|
|
|
|
from src.bot.handlers import get_router
|
|
from src.processor.dto import EnrichedNewsItemDTO
|
|
|
|
@pytest.fixture
|
|
def mock_storage():
|
|
return AsyncMock()
|
|
|
|
@pytest.fixture
|
|
def mock_processor():
|
|
processor = MagicMock()
|
|
processor.get_info.return_value = {"model": "test-model"}
|
|
return processor
|
|
|
|
@pytest.fixture
|
|
def allowed_chat_id():
|
|
return "123456789"
|
|
|
|
@pytest.fixture
|
|
def router(mock_storage, mock_processor, allowed_chat_id):
|
|
return get_router(mock_storage, mock_processor, allowed_chat_id)
|
|
|
|
def get_handler(router, callback_name):
|
|
for handler in router.message.handlers:
|
|
if handler.callback.__name__ == callback_name:
|
|
return handler.callback
|
|
raise ValueError(f"Handler {callback_name} not found")
|
|
|
|
@pytest.fixture
|
|
def mock_items():
|
|
return [
|
|
EnrichedNewsItemDTO(
|
|
title=f"Hot News {i}",
|
|
url=f"https://example.com/{i}",
|
|
content_text=f"Content {i}",
|
|
source="Source",
|
|
timestamp=datetime.now(),
|
|
relevance_score=10-i,
|
|
summary_ru=f"Сводка {i}",
|
|
anomalies_detected=[],
|
|
category="Tech"
|
|
) for i in range(3)
|
|
]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_command_get_hottest_handler_no_args(router, mock_storage, allowed_chat_id, mock_items):
|
|
"""
|
|
Test /get_hottest with no arguments (default limit 10, format csv).
|
|
"""
|
|
# 1. Arrange
|
|
handler = get_handler(router, "command_get_hottest_handler")
|
|
message = AsyncMock()
|
|
message.chat = MagicMock()
|
|
message.chat.id = int(allowed_chat_id)
|
|
|
|
mock_storage.get_top_ranked.return_value = mock_items
|
|
|
|
# 2. Act
|
|
command = CommandObject(prefix='/', command='get_hottest', args=None)
|
|
with patch("src.bot.handlers.CsvTrendExporter") as MockCsvExporter:
|
|
mock_exporter = AsyncMock()
|
|
mock_exporter.export.return_value = b"csv data"
|
|
MockCsvExporter.return_value = mock_exporter
|
|
|
|
await handler(message=message, command=command)
|
|
|
|
# 3. Assert
|
|
mock_storage.get_top_ranked.assert_called_once_with(limit=10)
|
|
message.answer_document.assert_called_once()
|
|
|
|
args, kwargs = message.answer_document.call_args
|
|
assert "document" in kwargs
|
|
assert isinstance(kwargs["document"], BufferedInputFile)
|
|
assert kwargs["document"].filename == "hottest_trends.csv"
|
|
assert kwargs["caption"] == "🔥 Top 3 hottest trends!"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_command_get_hottest_handler_invalid_limit(router, mock_storage, allowed_chat_id):
|
|
"""
|
|
Test /get_hottest with invalid limit (not a number).
|
|
"""
|
|
# 1. Arrange
|
|
handler = get_handler(router, "command_get_hottest_handler")
|
|
message = AsyncMock()
|
|
message.chat = MagicMock()
|
|
message.chat.id = int(allowed_chat_id)
|
|
|
|
# 2. Act
|
|
command = CommandObject(prefix='/', command='get_hottest', args='abc')
|
|
await handler(message=message, command=command)
|
|
|
|
# 3. Assert
|
|
message.answer.assert_called_once_with("Please provide a valid number, e.g., /get_hottest 10")
|
|
mock_storage.get_top_ranked.assert_not_called()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_command_get_hottest_handler_capped_limit(router, mock_storage, allowed_chat_id, mock_items):
|
|
"""
|
|
Test /get_hottest with limit > 50 (should be capped).
|
|
"""
|
|
# 1. Arrange
|
|
handler = get_handler(router, "command_get_hottest_handler")
|
|
message = AsyncMock()
|
|
message.chat = MagicMock()
|
|
message.chat.id = int(allowed_chat_id)
|
|
|
|
mock_storage.get_top_ranked.return_value = mock_items
|
|
|
|
# 2. Act
|
|
command = CommandObject(prefix='/', command='get_hottest', args='100')
|
|
await handler(message=message, command=command)
|
|
|
|
# 3. Assert
|
|
mock_storage.get_top_ranked.assert_called_once_with(limit=50)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_command_get_hottest_handler_custom_limit_md(router, mock_storage, allowed_chat_id, mock_items):
|
|
"""
|
|
Test /get_hottest with limit and md format.
|
|
"""
|
|
# 1. Arrange
|
|
handler = get_handler(router, "command_get_hottest_handler")
|
|
message = AsyncMock()
|
|
message.chat = MagicMock()
|
|
message.chat.id = int(allowed_chat_id)
|
|
|
|
mock_storage.get_top_ranked.return_value = mock_items
|
|
|
|
# 2. Act
|
|
command = CommandObject(prefix='/', command='get_hottest', args='5 md')
|
|
with patch("src.bot.handlers.MarkdownTrendExporter") as MockMdExporter:
|
|
mock_exporter = AsyncMock()
|
|
mock_exporter.export.return_value = b"md data"
|
|
MockMdExporter.return_value = mock_exporter
|
|
|
|
await handler(message=message, command=command)
|
|
|
|
# 3. Assert
|
|
mock_storage.get_top_ranked.assert_called_once_with(limit=5)
|
|
message.answer_document.assert_called_once()
|
|
|
|
args, kwargs = message.answer_document.call_args
|
|
assert kwargs["document"].filename == "hottest_trends.md"
|
|
assert kwargs["caption"] == "🔥 Top 3 hottest trends!"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_command_get_hottest_handler_no_records(router, mock_storage, allowed_chat_id):
|
|
"""
|
|
Test /get_hottest when no records found.
|
|
"""
|
|
# 1. Arrange
|
|
handler = get_handler(router, "command_get_hottest_handler")
|
|
message = AsyncMock()
|
|
message.chat = MagicMock()
|
|
message.chat.id = int(allowed_chat_id)
|
|
|
|
mock_storage.get_top_ranked.return_value = []
|
|
|
|
# 2. Act
|
|
command = CommandObject(prefix='/', command='get_hottest', args=None)
|
|
await handler(message=message, command=command)
|
|
|
|
# 3. Assert
|
|
message.answer.assert_called_once_with("No hot trends found yet.")
|
|
message.answer_document.assert_not_called()
|