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
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>
210 lines
7.7 KiB
Python
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
|