Files
breakpilot-compliance/backend-compliance/tests/test_dashboard_routes_extended.py
Benjamin Admin 49ce417428
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 32s
CI/CD / test-python-backend-compliance (push) Successful in 34s
CI/CD / test-python-document-crawler (push) Successful in 23s
CI/CD / test-python-dsms-gateway (push) Successful in 21s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Successful in 2s
feat: add compliance modules 2-5 (dashboard, security templates, process manager, evidence collector)
Module 2: Extended Compliance Dashboard with roadmap, module-status, next-actions, snapshots, score-history
Module 3: 7 German security document templates (IT-Sicherheitskonzept, Datenschutz, Backup, Logging, Incident-Response, Zugriff, Risikomanagement)
Module 4: Compliance Process Manager with CRUD, complete/skip/seed, ~50 seed tasks, 3-tab UI
Module 5: Evidence Collector Extended with automated checks, control-mapping, coverage report, 4-tab UI

Also includes: canonical control library enhancements (verification method, categories, dedup), control generator improvements, RAG client extensions

52 tests pass, frontend builds clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 21:03:04 +01:00

210 lines
7.7 KiB
Python

"""Tests for extended dashboard routes (roadmap, module-status, next-actions, snapshots)."""
import pytest
from unittest.mock import MagicMock, patch
from fastapi.testclient import TestClient
from fastapi import FastAPI
from datetime import datetime, date, timedelta
from decimal import Decimal
from compliance.api.dashboard_routes import router
from classroom_engine.database import get_db
from compliance.api.tenant_utils import get_tenant_id
DEFAULT_TENANT_ID = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e"
# =============================================================================
# Test App Setup
# =============================================================================
app = FastAPI()
app.include_router(router)
mock_db = MagicMock()
def override_get_db():
yield mock_db
def override_tenant():
return DEFAULT_TENANT_ID
app.dependency_overrides[get_db] = override_get_db
app.dependency_overrides[get_tenant_id] = override_tenant
client = TestClient(app)
# =============================================================================
# Helpers
# =============================================================================
class MockControl:
def __init__(self, id="ctrl-001", control_id="CTRL-001", title="Test Control",
status_val="planned", domain_val="gov", owner="ISB",
next_review_at=None, category="security"):
self.id = id
self.control_id = control_id
self.title = title
self.status = MagicMock(value=status_val)
self.domain = MagicMock(value=domain_val)
self.owner = owner
self.next_review_at = next_review_at
self.category = category
class MockRisk:
def __init__(self, inherent_risk_val="high", status="open"):
self.inherent_risk = MagicMock(value=inherent_risk_val)
self.status = status
def make_snapshot_row(overrides=None):
data = {
"id": "snap-001",
"tenant_id": DEFAULT_TENANT_ID,
"project_id": None,
"score": Decimal("72.50"),
"controls_total": 20,
"controls_pass": 12,
"controls_partial": 5,
"evidence_total": 10,
"evidence_valid": 8,
"risks_total": 5,
"risks_high": 2,
"snapshot_date": date(2026, 3, 14),
"created_at": datetime(2026, 3, 14),
}
if overrides:
data.update(overrides)
row = MagicMock()
row._mapping = data
return row
# =============================================================================
# Tests
# =============================================================================
class TestDashboardRoadmap:
def test_roadmap_returns_buckets(self):
"""Roadmap returns 4 buckets with controls."""
overdue = datetime.utcnow() - timedelta(days=10)
future = datetime.utcnow() + timedelta(days=30)
with patch("compliance.api.dashboard_routes.ControlRepository") as MockCtrlRepo:
instance = MockCtrlRepo.return_value
instance.get_all.return_value = [
MockControl(id="c1", status_val="planned", category="legal", next_review_at=overdue),
MockControl(id="c2", status_val="partial", category="security"),
MockControl(id="c3", status_val="planned", category="best_practice"),
MockControl(id="c4", status_val="pass"), # should be excluded
]
resp = client.get("/dashboard/roadmap")
assert resp.status_code == 200
data = resp.json()
assert "buckets" in data
assert "counts" in data
# c4 is pass, so excluded; c1 is legal+overdue → quick_wins
total_in_buckets = sum(data["counts"].values())
assert total_in_buckets == 3
def test_roadmap_empty_controls(self):
"""Roadmap with no controls returns empty buckets."""
with patch("compliance.api.dashboard_routes.ControlRepository") as MockCtrlRepo:
MockCtrlRepo.return_value.get_all.return_value = []
resp = client.get("/dashboard/roadmap")
assert resp.status_code == 200
assert all(v == 0 for v in resp.json()["counts"].values())
class TestModuleStatus:
def test_module_status_returns_modules(self):
"""Module status returns list of modules with counts."""
# Mock db.execute for each module's COUNT query
count_result = MagicMock()
count_result.fetchone.return_value = (5,)
mock_db.execute.return_value = count_result
resp = client.get("/dashboard/module-status")
assert resp.status_code == 200
data = resp.json()
assert "modules" in data
assert data["total"] > 0
assert all(m["count"] == 5 for m in data["modules"])
def test_module_status_handles_missing_tables(self):
"""Module status handles missing tables gracefully."""
mock_db.execute.side_effect = Exception("relation does not exist")
resp = client.get("/dashboard/module-status")
assert resp.status_code == 200
data = resp.json()
# All modules should have count=0 and status=not_started
assert all(m["count"] == 0 for m in data["modules"])
assert all(m["status"] == "not_started" for m in data["modules"])
mock_db.execute.side_effect = None # reset
class TestNextActions:
def test_next_actions_returns_sorted(self):
"""Next actions returns controls sorted by urgency."""
overdue = datetime.utcnow() - timedelta(days=30)
with patch("compliance.api.dashboard_routes.ControlRepository") as MockCtrlRepo:
instance = MockCtrlRepo.return_value
instance.get_all.return_value = [
MockControl(id="c1", status_val="planned", category="legal", next_review_at=overdue),
MockControl(id="c2", status_val="partial", category="best_practice"),
MockControl(id="c3", status_val="pass"), # excluded
]
resp = client.get("/dashboard/next-actions?limit=5")
assert resp.status_code == 200
data = resp.json()
assert len(data["actions"]) == 2
# c1 should be first (higher urgency due to legal + overdue)
assert data["actions"][0]["control_id"] == "CTRL-001"
class TestScoreSnapshot:
def test_create_snapshot(self):
"""Creating a snapshot saves current score."""
with patch("compliance.api.dashboard_routes.ControlRepository") as MockCtrlRepo, \
patch("compliance.api.dashboard_routes.EvidenceRepository") as MockEvRepo, \
patch("compliance.api.dashboard_routes.RiskRepository") as MockRiskRepo:
MockCtrlRepo.return_value.get_statistics.return_value = {
"total": 20, "pass": 12, "partial": 5, "by_status": {}
}
MockEvRepo.return_value.get_statistics.return_value = {
"total": 10, "by_status": {"valid": 8}
}
MockRiskRepo.return_value.get_all.return_value = [
MockRisk("high"), MockRisk("critical"), MockRisk("low")
]
snap_row = make_snapshot_row()
mock_db.execute.return_value.fetchone.return_value = snap_row
resp = client.post("/dashboard/snapshot")
assert resp.status_code == 200
data = resp.json()
assert "score" in data
def test_score_history(self):
"""Score history returns snapshots."""
rows = [make_snapshot_row({"snapshot_date": date(2026, 3, i)}) for i in range(1, 4)]
mock_db.execute.return_value.fetchall.return_value = rows
resp = client.get("/dashboard/score-history?months=3")
assert resp.status_code == 200
data = resp.json()
assert data["total"] == 3
assert len(data["snapshots"]) == 3