"""Offline-тесты приёмника GlitchTip webhook-алертов (POST /api/v1/trade-in/ops/glitchtip-webhook) — app/api/v1/glitchtip.py. Проверяет HTTP-контракт: успешная пересылка issue-/uptime-алертов в Telegram (клиент замокан), отказ без валидного секрета, отказ при несконфигурированных настройках, обрезка длинного текста под лимит Telegram (4096), graceful-обработка неизвестной формы payload (НЕ 500). NEVER touches real DB / real Telegram API. """ from __future__ import annotations import os os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test") import json from typing import Any, ClassVar import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from app.api.v1 import glitchtip as glitchtip_module from app.services.tgbot.client import TelegramApiError _SECRET = "test-shared-secret" _ENDPOINT = "/api/v1/trade-in/ops/glitchtip-webhook" @pytest.fixture(autouse=True) def _configured(monkeypatch: pytest.MonkeyPatch) -> None: """По умолчанию вебхук полностью сконфигурирован — отдельные тесты переопределяют конкретные поля.""" monkeypatch.setattr(glitchtip_module.settings, "tradein_internal_auth_secret", _SECRET) monkeypatch.setattr(glitchtip_module.settings, "telegram_bot_token", "fake-token") monkeypatch.setattr(glitchtip_module.settings, "telegram_alerts_chat_id", -1004443088679) monkeypatch.setattr(glitchtip_module.settings, "telegram_alerts_topic_id", 158) class _FakeTelegramClient: """Подменяет `TelegramClient` внутри модуля `glitchtip` — никакого httpx/сети.""" calls: ClassVar[list[dict[str, Any]]] = [] _response: ClassVar[dict[str, Any] | Exception] = {"message_id": 1} def __init__(self, _token: str) -> None: pass async def send_message(self, **kwargs: Any) -> dict[str, Any]: _FakeTelegramClient.calls.append(kwargs) if isinstance(_FakeTelegramClient._response, Exception): raise _FakeTelegramClient._response return _FakeTelegramClient._response @pytest.fixture(autouse=True) def _fake_telegram_client(monkeypatch: pytest.MonkeyPatch) -> Any: _FakeTelegramClient.calls = [] _FakeTelegramClient._response = {"message_id": 1} monkeypatch.setattr(glitchtip_module, "TelegramClient", _FakeTelegramClient) return _FakeTelegramClient @pytest.fixture def client() -> TestClient: app = FastAPI() app.include_router(glitchtip_module.router, prefix="/api/v1/trade-in") return TestClient(app) _ISSUE_PAYLOAD = { "text": "GlitchTip Alert", "attachments": [ { "title": "ValueError: something broke", "title_link": "https://errors.gendsgn.ru/organizations/gendesign/issues/123/", "text": "app/services/foo.py in bar", "color": "#e03131", "fields": [ {"title": "Project", "value": "tradein-backend", "short": True}, {"title": "Environment", "value": "production", "short": True}, ], "mrkdown_in": ["text"], } ], } _UPTIME_PAYLOAD = { "text": "GlitchTip Uptime Alert", "attachments": [ { "title": "gendsgn.ru", "title_link": "https://errors.gendsgn.ru/organizations/gendesign/uptime/1/", "text": "The monitored site has gone down.", "image_url": None, "color": None, "fields": None, "mrkdown_in": None, } ], } # ── успешная пересылка ────────────────────────────────────────────────────── def test_issue_alert_forwarded_to_telegram(client: TestClient, _fake_telegram_client: Any) -> None: r = client.post(f"{_ENDPOINT}?secret={_SECRET}", json=_ISSUE_PAYLOAD) assert r.status_code == 200, r.text assert r.json() == {"status": "ok"} assert len(_fake_telegram_client.calls) == 1 call = _fake_telegram_client.calls[0] assert call["chat_id"] == -1004443088679 assert call["message_thread_id"] == 158 assert "ValueError: something broke" in call["text"] assert "tradein-backend" in call["text"] # Project field assert "errors.gendsgn.ru" in call["text"] def test_uptime_alert_forwarded_to_telegram(client: TestClient, _fake_telegram_client: Any) -> None: r = client.post(f"{_ENDPOINT}?secret={_SECRET}", json=_UPTIME_PAYLOAD) assert r.status_code == 200, r.text assert len(_fake_telegram_client.calls) == 1 call = _fake_telegram_client.calls[0] assert "GlitchTip Uptime Alert" in call["text"] assert "gone down" in call["text"] assert call["message_thread_id"] == 158 def test_telegram_failure_returns_502_not_500( client: TestClient, _fake_telegram_client: Any ) -> None: _FakeTelegramClient._response = TelegramApiError("sendMessage", 400, "chat not found") r = client.post(f"{_ENDPOINT}?secret={_SECRET}", json=_ISSUE_PAYLOAD) assert r.status_code == 502 assert r.status_code != 500 # ── auth ───────────────────────────────────────────────────────────────────── def test_missing_secret_401(client: TestClient, _fake_telegram_client: Any) -> None: r = client.post(_ENDPOINT, json=_ISSUE_PAYLOAD) assert r.status_code == 401 assert _fake_telegram_client.calls == [] def test_wrong_secret_401(client: TestClient, _fake_telegram_client: Any) -> None: r = client.post(f"{_ENDPOINT}?secret=wrong-value", json=_ISSUE_PAYLOAD) assert r.status_code == 401 assert _fake_telegram_client.calls == [] def test_secret_not_configured_returns_503_not_500( client: TestClient, monkeypatch: pytest.MonkeyPatch, _fake_telegram_client: Any ) -> None: monkeypatch.setattr(glitchtip_module.settings, "tradein_internal_auth_secret", "") r = client.post(f"{_ENDPOINT}?secret={_SECRET}", json=_ISSUE_PAYLOAD) assert r.status_code == 503 assert r.status_code != 500 assert _fake_telegram_client.calls == [] def test_bot_not_configured_returns_503( client: TestClient, monkeypatch: pytest.MonkeyPatch, _fake_telegram_client: Any ) -> None: monkeypatch.setattr(glitchtip_module.settings, "telegram_bot_token", "") r = client.post(f"{_ENDPOINT}?secret={_SECRET}", json=_ISSUE_PAYLOAD) assert r.status_code == 503 assert _fake_telegram_client.calls == [] def test_alerts_chat_id_not_configured_returns_503( client: TestClient, monkeypatch: pytest.MonkeyPatch, _fake_telegram_client: Any ) -> None: monkeypatch.setattr(glitchtip_module.settings, "telegram_alerts_chat_id", 0) r = client.post(f"{_ENDPOINT}?secret={_SECRET}", json=_ISSUE_PAYLOAD) assert r.status_code == 503 assert _fake_telegram_client.calls == [] # ── обрезка длинного текста ───────────────────────────────────────────────── def test_long_payload_truncated_to_telegram_limit( client: TestClient, _fake_telegram_client: Any ) -> None: huge_payload = { "text": "GlitchTip Alert", "attachments": [ { "title": "Huge issue", "title_link": "https://errors.gendsgn.ru/x", "text": "x" * 10000, } ], } r = client.post(f"{_ENDPOINT}?secret={_SECRET}", json=huge_payload) assert r.status_code == 200, r.text sent_text = _fake_telegram_client.calls[0]["text"] assert len(sent_text) <= 4096 assert sent_text.endswith("(обрезано)") def test_unknown_form_huge_raw_body_truncated( client: TestClient, _fake_telegram_client: Any ) -> None: r = client.post( f"{_ENDPOINT}?secret={_SECRET}", content=("x" * 10000).encode(), headers={"content-type": "application/json"}, ) assert r.status_code == 200, r.text sent_text = _fake_telegram_client.calls[0]["text"] assert len(sent_text) <= 4096 # ── неизвестная форма payload — НЕ 500 ────────────────────────────────────── def test_unknown_json_shape_forwarded_with_marker( client: TestClient, _fake_telegram_client: Any ) -> None: """Ни `text`, ни `attachments` — форма, которую GlitchTip НЕ шлёт сегодня, но контракт задачи требует не падать 500, а переслать как есть.""" r = client.post(f"{_ENDPOINT}?secret={_SECRET}", json={"some_field": "some_value", "n": 42}) assert r.status_code == 200, r.text sent_text = _fake_telegram_client.calls[0]["text"] assert "неизвестный формат" in sent_text assert "some_value" in sent_text def test_non_json_body_forwarded_not_500(client: TestClient, _fake_telegram_client: Any) -> None: r = client.post( f"{_ENDPOINT}?secret={_SECRET}", content=b"not-json-at-all {{{", headers={"content-type": "text/plain"}, ) assert r.status_code == 200, r.text sent_text = _fake_telegram_client.calls[0]["text"] assert "неизвестный формат" in sent_text assert "not-json-at-all" in sent_text def test_json_array_body_forwarded_not_500(client: TestClient, _fake_telegram_client: Any) -> None: """Валидный JSON, но не объект (top-level list) — тоже неизвестная форма.""" r = client.post(f"{_ENDPOINT}?secret={_SECRET}", json=[1, 2, 3]) assert r.status_code == 200, r.text assert len(_fake_telegram_client.calls) == 1 def test_empty_body_forwarded_not_500(client: TestClient, _fake_telegram_client: Any) -> None: r = client.post(f"{_ENDPOINT}?secret={_SECRET}", content=b"") assert r.status_code == 200, r.text assert len(_fake_telegram_client.calls) == 1 # ── формат сообщения ───────────────────────────────────────────────────────── def test_message_format_json_roundtrip(client: TestClient, _fake_telegram_client: Any) -> None: """Sanity: убеждаемся, что тестовый payload реально валиден как JSON (не полагаемся на literal dict без проверки сериализации).""" body = json.dumps(_ISSUE_PAYLOAD) r = client.post( f"{_ENDPOINT}?secret={_SECRET}", content=body.encode(), headers={"content-type": "application/json"}, ) assert r.status_code == 200, r.text