from __future__ import annotations import json import tempfile import unittest from datetime import datetime, timezone from pathlib import Path from cold_display_guard.webhooks import ( build_batch_event_payload, build_case_event_payload, load_webhook_settings, send_batch_event_webhooks, send_case_webhooks, ) UTC = timezone.utc class WebhookTests(unittest.TestCase): def test_load_webhook_settings_from_config(self) -> None: settings = load_webhook_settings( { "webhooks": { "enabled": True, "event_url": "https://example.com/events", "case_url": "https://example.com/cases", "callback_token": "secret", "connect_timeout_seconds": 4, "read_timeout_seconds": 6, } } ) self.assertTrue(settings.enabled) self.assertEqual(settings.event_url, "https://example.com/events") self.assertEqual(settings.case_url, "https://example.com/cases") self.assertEqual(settings.callback_token, "secret") self.assertEqual(settings.connect_timeout_seconds, 4) self.assertEqual(settings.read_timeout_seconds, 6) def test_build_batch_event_payload_wraps_runtime_event(self) -> None: payload = build_batch_event_payload( { "event": "time_alarm", "ts": datetime(2026, 6, 9, 9, 0, tzinfo=UTC).isoformat(), "batch_id": "batch_000001", "camera_id": "cam_01", "zone_id": "1", "zone_label": "区域 1", "severity": "alarm", "state": "alerted", } ) self.assertEqual(payload["kind"], "batch_event") self.assertEqual(payload["event"], "time_alarm") self.assertEqual(payload["zone_label"], "区域 1") def test_build_case_event_payload_wraps_case_snapshot(self) -> None: payload = build_case_event_payload( { "case_id": "case_batch_000001", "case_type": "warning_escalated", "case_status": "open", "batch_id": "batch_000001", "source_event": "warning_escalated", "handled_source": "", "updated_at": datetime(2026, 6, 9, 9, 5, tzinfo=UTC).isoformat(), } ) self.assertEqual(payload["kind"], "case_event") self.assertEqual(payload["action"], "updated") self.assertEqual(payload["case_id"], "case_batch_000001") def test_send_batch_event_webhooks_delivers_payload(self) -> None: deliveries: list[tuple[str, dict[str, object], tuple[float, float]]] = [] def fake_post(url: str, payload: dict[str, object], timeout: tuple[float, float]) -> tuple[int, str]: deliveries.append((url, payload, timeout)) return 202, "ok" with tempfile.TemporaryDirectory() as tmpdir: audit_path = Path(tmpdir) / "webhook_delivery.jsonl" send_batch_event_webhooks( [ { "event": "time_alarm", "ts": datetime(2026, 6, 9, 9, 0, tzinfo=UTC).isoformat(), "batch_id": "batch_000001", "camera_id": "cam_01", "zone_id": "1", "zone_label": "区域 1", "severity": "alarm", "state": "alerted", } ], { "webhooks": { "enabled": True, "event_url": "https://example.com/events", "connect_timeout_seconds": 4, "read_timeout_seconds": 6, } }, audit_path, http_post=fake_post, ) self.assertEqual(deliveries[0][0], "https://example.com/events") self.assertEqual(deliveries[0][1]["kind"], "batch_event") self.assertEqual(deliveries[0][2], (4.0, 6.0)) def test_send_case_webhooks_delivers_payload(self) -> None: deliveries: list[tuple[str, dict[str, object]]] = [] def fake_post(url: str, payload: dict[str, object], timeout: tuple[float, float]) -> tuple[int, str]: deliveries.append((url, payload)) return 200, "ok" with tempfile.TemporaryDirectory() as tmpdir: audit_path = Path(tmpdir) / "webhook_delivery.jsonl" send_case_webhooks( [ { "case_id": "case_batch_000001", "case_type": "time_alarm", "case_status": "handled", "batch_id": "batch_000001", "source_event": "time_alarm", "handled_source": "manual", "updated_at": datetime(2026, 6, 9, 9, 10, tzinfo=UTC).isoformat(), } ], { "webhooks": { "enabled": True, "case_url": "https://example.com/cases", } }, audit_path, http_post=fake_post, ) self.assertEqual(deliveries[0][0], "https://example.com/cases") self.assertEqual(deliveries[0][1]["kind"], "case_event") self.assertEqual(deliveries[0][1]["action"], "handled") def test_failed_delivery_is_logged_without_raising(self) -> None: def fake_post(url: str, payload: dict[str, object], timeout: tuple[float, float]) -> tuple[int, str]: raise OSError("network down") with tempfile.TemporaryDirectory() as tmpdir: audit_path = Path(tmpdir) / "webhook_delivery.jsonl" send_batch_event_webhooks( [ { "event": "time_alarm", "ts": datetime(2026, 6, 9, 9, 0, tzinfo=UTC).isoformat(), "batch_id": "batch_000001", "camera_id": "cam_01", "zone_id": "1", "zone_label": "区域 1", "severity": "alarm", "state": "alerted", } ], { "webhooks": { "enabled": True, "event_url": "https://example.com/events", } }, audit_path, http_post=fake_post, ) logged = [json.loads(line) for line in audit_path.read_text(encoding="utf-8").splitlines()] self.assertEqual(logged[0]["status"], "error") self.assertEqual(logged[0]["target"], "batch_event") self.assertIn("network down", logged[0]["message"]) if __name__ == "__main__": unittest.main()