"""Tests for Controls routes (routes.py → /compliance/controls). Control status enum values: "pass", "partial", "fail", "n/a", "planned" Control domain enum values: "gov", "priv", "iam", "crypto", "sdlc", "ops", "ai", "cra", "aud" """ from datetime import datetime from unittest.mock import MagicMock, patch from fastapi import FastAPI from fastapi.testclient import TestClient from compliance.api.routes import router as compliance_router from classroom_engine.database import get_db # --------------------------------------------------------------------------- # App setup with mocked DB dependency # --------------------------------------------------------------------------- app = FastAPI() app.include_router(compliance_router) mock_db = MagicMock() def override_get_db(): yield mock_db app.dependency_overrides[get_db] = override_get_db client = TestClient(app) CONTROL_UUID = "cccccccc-4444-5555-6666-cccccccccccc" NOW = datetime(2024, 3, 1, 12, 0, 0) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def make_control(overrides=None): c = MagicMock() c.id = CONTROL_UUID c.control_id = "GOV-001" # domain and control_type are Enum → need .value c.domain = MagicMock() c.domain.value = "gov" c.control_type = MagicMock() c.control_type.value = "preventive" c.title = "Datenschutzbeauftragter bestellt" c.description = "DSB nach Art. 37 DSGVO" c.pass_criteria = "DSB bestellt und dokumentiert" c.implementation_guidance = "Ernennung per Urkunde" c.code_reference = None c.documentation_url = None c.is_automated = False c.automation_tool = None c.automation_config = None c.owner = "GF" c.review_frequency_days = 365 # status is Enum → need .value c.status = MagicMock() c.status.value = "planned" c.status_notes = None c.last_reviewed_at = None c.next_review_at = None c.created_at = NOW c.updated_at = NOW c.evidence_count = 0 c.requirement_count = 1 if overrides: for k, v in overrides.items(): setattr(c, k, v) return c # --------------------------------------------------------------------------- # Tests: GET /compliance/controls # --------------------------------------------------------------------------- class TestListControls: """Tests for GET /compliance/controls.""" def test_list_empty(self): with patch("compliance.api.routes.ControlRepository") as MockRepo, \ patch("compliance.api.routes.EvidenceRepository") as MockEvRepo: MockRepo.return_value.get_all.return_value = [] MockEvRepo.return_value.get_by_control.return_value = [] response = client.get("/compliance/controls") assert response.status_code == 200 data = response.json() assert data["controls"] == [] assert data["total"] == 0 def test_list_with_control(self): with patch("compliance.api.routes.ControlRepository") as MockRepo, \ patch("compliance.api.routes.EvidenceRepository") as MockEvRepo: MockRepo.return_value.get_all.return_value = [make_control()] MockEvRepo.return_value.get_by_control.return_value = [] response = client.get("/compliance/controls") assert response.status_code == 200 data = response.json() assert data["total"] == 1 c = data["controls"][0] assert c["control_id"] == "GOV-001" assert c["domain"] == "gov" assert c["is_automated"] is False assert c["status"] == "planned" def test_list_filter_domain(self): with patch("compliance.api.routes.ControlRepository") as MockRepo, \ patch("compliance.api.routes.EvidenceRepository") as MockEvRepo: ctrl = make_control() ctrl.domain.value = "priv" MockRepo.return_value.get_all.return_value = [ctrl] MockEvRepo.return_value.get_by_control.return_value = [] response = client.get("/compliance/controls", params={"domain": "priv"}) assert response.status_code == 200 def test_list_filter_status(self): with patch("compliance.api.routes.ControlRepository") as MockRepo, \ patch("compliance.api.routes.EvidenceRepository") as MockEvRepo: MockRepo.return_value.get_all.return_value = [] MockEvRepo.return_value.get_by_control.return_value = [] response = client.get("/compliance/controls", params={"status": "pass"}) assert response.status_code == 200 def test_list_filter_is_automated(self): with patch("compliance.api.routes.ControlRepository") as MockRepo, \ patch("compliance.api.routes.EvidenceRepository") as MockEvRepo: ctrl = make_control({"is_automated": True}) MockRepo.return_value.get_all.return_value = [ctrl] MockEvRepo.return_value.get_by_control.return_value = [] response = client.get("/compliance/controls", params={"is_automated": "true"}) assert response.status_code == 200 def test_list_filter_search(self): with patch("compliance.api.routes.ControlRepository") as MockRepo, \ patch("compliance.api.routes.EvidenceRepository") as MockEvRepo: MockRepo.return_value.get_all.return_value = [] MockEvRepo.return_value.get_by_control.return_value = [] response = client.get("/compliance/controls", params={"search": "Datenschutz"}) assert response.status_code == 200 def test_list_multiple(self): c2 = make_control() c2.id = "dddddddd-1111-2222-3333-dddddddddddd" c2.control_id = "PRIV-001" c2.domain.value = "priv" with patch("compliance.api.routes.ControlRepository") as MockRepo, \ patch("compliance.api.routes.EvidenceRepository") as MockEvRepo: MockRepo.return_value.get_all.return_value = [make_control(), c2] MockEvRepo.return_value.get_by_control.return_value = [] response = client.get("/compliance/controls") assert response.status_code == 200 assert response.json()["total"] == 2 class TestListControlsPaginated: """Tests for GET /compliance/controls/paginated.""" def test_paginated_empty(self): with patch("compliance.api.routes.ControlRepository") as MockRepo: MockRepo.return_value.get_paginated.return_value = ([], 0) response = client.get("/compliance/controls/paginated") assert response.status_code == 200 data = response.json() assert data["data"] == [] assert data["pagination"]["total"] == 0 def test_paginated_with_data(self): ctrl = make_control() with patch("compliance.api.routes.ControlRepository") as MockRepo: MockRepo.return_value.get_paginated.return_value = ([ctrl], 1) response = client.get("/compliance/controls/paginated", params={"page": 1, "page_size": 25}) assert response.status_code == 200 data = response.json() assert data["pagination"]["total"] == 1 assert data["data"][0]["control_id"] == "GOV-001" def test_paginated_filters(self): with patch("compliance.api.routes.ControlRepository") as MockRepo: MockRepo.return_value.get_paginated.return_value = ([], 0) response = client.get( "/compliance/controls/paginated", params={"domain": "gov", "status": "pass"}, ) assert response.status_code == 200 class TestGetControlById: """Tests for GET /compliance/controls/{control_id}.""" def test_get_existing_by_control_id(self): ctrl = make_control() with patch("compliance.api.routes.ControlRepository") as MockRepo, \ patch("compliance.api.routes.EvidenceRepository") as MockEvRepo: MockRepo.return_value.get_by_control_id.return_value = ctrl MockEvRepo.return_value.get_by_control.return_value = [] response = client.get("/compliance/controls/GOV-001") assert response.status_code == 200 data = response.json() assert data["id"] == CONTROL_UUID assert data["control_id"] == "GOV-001" assert data["domain"] == "gov" assert data["status"] == "planned" def test_get_not_found(self): with patch("compliance.api.routes.ControlRepository") as MockRepo: MockRepo.return_value.get_by_control_id.return_value = None response = client.get("/compliance/controls/DOES-NOT-EXIST") assert response.status_code == 404 def test_get_evidence_count_included(self): """Evidence count is included in response.""" ctrl = make_control() fake_evidence = [MagicMock(), MagicMock()] with patch("compliance.api.routes.ControlRepository") as MockRepo, \ patch("compliance.api.routes.EvidenceRepository") as MockEvRepo: MockRepo.return_value.get_by_control_id.return_value = ctrl MockEvRepo.return_value.get_by_control.return_value = fake_evidence response = client.get("/compliance/controls/GOV-001") assert response.status_code == 200 assert response.json()["evidence_count"] == 2 class TestUpdateControl: """Tests for PUT /compliance/controls/{control_id}.""" def test_update_title(self): updated = make_control() updated.title = "Neuer Titel" with patch("compliance.api.routes.ControlRepository") as MockRepo: MockRepo.return_value.get_by_control_id.return_value = make_control() MockRepo.return_value.update.return_value = updated response = client.put( "/compliance/controls/GOV-001", json={"title": "Neuer Titel"}, ) assert response.status_code == 200 assert response.json()["title"] == "Neuer Titel" def test_update_not_found(self): with patch("compliance.api.routes.ControlRepository") as MockRepo: MockRepo.return_value.get_by_control_id.return_value = None response = client.put( "/compliance/controls/DOES-NOT-EXIST", json={"title": "Test"}, ) assert response.status_code == 404 def test_update_status_with_valid_enum(self): """Status must be a valid ControlStatusEnum value.""" updated = make_control() updated.status.value = "pass" with patch("compliance.api.routes.ControlRepository") as MockRepo: MockRepo.return_value.get_by_control_id.return_value = make_control() MockRepo.return_value.update.return_value = updated response = client.put( "/compliance/controls/GOV-001", json={"status": "pass"}, ) assert response.status_code == 200 def test_update_status_invalid_enum(self): """Invalid status → 400.""" with patch("compliance.api.routes.ControlRepository") as MockRepo: MockRepo.return_value.get_by_control_id.return_value = make_control() response = client.put( "/compliance/controls/GOV-001", json={"status": "invalid_status"}, ) assert response.status_code == 400 class TestReviewControl: """Tests for PUT /compliance/controls/{control_id}/review.""" def test_review_success(self): reviewed = make_control() reviewed.status.value = "pass" reviewed.status_notes = "Geprueft OK" with patch("compliance.api.routes.ControlRepository") as MockRepo: MockRepo.return_value.get_by_control_id.return_value = make_control() MockRepo.return_value.mark_reviewed.return_value = reviewed response = client.put( "/compliance/controls/GOV-001/review", json={"status": "pass", "status_notes": "Geprueft OK"}, ) assert response.status_code == 200 data = response.json() assert data["status"] == "pass" assert data["status_notes"] == "Geprueft OK" def test_review_not_found(self): with patch("compliance.api.routes.ControlRepository") as MockRepo: MockRepo.return_value.get_by_control_id.return_value = None response = client.put( "/compliance/controls/DOES-NOT-EXIST/review", json={"status": "pass"}, ) assert response.status_code == 404 def test_review_missing_status(self): """Missing required status field → 422.""" response = client.put( "/compliance/controls/GOV-001/review", json={}, ) assert response.status_code == 422 def test_review_invalid_status(self): """Invalid status enum → 400.""" with patch("compliance.api.routes.ControlRepository") as MockRepo: MockRepo.return_value.get_by_control_id.return_value = make_control() response = client.put( "/compliance/controls/GOV-001/review", json={"status": "invalid_status"}, ) assert response.status_code == 400 class TestGetControlsByDomain: """Tests for GET /compliance/controls/by-domain/{domain}.""" def test_get_by_valid_domain(self): ctrl = make_control() with patch("compliance.api.routes.ControlRepository") as MockRepo: MockRepo.return_value.get_by_domain.return_value = [ctrl] response = client.get("/compliance/controls/by-domain/gov") assert response.status_code == 200 data = response.json() assert data["total"] == 1 assert data["controls"][0]["domain"] == "gov" def test_get_by_domain_empty_result(self): with patch("compliance.api.routes.ControlRepository") as MockRepo: MockRepo.return_value.get_by_domain.return_value = [] response = client.get("/compliance/controls/by-domain/ai") assert response.status_code == 200 assert response.json()["total"] == 0 def test_get_by_invalid_domain(self): """Invalid domain enum → 400.""" response = client.get("/compliance/controls/by-domain/invalid_domain") assert response.status_code == 400