""" Юнит и интеграционные тесты для NotificationService. Тестирование улучшенных функций: retry, rate limiting, статистика, обработка ошибок. """ import pytest import asyncio import os from unittest.mock import Mock, AsyncMock, patch, call from datetime import datetime, timedelta # Добавляем корневую директорию в путь import sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from services.notification_service import NotificationService from telegram.error import TelegramError, NetworkError, TimedOut, RetryAfter class TestNotificationService: """Тесты для NotificationService""" @pytest.fixture def notification_service(self): """Фикстура сервиса уведомлений для тестов""" service = NotificationService("test_token", [123456789]) # Мокаем бота для тестов mock_bot = AsyncMock() service.bot = mock_bot return service @pytest.mark.asyncio async def test_successful_notification_send(self, notification_service): """Тест успешной отправки уведомления""" user_id = 987654321 message = "Тестовое сообщение" # Мокаем бота with patch.object(notification_service.bot, 'send_message', new_callable=AsyncMock) as mock_send: await notification_service.send_custom_notification(user_id, message) # Проверяем вызов mock_send.assert_called_once_with( chat_id=user_id, text=message, parse_mode=None, disable_web_page_preview=True ) # Проверяем статистику stats = notification_service.get_notification_stats() assert stats['successful_sends'] == 1 assert stats['failed_sends'] == 0 @pytest.mark.asyncio async def test_network_error_retry(self, notification_service): """Тест повторных попыток при сетевой ошибке""" user_id = 987654321 message = "Тестовое сообщение" # Мокаем бота для симуляции сетевой ошибки, затем успешной отправки call_count = 0 async def mock_send(*args, **kwargs): nonlocal call_count call_count += 1 if call_count == 1: raise NetworkError("Connection failed") # Вторая попытка успешна with patch.object(notification_service.bot, 'send_message', side_effect=mock_send): await notification_service.send_custom_notification(user_id, message) # Проверяем, что было 2 вызова (первая неудачная, вторая успешная) assert call_count == 2 # Проверяем статистику stats = notification_service.get_notification_stats() assert stats['successful_sends'] == 1 assert stats['network_errors'] == 1 assert stats['failed_sends'] == 0 @pytest.mark.asyncio async def test_timeout_error_retry(self, notification_service): """Тест повторных попыток при таймауте""" user_id = 987654321 message = "Тестовое сообщение" call_count = 0 async def mock_send(*args, **kwargs): nonlocal call_count call_count += 1 if call_count <= 2: raise TimedOut("Request timeout") # Третья попытка успешна with patch.object(notification_service.bot, 'send_message', side_effect=mock_send): await notification_service.send_custom_notification(user_id, message) assert call_count == 3 stats = notification_service.get_notification_stats() assert stats['successful_sends'] == 1 assert stats['timeout_errors'] == 2 # Две неудачные попытки assert stats['failed_sends'] == 0 @pytest.mark.asyncio async def test_rate_limit_handling(self, notification_service): """Тест обработки rate limiting от Telegram API""" user_id = 987654321 message = "Тестовое сообщение" call_count = 0 async def mock_send(*args, **kwargs): nonlocal call_count call_count += 1 if call_count == 1: raise RetryAfter(5) # Ждать 5 секунд # Вторая попытка успешна with patch.object(notification_service.bot, 'send_message', side_effect=mock_send): start_time = asyncio.get_event_loop().time() await notification_service.send_custom_notification(user_id, message) end_time = asyncio.get_event_loop().time() # Проверяем, что прошло время задержки (минимум 5 секунд) assert end_time - start_time >= 5 stats = notification_service.get_notification_stats() assert stats['successful_sends'] == 1 assert stats['rate_limit_errors'] == 1 @pytest.mark.asyncio async def test_max_retries_exceeded(self, notification_service): """Тест превышения максимального количества попыток""" user_id = 987654321 message = "Тестовое сообщение" # Всегда выбрасывать сетевую ошибку with patch.object(notification_service.bot, 'send_message', side_effect=NetworkError("Persistent network error")): await notification_service.send_custom_notification(user_id, message) # Проверяем статистику stats = notification_service.get_notification_stats() assert stats['successful_sends'] == 0 assert stats['network_errors'] == 3 # max_retries = 3 assert stats['failed_sends'] == 1 @pytest.mark.asyncio async def test_exponential_backoff(self, notification_service): """Тест экспоненциальной задержки между попытками""" user_id = 987654321 message = "Тестовое сообщение" delays = [] call_count = 0 async def mock_send(*args, **kwargs): nonlocal call_count call_count += 1 if call_count < 3: # Первые две попытки неудачные raise NetworkError("Network error") # Третья успешна # Фильтруем только задержки retry (не rate limiting) async def mock_sleep(delay): if delay >= 1.0: # Только задержки retry delays.append(delay) # Не используем await asyncio.sleep чтобы избежать рекурсии with patch.object(notification_service.bot, 'send_message', side_effect=mock_send), \ patch('asyncio.sleep', side_effect=mock_sleep): await notification_service.send_custom_notification(user_id, message) # Проверяем задержки: 1s, 2s (экспоненциально) assert len(delays) == 2 assert delays[0] == 1.0 # base_retry_delay assert delays[1] == 2.0 # base_retry_delay * 2 @pytest.mark.asyncio async def test_rate_limiting_between_requests(self, notification_service): """Тест rate limiting между последовательными запросами""" user_id1 = 987654321 user_id2 = 111222333 message = "Тестовое сообщение" with patch.object(notification_service.bot, 'send_message', new_callable=AsyncMock) as mock_send: start_time = asyncio.get_event_loop().time() # Отправляем два уведомления подряд await notification_service.send_custom_notification(user_id1, message) await notification_service.send_custom_notification(user_id2, message) end_time = asyncio.get_event_loop().time() # Проверяем, что прошло минимум 0.1 секунды между запросами assert end_time - start_time >= 0.1 # Проверяем, что оба сообщения отправлены assert mock_send.call_count == 2 @pytest.mark.asyncio async def test_telegram_error_handling(self, notification_service): """Тест обработки общих ошибок Telegram""" user_id = 987654321 message = "Тестовое сообщение" with patch.object(notification_service.bot, 'send_message', side_effect=TelegramError("Invalid chat_id")): await notification_service.send_custom_notification(user_id, message) stats = notification_service.get_notification_stats() assert stats['successful_sends'] == 0 assert stats['other_errors'] == 1 assert stats['failed_sends'] == 1 @pytest.mark.asyncio async def test_unexpected_error_handling(self, notification_service): """Тест обработки неожиданных ошибок""" user_id = 987654321 message = "Тестовое сообщение" with patch.object(notification_service.bot, 'send_message', side_effect=Exception("Unexpected error")): await notification_service.send_custom_notification(user_id, message) stats = notification_service.get_notification_stats() assert stats['successful_sends'] == 0 assert stats['other_errors'] == 1 assert stats['failed_sends'] == 1 def test_stats_calculation(self, notification_service): """Тест расчета статистики""" # Имитируем успешную отправку notification_service.stats['successful_sends'] = 10 notification_service.stats['failed_sends'] = 2 notification_service.stats['network_errors'] = 1 notification_service.stats['timeout_errors'] = 1 notification_service.stats['rate_limit_errors'] = 1 notification_service.stats['other_errors'] = 1 stats = notification_service.get_notification_stats() assert stats['total_attempts'] == 16 # 10 + 2 + 1 + 1 + 1 + 1 assert stats['success_rate'] == 62.5 # 10/16 * 100 def test_stats_reset(self, notification_service): """Тест сброса статистики""" # Заполняем статистику notification_service.stats['successful_sends'] = 5 notification_service.stats['network_errors'] = 3 # Сбрасываем notification_service.reset_stats() # Проверяем сброс stats = notification_service.get_notification_stats() assert stats['successful_sends'] == 0 assert stats['network_errors'] == 0 assert stats['total_attempts'] == 0 @pytest.mark.asyncio async def test_long_message_handling(self, notification_service): """Тест отправки очень длинных сообщений""" user_id = 987654321 # Создаем очень длинное сообщение (более 4096 символов) long_message = "A" * 5000 with patch.object(notification_service.bot, 'send_message', new_callable=AsyncMock) as mock_send: await notification_service.send_custom_notification(user_id, long_message) # Сообщение должно быть отправлено несмотря на длину mock_send.assert_called_once() call_args = mock_send.call_args assert call_args[1]['chat_id'] == user_id assert len(call_args[1]['text']) == 5000 @pytest.mark.asyncio async def test_concurrent_notifications(self, notification_service): """Тест одновременной отправки множества уведомлений""" import asyncio user_ids = [100000000 + i for i in range(10)] message = "Массовое уведомление" with patch.object(notification_service.bot, 'send_message', new_callable=AsyncMock) as mock_send: # Создаем задачи для одновременной отправки tasks = [ notification_service.send_custom_notification(user_id, message) for user_id in user_ids ] # Выполняем все задачи параллельно await asyncio.gather(*tasks) # Проверяем, что все сообщения отправлены assert mock_send.call_count == 10 # Проверяем статистику stats = notification_service.get_notification_stats() assert stats['successful_sends'] == 10 @pytest.mark.asyncio async def test_api_unavailable_simulation(self, notification_service): """Тест симуляции недоступности Telegram API""" user_id = 987654321 message = "Тестовое сообщение" # Имитируем недоступность API (все попытки неудачные) with patch.object(notification_service.bot, 'send_message', side_effect=NetworkError("API unavailable")): await notification_service.send_custom_notification(user_id, message) stats = notification_service.get_notification_stats() assert stats['successful_sends'] == 0 assert stats['network_errors'] == 3 # max_retries assert stats['failed_sends'] == 1 @pytest.mark.asyncio async def test_mixed_error_types(self, notification_service): """Тест смеси различных типов ошибок""" user_id = 987654321 message = "Тестовое сообщение" call_count = 0 async def mock_send(*args, **kwargs): nonlocal call_count call_count += 1 if call_count == 1: raise TimedOut("Timeout") elif call_count == 2: raise NetworkError("Network error") elif call_count == 3: raise TelegramError("Other error") # Не должно дойти до четвертой попытки with patch.object(notification_service.bot, 'send_message', side_effect=mock_send): await notification_service.send_custom_notification(user_id, message) stats = notification_service.get_notification_stats() assert stats['successful_sends'] == 0 assert stats['timeout_errors'] == 1 assert stats['network_errors'] == 1 assert stats['other_errors'] == 1 assert stats['failed_sends'] == 1