mirror of
https://github.com/FerraSoft/bottohelp.git
synced 2026-08-06 21:55:03 +00:00
445 lines
20 KiB
Python
445 lines
20 KiB
Python
"""
|
|
Интеграционные тесты для системы триггеров.
|
|
"""
|
|
|
|
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 |