"""Tests for Anti-Fake-Evidence Phase 3: Enforcement. ~8 tests covering: - Evidence distribution endpoint (confidence counts, four-eyes pending) - Dashboard multi-score presence """ from datetime import datetime, timedelta from unittest.mock import MagicMock, patch, PropertyMock from fastapi import FastAPI from fastapi.testclient import TestClient from compliance.api.dashboard_routes import router as dashboard_router from compliance.db.models import EvidenceConfidenceEnum, EvidenceTruthStatusEnum 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) NOW = datetime(2026, 3, 23, 14, 0, 0) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def make_evidence(confidence="E1", requires_four_eyes=False, approval_status="none"): e = MagicMock() e.confidence_level = MagicMock() e.confidence_level.value = confidence e.requires_four_eyes = requires_four_eyes e.approval_status = approval_status return e # =========================================================================== # 1. TestEvidenceDistributionEndpoint # =========================================================================== class TestEvidenceDistributionEndpoint: """Test GET /dashboard/evidence-distribution endpoint.""" def _setup_evidence(self, evidence_list): """Configure mock DB to return evidence list via EvidenceRepository.""" mock_db.reset_mock() # EvidenceRepository(db).get_all() internally does db.query(...).all() # We patch the EvidenceRepository class to return our list return evidence_list @patch("compliance.api.dashboard_routes.EvidenceRepository") def test_empty_db_returns_zero_counts(self, mock_repo_cls): mock_repo = MagicMock() mock_repo.get_all.return_value = [] mock_repo_cls.return_value = mock_repo resp = client.get("/dashboard/evidence-distribution") assert resp.status_code == 200 data = resp.json() assert data["total"] == 0 assert data["four_eyes_pending"] == 0 assert data["by_confidence"] == {"E0": 0, "E1": 0, "E2": 0, "E3": 0, "E4": 0} @patch("compliance.api.dashboard_routes.EvidenceRepository") def test_counts_by_confidence_level(self, mock_repo_cls): evidence = [ make_evidence("E0"), make_evidence("E1"), make_evidence("E1"), make_evidence("E2"), make_evidence("E3"), make_evidence("E3"), make_evidence("E3"), make_evidence("E4"), ] mock_repo = MagicMock() mock_repo.get_all.return_value = evidence mock_repo_cls.return_value = mock_repo resp = client.get("/dashboard/evidence-distribution") assert resp.status_code == 200 data = resp.json() assert data["total"] == 8 assert data["by_confidence"]["E0"] == 1 assert data["by_confidence"]["E1"] == 2 assert data["by_confidence"]["E2"] == 1 assert data["by_confidence"]["E3"] == 3 assert data["by_confidence"]["E4"] == 1 @patch("compliance.api.dashboard_routes.EvidenceRepository") def test_four_eyes_pending_count(self, mock_repo_cls): evidence = [ make_evidence("E1", requires_four_eyes=True, approval_status="pending_first"), make_evidence("E2", requires_four_eyes=True, approval_status="first_approved"), make_evidence("E2", requires_four_eyes=True, approval_status="approved"), make_evidence("E1", requires_four_eyes=True, approval_status="rejected"), make_evidence("E1", requires_four_eyes=False, approval_status="none"), ] mock_repo = MagicMock() mock_repo.get_all.return_value = evidence mock_repo_cls.return_value = mock_repo resp = client.get("/dashboard/evidence-distribution") assert resp.status_code == 200 data = resp.json() # pending_first and first_approved are pending; approved and rejected are not assert data["four_eyes_pending"] == 2 assert data["total"] == 5 @patch("compliance.api.dashboard_routes.EvidenceRepository") def test_null_confidence_defaults_to_e1(self, mock_repo_cls): e = MagicMock() e.confidence_level = None e.requires_four_eyes = False e.approval_status = "none" mock_repo = MagicMock() mock_repo.get_all.return_value = [e] mock_repo_cls.return_value = mock_repo resp = client.get("/dashboard/evidence-distribution") assert resp.status_code == 200 data = resp.json() assert data["by_confidence"]["E1"] == 1 assert data["total"] == 1 @patch("compliance.api.dashboard_routes.EvidenceRepository") def test_all_four_eyes_approved_zero_pending(self, mock_repo_cls): evidence = [ make_evidence("E2", requires_four_eyes=True, approval_status="approved"), make_evidence("E3", requires_four_eyes=True, approval_status="approved"), ] mock_repo = MagicMock() mock_repo.get_all.return_value = evidence mock_repo_cls.return_value = mock_repo resp = client.get("/dashboard/evidence-distribution") assert resp.status_code == 200 data = resp.json() assert data["four_eyes_pending"] == 0 # =========================================================================== # 2. TestDashboardMultiScore # =========================================================================== class TestDashboardMultiScore: """Test that dashboard response includes multi_score.""" def test_dashboard_response_schema_includes_multi_score(self): """DashboardResponse schema must include the multi_score field.""" from compliance.api.schemas import DashboardResponse fields = DashboardResponse.model_fields assert "multi_score" in fields, "DashboardResponse must have multi_score field" def test_multi_score_schema_has_required_fields(self): """MultiDimensionalScore schema should have all 7 fields.""" from compliance.api.schemas import MultiDimensionalScore fields = MultiDimensionalScore.model_fields required = [ "requirement_coverage", "evidence_strength", "validation_quality", "evidence_freshness", "control_effectiveness", "overall_readiness", "hard_blocks", ] for field in required: assert field in fields, f"Missing field: {field}" def test_multi_score_default_values(self): """MultiDimensionalScore defaults should be sensible.""" from compliance.api.schemas import MultiDimensionalScore score = MultiDimensionalScore() assert score.overall_readiness == 0.0 assert score.hard_blocks == [] assert score.requirement_coverage == 0.0