79 lines
3.5 KiB
Python
79 lines
3.5 KiB
Python
"""Static contract checks for the T-020 Bell alert vertical slice."""
|
||||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import json
|
|||
|
|
import re
|
|||
|
|
import unittest
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
|
|||
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|||
|
|
OPENAPI = ROOT / "docs" / "contracts" / "bell-alert-console-v1.openapi.json"
|
|||
|
|
WEB = ROOT / "Bell" / "web" / "assets"
|
|||
|
|
|
|||
|
|
|
|||
|
|
class BellAlertContractTests(unittest.TestCase):
|
|||
|
|
@classmethod
|
|||
|
|
def setUpClass(cls) -> None:
|
|||
|
|
cls.contract = json.loads(OPENAPI.read_text(encoding="utf-8"))
|
|||
|
|
cls.html = (WEB / "index.html").read_text(encoding="utf-8")
|
|||
|
|
cls.css = (WEB / "style.css").read_text(encoding="utf-8")
|
|||
|
|
cls.js = (WEB / "app.js").read_text(encoding="utf-8")
|
|||
|
|
|
|||
|
|
def test_loopback_routes_and_bounds_are_frozen(self) -> None:
|
|||
|
|
self.assertEqual("3.1.0", self.contract["openapi"])
|
|||
|
|
self.assertTrue(self.contract["servers"][0]["url"].startswith("http://127.0.0.1:"))
|
|||
|
|
paths = self.contract["paths"]
|
|||
|
|
self.assertEqual(
|
|||
|
|
{"/alerts", "/alerts/{alert_id}", "/alerts/{alert_id}:ack", "/alerts/{alert_id}:close"},
|
|||
|
|
set(paths),
|
|||
|
|
)
|
|||
|
|
limit = paths["/alerts"]["get"]["parameters"][1]["schema"]
|
|||
|
|
self.assertEqual({"type": "integer", "minimum": 1, "maximum": 100, "default": 16}, limit)
|
|||
|
|
key = self.contract["components"]["parameters"]["IdempotencyKey"]["schema"]
|
|||
|
|
self.assertEqual((8, 128), (key["minLength"], key["maxLength"]))
|
|||
|
|
|
|||
|
|
def test_alert_is_separate_from_event_and_missing_capabilities_are_explicit(self) -> None:
|
|||
|
|
detail = self.contract["components"]["schemas"]["AlertDetail"]
|
|||
|
|
encoded = json.dumps(detail, ensure_ascii=False)
|
|||
|
|
self.assertIn("events", encoded)
|
|||
|
|
self.assertIn("transitions", encoded)
|
|||
|
|
self.assertIn("evidence_status", encoded)
|
|||
|
|
self.assertIn("delivery_status", encoded)
|
|||
|
|
self.assertIn("证据切片尚未启用", self.html)
|
|||
|
|
self.assertIn("升级与通知尚未启用", self.html)
|
|||
|
|
self.assertNotRegex(self.html + self.js, r"短信已发送|语音已接通|倒计时[::]\s*\d")
|
|||
|
|
|
|||
|
|
def test_console_is_dependency_free_and_does_not_embed_secrets(self) -> None:
|
|||
|
|
combined = self.html + self.css + self.js
|
|||
|
|
self.assertNotRegex(combined, r"https?://|cdn\.|localStorage|sessionStorage")
|
|||
|
|
self.assertNotRegex(combined, r"postgres(?:ql)?://|rtsp://|BELL_DB_DSN")
|
|||
|
|
self.assertIn('type="password"', self.html)
|
|||
|
|
self.assertIn('Cache-Control", "no-store"', (ROOT / "Bell" / "web" / "web.go").read_text(encoding="utf-8"))
|
|||
|
|
|
|||
|
|
def test_accessibility_and_small_screen_contract(self) -> None:
|
|||
|
|
for marker in (
|
|||
|
|
'aria-live="polite"',
|
|||
|
|
'aria-live="assertive"',
|
|||
|
|
'aria-busy="false"',
|
|||
|
|
'aria-pressed="true"',
|
|||
|
|
'href="#alerts"',
|
|||
|
|
):
|
|||
|
|
self.assertIn(marker, self.html)
|
|||
|
|
self.assertIn("@media(max-width:480px)", self.css.replace(" ", ""))
|
|||
|
|
self.assertIn("prefers-reduced-motion:reduce", self.css.replace(" ", ""))
|
|||
|
|
self.assertIn("[hidden]{display:none!important}", self.css.replace(" ", ""))
|
|||
|
|
self.assertRegex(self.css, r"min-height:44px")
|
|||
|
|
self.assertIn("textContent", self.js)
|
|||
|
|
self.assertNotIn("innerHTML", self.js)
|
|||
|
|
|
|||
|
|
def test_console_has_loading_empty_retry_conflict_and_keyboard_paths(self) -> None:
|
|||
|
|
for marker in ("skeleton", "没有预警", "加载失败,重试", "already_acknowledged", "keydown"):
|
|||
|
|
self.assertIn(marker, self.js)
|
|||
|
|
self.assertRegex(self.js, re.escape('limit:"16"'))
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
unittest.main()
|