Подготовка к релизу

This commit is contained in:
2025-10-30 18:28:26 +03:00
parent 69c0162c6b
commit 4981db90a0
191 changed files with 45557 additions and 3651 deletions
@@ -0,0 +1,445 @@
"""
Интеграционные тесты для системы триггеров.
"""
import pytest
from unittest.mock import Mock, AsyncMock, patch
from telegram import Update, Message, User
from core.message_router import create_message_router
from services.trigger_service import TriggerService
from database.repository import TriggerRepository
class TestTriggersIntegration:
"""Интеграционные тесты системы триггеров"""
@pytest.fixture
def mock_database_url(self):
"""URL тестовой базы данных"""
return ':memory:'
@pytest.fixture
async def trigger_service(self, mock_database_url):
"""Фикстура сервиса триггеров с реальной базой данных"""
service = TriggerService(mock_database_url)
# Создаем тестовый триггер
await service.add_trigger({
'name': 'test_hello',
'pattern': r'hello\s+world',
'response_text': 'Hi there!',
'chat_type': 'group'
})
yield service
# Очистка после тестов
await service.clear_cache()
@pytest.fixture
def message_router(self, trigger_service):
"""Фикстура маршрутизатора с сервисом триггеров"""
return create_message_router(trigger_service)
@pytest.fixture
def mock_update_group_message(self):
"""Мок-объект для сообщения в группе"""
update = Mock(spec=Update)
message = Mock(spec=Message)
user = Mock(spec=User)
chat = Mock()
user.id = 123456
message.text = "hello world test"
message.message_id = 789
message.chat_id = 987654
chat.type = "group"
update.message = message
update.effective_user = user
update.effective_chat = chat
return update
@pytest.fixture
def mock_update_private_message(self):
"""Мок-объект для сообщения в привате"""
update = Mock(spec=Update)
message = Mock(spec=Message)
user = Mock(spec=User)
chat = Mock()
user.id = 123456
message.text = "hello world private"
chat.type = "private"
update.message = message
update.effective_user = user
update.effective_chat = chat
return update
@pytest.fixture
def mock_context(self):
"""Мок-объект для контекста бота"""
context = Mock()
context.bot = Mock()
context.bot.send_message = AsyncMock()
context.bot.send_sticker = AsyncMock()
context.bot.send_animation = AsyncMock()
context.bot.set_message_reaction = AsyncMock()
return context
@pytest.mark.asyncio
async def test_trigger_fires_in_group_chat(self, trigger_service, message_router,
mock_update_group_message, mock_context):
"""Тест срабатывания триггера в групповом чате"""
# Проверяем, что триггер найден
matched_triggers = await trigger_service.check_triggers("hello world test", "group")
assert len(matched_triggers) == 1
assert matched_triggers[0]['name'] == 'test_hello'
# Выполняем действия триггера
actions = await trigger_service.execute_trigger_actions(
matched_triggers, 987654, "hello world test", 789
)
assert len(actions) == 1
assert actions[0]['type'] == 'text'
assert actions[0]['content'] == 'Hi there!'
# Выполняем действия через маршрутизатор
await message_router._execute_trigger_actions(actions, mock_update_group_message, mock_context)
# Проверяем, что сообщение было отправлено
mock_context.bot.send_message.assert_called_once_with(
chat_id=987654,
text='Hi there!',
reply_to_message_id=789
)
@pytest.mark.asyncio
async def test_trigger_not_fires_in_private_chat(self, trigger_service, message_router,
mock_update_private_message, mock_context):
"""Тест, что триггер не срабатывает в приватном чате"""
# Проверяем, что триггер не найден для приватного чата
matched_triggers = await trigger_service.check_triggers("hello world private", "private")
assert len(matched_triggers) == 0
# Маршрутизатор не должен вызывать проверку триггеров для приватных чатов
result = await message_router.route_text_message(mock_update_private_message, mock_context)
# Проверяем, что сервис триггеров не был вызван для проверки
# (в приватных чатах триггеры не проверяются)
@pytest.mark.asyncio
async def test_trigger_with_reaction(self, trigger_service, message_router,
mock_update_group_message, mock_context):
"""Тест триггера с реакцией"""
# Создаем триггер с реакцией
await trigger_service.add_trigger({
'name': 'reaction_trigger',
'pattern': r'react\s+test',
'reaction_type': 'emoji',
'action_data': '👍',
'chat_type': 'group'
})
# Проверяем срабатывание
matched_triggers = await trigger_service.check_triggers("react test message", "group")
assert len(matched_triggers) == 1
# Выполняем действия
actions = await trigger_service.execute_trigger_actions(
matched_triggers, 987654, "react test message", 789
)
assert len(actions) == 1
assert actions[0]['type'] == 'reaction'
assert actions[0]['content'] == '👍'
# Выполняем через маршрутизатор
await message_router._execute_trigger_actions(actions, mock_update_group_message, mock_context)
# Проверяем установку реакции
mock_context.bot.set_message_reaction.assert_called_once_with(
chat_id=987654,
message_id=789,
reaction='👍'
)
@pytest.mark.asyncio
async def test_trigger_with_multiple_responses(self, trigger_service, message_router,
mock_update_group_message, mock_context):
"""Тест триггера с множественными ответами"""
# Создаем триггер с множественными ответами
await trigger_service.add_trigger({
'name': 'multi_trigger',
'pattern': r'multi\s+test',
'response_text': 'Text response',
'response_sticker': 'sticker123',
'response_gif': 'gif456',
'reaction_type': 'emoji',
'action_data': '❤️',
'chat_type': 'group'
})
# Проверяем срабатывание
matched_triggers = await trigger_service.check_triggers("multi test message", "group")
assert len(matched_triggers) == 1
# Выполняем действия
actions = await trigger_service.execute_trigger_actions(
matched_triggers, 987654, "multi test message", 789
)
assert len(actions) == 4 # text, sticker, gif, reaction
action_types = [a['type'] for a in actions]
assert 'text' in action_types
assert 'sticker' in action_types
assert 'gif' in action_types
assert 'reaction' in action_types
# Выполняем через маршрутизатор
await message_router._execute_trigger_actions(actions, mock_update_group_message, mock_context)
# Проверяем выполнение всех действий
assert mock_context.bot.send_message.call_count == 1
assert mock_context.bot.send_sticker.call_count == 1
assert mock_context.bot.send_animation.call_count == 1
assert mock_context.bot.set_message_reaction.call_count == 1
@pytest.mark.asyncio
async def test_trigger_statistics_update(self, trigger_service):
"""Тест обновления статистики триггера"""
# Получаем триггер до срабатывания
all_triggers = await trigger_service.get_all_triggers()
initial_trigger = next(t for t in all_triggers if t['name'] == 'test_hello')
initial_count = initial_trigger['trigger_count']
# Имитируем срабатывание
success = await trigger_service.update_trigger_stats(initial_trigger['id'])
assert success == True
# Проверяем обновление статистики
all_triggers_after = await trigger_service.get_all_triggers()
updated_trigger = next(t for t in all_triggers_after if t['name'] == 'test_hello')
assert updated_trigger['trigger_count'] == initial_count + 1
assert updated_trigger['last_triggered'] is not None
@pytest.mark.asyncio
async def test_trigger_cache_invalidation(self, trigger_service):
"""Тест инвалидации кеша триггеров"""
# Получаем триггеры (заполняет кеш)
triggers = await trigger_service.get_active_triggers('group')
assert len(triggers) > 0
# Проверяем, что кеш используется
triggers_cached = await trigger_service.get_active_triggers('group')
assert triggers_cached == triggers
# Добавляем новый триггер (должен инвалидировать кеш)
await trigger_service.add_trigger({
'name': 'new_trigger',
'pattern': r'new\s+pattern',
'response_text': 'New response',
'chat_type': 'group'
})
# Получаем триггеры снова (кеш должен быть инвалидирован)
triggers_after = await trigger_service.get_active_triggers('group')
assert len(triggers_after) == len(triggers) + 1
# Проверяем наличие нового триггера
new_trigger_names = [t['name'] for t in triggers_after]
assert 'new_trigger' in new_trigger_names
@pytest.mark.asyncio
async def test_trigger_toggle_activation(self, trigger_service):
"""Тест активации/деактивации триггера"""
# Получаем ID первого триггера
all_triggers = await trigger_service.get_all_triggers()
trigger_id = all_triggers[0]['id']
# Деактивируем триггер
success = await trigger_service.toggle_trigger(trigger_id, False)
assert success == True
# Проверяем, что триггер не активен
active_triggers = await trigger_service.get_active_triggers('group')
active_ids = [t['id'] for t in active_triggers]
assert trigger_id not in active_ids
# Активируем триггер обратно
success = await trigger_service.toggle_trigger(trigger_id, True)
assert success == True
# Проверяем, что триггер снова активен
active_triggers_after = await trigger_service.get_active_triggers('group')
active_ids_after = [t['id'] for t in active_triggers_after]
assert trigger_id in active_ids_after
@pytest.mark.asyncio
async def test_regex_pattern_matching(self, trigger_service):
"""Тест сопоставления регулярных выражений"""
# Создаем триггеры с разными паттернами
await trigger_service.add_trigger({
'name': 'word_boundary',
'pattern': r'\bhello\b',
'response_text': 'Word boundary match',
'chat_type': 'group'
})
await trigger_service.add_trigger({
'name': 'case_insensitive',
'pattern': r'HELLO',
'response_text': 'Case insensitive match',
'chat_type': 'group'
})
# Тест границ слова
matches = await trigger_service.check_triggers("hello world", "group")
assert len(matches) == 3 # test_hello, word_boundary, case_insensitive
# Тест без границы слова
matches = await trigger_service.check_triggers("helloworld", "group")
# word_boundary не должен сработать
boundary_trigger = next((m for m in matches if m['name'] == 'word_boundary'), None)
assert boundary_trigger is None
@pytest.mark.asyncio
async def test_trigger_error_handling(self, trigger_service, message_router,
mock_update_group_message, mock_context):
"""Тест обработки ошибок в системе триггеров"""
# Создаем триггер с невалидным регулярным выражением
await trigger_service.add_trigger({
'name': 'invalid_regex',
'pattern': r'[invalid', # Невалидный regex
'response_text': 'Should not work',
'chat_type': 'group'
})
# Проверяем, что невалидный триггер не ломается всю систему
matched_triggers = await trigger_service.check_triggers("test message", "group")
# Должен сработать только валидный триггер
valid_matches = [t for t in matched_triggers if t['name'] == 'test_hello']
assert len(valid_matches) == 1
# Тестируем выполнение действий с ошибкой в боте
mock_context.bot.send_message.side_effect = Exception("Bot error")
actions = await trigger_service.execute_trigger_actions(
valid_matches, 987654, "test message", 789
)
# Не должно выбрасывать исключение
await message_router._execute_trigger_actions(actions, mock_update_group_message, mock_context)
# Метод должен был быть вызван несмотря на ошибку
mock_context.bot.send_message.assert_called_once()
@pytest.mark.asyncio
async def test_trigger_reaction_priority(self, trigger_service, message_router,
mock_update_group_message, mock_context):
"""Тест приоритета реакций при множественных триггерах"""
# Создаем два триггера с реакциями
await trigger_service.add_trigger({
'name': 'first_reaction',
'pattern': r'priority\s+test',
'reaction_type': 'emoji',
'action_data': '👍',
'chat_type': 'group'
})
await trigger_service.add_trigger({
'name': 'second_reaction',
'pattern': r'priority\s+test',
'reaction_type': 'emoji',
'action_data': '❤️',
'chat_type': 'group'
})
# Проверяем срабатывание обоих триггеров
matched_triggers = await trigger_service.check_triggers("priority test", "group")
assert len(matched_triggers) == 2
# Выполняем действия
actions = await trigger_service.execute_trigger_actions(
matched_triggers, 987654, "priority test", 789
)
# Должны быть две реакции
reactions = [a for a in actions if a['type'] == 'reaction']
assert len(reactions) == 2
# Выполняем через маршрутизатор (только первая реакция)
await message_router._execute_trigger_actions(actions, mock_update_group_message, mock_context)
# Проверяем, что установлена только первая реакция
mock_context.bot.set_message_reaction.assert_called_once()
call_args = mock_context.bot.set_message_reaction.call_args
assert call_args[1]['reaction'] in ['👍', '❤️'] # Одна из реакций
@pytest.mark.asyncio
async def test_end_to_end_trigger_flow(self, trigger_service, message_router,
mock_update_group_message, mock_context):
"""Полноценный end-to-end тест потока триггеров"""
# 1. Создаем триггер
trigger_id = await trigger_service.add_trigger({
'name': 'e2e_trigger',
'pattern': r'e2e\s+test',
'response_text': 'E2E Response',
'response_sticker': 'sticker123',
'reaction_type': 'emoji',
'action_data': '🎉',
'chat_type': 'group'
})
assert trigger_id is not None
# 2. Проверяем, что триггер активен
active_triggers = await trigger_service.get_active_triggers('group')
e2e_trigger = next((t for t in active_triggers if t['name'] == 'e2e_trigger'), None)
assert e2e_trigger is not None
assert e2e_trigger['trigger_count'] == 0
# 3. Имитируем входящее сообщение
mock_update_group_message.message.text = "e2e test message"
# 4. Маршрутизатор обрабатывает сообщение
result = await message_router.route_text_message(mock_update_group_message, mock_context)
# 5. Проверяем, что действия были выполнены
assert mock_context.bot.send_message.call_count == 1
assert mock_context.bot.send_sticker.call_count == 1
assert mock_context.bot.set_message_reaction.call_count == 1
# 6. Проверяем обновление статистики
updated_trigger = await trigger_service.get_trigger_by_id(trigger_id)
assert updated_trigger['trigger_count'] == 1
assert updated_trigger['last_triggered'] is not None
@pytest.mark.asyncio
async def test_private_chat_trigger_isolation(self, trigger_service, message_router,
mock_update_private_message, mock_context):
"""Тест изоляции триггеров в приватных чатах"""
# Создаем триггер только для приватных чатов
await trigger_service.add_trigger({
'name': 'private_trigger',
'pattern': r'private\s+only',
'response_text': 'Private response',
'chat_type': 'private'
})
# В приватном чате триггер должен сработать
matched_private = await trigger_service.check_triggers("private only message", "private")
assert len(matched_private) == 1
assert matched_private[0]['name'] == 'private_trigger'
# В групповом чате тот же триггер не должен сработать
matched_group = await trigger_service.check_triggers("private only message", "group")
private_triggers_in_group = [t for t in matched_group if t['name'] == 'private_trigger']
assert len(private_triggers_in_group) == 0