180 lines
6.5 KiB
Python
180 lines
6.5 KiB
Python
"""Тесты уведомлений в чат логов и ежечасного пинга «бот живой»."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections.abc import Callable
|
|
from datetime import UTC, datetime
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from bot.services import alerts, heartbeat
|
|
from common.analytics import EventType, close_analytics, init_analytics
|
|
from common.config import Settings
|
|
from common.models import PurificationResult
|
|
|
|
|
|
class FakeBot:
|
|
"""Заглушка aiogram.Bot: записывает отправленные и пересланные сообщения."""
|
|
|
|
def __init__(self, *, fail: bool = False) -> None:
|
|
self.messages: list[tuple[int, str]] = []
|
|
self.forwarded: list[tuple[int, int, int]] = []
|
|
self._fail = fail
|
|
|
|
async def send_message(self, chat_id: int, text: str) -> None:
|
|
if self._fail:
|
|
raise RuntimeError("Telegram недоступен")
|
|
self.messages.append((chat_id, text))
|
|
|
|
async def forward_message(self, chat_id: int, from_chat_id: int, message_id: int) -> None:
|
|
if self._fail:
|
|
raise RuntimeError("Telegram недоступен")
|
|
self.forwarded.append((chat_id, from_chat_id, message_id))
|
|
|
|
|
|
class FakeClickHouse:
|
|
"""Заглушка клиента ClickHouse (см. тесты аналитики)."""
|
|
|
|
def __init__(self) -> None:
|
|
self.rows: list[list[Any]] = []
|
|
|
|
async def insert(self, table: str, data: list[list[Any]], column_names: list[str]) -> None:
|
|
self.rows.extend(data)
|
|
|
|
async def close(self) -> None:
|
|
pass
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
async def _clean_singleton():
|
|
await close_analytics()
|
|
yield
|
|
await close_analytics()
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _reset_ping_hour(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Пинг не должен «протекать» между тестами."""
|
|
monkeypatch.setattr(heartbeat, "_last_ping_hour", None)
|
|
|
|
|
|
def _patch_settings(monkeypatch: pytest.MonkeyPatch, settings: Settings) -> Settings:
|
|
"""Подменить настройки во всех модулях, которые их читают."""
|
|
monkeypatch.setattr("bot.services.alerts.get_settings", lambda: settings)
|
|
monkeypatch.setattr("bot.services.heartbeat.get_settings", lambda: settings)
|
|
return settings
|
|
|
|
|
|
# --- отправка в чат логов ---
|
|
|
|
|
|
async def test_notify_alert_skips_when_chat_disabled(
|
|
settings_factory: Callable[..., Settings], monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
_patch_settings(monkeypatch, settings_factory(alert_chat_id=0))
|
|
bot = FakeBot()
|
|
|
|
assert await alerts.notify_alert(bot, "текст") is False
|
|
assert bot.messages == []
|
|
|
|
|
|
async def test_notify_alert_sends_to_alert_chat(
|
|
settings_factory: Callable[..., Settings], monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
_patch_settings(monkeypatch, settings_factory(alert_chat_id=-100))
|
|
bot = FakeBot()
|
|
|
|
assert await alerts.notify_alert(bot, "текст") is True
|
|
assert bot.messages == [(-100, "текст")]
|
|
|
|
|
|
async def test_notify_alert_failure_is_logged(
|
|
settings_factory: Callable[..., Settings],
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
_patch_settings(monkeypatch, settings_factory(alert_chat_id=-100))
|
|
|
|
with caplog.at_level(logging.ERROR):
|
|
assert await alerts.notify_alert(FakeBot(fail=True), "текст") is False
|
|
|
|
assert any("не удалось отправить уведомление" in message.lower() for message in caplog.messages)
|
|
|
|
|
|
async def test_notify_alert_forwards_message(
|
|
settings_factory: Callable[..., Settings], monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
_patch_settings(monkeypatch, settings_factory(alert_chat_id=-100))
|
|
bot = FakeBot()
|
|
message = SimpleNamespace(chat=SimpleNamespace(id=555), message_id=42)
|
|
|
|
assert await alerts.notify_alert(bot, "текст", forward=message) is True
|
|
assert bot.forwarded == [(-100, 555, 42)]
|
|
|
|
|
|
async def test_notify_join_and_leave_text(
|
|
settings_factory: Callable[..., Settings], monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
_patch_settings(monkeypatch, settings_factory(alert_chat_id=-100))
|
|
bot = FakeBot()
|
|
|
|
await alerts.notify_join(bot, username="vasya", user_id=7, chat_title="Клан")
|
|
await alerts.notify_leave(bot, username=None, user_id=7, chat_title="Клан")
|
|
|
|
assert bot.messages[0] == (-100, "[вход] @vasya вошёл в чат «Клан»")
|
|
assert bot.messages[1] == (-100, "[выход] 7 покинул чат «Клан»")
|
|
|
|
|
|
async def test_notify_purification_text(
|
|
settings_factory: Callable[..., Settings], monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
_patch_settings(monkeypatch, settings_factory(alert_chat_id=-100))
|
|
bot = FakeBot()
|
|
|
|
result = PurificationResult(success=True, exp_add=3, money_add=41)
|
|
await alerts.notify_purification(bot, username="vasya", user_id=7, result=result)
|
|
|
|
assert bot.messages == [(-100, "[очищение] @vasya — опыт +3, монеты +41")]
|
|
|
|
|
|
# --- ежечасный пинг «бот живой» ---
|
|
|
|
|
|
async def test_ping_hourly_sends_once_per_hour(
|
|
settings_factory: Callable[..., Settings], monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
settings = _patch_settings(monkeypatch, settings_factory(alert_chat_id=-100, app_env="prod"))
|
|
fake = FakeClickHouse()
|
|
await init_analytics(settings, client=fake)
|
|
bot = FakeBot()
|
|
|
|
assert await heartbeat.ping_hourly(bot, now=datetime(2026, 1, 1, 10, 0, tzinfo=UTC)) is True
|
|
assert await heartbeat.ping_hourly(bot, now=datetime(2026, 1, 1, 10, 30, tzinfo=UTC)) is False
|
|
assert await heartbeat.ping_hourly(bot, now=datetime(2026, 1, 1, 11, 5, tzinfo=UTC)) is True
|
|
|
|
await close_analytics()
|
|
|
|
assert len(bot.messages) == 2
|
|
assert "бот живой" in bot.messages[0][1]
|
|
assert [row[1] for row in fake.rows] == [EventType.BOT_ALIVE, EventType.BOT_ALIVE]
|
|
|
|
|
|
async def test_ping_hourly_records_event_without_alert_chat(
|
|
settings_factory: Callable[..., Settings], monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
settings = _patch_settings(monkeypatch, settings_factory(alert_chat_id=0))
|
|
fake = FakeClickHouse()
|
|
await init_analytics(settings, client=fake)
|
|
bot = FakeBot()
|
|
|
|
assert await heartbeat.ping_hourly(bot, now=datetime(2026, 1, 1, 10, 0, tzinfo=UTC)) is False
|
|
assert await heartbeat.ping_hourly(bot, now=datetime(2026, 1, 1, 10, 45, tzinfo=UTC)) is False
|
|
|
|
await close_analytics()
|
|
|
|
assert bot.messages == []
|
|
assert [row[1] for row in fake.rows] == [EventType.BOT_ALIVE]
|