""" Unit-тесты для TriggerService. """ import pytest from unittest.mock import Mock, AsyncMock, patch, MagicMock from datetime import datetime from services.trigger_service import TriggerService from core.exceptions import ValidationError class TestTriggerService: """Тесты сервиса триггеров""" @pytest.fixture def mock_repository(self): """Мок репозитория триггеров""" repo = Mock() repo.get_active_triggers_async = AsyncMock(return_value=[ { 'id': 1, 'name': 'test_trigger', 'pattern': r'hello', 'response_text': 'Hi there!', 'is_active': True, 'chat_type': 'group' } ]) repo.update_trigger_stats = Mock(return_value=True) return repo @pytest.fixture def trigger_service(self, mock_repository): """Фикстура сервиса триггеров с мок репозиторием""" with patch('services.trigger_service.TriggerRepository', return_value=mock_repository): service = TriggerService(':memory:') return service @pytest.mark.asyncio async def test_check_triggers_empty_text(self, trigger_service): """Тест проверки триггеров с пустым текстом""" result = await trigger_service.check_triggers("") assert result == [] result = await trigger_service.check_triggers(None) assert result == [] @pytest.mark.asyncio async def test_check_triggers_no_match(self, trigger_service): """Тест проверки триггеров без совпадений""" result = await trigger_service.check_triggers("goodbye") assert result == [] @pytest.mark.asyncio async def test_check_triggers_match(self, trigger_service): """Тест проверки триггеров с совпадением""" result = await trigger_service.check_triggers("hello world") assert len(result) == 1 assert result[0]['id'] == 1 assert result[0]['name'] == 'test_trigger' @pytest.mark.asyncio async def test_check_triggers_invalid_regex(self, trigger_service): """Тест обработки невалидного регулярного выражения""" # Меняем паттерн на невалидный trigger_service.repository.get_active_triggers_async.return_value = [ { 'id': 2, 'name': 'invalid_trigger', 'pattern': r'[invalid', 'response_text': 'Invalid regex', 'is_active': True, 'chat_type': 'group' } ] result = await trigger_service.check_triggers("test") assert result == [] # Никакие триггеры не должны сработать @pytest.mark.asyncio async def test_get_active_triggers_cache_hit(self, trigger_service): """Тест получения активных триггеров из кеша""" # Заполняем кеш trigger_service._triggers_cache = [{'id': 1, 'name': 'cached'}] trigger_service._last_cache_update = datetime.now() result = await trigger_service.get_active_triggers() assert result == [{'id': 1, 'name': 'cached'}] # Репозиторий не должен вызываться при хите кеша trigger_service.repository.get_active_triggers_async.assert_not_called() @pytest.mark.asyncio async def test_get_active_triggers_cache_miss(self, trigger_service): """Тест получения активных триггеров из базы при отсутствии кеша""" result = await trigger_service.get_active_triggers() assert len(result) == 1 assert result[0]['id'] == 1 trigger_service.repository.get_active_triggers_async.assert_called_once_with('group') @pytest.mark.asyncio async def test_get_active_triggers_private_chat(self, trigger_service): """Тест получения активных триггеров для приватного чата""" result = await trigger_service.get_active_triggers('private') trigger_service.repository.get_active_triggers_async.assert_called_once_with('private') @pytest.mark.asyncio async def test_execute_trigger_actions_text_response(self, trigger_service): """Тест выполнения действий триггера с текстовым ответом""" matched_triggers = [ { 'id': 1, 'name': 'text_trigger', 'response_text': 'Hello!', 'response_sticker': None, 'response_gif': None, 'reaction_type': None } ] actions = await trigger_service.execute_trigger_actions( matched_triggers, 123456, "test message", 789 ) assert len(actions) == 1 assert actions[0]['type'] == 'text' assert actions[0]['content'] == 'Hello!' assert actions[0]['trigger_id'] == 1 # Проверяем обновление статистики trigger_service.repository.update_trigger_stats.assert_called_with(1) @pytest.mark.asyncio async def test_execute_trigger_actions_reaction(self, trigger_service): """Тест выполнения действий триггера с реакцией""" matched_triggers = [ { 'id': 2, 'name': 'reaction_trigger', 'response_text': None, 'response_sticker': None, 'response_gif': None, 'reaction_type': 'emoji', 'action_data': '👍' } ] actions = await trigger_service.execute_trigger_actions( matched_triggers, 123456, "test message", 789 ) assert len(actions) == 1 assert actions[0]['type'] == 'reaction' assert actions[0]['reaction_type'] == 'emoji' assert actions[0]['content'] == '👍' assert actions[0]['message_id'] == 789 @pytest.mark.asyncio async def test_execute_trigger_actions_multiple_responses(self, trigger_service): """Тест выполнения действий триггера с множественными ответами""" matched_triggers = [ { 'id': 3, 'name': 'multi_trigger', 'response_text': 'Text response', 'response_sticker': 'sticker_id', 'response_gif': None, 'reaction_type': 'emoji', 'action_data': '❤️' } ] actions = await trigger_service.execute_trigger_actions( matched_triggers, 123456, "test message", 789 ) assert len(actions) == 3 # text, sticker, reaction action_types = [a['type'] for a in actions] assert 'text' in action_types assert 'sticker' in action_types assert 'reaction' in action_types @pytest.mark.asyncio async def test_update_trigger_stats_success(self, trigger_service): """Тест успешного обновления статистики триггера""" result = await trigger_service.update_trigger_stats(1) assert result == True trigger_service.repository.update_trigger_stats.assert_called_once_with(1) @pytest.mark.asyncio async def test_update_trigger_stats_failure(self, trigger_service): """Тест неудачного обновления статистики триггера""" trigger_service.repository.update_trigger_stats.return_value = False result = await trigger_service.update_trigger_stats(1) assert result == False @pytest.mark.asyncio async def test_add_trigger_success(self, trigger_service): """Тест успешного добавления триггера""" trigger_data = { 'name': 'new_trigger', 'pattern': r'test\s+pattern', 'response_text': 'Test response', 'chat_type': 'group' } trigger_service.repository.add_trigger.return_value = 5 result = await trigger_service.add_trigger(trigger_data) assert result == 5 trigger_service.repository.add_trigger.assert_called_once() @pytest.mark.asyncio async def test_add_trigger_validation_error(self, trigger_service): """Тест добавления триггера с ошибкой валидации""" trigger_data = { 'name': '', # Пустое имя 'pattern': r'test', 'chat_type': 'group' } with pytest.raises(ValidationError): await trigger_service.add_trigger(trigger_data) @pytest.mark.asyncio async def test_add_trigger_invalid_regex(self, trigger_service): """Тест добавления триггера с невалидным regex""" trigger_data = { 'name': 'invalid_trigger', 'pattern': r'[invalid', # Невалидный regex 'chat_type': 'group' } with pytest.raises(ValidationError): await trigger_service.add_trigger(trigger_data) @pytest.mark.asyncio async def test_update_trigger_success(self, trigger_service): """Тест успешного обновления триггера""" update_data = {'name': 'updated_name'} result = await trigger_service.update_trigger(1, update_data) assert result == True trigger_service.repository.update_trigger.assert_called_once_with(1, update_data) @pytest.mark.asyncio async def test_delete_trigger_success(self, trigger_service): """Тест успешного удаления триггера""" result = await trigger_service.delete_trigger(1) assert result == True trigger_service.repository.delete_trigger.assert_called_once_with(1) @pytest.mark.asyncio async def test_toggle_trigger_success(self, trigger_service): """Тест успешного переключения статуса триггера""" result = await trigger_service.toggle_trigger(1, True) assert result == True trigger_service.repository.toggle_trigger.assert_called_once_with(1, True) @pytest.mark.asyncio async def test_get_all_triggers(self, trigger_service): """Тест получения всех триггеров""" trigger_service.repository.get_all_triggers.return_value = [ {'id': 1, 'name': 'trigger1'}, {'id': 2, 'name': 'trigger2'} ] result = await trigger_service.get_all_triggers() assert len(result) == 2 trigger_service.repository.get_all_triggers.assert_called_once() @pytest.mark.asyncio async def test_get_trigger_by_id(self, trigger_service): """Тест получения триггера по ID""" trigger_service.repository.get_trigger_by_id.return_value = {'id': 1, 'name': 'test'} result = await trigger_service.get_trigger_by_id(1) assert result['id'] == 1 trigger_service.repository.get_trigger_by_id.assert_called_once_with(1) def test_build_trigger_actions_text_only(self, trigger_service): """Тест построения действий триггера только с текстом""" trigger = { 'id': 1, 'name': 'text_trigger', 'response_text': 'Hello!', 'response_sticker': None, 'response_gif': None, 'reaction_type': None } actions = trigger_service._build_trigger_actions(trigger, 123, "test", 456) assert len(actions) == 1 assert actions[0]['type'] == 'text' assert actions[0]['content'] == 'Hello!' def test_build_trigger_actions_reaction_only(self, trigger_service): """Тест построения действий триггера только с реакцией""" trigger = { 'id': 2, 'name': 'reaction_trigger', 'response_text': None, 'response_sticker': None, 'response_gif': None, 'reaction_type': 'emoji', 'action_data': '👍' } actions = trigger_service._build_trigger_actions(trigger, 123, "test", 456) assert len(actions) == 1 assert actions[0]['type'] == 'reaction' assert actions[0]['reaction_type'] == 'emoji' assert actions[0]['content'] == '👍' assert actions[0]['message_id'] == 456 def test_build_trigger_actions_reaction_no_message_id(self, trigger_service): """Тест построения действий триггера с реакцией без message_id""" trigger = { 'id': 3, 'name': 'reaction_trigger', 'response_text': None, 'response_sticker': None, 'response_gif': None, 'reaction_type': 'emoji', 'action_data': '👍' } actions = trigger_service._build_trigger_actions(trigger, 123, "test", None) assert len(actions) == 0 # Реакция не должна добавляться без message_id def test_build_trigger_actions_multiple_types(self, trigger_service): """Тест построения действий триггера со множественными типами""" trigger = { 'id': 4, 'name': 'multi_trigger', 'response_text': 'Text response', 'response_sticker': 'sticker123', 'response_gif': 'gif456', 'reaction_type': 'emoji', 'action_data': '❤️' } actions = trigger_service._build_trigger_actions(trigger, 123, "test", 789) assert len(actions) == 4 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 def test_validate_trigger_data_valid(self, trigger_service): """Тест валидации корректных данных триггера""" data = { 'name': 'Valid Trigger', 'pattern': r'hello\s+world', 'chat_type': 'group' } # Не должно выбрасывать исключение trigger_service._validate_trigger_data(data) def test_validate_trigger_data_missing_name(self, trigger_service): """Тест валидации данных триггера без имени""" data = { 'pattern': r'test', 'chat_type': 'group' } with pytest.raises(ValidationError): trigger_service._validate_trigger_data(data) def test_validate_trigger_data_empty_name(self, trigger_service): """Тест валидации данных триггера с пустым именем""" data = { 'name': '', 'pattern': r'test', 'chat_type': 'group' } with pytest.raises(ValidationError): trigger_service._validate_trigger_data(data) def test_validate_trigger_data_long_name(self, trigger_service): """Тест валидации данных триггера с слишком длинным именем""" data = { 'name': 'a' * 101, # 101 символ 'pattern': r'test', 'chat_type': 'group' } with pytest.raises(ValidationError): trigger_service._validate_trigger_data(data) def test_validate_trigger_data_missing_pattern(self, trigger_service): """Тест валидации данных триггера без паттерна""" data = { 'name': 'Test Trigger', 'chat_type': 'group' } with pytest.raises(ValidationError): trigger_service._validate_trigger_data(data) def test_validate_trigger_data_empty_pattern(self, trigger_service): """Тест валидации данных триггера с пустым паттерном""" data = { 'name': 'Test Trigger', 'pattern': '', 'chat_type': 'group' } with pytest.raises(ValidationError): trigger_service._validate_trigger_data(data) def test_validate_trigger_data_long_pattern(self, trigger_service): """Тест валидации данных триггера с слишком длинным паттерном""" data = { 'name': 'Test Trigger', 'pattern': 'a' * 501, # 501 символ 'chat_type': 'group' } with pytest.raises(ValidationError): trigger_service._validate_trigger_data(data) def test_validate_trigger_data_invalid_chat_type(self, trigger_service): """Тест валидации данных триггера с некорректным типом чата""" data = { 'name': 'Test Trigger', 'pattern': r'test', 'chat_type': 'invalid' } with pytest.raises(ValidationError): trigger_service._validate_trigger_data(data) def test_validate_trigger_data_reaction_without_action_data(self, trigger_service): """Тест валидации данных триггера с реакцией без action_data""" data = { 'name': 'Test Trigger', 'pattern': r'test', 'chat_type': 'group', 'reaction_type': 'emoji' # action_data отсутствует } with pytest.raises(ValidationError): trigger_service._validate_trigger_data(data) def test_validate_trigger_data_invalid_reaction_type(self, trigger_service): """Тест валидации данных триггера с некорректным типом реакции""" data = { 'name': 'Test Trigger', 'pattern': r'test', 'chat_type': 'group', 'reaction_type': 'invalid_type', 'action_data': 'test' } with pytest.raises(ValidationError): trigger_service._validate_trigger_data(data) def test_validate_regex_pattern_valid(self, trigger_service): """Тест валидации корректного регулярного выражения""" # Не должно выбрасывать исключение trigger_service._validate_regex_pattern(r'hello\s+world') def test_validate_regex_pattern_invalid(self, trigger_service): """Тест валидации некорректного регулярного выражения""" with pytest.raises(ValidationError): trigger_service._validate_regex_pattern(r'[invalid') def test_should_use_cache_no_cache(self, trigger_service): """Тест проверки использования кеша при отсутствии кеша""" assert trigger_service._should_use_cache() == False def test_should_use_cache_expired(self, trigger_service): """Тест проверки использования кеша при истекшем кеше""" trigger_service._triggers_cache = [{'id': 1}] trigger_service._last_cache_update = datetime.now() trigger_service._cache_ttl_seconds = 0 # Кеш сразу истекает assert trigger_service._should_use_cache() == False def test_should_use_cache_valid(self, trigger_service): """Тест проверки использования кеша при валидном кеше""" trigger_service._triggers_cache = [{'id': 1}] trigger_service._last_cache_update = datetime.now() assert trigger_service._should_use_cache() == True def test_invalidate_cache(self, trigger_service): """Тест инвалидации кеша""" trigger_service._triggers_cache = [{'id': 1}] trigger_service._last_cache_update = datetime.now() trigger_service._invalidate_cache() assert trigger_service._triggers_cache is None assert trigger_service._last_cache_update is None @pytest.mark.asyncio async def test_clear_cache(self, trigger_service): """Тест очистки кеша""" trigger_service._triggers_cache = [{'id': 1}] trigger_service._last_cache_update = datetime.now() await trigger_service.clear_cache() assert trigger_service._triggers_cache is None assert trigger_service._last_cache_update is None