AI-Trend-Scout/tests/bot/test_handlers.py
Artur Mukhamadiev 65fccbc614 feat(storage): implement hybrid search and fix async chroma i/o
- Add ADR 001 for Hybrid Search Architecture
- Implement Phase 1 (Exact Match) and Phase 2 (Semantic Fallback) in ChromaStore
- Wrap blocking ChromaDB calls in asyncio.to_thread
- Update IVectorStore interface to support category filtering and thresholds
- Add comprehensive tests for hybrid search logic
2026-03-16 00:11:07 +03:00

237 lines
8.3 KiB
Python

import uuid
import pytest
from unittest.mock import AsyncMock, MagicMock
from aiogram.types import Message, InlineKeyboardMarkup, CallbackQuery
from aiogram.filters import CommandObject
from datetime import datetime
from src.bot.handlers import get_router, AccessMiddleware
from src.processor.dto import EnrichedNewsItemDTO
@pytest.fixture
def mock_item():
return EnrichedNewsItemDTO(
title="Breakthrough in Quantum Computing",
url="https://example.com/quantum-breakthrough",
content_text="Researchers have achieved a new milestone in quantum supremacy...",
source="Example Domain News",
timestamp=datetime.now(),
relevance_score=9,
summary_ru="Исследователи достигли нового рубежа в квантовом превосходстве.",
anomalies_detected=["Quantum supremacy", "Room temperature superconductors"],
category="Science"
)
@pytest.fixture
def mock_storage(mock_item):
storage = AsyncMock()
storage.search.return_value = [mock_item]
storage.get_latest.return_value = [mock_item]
storage.get_by_id.return_value = mock_item
storage.get_stats.return_value = {"total": 1, "AI": 1}
return storage
@pytest.fixture
def allowed_chat_id():
return "123456789"
@pytest.fixture
def mock_processor():
processor = MagicMock()
processor.get_info.return_value = {
"model": "test-model",
"base_url": "http://test-url",
"prompt_summary": "Test summary"
}
return processor
@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")
def get_callback_handler(router, callback_name):
for handler in router.callback_query.handlers:
if handler.callback.__name__ == callback_name:
return handler.callback
raise ValueError(f"Handler {callback_name} not found")
@pytest.mark.asyncio
async def test_command_start_handler(router, allowed_chat_id):
handler = get_handler(router, "command_start_handler")
message = AsyncMock()
message.chat.id = int(allowed_chat_id)
message.from_user = MagicMock()
message.from_user.full_name = "Test User"
message.answer = AsyncMock()
await handler(message)
message.answer.assert_called_once()
args, kwargs = message.answer.call_args
assert "Welcome" in args[0] or "Trend-Scout" in args[0]
@pytest.mark.asyncio
async def test_command_help_handler(router, allowed_chat_id):
handler = get_handler(router, "command_help_handler")
message = AsyncMock()
message.chat.id = int(allowed_chat_id)
message.answer = AsyncMock()
await handler(message)
message.answer.assert_called_once()
args, kwargs = message.answer.call_args
assert "/start" in args[0]
assert "/latest" in args[0]
@pytest.mark.asyncio
async def test_command_params_handler(router, mock_processor, allowed_chat_id):
handler = get_handler(router, "command_params_handler")
message = AsyncMock()
message.chat.id = int(allowed_chat_id)
message.answer = AsyncMock()
await handler(message)
mock_processor.get_info.assert_called_once()
message.answer.assert_called_once()
args, kwargs = message.answer.call_args
assert "LLM Processor Parameters" in args[0]
assert "test-model" in args[0]
assert "HTML" in kwargs.get("parse_mode", "")
@pytest.mark.asyncio
async def test_command_latest_handler(router, mock_storage, allowed_chat_id):
handler = get_handler(router, "command_latest_handler")
message = AsyncMock()
message.chat.id = int(allowed_chat_id)
message.answer = AsyncMock()
command = CommandObject(prefix="/", command="latest", args=None)
await handler(message=message, command=command)
mock_storage.get_latest.assert_called_once_with(limit=10, category=None)
message.answer.assert_called_once()
args, kwargs = message.answer.call_args
assert "Latest news:" in args[0]
assert "reply_markup" in kwargs
assert isinstance(kwargs["reply_markup"], InlineKeyboardMarkup)
assert len(kwargs["reply_markup"].inline_keyboard) == 1
@pytest.mark.asyncio
async def test_command_search_handler(router, mock_storage, allowed_chat_id):
handler = get_handler(router, "command_search_handler")
message = AsyncMock()
message.chat.id = int(allowed_chat_id)
message.answer = AsyncMock()
command = CommandObject(prefix="/", command="search", args="quantum")
await handler(message=message, command=command)
message.answer.assert_called_once()
args, kwargs = message.answer.call_args
assert "Search results:" in args[0]
assert "reply_markup" in kwargs
mock_storage.search.assert_called_once_with(query="quantum", limit=10, threshold=0.6)
@pytest.mark.asyncio
async def test_detail_callback_handler(router, mock_storage, mock_item):
handler = get_callback_handler(router, "detail_callback_handler")
callback = AsyncMock(spec=CallbackQuery)
item_id = str(uuid.uuid5(uuid.NAMESPACE_URL, mock_item.url))
callback.data = f"detail:{item_id}"
callback.message = AsyncMock(spec=Message)
callback.message.answer = AsyncMock()
callback.answer = AsyncMock()
await handler(callback)
mock_storage.get_by_id.assert_called_once_with(item_id)
callback.message.answer.assert_called_once()
args, kwargs = callback.message.answer.call_args
assert mock_item.title in args[0]
assert mock_item.summary_ru in args[0]
callback.answer.assert_called_once()
@pytest.mark.asyncio
async def test_command_stats_handler(router, mock_storage, allowed_chat_id):
handler = get_handler(router, "command_stats_handler")
message = AsyncMock()
message.chat.id = int(allowed_chat_id)
message.answer = AsyncMock()
await handler(message=message)
message.answer.assert_called_once()
args, kwargs = message.answer.call_args
assert "Database Statistics" in args[0]
@pytest.mark.asyncio
async def test_command_hottest_handler(router, mock_storage, allowed_chat_id, mock_item):
handler = get_handler(router, "command_hottest_handler")
message = AsyncMock()
message.chat.id = int(allowed_chat_id)
message.answer = AsyncMock()
command = CommandObject(prefix="/", command="hottest", args=None)
mock_storage.get_top_ranked.return_value = [mock_item]
await handler(message=message, command=command)
mock_storage.get_top_ranked.assert_called_once_with(limit=10, category=None)
message.answer.assert_called_once()
args, kwargs = message.answer.call_args
assert "Top 1 Hottest Trends:" in args[0]
assert "reply_markup" in kwargs
assert "🔥" in str(kwargs["reply_markup"])
@pytest.mark.asyncio
async def test_command_hottest_handler_empty(router, mock_storage, allowed_chat_id):
handler = get_handler(router, "command_hottest_handler")
message = AsyncMock()
message.chat.id = int(allowed_chat_id)
message.answer = AsyncMock()
command = CommandObject(prefix="/", command="hottest", args=None)
mock_storage.get_top_ranked.return_value = []
await handler(message=message, command=command)
message.answer.assert_called_once_with("No hot trends found yet.")
@pytest.mark.asyncio
async def test_access_middleware_allowed(allowed_chat_id):
middleware = AccessMiddleware(allowed_chat_id)
handler = AsyncMock()
event = MagicMock(spec=Message)
event.chat = MagicMock()
event.chat.id = int(allowed_chat_id)
event.answer = AsyncMock()
data = {}
await middleware(handler, event, data)
handler.assert_called_once_with(event, data)
event.answer.assert_not_called()
@pytest.mark.asyncio
async def test_access_middleware_denied(allowed_chat_id):
middleware = AccessMiddleware(allowed_chat_id)
handler = AsyncMock()
event = MagicMock(spec=Message)
event.chat = MagicMock()
event.chat.id = 999999999 # Different ID
event.answer = AsyncMock()
data = {}
await middleware(handler, event, data)
handler.assert_not_called()
event.answer.assert_called_once_with("Access Denied")