- Alembic 005: таблицы irm_alerts и incident_alert_links - Модуль alerts: API/UI, Ack/Resolve, привязка к инциденту через alert_ids - Вебхук Grafana: одна транзакция ingress + irm_alerts; разбор payload в grafana_payload - По умолчанию инцидент из вебхука не создаётся (AUTO_INCIDENT_FROM_ALERT) - Документация IRM_GRAFANA_PARITY.md, обновления IRM.md и CHANGELOG Made-with: Cursor
181 lines
5.9 KiB
Python
181 lines
5.9 KiB
Python
import json
|
||
from unittest.mock import AsyncMock, MagicMock, patch
|
||
from fastapi.testclient import TestClient
|
||
|
||
|
||
def _webhook_mock_pool(mock_conn: AsyncMock) -> MagicMock:
|
||
"""Пул с транзакцией и execute — как после вставки ingress + irm_alerts."""
|
||
tx = AsyncMock()
|
||
tx.__aenter__ = AsyncMock(return_value=None)
|
||
tx.__aexit__ = AsyncMock(return_value=None)
|
||
mock_conn.transaction = MagicMock(return_value=tx)
|
||
mock_conn.execute = AsyncMock()
|
||
mock_cm = AsyncMock()
|
||
mock_cm.__aenter__ = AsyncMock(return_value=mock_conn)
|
||
mock_cm.__aexit__ = AsyncMock(return_value=None)
|
||
mock_pool = MagicMock()
|
||
mock_pool.acquire = MagicMock(return_value=mock_cm)
|
||
return mock_pool
|
||
|
||
|
||
def test_grafana_webhook_no_db(client: TestClient) -> None:
|
||
"""Без пула БД — 202, запись не падает."""
|
||
r = client.post(
|
||
"/api/v1/ingress/grafana",
|
||
content=json.dumps({"title": "t"}),
|
||
headers={"Content-Type": "application/json"},
|
||
)
|
||
assert r.status_code == 202
|
||
|
||
|
||
def test_grafana_webhook_unauthorized_when_secret_set(client: TestClient) -> None:
|
||
app = client.app
|
||
real = app.state.settings.grafana_webhook_secret
|
||
app.state.settings.grafana_webhook_secret = "s3cr3t"
|
||
try:
|
||
r = client.post(
|
||
"/api/v1/ingress/grafana",
|
||
content=b"{}",
|
||
headers={"Content-Type": "application/json"},
|
||
)
|
||
assert r.status_code == 401
|
||
r2 = client.post(
|
||
"/api/v1/ingress/grafana",
|
||
content=b"{}",
|
||
headers={"Content-Type": "application/json", "X-OnGuard-Secret": "s3cr3t"},
|
||
)
|
||
assert r2.status_code == 202
|
||
finally:
|
||
app.state.settings.grafana_webhook_secret = real
|
||
|
||
|
||
def test_grafana_webhook_inserts_with_mock_pool(client: TestClient) -> None:
|
||
from uuid import uuid4
|
||
|
||
mock_conn = AsyncMock()
|
||
uid = uuid4()
|
||
mock_conn.fetchrow = AsyncMock(return_value={"id": uid})
|
||
mock_pool = _webhook_mock_pool(mock_conn)
|
||
|
||
app = client.app
|
||
real_pool = app.state.pool
|
||
app.state.pool = mock_pool
|
||
try:
|
||
r = client.post(
|
||
"/api/v1/ingress/grafana",
|
||
content=json.dumps({"a": 1}),
|
||
headers={"Content-Type": "application/json"},
|
||
)
|
||
assert r.status_code == 202
|
||
mock_conn.fetchrow.assert_called_once()
|
||
mock_conn.execute.assert_called_once()
|
||
finally:
|
||
app.state.pool = real_pool
|
||
|
||
|
||
def test_grafana_webhook_auto_org_from_external_url(client: TestClient) -> None:
|
||
from uuid import uuid4
|
||
|
||
mock_conn = AsyncMock()
|
||
uid = uuid4()
|
||
mock_conn.fetchrow = AsyncMock(return_value={"id": uid})
|
||
mock_pool = _webhook_mock_pool(mock_conn)
|
||
|
||
app = client.app
|
||
real_pool = app.state.pool
|
||
app.state.pool = mock_pool
|
||
try:
|
||
r = client.post(
|
||
"/api/v1/ingress/grafana",
|
||
content=json.dumps(
|
||
{"externalURL": "https://grafana-adibrov.example.com/", "title": "x"}
|
||
),
|
||
headers={"Content-Type": "application/json"},
|
||
)
|
||
assert r.status_code == 202
|
||
assert mock_conn.fetchrow.call_args[0][3] == "grafana-adibrov.example.com"
|
||
finally:
|
||
app.state.pool = real_pool
|
||
|
||
|
||
def test_grafana_webhook_publishes_alert_received(client: TestClient) -> None:
|
||
from unittest.mock import patch
|
||
from uuid import uuid4
|
||
|
||
mock_conn = AsyncMock()
|
||
uid = uuid4()
|
||
mock_conn.fetchrow = AsyncMock(return_value={"id": uid})
|
||
mock_pool = _webhook_mock_pool(mock_conn)
|
||
|
||
app = client.app
|
||
bus = app.state.event_bus
|
||
with patch.object(bus, "publish_alert_received", new_callable=AsyncMock) as spy:
|
||
real_pool = app.state.pool
|
||
app.state.pool = mock_pool
|
||
try:
|
||
r = client.post(
|
||
"/api/v1/ingress/grafana",
|
||
content=json.dumps({"title": "x"}),
|
||
headers={"Content-Type": "application/json"},
|
||
)
|
||
assert r.status_code == 202
|
||
spy.assert_awaited_once()
|
||
assert spy.await_args.kwargs.get("raw_payload_ref") == uid
|
||
finally:
|
||
app.state.pool = real_pool
|
||
|
||
|
||
def test_grafana_webhook_org_any_slug_without_json_config(client: TestClient) -> None:
|
||
"""Путь /{slug} не требует GRAFANA_SOURCES_JSON — slug просто сохраняется."""
|
||
from uuid import uuid4
|
||
|
||
mock_conn = AsyncMock()
|
||
uid = uuid4()
|
||
mock_conn.fetchrow = AsyncMock(return_value={"id": uid})
|
||
mock_pool = _webhook_mock_pool(mock_conn)
|
||
|
||
app = client.app
|
||
real_pool = app.state.pool
|
||
app.state.pool = mock_pool
|
||
try:
|
||
r = client.post(
|
||
"/api/v1/ingress/grafana/other",
|
||
content=b"{}",
|
||
headers={"Content-Type": "application/json"},
|
||
)
|
||
assert r.status_code == 202
|
||
assert mock_conn.fetchrow.call_args[0][3] == "other"
|
||
finally:
|
||
app.state.pool = real_pool
|
||
|
||
|
||
def test_grafana_webhook_org_ok(client: TestClient) -> None:
|
||
from uuid import uuid4
|
||
|
||
mock_conn = AsyncMock()
|
||
uid = uuid4()
|
||
mock_conn.fetchrow = AsyncMock(return_value={"id": uid})
|
||
mock_pool = _webhook_mock_pool(mock_conn)
|
||
|
||
app = client.app
|
||
real_json = app.state.settings.grafana_sources_json
|
||
real_pool = app.state.pool
|
||
app.state.settings.grafana_sources_json = (
|
||
'[{"slug":"adibrov","api_url":"http://192.168.0.1:3000","api_token":"","webhook_secret":""}]'
|
||
)
|
||
app.state.pool = mock_pool
|
||
try:
|
||
r = client.post(
|
||
"/api/v1/ingress/grafana/adibrov",
|
||
content='{"title":"t"}',
|
||
headers={"Content-Type": "application/json"},
|
||
)
|
||
assert r.status_code == 202
|
||
call = mock_conn.fetchrow.call_args
|
||
assert "org_slug" in call[0][0].lower() or "org_slug" in str(call)
|
||
assert call[0][1] == "grafana"
|
||
assert call[0][3] == "adibrov"
|
||
finally:
|
||
app.state.settings.grafana_sources_json = real_json
|
||
app.state.pool = real_pool
|