"""Tests for Anti-Fake-Evidence Phase 4a: Traceability Matrix. 6 tests covering: - Empty DB returns empty controls + zero summary - Nested structure: Control → Evidence → Assertions - Assertions appear under correct evidence - Coverage flags computed correctly - Control without evidence has correct coverage - Summary counts match """ from datetime import datetime from unittest.mock import MagicMock, patch from fastapi import FastAPI from fastapi.testclient import TestClient from compliance.api.dashboard_routes import router as dashboard_router from classroom_engine.database import get_db # --------------------------------------------------------------------------- # App setup with mocked DB dependency # --------------------------------------------------------------------------- app = FastAPI() app.include_router(dashboard_router) mock_db = MagicMock() def override_get_db(): yield mock_db app.dependency_overrides[get_db] = override_get_db client = TestClient(app) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def make_control(id="c1", control_id="CTRL-001", title="Test Control", status="pass", domain="gov"): ctrl = MagicMock() ctrl.id = id ctrl.control_id = control_id ctrl.title = title ctrl.status = MagicMock() ctrl.status.value = status ctrl.domain = MagicMock() ctrl.domain.value = domain return ctrl def make_evidence(id="e1", control_id="c1", title="Evidence 1", evidence_type="scan_report", confidence="E2", status="valid"): e = MagicMock() e.id = id e.control_id = control_id e.title = title e.evidence_type = evidence_type e.confidence_level = MagicMock() e.confidence_level.value = confidence e.status = MagicMock() e.status.value = status return e def make_assertion(id="a1", entity_id="e1", sentence_text="System encrypts data at rest.", assertion_type="assertion", confidence=0.85, verified_by=None): a = MagicMock() a.id = id a.entity_id = entity_id a.sentence_text = sentence_text a.assertion_type = assertion_type a.confidence = confidence a.verified_by = verified_by return a # =========================================================================== # Tests # =========================================================================== class TestTraceabilityMatrix: """Test GET /dashboard/traceability-matrix endpoint.""" @patch("compliance.api.dashboard_routes.EvidenceRepository") @patch("compliance.api.dashboard_routes.ControlRepository") def test_empty_db_returns_empty_matrix(self, mock_ctrl_cls, mock_ev_cls): """Empty DB should return zero controls and zero summary counts.""" mock_ctrl = MagicMock() mock_ctrl.get_all.return_value = [] mock_ctrl_cls.return_value = mock_ctrl mock_ev = MagicMock() mock_ev.get_all.return_value = [] mock_ev_cls.return_value = mock_ev # Mock db.query(AssertionDB).filter(...).all() mock_db.reset_mock() mock_query = MagicMock() mock_query.filter.return_value.all.return_value = [] mock_db.query.return_value = mock_query resp = client.get("/dashboard/traceability-matrix") assert resp.status_code == 200 data = resp.json() assert data["controls"] == [] assert data["summary"]["total_controls"] == 0 assert data["summary"]["covered_controls"] == 0 assert data["summary"]["fully_verified"] == 0 assert data["summary"]["uncovered_controls"] == 0 @patch("compliance.api.dashboard_routes.EvidenceRepository") @patch("compliance.api.dashboard_routes.ControlRepository") def test_nested_structure(self, mock_ctrl_cls, mock_ev_cls): """Control with evidence and assertions should return nested structure.""" ctrl = make_control(id="c1", control_id="PRIV-001", title="Privacy Control") ev = make_evidence(id="e1", control_id="c1", confidence="E3") assertion = make_assertion(id="a1", entity_id="e1", verified_by="auditor@example.com") mock_ctrl = MagicMock() mock_ctrl.get_all.return_value = [ctrl] mock_ctrl_cls.return_value = mock_ctrl mock_ev = MagicMock() mock_ev.get_all.return_value = [ev] mock_ev_cls.return_value = mock_ev mock_db.reset_mock() mock_query = MagicMock() mock_query.filter.return_value.all.return_value = [assertion] mock_db.query.return_value = mock_query resp = client.get("/dashboard/traceability-matrix") assert resp.status_code == 200 data = resp.json() assert len(data["controls"]) == 1 c = data["controls"][0] assert c["control_id"] == "PRIV-001" assert len(c["evidence"]) == 1 assert c["evidence"][0]["confidence_level"] == "E3" assert len(c["evidence"][0]["assertions"]) == 1 assert c["evidence"][0]["assertions"][0]["verified"] is True @patch("compliance.api.dashboard_routes.EvidenceRepository") @patch("compliance.api.dashboard_routes.ControlRepository") def test_assertions_grouped_under_correct_evidence(self, mock_ctrl_cls, mock_ev_cls): """Assertions should only appear under the evidence they reference.""" ctrl = make_control(id="c1") ev1 = make_evidence(id="e1", control_id="c1", title="Evidence A") ev2 = make_evidence(id="e2", control_id="c1", title="Evidence B") a1 = make_assertion(id="a1", entity_id="e1", sentence_text="Assertion for E1") a2 = make_assertion(id="a2", entity_id="e2", sentence_text="Assertion for E2") a3 = make_assertion(id="a3", entity_id="e2", sentence_text="Second assertion for E2") mock_ctrl = MagicMock() mock_ctrl.get_all.return_value = [ctrl] mock_ctrl_cls.return_value = mock_ctrl mock_ev = MagicMock() mock_ev.get_all.return_value = [ev1, ev2] mock_ev_cls.return_value = mock_ev mock_db.reset_mock() mock_query = MagicMock() mock_query.filter.return_value.all.return_value = [a1, a2, a3] mock_db.query.return_value = mock_query resp = client.get("/dashboard/traceability-matrix") assert resp.status_code == 200 data = resp.json() c = data["controls"][0] ev1_data = next(e for e in c["evidence"] if e["id"] == "e1") ev2_data = next(e for e in c["evidence"] if e["id"] == "e2") assert len(ev1_data["assertions"]) == 1 assert len(ev2_data["assertions"]) == 2 @patch("compliance.api.dashboard_routes.EvidenceRepository") @patch("compliance.api.dashboard_routes.ControlRepository") def test_coverage_flags_correct(self, mock_ctrl_cls, mock_ev_cls): """Coverage flags should reflect evidence, assertions, and verification state.""" ctrl = make_control(id="c1") ev = make_evidence(id="e1", control_id="c1", confidence="E2") # One verified, one not a1 = make_assertion(id="a1", entity_id="e1", verified_by="alice") a2 = make_assertion(id="a2", entity_id="e1", verified_by=None) mock_ctrl = MagicMock() mock_ctrl.get_all.return_value = [ctrl] mock_ctrl_cls.return_value = mock_ctrl mock_ev = MagicMock() mock_ev.get_all.return_value = [ev] mock_ev_cls.return_value = mock_ev mock_db.reset_mock() mock_query = MagicMock() mock_query.filter.return_value.all.return_value = [a1, a2] mock_db.query.return_value = mock_query resp = client.get("/dashboard/traceability-matrix") assert resp.status_code == 200 cov = resp.json()["controls"][0]["coverage"] assert cov["has_evidence"] is True assert cov["has_assertions"] is True assert cov["all_assertions_verified"] is False # a2 not verified assert cov["min_confidence_level"] == "E2" @patch("compliance.api.dashboard_routes.EvidenceRepository") @patch("compliance.api.dashboard_routes.ControlRepository") def test_coverage_without_evidence(self, mock_ctrl_cls, mock_ev_cls): """Control with no evidence should have all coverage flags False/None.""" ctrl = make_control(id="c1") mock_ctrl = MagicMock() mock_ctrl.get_all.return_value = [ctrl] mock_ctrl_cls.return_value = mock_ctrl mock_ev = MagicMock() mock_ev.get_all.return_value = [] mock_ev_cls.return_value = mock_ev mock_db.reset_mock() mock_query = MagicMock() mock_query.filter.return_value.all.return_value = [] mock_db.query.return_value = mock_query resp = client.get("/dashboard/traceability-matrix") assert resp.status_code == 200 cov = resp.json()["controls"][0]["coverage"] assert cov["has_evidence"] is False assert cov["has_assertions"] is False assert cov["all_assertions_verified"] is False assert cov["min_confidence_level"] is None @patch("compliance.api.dashboard_routes.EvidenceRepository") @patch("compliance.api.dashboard_routes.ControlRepository") def test_summary_counts(self, mock_ctrl_cls, mock_ev_cls): """Summary should count total, covered, fully verified, and uncovered controls.""" # c1: has evidence + verified assertions → fully verified # c2: has evidence but no assertions → covered, not fully verified # c3: no evidence → uncovered c1 = make_control(id="c1", control_id="C-001") c2 = make_control(id="c2", control_id="C-002") c3 = make_control(id="c3", control_id="C-003") ev1 = make_evidence(id="e1", control_id="c1", confidence="E3") ev2 = make_evidence(id="e2", control_id="c2", confidence="E1") a1 = make_assertion(id="a1", entity_id="e1", verified_by="auditor") mock_ctrl = MagicMock() mock_ctrl.get_all.return_value = [c1, c2, c3] mock_ctrl_cls.return_value = mock_ctrl mock_ev = MagicMock() mock_ev.get_all.return_value = [ev1, ev2] mock_ev_cls.return_value = mock_ev mock_db.reset_mock() mock_query = MagicMock() mock_query.filter.return_value.all.return_value = [a1] mock_db.query.return_value = mock_query resp = client.get("/dashboard/traceability-matrix") assert resp.status_code == 200 summary = resp.json()["summary"] assert summary["total_controls"] == 3 assert summary["covered_controls"] == 2 assert summary["fully_verified"] == 1 assert summary["uncovered_controls"] == 1