""" Тесты для helper_bot.utils.rate_limit_monitor. """ import time from collections import deque from unittest.mock import patch import pytest from helper_bot.utils.rate_limit_monitor import ( RateLimitMonitor, RateLimitStats, get_rate_limit_summary, record_rate_limit_request, ) @pytest.mark.unit class TestRateLimitStats: """Тесты для RateLimitStats.""" def test_success_rate_zero_requests(self): """При нуле запросов success_rate равен 1.0.""" stats = RateLimitStats(chat_id=1) assert stats.success_rate == 1.0 def test_success_rate_all_success(self): """При всех успешных запросах success_rate равен 1.0.""" stats = RateLimitStats(chat_id=1, total_requests=5, successful_requests=5) assert stats.success_rate == 1.0 def test_success_rate_partial(self): """Частичный успех: 3 из 5.""" stats = RateLimitStats(chat_id=1, total_requests=5, successful_requests=3) assert stats.success_rate == 0.6 def test_error_rate(self): """error_rate = 1 - success_rate.""" stats = RateLimitStats(chat_id=1, total_requests=10, successful_requests=7) assert stats.error_rate == pytest.approx(0.3) def test_average_wait_time_zero_requests(self): """При нуле запросов average_wait_time равен 0.""" stats = RateLimitStats(chat_id=1) assert stats.average_wait_time == 0.0 def test_average_wait_time(self): """Среднее время ожидания считается корректно.""" stats = RateLimitStats(chat_id=1, total_requests=4, total_wait_time=2.0) assert stats.average_wait_time == 0.5 def test_requests_per_minute_empty(self): """При пустом request_times возвращается 0.""" stats = RateLimitStats(chat_id=1) assert stats.requests_per_minute == 0.0 def test_requests_per_minute_recent(self): """Подсчёт запросов за последнюю минуту.""" now = time.time() stats = RateLimitStats( chat_id=1, request_times=deque([now, now - 30], maxlen=100) ) assert stats.requests_per_minute == 2 def test_requests_per_minute_old_ignored(self): """Запросы старше минуты не учитываются.""" now = time.time() stats = RateLimitStats( chat_id=1, request_times=deque([now, now - 90], maxlen=100), ) assert stats.requests_per_minute == 1 @pytest.mark.unit class TestRateLimitMonitor: """Тесты для RateLimitMonitor.""" def test_init(self): """Инициализация с дефолтными и кастомными параметрами.""" monitor = RateLimitMonitor(max_history_size=500) assert monitor.max_history_size == 500 assert monitor.global_stats.chat_id == 0 assert len(monitor.stats) == 0 assert len(monitor.error_history) == 0 def test_record_request_success(self): """Запись успешного запроса обновляет счётчики.""" monitor = RateLimitMonitor() monitor.record_request(chat_id=123, success=True, wait_time=1.5) chat_stats = monitor.get_chat_stats(123) assert chat_stats is not None assert chat_stats.total_requests == 1 assert chat_stats.successful_requests == 1 assert chat_stats.failed_requests == 0 assert chat_stats.total_wait_time == 1.5 global_stats = monitor.get_global_stats() assert global_stats.total_requests == 1 assert global_stats.successful_requests == 1 def test_record_request_failure_retry_after(self): """Запись ошибки RetryAfter увеличивает retry_after_errors.""" monitor = RateLimitMonitor() monitor.record_request(chat_id=456, success=False, error_type="RetryAfter") chat_stats = monitor.get_chat_stats(456) assert chat_stats.failed_requests == 1 assert chat_stats.retry_after_errors == 1 assert chat_stats.other_errors == 0 assert len(monitor.error_history) == 1 assert monitor.error_history[0]["error_type"] == "RetryAfter" def test_record_request_failure_other(self): """Запись другой ошибки увеличивает other_errors.""" monitor = RateLimitMonitor() monitor.record_request(chat_id=789, success=False, error_type="Timeout") chat_stats = monitor.get_chat_stats(789) assert chat_stats.other_errors == 1 assert chat_stats.retry_after_errors == 0 def test_get_chat_stats_missing(self): """Для неизвестного чата возвращается None.""" monitor = RateLimitMonitor() assert monitor.get_chat_stats(999) is None def test_get_top_chats_by_requests(self): """Топ чатов по количеству запросов.""" monitor = RateLimitMonitor() monitor.record_request(1, True) monitor.record_request(1, True) monitor.record_request(2, True) monitor.record_request(3, True) monitor.record_request(3, True) monitor.record_request(3, True) top = monitor.get_top_chats_by_requests(limit=2) assert len(top) == 2 assert top[0][0] == 3 assert top[0][1].total_requests == 3 assert top[1][0] == 1 assert top[1][1].total_requests == 2 def test_get_chats_with_high_error_rate(self): """Чаты с высоким процентом ошибок (и более 5 запросов).""" monitor = RateLimitMonitor() for _ in range(6): monitor.record_request(100, True) for _ in range(4): monitor.record_request(100, False, error_type="Other") # 4/10 = 40% ошибок for _ in range(6): monitor.record_request(200, True) for _ in range(2): monitor.record_request(200, False, error_type="Other") # 2/8 < 20%, но порог 0.1 — попадёт если error_rate > 0.1 high = monitor.get_chats_with_high_error_rate(threshold=0.2) assert len(high) >= 1 chat_ids = [c[0] for c in high] assert 100 in chat_ids def test_get_recent_errors(self): """Недавние ошибки за указанный период.""" monitor = RateLimitMonitor() monitor.record_request(1, False, error_type="RetryAfter") recent = monitor.get_recent_errors(minutes=60) assert len(recent) == 1 assert recent[0]["error_type"] == "RetryAfter" assert recent[0]["chat_id"] == 1 def test_get_recent_errors_empty_old_window(self): """При окне 0 минут недавних ошибок нет (все старше).""" monitor = RateLimitMonitor() monitor.record_request(1, False, error_type="RetryAfter") recent = monitor.get_recent_errors(minutes=0) assert len(recent) == 0 def test_get_error_summary(self): """Сводка ошибок по типам.""" monitor = RateLimitMonitor() monitor.record_request(1, False, error_type="RetryAfter") monitor.record_request(1, False, error_type="RetryAfter") monitor.record_request(2, False, error_type="Timeout") summary = monitor.get_error_summary(minutes=60) assert summary["RetryAfter"] == 2 assert summary["Timeout"] == 1 def test_reset_stats_all(self): """Сброс всей статистики.""" monitor = RateLimitMonitor() monitor.record_request(1, True) monitor.record_request(2, False, error_type="RetryAfter") monitor.reset_stats() assert monitor.get_chat_stats(1) is None assert monitor.get_global_stats().total_requests == 0 assert len(monitor.error_history) == 0 def test_reset_stats_single_chat(self): """Сброс статистики для одного чата.""" monitor = RateLimitMonitor() monitor.record_request(1, True) monitor.record_request(2, True) monitor.reset_stats(chat_id=1) assert monitor.get_chat_stats(1) is None assert monitor.get_chat_stats(2) is not None assert monitor.get_global_stats().total_requests == 2 def test_reset_stats_nonexistent_chat(self): """Сброс несуществующего чата не падает.""" monitor = RateLimitMonitor() monitor.reset_stats(chat_id=999) @patch("helper_bot.utils.rate_limit_monitor.logger") def test_log_statistics(self, mock_logger): """log_statistics вызывает logger с нужным уровнем.""" monitor = RateLimitMonitor() monitor.record_request(1, True) monitor.log_statistics(log_level="info") mock_logger.info.assert_called() mock_logger.reset_mock() monitor.log_statistics(log_level="warning") mock_logger.warning.assert_called() mock_logger.reset_mock() monitor.log_statistics(log_level="error") mock_logger.error.assert_called() @pytest.mark.unit class TestModuleFunctions: """Тесты для функций модуля record_rate_limit_request и get_rate_limit_summary.""" def test_record_rate_limit_request(self): """record_rate_limit_request делегирует в глобальный монитор.""" monitor = RateLimitMonitor() with patch("helper_bot.utils.rate_limit_monitor.rate_limit_monitor", monitor): record_rate_limit_request(chat_id=111, success=True, wait_time=0.5) stats = monitor.get_chat_stats(111) assert stats is not None assert stats.total_requests == 1 assert stats.total_wait_time == 0.5 def test_get_rate_limit_summary(self): """get_rate_limit_summary возвращает словарь с ожидаемыми ключами.""" monitor = RateLimitMonitor() monitor.record_request(1, True) with patch("helper_bot.utils.rate_limit_monitor.rate_limit_monitor", monitor): summary = get_rate_limit_summary() assert "total_requests" in summary assert "success_rate" in summary assert "error_rate" in summary assert "recent_errors_count" in summary assert "active_chats" in summary assert "requests_per_minute" in summary assert "average_wait_time" in summary assert summary["total_requests"] == 1 assert summary["active_chats"] == 1