Files
telegram-helper-bot/tests/test_rate_limit_monitor.py
Andrey 3d6b4353f9
All checks were successful
CI pipeline / Test & Code Quality (push) Successful in 34s
Refactor imports across multiple files to improve code organization and readability.
2026-02-28 23:24:25 +03:00

264 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Тесты для 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