"""Tests for Service Module Registry routes (module_routes.py).""" from datetime import datetime from unittest.mock import MagicMock, patch from fastapi import FastAPI from fastapi.testclient import TestClient from compliance.api.module_routes import router as module_router from classroom_engine.database import get_db # --------------------------------------------------------------------------- # App setup with mocked DB dependency # --------------------------------------------------------------------------- app = FastAPI() app.include_router(module_router) mock_db = MagicMock() def override_get_db(): yield mock_db app.dependency_overrides[get_db] = override_get_db client = TestClient(app) MODULE_UUID = "aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb" REG_UUID = "cccccccc-4444-5555-6666-dddddddddddd" TENANT_ID = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e" NOW = datetime(2024, 1, 15, 10, 0, 0) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def make_module(overrides=None): """Return a MagicMock that behaves like a ServiceModuleDB instance.""" m = MagicMock() m.id = MODULE_UUID m.name = "consent-service" m.display_name = "Go Consent Service" m.description = "Manages user consents" m.service_type = MagicMock() m.service_type.value = "backend" m.port = 8080 m.technology_stack = ["Go", "Gin", "PostgreSQL"] m.repository_path = "/consent-service" m.docker_image = "breakpilot-consent-service" m.data_categories = ["consent_records", "personal_data"] m.processes_pii = True m.processes_health_data = False m.ai_components = False m.criticality = "critical" m.owner_team = "Backend Team" m.owner_contact = "backend@breakpilot.app" m.is_active = True m.compliance_score = 85.0 m.last_compliance_check = None m.created_at = NOW m.updated_at = NOW m.regulation_mappings = [] m.module_risks = [] if overrides: for k, v in overrides.items(): setattr(m, k, v) return m def make_overview(): return { "total_modules": 5, "modules_by_type": {"backend": 3, "ai": 2}, "modules_by_criticality": {"critical": 1, "high": 2, "medium": 2}, "modules_processing_pii": 3, "modules_with_ai": 2, "average_compliance_score": 78.5, "regulations_coverage": {"GDPR": 3, "AI_ACT": 2}, } # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- class TestListModules: """Tests for GET /modules.""" def test_list_empty_db(self): with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo: instance = MockRepo.return_value instance.get_all.return_value = [] response = client.get("/modules") assert response.status_code == 200 data = response.json() assert data["modules"] == [] assert data["total"] == 0 def test_list_with_module(self): with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo: instance = MockRepo.return_value instance.get_all.return_value = [make_module()] response = client.get("/modules") assert response.status_code == 200 data = response.json() assert data["total"] == 1 m = data["modules"][0] assert m["name"] == "consent-service" assert m["display_name"] == "Go Consent Service" assert m["is_active"] is True assert m["processes_pii"] is True def test_list_filter_processes_pii_true(self): """processes_pii=true filter is forwarded.""" with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo: instance = MockRepo.return_value pii_module = make_module({"processes_pii": True}) instance.get_all.return_value = [pii_module] response = client.get("/modules", params={"processes_pii": "true"}) assert response.status_code == 200 data = response.json() assert data["modules"][0]["processes_pii"] is True def test_list_filter_ai_components(self): """ai_components filter is forwarded.""" with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo: instance = MockRepo.return_value ai_module = make_module({"ai_components": True}) instance.get_all.return_value = [ai_module] response = client.get("/modules", params={"ai_components": "true"}) assert response.status_code == 200 data = response.json() assert data["modules"][0]["ai_components"] is True def test_list_multiple_modules(self): """Multiple modules returned correctly.""" with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo: instance = MockRepo.return_value m1 = make_module({"name": "service-a", "display_name": "Service A"}) m2 = make_module({"name": "service-b", "display_name": "Service B"}) instance.get_all.return_value = [m1, m2] response = client.get("/modules") assert response.status_code == 200 assert response.json()["total"] == 2 class TestModuleOverview: """Tests for GET /modules/overview.""" def test_overview_returns_stats(self): with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo: instance = MockRepo.return_value instance.get_overview.return_value = make_overview() response = client.get("/modules/overview") assert response.status_code == 200 data = response.json() assert data["total_modules"] == 5 assert data["modules_processing_pii"] == 3 def test_overview_empty(self): with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo: instance = MockRepo.return_value empty = { "total_modules": 0, "modules_by_type": {}, "modules_by_criticality": {}, "modules_processing_pii": 0, "modules_with_ai": 0, "average_compliance_score": None, "regulations_coverage": {}, } instance.get_overview.return_value = empty response = client.get("/modules/overview") assert response.status_code == 200 data = response.json() assert data["total_modules"] == 0 assert data["modules_processing_pii"] == 0 class TestGetModuleDetail: """Tests for GET /modules/{module_id}.""" def test_get_existing_module(self): module = make_module() with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo: instance = MockRepo.return_value instance.get_with_regulations.return_value = module response = client.get(f"/modules/{MODULE_UUID}") assert response.status_code == 200 data = response.json() assert data["id"] == MODULE_UUID assert data["name"] == "consent-service" assert "regulations" in data assert "risks" in data def test_get_module_not_found(self): with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo: instance = MockRepo.return_value instance.get_with_regulations.return_value = None instance.get_by_name.return_value = None response = client.get("/modules/nonexistent-id") assert response.status_code == 404 def test_get_module_fallback_to_name_lookup(self): """Falls back to name lookup when ID lookup returns None.""" module = make_module() with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo: instance = MockRepo.return_value # First get_with_regulations(id) → None, then get_by_name → module, then get_with_regulations(id) → module instance.get_with_regulations.side_effect = [None, module] instance.get_by_name.return_value = module response = client.get("/modules/consent-service") assert response.status_code == 200 class TestActivateDeactivate: """Tests for POST /modules/{id}/activate and /deactivate.""" def test_activate_module(self): module = make_module({"is_active": False}) with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo: instance = MockRepo.return_value instance.get_by_id.return_value = module response = client.post(f"/modules/{MODULE_UUID}/activate") assert response.status_code == 200 data = response.json() assert data["status"] == "activated" assert module.is_active is True mock_db.commit.assert_called() def test_activate_already_active_is_idempotent(self): module = make_module({"is_active": True}) with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo: instance = MockRepo.return_value instance.get_by_id.return_value = module response = client.post(f"/modules/{MODULE_UUID}/activate") assert response.status_code == 200 assert response.json()["status"] == "activated" def test_activate_not_found(self): with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo: instance = MockRepo.return_value instance.get_by_id.return_value = None instance.get_by_name.return_value = None response = client.post("/modules/nonexistent/activate") assert response.status_code == 404 def test_deactivate_module(self): module = make_module({"is_active": True}) with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo: instance = MockRepo.return_value instance.get_by_id.return_value = module response = client.post(f"/modules/{MODULE_UUID}/deactivate") assert response.status_code == 200 data = response.json() assert data["status"] == "deactivated" assert module.is_active is False def test_deactivate_not_found(self): with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo: instance = MockRepo.return_value instance.get_by_id.return_value = None instance.get_by_name.return_value = None response = client.post("/modules/nonexistent/deactivate") assert response.status_code == 404 class TestSeedModules: """Tests for POST /modules/seed.""" def test_seed_creates_modules(self): with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo, \ patch("compliance.db.models.ServiceModuleDB") as MockSMDB, \ patch("compliance.db.models.ModuleRegulationMappingDB") as MockMRMDB, \ patch("compliance.db.models.ModuleRiskDB") as MockMRDB, \ patch("classroom_engine.database.engine") as mock_engine: instance = MockRepo.return_value instance.seed_from_data.return_value = { "modules_created": 10, "mappings_created": 25, } # Prevent actual DB table creation MockSMDB.__table__ = MagicMock() MockMRMDB.__table__ = MagicMock() MockMRDB.__table__ = MagicMock() response = client.post("/modules/seed", json={"force": False}) assert response.status_code == 200 data = response.json() assert data["success"] is True assert data["modules_created"] == 10 assert data["mappings_created"] == 25 def test_seed_force_flag(self): """force=True is forwarded to seed_from_data.""" with patch("compliance.db.repository.ServiceModuleRepository") as MockRepo, \ patch("compliance.db.models.ServiceModuleDB") as MockSMDB, \ patch("compliance.db.models.ModuleRegulationMappingDB") as MockMRMDB, \ patch("compliance.db.models.ModuleRiskDB") as MockMRDB, \ patch("classroom_engine.database.engine"): instance = MockRepo.return_value instance.seed_from_data.return_value = {"modules_created": 0, "mappings_created": 0} MockSMDB.__table__ = MagicMock() MockMRMDB.__table__ = MagicMock() MockMRDB.__table__ = MagicMock() response = client.post("/modules/seed", json={"force": True}) assert response.status_code == 200 _, kwargs = instance.seed_from_data.call_args assert kwargs.get("force") is True class TestRegulationMapping: """Tests for POST /modules/{id}/regulations.""" def test_add_regulation_not_found_module(self): with patch("compliance.db.repository.ServiceModuleRepository") as MockModuleRepo, \ patch("compliance.api.module_routes.RegulationRepository"): module_instance = MockModuleRepo.return_value module_instance.get_by_id.return_value = None module_instance.get_by_name.return_value = None response = client.post( f"/modules/{MODULE_UUID}/regulations", json={"module_id": MODULE_UUID, "regulation_id": REG_UUID, "relevance_level": "high"}, ) assert response.status_code == 404 def test_add_regulation_not_found_regulation(self): module = make_module() with patch("compliance.db.repository.ServiceModuleRepository") as MockModuleRepo, \ patch("compliance.api.module_routes.RegulationRepository") as MockRegRepo: module_instance = MockModuleRepo.return_value module_instance.get_by_id.return_value = module reg_instance = MockRegRepo.return_value reg_instance.get_by_id.return_value = None reg_instance.get_by_code.return_value = None response = client.post( f"/modules/{MODULE_UUID}/regulations", json={"module_id": MODULE_UUID, "regulation_id": "nonexistent-reg", "relevance_level": "high"}, ) assert response.status_code == 404 def test_add_regulation_success(self): module = make_module() fake_regulation = MagicMock() fake_regulation.id = REG_UUID fake_regulation.code = "GDPR" fake_regulation.name = "DSGVO" fake_mapping = MagicMock() fake_mapping.id = "mapping-uuid" fake_mapping.module_id = MODULE_UUID fake_mapping.regulation_id = REG_UUID fake_mapping.relevance_level = MagicMock() fake_mapping.relevance_level.value = "high" fake_mapping.notes = None fake_mapping.applicable_articles = [] fake_mapping.created_at = NOW with patch("compliance.db.repository.ServiceModuleRepository") as MockModuleRepo, \ patch("compliance.api.module_routes.RegulationRepository") as MockRegRepo: module_instance = MockModuleRepo.return_value module_instance.get_by_id.return_value = module module_instance.add_regulation_mapping.return_value = fake_mapping reg_instance = MockRegRepo.return_value reg_instance.get_by_id.return_value = fake_regulation response = client.post( f"/modules/{MODULE_UUID}/regulations", json={"module_id": MODULE_UUID, "regulation_id": REG_UUID, "relevance_level": "high"}, ) assert response.status_code == 200 data = response.json() assert data["relevance_level"] == "high" assert data["regulation_code"] == "GDPR"