263 lines
11 KiB
Python
263 lines
11 KiB
Python
"""
|
||
Тесты для 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
|