Files
breakpilot-compliance/backend-compliance/tests/test_module_routes.py
Benjamin Admin 3913931d5b
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
feat(freigabe): Import/Screening/Modules/RAG — API-Tests, Migration 031, Bug-Fix
- 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>
2026-03-05 11:42:19 +01:00

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"