v1.4.0: модули с веб-UI, правое меню, расширенные тесты

Реестр MODULE_MOUNTS: API, ui_router, фрагменты главной, EventBus.
Главная и страницы модулей с правой навигацией из реестра; wrap_module_html_page.
Ingress: публикация alert.received после сохранения в БД.
Документация MODULES.md; pytest покрывает API, UI и навигацию.

Made-with: Cursor
This commit is contained in:
Alexandr
2026-04-03 08:45:19 +03:00
parent 85eb61b576
commit 349cea85a3
17 changed files with 571 additions and 24 deletions

View File

@ -36,8 +36,11 @@ def test_grafana_webhook_unauthorized_when_secret_set(client: TestClient) -> Non
def test_grafana_webhook_inserts_with_mock_pool(client: TestClient) -> None:
from uuid import uuid4
mock_conn = AsyncMock()
mock_conn.execute = AsyncMock()
uid = uuid4()
mock_conn.fetchrow = AsyncMock(return_value={"id": uid})
mock_cm = AsyncMock()
mock_cm.__aenter__ = AsyncMock(return_value=mock_conn)
mock_cm.__aexit__ = AsyncMock(return_value=None)
@ -55,6 +58,37 @@ def test_grafana_webhook_inserts_with_mock_pool(client: TestClient) -> None:
headers={"Content-Type": "application/json"},
)
assert r.status_code == 202
mock_conn.execute.assert_called_once()
mock_conn.fetchrow.assert_called_once()
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_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)
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

69
tests/test_root_ui.py Normal file
View File

@ -0,0 +1,69 @@
"""Главная страница и изолированные UI модулей."""
from unittest.mock import patch
from fastapi.testclient import TestClient
def test_root_html_includes_module_cards(client: TestClient) -> None:
r = client.get("/")
assert r.status_code == 200
body = r.text
assert "Модули" in body
assert "module-card" in body
assert "/ui/modules/schedules/" in body
assert "Календарь дежурств" in body
assert "app-rail" in body
assert "rail-nav" in body
def test_module_ui_page_schedules(client: TestClient) -> None:
r = client.get("/ui/modules/schedules/")
assert r.status_code == 200
assert "text/html" in r.headers.get("content-type", "")
assert "Календарь дежурств" in r.text
assert "app-rail" in r.text
assert 'aria-current="page"' in r.text
def test_rail_lists_all_registered_ui_modules(client: TestClient) -> None:
"""Правое меню синхронизировано с реестром: все модули с ui_router."""
r = client.get("/")
assert r.status_code == 200
t = r.text
expected = (
("schedules", "Календарь дежурств"),
("contacts", "Контакты"),
("statusboard", "Светофор"),
)
for slug, title in expected:
assert f"/ui/modules/{slug}/" in t
assert title in t
def test_each_module_page_single_active_nav_item(client: TestClient) -> None:
"""На странице модуля ровно один пункт с aria-current (текущий раздел)."""
for slug in ("schedules", "contacts", "statusboard"):
r = client.get(f"/ui/modules/{slug}/")
assert r.status_code == 200
assert r.text.count('aria-current="page"') == 1
def test_root_survives_broken_module_fragment(client: TestClient) -> None:
"""MODULE_MOUNTS держит ссылки на функции при импорте — ломаем фрагмент через обёртку safe_fragment."""
async def bad_fragment(_request):
raise RuntimeError("simulated module bug")
async def patched_safe_fragment(slug, fn, request):
from onguard24.modules import ui_support as us
if slug == "schedules":
return await us.safe_fragment(slug, bad_fragment, request)
return await us.safe_fragment(slug, fn, request)
with patch("onguard24.root_html.safe_fragment", new=patched_safe_fragment):
r = client.get("/")
assert r.status_code == 200
assert "module-err" in r.text
assert "schedules" in r.text