From 467dd3b3aaedede5f5534df030978dccba9e7ea4 Mon Sep 17 00:00:00 2001 From: mavis Date: Sun, 5 Jul 2026 21:21:54 +0000 Subject: [PATCH] test: add unit tests for serialize_notification_rule --- .../secuscan/routes_notification_helpers.py | 39 ++++++++++ .../unit/test_routes_notification_helpers.py | 74 +++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 backend/secuscan/routes_notification_helpers.py create mode 100644 testing/backend/unit/test_routes_notification_helpers.py diff --git a/backend/secuscan/routes_notification_helpers.py b/backend/secuscan/routes_notification_helpers.py new file mode 100644 index 00000000..ac6c0bb5 --- /dev/null +++ b/backend/secuscan/routes_notification_helpers.py @@ -0,0 +1,39 @@ +""" +Notification rule/history serialization helpers. + +Extracted from routes.py so they can be unit-tested without pulling in the +FastAPI / xhtml2pdf / reportlab import chain. + +Public API +---------- +serialize_notification_rule(row: Dict) -> Dict + Converts a database row dict into the API response shape for a notification + rule. Handles missing optional fields and coerces ``is_active`` to bool. +""" + +from __future__ import annotations + +from typing import Any, Dict + + +def serialize_notification_rule(row: Dict[str, Any]) -> Dict[str, Any]: + """Serialize a notification rule DB row into the API response format. + + Args: + row: A database row dict. Required keys: ``id``, ``name``, + ``severity_threshold``, ``channel_type``, ``target_url_or_email``, + ``is_active``. Optional keys: ``created_at``, ``updated_at``. + + Returns: + A dict with the API response shape. + """ + return { + "id": row["id"], + "name": row["name"], + "severity_threshold": row["severity_threshold"], + "channel_type": row["channel_type"], + "target_url_or_email": row["target_url_or_email"], + "is_active": bool(row.get("is_active")), + "created_at": row.get("created_at"), + "updated_at": row.get("updated_at"), + } diff --git a/testing/backend/unit/test_routes_notification_helpers.py b/testing/backend/unit/test_routes_notification_helpers.py new file mode 100644 index 00000000..539ad26c --- /dev/null +++ b/testing/backend/unit/test_routes_notification_helpers.py @@ -0,0 +1,74 @@ +""" +Unit tests for serialize_notification_rule in +backend.secuscan.routes_notification_helpers. +""" + +import pytest +from backend.secuscan.routes_notification_helpers import serialize_notification_rule + + +class TestSerializeNotificationRule: + def test_all_fields_present(self): + row = { + "id": 42, + "name": "Discord Alert", + "severity_threshold": 7, + "channel_type": "webhook", + "target_url_or_email": "https://discord.com/api/webhooks/1/abc", + "is_active": 1, + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-02T00:00:00Z", + } + result = serialize_notification_rule(row) + assert result["id"] == 42 + assert result["name"] == "Discord Alert" + assert result["severity_threshold"] == 7 + assert result["channel_type"] == "webhook" + assert result["target_url_or_email"] == "https://discord.com/api/webhooks/1/abc" + assert result["is_active"] is True + assert result["created_at"] == "2026-01-01T00:00:00Z" + assert result["updated_at"] == "2026-01-02T00:00:00Z" + + def test_is_active_true_coerced_from_truthy_value(self): + for val in [1, True, "yes", "1"]: + row = {"id": 1, "name": "R", "severity_threshold": 5, + "channel_type": "email", "target_url_or_email": "a@b.com", + "is_active": val} + result = serialize_notification_rule(row) + assert result["is_active"] is True, f"is_active={val!r}" + + def test_is_active_false_coerced_from_falsy_value(self): + for val in [0, False, None, ""]: + row = {"id": 1, "name": "R", "severity_threshold": 5, + "channel_type": "email", "target_url_or_email": "a@b.com", + "is_active": val} + result = serialize_notification_rule(row) + assert result["is_active"] is False, f"is_active={val!r}" + + def test_missing_created_at_yields_none(self): + row = {"id": 1, "name": "R", "severity_threshold": 5, + "channel_type": "email", "target_url_or_email": "a@b.com", + "is_active": True} + result = serialize_notification_rule(row) + assert result["created_at"] is None + + def test_missing_updated_at_yields_none(self): + row = {"id": 1, "name": "R", "severity_threshold": 5, + "channel_type": "email", "target_url_or_email": "a@b.com", + "is_active": True} + result = serialize_notification_rule(row) + assert result["updated_at"] is None + + def test_email_channel_preserved(self): + row = {"id": 3, "name": "Email Report", "severity_threshold": 3, + "channel_type": "email", "target_url_or_email": "ops@corp.com", + "is_active": 1} + result = serialize_notification_rule(row) + assert result["channel_type"] == "email" + + def test_severity_threshold_unchanged(self): + row = {"id": 4, "name": "T", "severity_threshold": 0, + "channel_type": "webhook", "target_url_or_email": "https://x.com", + "is_active": False} + result = serialize_notification_rule(row) + assert result["severity_threshold"] == 0