feat: add webhook case management
This commit is contained in:
187
tests/test_webhooks.py
Normal file
187
tests/test_webhooks.py
Normal file
@@ -0,0 +1,187 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user