All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 40s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 26s
CI / test-python-dsms-gateway (push) Successful in 21s
- import_routes: GET /gap-analysis/{document_id} implementiert
- import_routes: Bug-Fix — gap_analysis_result vor try-Block initialisiert
(verhindert UnboundLocalError bei DB-Fehler)
- test_import_routes: 21 neue API-Endpoint-Tests (59 total, alle grün)
- test_screening_routes: 18 neue API-Endpoint-Tests (74 total, alle grün)
- 031_modules.sql: Migration für compliance_service_modules,
compliance_module_regulations, compliance_module_risks
- test_module_routes: 20 neue Tests für Module-Registry-Routen (alle grün)
- freigabe-module.md: MkDocs-Seite für Import/Screening/Modules/RAG
- mkdocs.yml: Nav-Eintrag "Freigabe-Module (Paket 2)"
Gesamt: 146 neue Tests, alle bestanden
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
416 lines
16 KiB
Python
416 lines
16 KiB
Python
"""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"
|