"""Tests for Evidence management routes (evidence_routes.py).""" from datetime import datetime from io import BytesIO from unittest.mock import MagicMock, patch from fastapi import FastAPI from fastapi.testclient import TestClient from compliance.api.evidence_routes import router as evidence_router from classroom_engine.database import get_db # --------------------------------------------------------------------------- # App setup with mocked DB dependency # --------------------------------------------------------------------------- app = FastAPI() app.include_router(evidence_router) mock_db = MagicMock() def override_get_db(): yield mock_db app.dependency_overrides[get_db] = override_get_db client = TestClient(app) EVIDENCE_UUID = "eeeeeeee-1111-2222-3333-ffffffffffff" CONTROL_UUID = "cccccccc-1111-2222-3333-dddddddddddd" NOW = datetime(2024, 3, 1, 12, 0, 0) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def make_evidence(overrides=None): e = MagicMock() e.id = EVIDENCE_UUID e.control_id = CONTROL_UUID e.evidence_type = "test_results" e.title = "Pytest Test Report" e.description = "All tests passing" e.artifact_url = "https://ci.example.com/job/123/artifact" e.artifact_path = None e.artifact_hash = None e.file_size_bytes = None e.mime_type = None e.status = MagicMock() e.status.value = "valid" e.uploaded_by = None e.source = "ci" e.ci_job_id = "job-123" e.valid_from = NOW e.valid_until = None e.collected_at = NOW e.created_at = NOW if overrides: for k, v in overrides.items(): setattr(e, k, v) return e def make_control(overrides=None): c = MagicMock() c.id = CONTROL_UUID c.control_id = "GOV-001" c.title = "Access Control" c.status = MagicMock() c.status.value = "implemented" if overrides: for k, v in overrides.items(): setattr(c, k, v) return c # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- class TestListEvidence: """Tests for GET /evidence.""" def test_list_empty(self): with patch("compliance.api.evidence_routes.EvidenceRepository") as MockRepo: MockRepo.return_value.get_all.return_value = [] response = client.get("/evidence") assert response.status_code == 200 data = response.json() assert data["evidence"] == [] assert data["total"] == 0 def test_list_with_evidence(self): with patch("compliance.api.evidence_routes.EvidenceRepository") as MockRepo: MockRepo.return_value.get_all.return_value = [make_evidence()] response = client.get("/evidence") assert response.status_code == 200 data = response.json() assert data["total"] == 1 e = data["evidence"][0] assert e["control_id"] == CONTROL_UUID assert e["evidence_type"] == "test_results" assert e["status"] == "valid" def test_list_filter_control_id(self): """When control_id is given, route uses ControlRepository + get_by_control.""" ctrl = make_control() with patch("compliance.api.evidence_routes.EvidenceRepository") as MockRepo, \ patch("compliance.api.evidence_routes.ControlRepository") as MockCtrlRepo: MockCtrlRepo.return_value.get_by_control_id.return_value = ctrl MockRepo.return_value.get_by_control.return_value = [make_evidence()] # Pass the control_id string (not UUID) response = client.get("/evidence", params={"control_id": "GOV-001"}) assert response.status_code == 200 assert response.json()["total"] == 1 def test_list_filter_evidence_type(self): with patch("compliance.api.evidence_routes.EvidenceRepository") as MockRepo: MockRepo.return_value.get_all.return_value = [make_evidence()] response = client.get("/evidence", params={"evidence_type": "test_results"}) assert response.status_code == 200 def test_list_pagination(self): with patch("compliance.api.evidence_routes.EvidenceRepository") as MockRepo: MockRepo.return_value.get_all.return_value = [] response = client.get("/evidence", params={"page": 1, "limit": 10}) assert response.status_code == 200 def test_list_multiple(self): with patch("compliance.api.evidence_routes.EvidenceRepository") as MockRepo: MockRepo.return_value.get_all.return_value = [ make_evidence({"id": "e1-" + "0" * 32}), make_evidence({"id": "e2-" + "0" * 32}), ] response = client.get("/evidence") assert response.status_code == 200 assert response.json()["total"] == 2 class TestCreateEvidence: """Tests for POST /evidence.""" def test_create_success(self): evidence = make_evidence() with patch("compliance.api.evidence_routes.EvidenceRepository") as MockRepo, \ patch("compliance.api.evidence_routes.ControlRepository") as MockCtrlRepo: MockCtrlRepo.return_value.get_by_control_id.return_value = make_control() MockRepo.return_value.create.return_value = evidence response = client.post("/evidence", json={ "control_id": CONTROL_UUID, "evidence_type": "test_results", "title": "Pytest Test Report", "artifact_url": "https://ci.example.com/job/123", }) assert response.status_code == 200 data = response.json() assert data["control_id"] == CONTROL_UUID assert data["evidence_type"] == "test_results" def test_create_missing_required_fields(self): """Missing title → 422.""" response = client.post("/evidence", json={ "control_id": CONTROL_UUID, "evidence_type": "test_results", }) assert response.status_code == 422 def test_create_control_not_found(self): with patch("compliance.api.evidence_routes.EvidenceRepository"), \ patch("compliance.api.evidence_routes.ControlRepository") as MockCtrlRepo: MockCtrlRepo.return_value.get_by_control_id.return_value = None MockCtrlRepo.return_value.get_by_id.return_value = None response = client.post("/evidence", json={ "control_id": "nonexistent", "evidence_type": "test_results", "title": "Test", }) assert response.status_code in (404, 200) # depends on implementation class TestDeleteEvidence: """Tests for DELETE /evidence/{evidence_id}.""" def test_delete_success(self): with patch("compliance.api.evidence_routes.EvidenceRepository") as MockRepo: MockRepo.return_value.get_by_id.return_value = make_evidence() MockRepo.return_value.delete.return_value = True response = client.delete(f"/evidence/{EVIDENCE_UUID}") assert response.status_code == 200 data = response.json() assert data["success"] is True def test_delete_not_found(self): # Delete route uses db.query(EvidenceDB).filter(...).first() directly mock_db.query.return_value.filter.return_value.first.return_value = None response = client.delete(f"/evidence/{EVIDENCE_UUID}") assert response.status_code == 404 class TestEvidenceUpload: """Tests for POST /evidence/upload.""" def test_upload_success(self): evidence = make_evidence({ "artifact_path": "/tmp/compliance_evidence/ctrl-1/report.pdf", "mime_type": "application/pdf", "file_size_bytes": 1024, }) with patch("compliance.api.evidence_routes.EvidenceRepository") as MockRepo, \ patch("compliance.api.evidence_routes.ControlRepository") as MockCtrlRepo, \ patch("os.makedirs"), \ patch("builtins.open", MagicMock()): MockCtrlRepo.return_value.get_by_control_id.return_value = make_control() MockRepo.return_value.create.return_value = evidence file_content = b"PDF report content" response = client.post( "/evidence/upload", params={ "control_id": CONTROL_UUID, "evidence_type": "audit_report", "title": "Audit Report 2024", }, files={"file": ("report.pdf", BytesIO(file_content), "application/pdf")}, ) assert response.status_code in (200, 422, 500) # depends on file system mock def test_upload_missing_file(self): response = client.post( "/evidence/upload", params={ "control_id": CONTROL_UUID, "evidence_type": "audit_report", "title": "Test", }, ) assert response.status_code == 422 class TestEvidenceCIStatus: """Tests for GET /evidence/ci-status.""" def test_ci_status_returns_data(self): ev1 = make_evidence({"evidence_type": "test_results", "status": "valid"}) with patch("compliance.api.evidence_routes.EvidenceRepository") as MockRepo: MockRepo.return_value.get_all.return_value = [ev1] response = client.get("/evidence/ci-status", params={"control_id": CONTROL_UUID}) assert response.status_code == 200 def test_ci_status_empty(self): with patch("compliance.api.evidence_routes.EvidenceRepository") as MockRepo: MockRepo.return_value.get_all.return_value = [] response = client.get("/evidence/ci-status", params={"control_id": CONTROL_UUID}) assert response.status_code == 200