"""Tests for Requirements routes (routes.py → /compliance/requirements).""" 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) REQ_UUID = "rrrrrrrr-1111-2222-3333-rrrrrrrrrrrr" REG_UUID = "gggggggg-1111-2222-3333-gggggggggggg" NOW = datetime(2024, 3, 1, 12, 0, 0) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def make_requirement(overrides=None): r = MagicMock() r.id = REQ_UUID r.regulation_id = REG_UUID r.regulation_code = "GDPR" # regulation is accessed as r.regulation.code in list route r.regulation = MagicMock() r.regulation.code = "GDPR" r.article = "Art. 32" r.paragraph = "Abs. 1" r.title = "Sicherheit der Verarbeitung" r.description = "Angemessene technische Massnahmen" r.requirement_text = "Implementierung geeigneter TOMs" r.breakpilot_interpretation = "Risikobasierter Ansatz" r.is_applicable = True r.applicability_reason = "Kernanforderung" r.priority = 1 r.implementation_status = "implemented" r.implementation_details = None r.code_references = None r.documentation_links = None r.evidence_description = None r.evidence_artifacts = None r.auditor_notes = None r.audit_status = "pending" r.last_audit_date = None r.last_auditor = None r.source_page = None r.source_section = None r.created_at = NOW r.updated_at = NOW if overrides: for k, v in overrides.items(): setattr(r, k, v) return r def make_regulation(overrides=None): reg = MagicMock() reg.id = REG_UUID reg.code = "GDPR" reg.name = "DSGVO" if overrides: for k, v in overrides.items(): setattr(reg, k, v) return reg # --------------------------------------------------------------------------- # Tests: GET /compliance/requirements (paginated) # --------------------------------------------------------------------------- class TestListRequirements: """Tests for GET /compliance/requirements.""" def test_list_empty(self): with patch("compliance.api.routes.RequirementRepository") as MockRepo: MockRepo.return_value.get_paginated.return_value = ([], 0) response = client.get("/compliance/requirements") assert response.status_code == 200 data = response.json() assert data["data"] == [] assert data["pagination"]["total"] == 0 def test_list_with_requirement(self): req = make_requirement() with patch("compliance.api.routes.RequirementRepository") as MockRepo: MockRepo.return_value.get_paginated.return_value = ([req], 1) response = client.get("/compliance/requirements") assert response.status_code == 200 data = response.json() assert data["pagination"]["total"] == 1 r = data["data"][0] assert r["article"] == "Art. 32" assert r["title"] == "Sicherheit der Verarbeitung" assert r["is_applicable"] is True assert r["regulation_code"] == "GDPR" def test_list_pagination_params(self): with patch("compliance.api.routes.RequirementRepository") as MockRepo: MockRepo.return_value.get_paginated.return_value = ([], 0) response = client.get("/compliance/requirements", params={"page": 2, "page_size": 25}) assert response.status_code == 200 data = response.json() assert data["pagination"]["page"] == 2 def test_list_filter_is_applicable(self): req = make_requirement({"is_applicable": True}) with patch("compliance.api.routes.RequirementRepository") as MockRepo: MockRepo.return_value.get_paginated.return_value = ([req], 1) response = client.get("/compliance/requirements", params={"is_applicable": "true"}) assert response.status_code == 200 assert response.json()["pagination"]["total"] == 1 def test_list_filter_search(self): with patch("compliance.api.routes.RequirementRepository") as MockRepo: MockRepo.return_value.get_paginated.return_value = ([], 0) response = client.get("/compliance/requirements", params={"search": "TOM"}) assert response.status_code == 200 def test_list_multiple_requirements(self): r2 = make_requirement() r2.id = "rrrrrrrr-2222-2222-2222-rrrrrrrrrrrr" r2.article = "Art. 5" with patch("compliance.api.routes.RequirementRepository") as MockRepo: MockRepo.return_value.get_paginated.return_value = ([make_requirement(), r2], 2) response = client.get("/compliance/requirements") assert response.status_code == 200 assert response.json()["pagination"]["total"] == 2 # --------------------------------------------------------------------------- # Tests: GET /compliance/requirements/{id} # Note: This route uses db.query() directly, not RequirementRepository. # --------------------------------------------------------------------------- class TestGetRequirementById: """Tests for GET /compliance/requirements/{id}.""" def test_get_existing(self): req = make_requirement() regulation = make_regulation() # db.query(RequirementDB).filter(...).first() → req mock_db.query.return_value.filter.return_value.first.side_effect = [req, regulation] response = client.get(f"/compliance/requirements/{REQ_UUID}") assert response.status_code == 200 data = response.json() assert data["id"] == REQ_UUID assert data["article"] == "Art. 32" def test_get_not_found(self): # Reset side_effect then set return_value to None mock_db.query.return_value.filter.return_value.first.side_effect = None mock_db.query.return_value.filter.return_value.first.return_value = None response = client.get("/compliance/requirements/nonexistent-id") assert response.status_code == 404 def test_get_returns_dict_structure(self): req = make_requirement() regulation = make_regulation() mock_db.query.return_value.filter.return_value.first.side_effect = [req, regulation] response = client.get(f"/compliance/requirements/{REQ_UUID}") assert response.status_code == 200 data = response.json() # dict response has these keys assert "id" in data assert "article" in data assert "implementation_status" in data assert "audit_status" in data # --------------------------------------------------------------------------- # Tests: GET /compliance/regulations/{code}/requirements # --------------------------------------------------------------------------- class TestGetRequirementsByRegulation: """Tests for GET /compliance/regulations/{code}/requirements.""" def test_get_by_regulation_code(self): req = make_requirement() regulation = make_regulation() with patch("compliance.api.routes.RegulationRepository") as MockRegRepo, \ patch("compliance.api.routes.RequirementRepository") as MockReqRepo: MockRegRepo.return_value.get_by_code.return_value = regulation MockReqRepo.return_value.get_by_regulation.return_value = [req] response = client.get("/compliance/regulations/GDPR/requirements") assert response.status_code == 200 data = response.json() assert data["total"] == 1 assert data["requirements"][0]["article"] == "Art. 32" def test_get_by_regulation_not_found(self): with patch("compliance.api.routes.RegulationRepository") as MockRegRepo: MockRegRepo.return_value.get_by_code.return_value = None response = client.get("/compliance/regulations/UNKNOWN/requirements") assert response.status_code == 404 def test_get_by_regulation_empty_requirements(self): regulation = make_regulation() with patch("compliance.api.routes.RegulationRepository") as MockRegRepo, \ patch("compliance.api.routes.RequirementRepository") as MockReqRepo: MockRegRepo.return_value.get_by_code.return_value = regulation MockReqRepo.return_value.get_by_regulation.return_value = [] response = client.get("/compliance/regulations/GDPR/requirements") assert response.status_code == 200 assert response.json()["total"] == 0 # --------------------------------------------------------------------------- # Tests: POST /compliance/requirements # --------------------------------------------------------------------------- class TestCreateRequirement: """Tests for POST /compliance/requirements.""" def test_create_success(self): req = make_requirement() regulation = make_regulation() with patch("compliance.api.routes.RequirementRepository") as MockRepo, \ patch("compliance.api.routes.RegulationRepository") as MockRegRepo: MockRegRepo.return_value.get_by_id.return_value = regulation MockRepo.return_value.create.return_value = req response = client.post("/compliance/requirements", json={ "regulation_id": REG_UUID, "article": "Art. 32", "title": "Sicherheit der Verarbeitung", "is_applicable": True, "priority": 1, }) assert response.status_code == 200 data = response.json() assert data["article"] == "Art. 32" assert data["regulation_id"] == REG_UUID def test_create_regulation_not_found(self): with patch("compliance.api.routes.RegulationRepository") as MockRegRepo: MockRegRepo.return_value.get_by_id.return_value = None response = client.post("/compliance/requirements", json={ "regulation_id": "nonexistent-reg", "article": "Art. 99", "title": "Test", "is_applicable": True, "priority": 2, }) assert response.status_code == 404 def test_create_missing_required_fields(self): """Missing title → 422.""" response = client.post("/compliance/requirements", json={ "regulation_id": REG_UUID, "article": "Art. 32", }) assert response.status_code == 422 # --------------------------------------------------------------------------- # Tests: DELETE /compliance/requirements/{id} # --------------------------------------------------------------------------- class TestDeleteRequirement: """Tests for DELETE /compliance/requirements/{id}.""" def test_delete_success(self): with patch("compliance.api.routes.RequirementRepository") as MockRepo: MockRepo.return_value.delete.return_value = True response = client.delete(f"/compliance/requirements/{REQ_UUID}") assert response.status_code == 200 assert response.json()["success"] is True def test_delete_not_found(self): with patch("compliance.api.routes.RequirementRepository") as MockRepo: MockRepo.return_value.delete.return_value = False response = client.delete("/compliance/requirements/nonexistent") assert response.status_code == 404 # --------------------------------------------------------------------------- # Tests: PUT /compliance/requirements/{id} # Note: This route uses db.query() directly. # --------------------------------------------------------------------------- class TestUpdateRequirement: """Tests for PUT /compliance/requirements/{id}.""" def test_update_success(self): req = make_requirement() req.implementation_status = "implemented" # Reset any previous side_effect before setting return_value mock_db.query.return_value.filter.return_value.first.side_effect = None mock_db.query.return_value.filter.return_value.first.return_value = req response = client.put( f"/compliance/requirements/{REQ_UUID}", json={"implementation_status": "implemented"}, ) assert response.status_code == 200 data = response.json() assert data["success"] is True def test_update_not_found(self): mock_db.query.return_value.filter.return_value.first.side_effect = None mock_db.query.return_value.filter.return_value.first.return_value = None response = client.put( "/compliance/requirements/nonexistent", json={"implementation_status": "implemented"}, ) assert response.status_code == 404