Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions backend/secuscan/routes_notification_helpers.py
Original file line number Diff line number Diff line change
@@ -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"),
}
74 changes: 74 additions & 0 deletions testing/backend/unit/test_routes_notification_helpers.py
Original file line number Diff line number Diff line change
@@ -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
Loading