Files
breakpilot-compliance/backend-compliance/tests/test_notfallplan_routes.py
Benjamin Admin 25d5da78ef
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 34s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 17s
feat: Alle 5 verbleibenden SDK-Module auf 100% — RAG, Security-Backlog, Quality, Notfallplan, Loeschfristen
Paket A — RAG Proxy:
- NEU: admin-compliance/app/api/sdk/v1/rag/[[...path]]/route.ts
  → Proxy zu ai-compliance-sdk:8090, GET+POST, UUID-Validierung
- UPDATE: rag/page.tsx — setTimeout Mock → echte API-Calls
  GET /regulations → dynamische suggestedQuestions
  POST /search → Qdrant-Ergebnisse mit score, title, reference

Paket B — Security-Backlog + Quality:
- NEU: migrations/014_security_backlog.sql + 015_quality.sql
- NEU: compliance/api/security_backlog_routes.py — CRUD + Stats
- NEU: compliance/api/quality_routes.py — Metrics + Tests CRUD + Stats
- UPDATE: security-backlog/page.tsx — mockItems → API
- UPDATE: quality/page.tsx — mockMetrics/mockTests → API
- UPDATE: compliance/api/__init__.py — Router-Registrierung
- NEU: tests/test_security_backlog_routes.py (48 Tests — 48/48 bestanden)
- NEU: tests/test_quality_routes.py (67 Tests — 67/67 bestanden)

Paket C — Notfallplan Incidents + Templates:
- NEU: migrations/016_notfallplan_incidents.sql
  compliance_notfallplan_incidents + compliance_notfallplan_templates
- UPDATE: notfallplan_routes.py — GET/POST/PUT/DELETE für /incidents + /templates
- UPDATE: notfallplan/page.tsx — Incidents-Tab + Templates-Tab → API
- UPDATE: tests/test_notfallplan_routes.py (+76 neue Tests — alle bestanden)

Paket D — Loeschfristen localStorage → API:
- NEU: migrations/017_loeschfristen.sql (JSONB: legal_holds, storage_locations, ...)
- NEU: compliance/api/loeschfristen_routes.py — CRUD + Stats + Status-Update
- UPDATE: loeschfristen/page.tsx — vollständige localStorage → API Migration
  createNewPolicy → POST (API-UUID als id), deletePolicy → DELETE,
  handleSaveAndClose → PUT, adoptGeneratedPolicies → POST je Policy
  apiToPolicy() + policyToPayload() Mapper, saving-State für Buttons
- NEU: tests/test_loeschfristen_routes.py (58 Tests — alle bestanden)

Gesamt: 253 neue Tests, alle bestanden (48 + 67 + 76 + 58 + bestehende)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 18:04:53 +01:00

807 lines
32 KiB
Python

"""Tests for Notfallplan routes and schemas (notfallplan_routes.py).
Covers existing schema tests plus the new incidents and templates HTTP endpoints.
"""
import pytest
from unittest.mock import MagicMock
from fastapi.testclient import TestClient
from fastapi import FastAPI
from datetime import datetime
from compliance.api.notfallplan_routes import (
ContactCreate,
ContactUpdate,
ScenarioCreate,
ScenarioUpdate,
ChecklistCreate,
ExerciseCreate,
IncidentCreate,
TemplateCreate,
router,
)
app = FastAPI()
app.include_router(router)
client = TestClient(app)
DEFAULT_TENANT = "default"
INCIDENT_ID = "dddddddd-0001-0001-0001-000000000001"
TEMPLATE_ID = "eeeeeeee-0001-0001-0001-000000000001"
UNKNOWN_ID = "aaaaaaaa-9999-9999-9999-999999999999"
# =============================================================================
# Helpers
# =============================================================================
def make_incident_row(overrides=None):
data = {
"id": INCIDENT_ID,
"tenant_id": DEFAULT_TENANT,
"title": "Test Incident",
"description": "An incident occurred",
"detected_at": datetime(2024, 1, 1),
"detected_by": "System",
"status": "detected",
"severity": "medium",
"affected_data_categories": [],
"estimated_affected_persons": 0,
"measures": [],
"art34_required": False,
"art34_justification": None,
"reported_to_authority_at": None,
"notified_affected_at": None,
"closed_at": None,
"closed_by": None,
"lessons_learned": None,
"created_at": datetime(2024, 1, 1),
"updated_at": datetime(2024, 1, 1),
}
if overrides:
data.update(overrides)
row = MagicMock()
for k, v in data.items():
setattr(row, k, v)
row._mapping = data
return row
def make_template_row(overrides=None):
data = {
"id": TEMPLATE_ID,
"tenant_id": DEFAULT_TENANT,
"type": "art33",
"title": "Art. 33 Template",
"content": "Sehr geehrte Behoerde...",
"created_at": datetime(2024, 1, 1),
"updated_at": datetime(2024, 1, 1),
}
if overrides:
data.update(overrides)
row = MagicMock()
for k, v in data.items():
setattr(row, k, v)
row._mapping = data
return row
@pytest.fixture
def mock_db():
from classroom_engine.database import get_db
db = MagicMock()
app.dependency_overrides[get_db] = lambda: db
yield db
app.dependency_overrides.clear()
# =============================================================================
# Existing Schema Tests — ContactCreate
# =============================================================================
class TestContactCreate:
def test_minimal_valid(self):
req = ContactCreate(name="Max Mustermann")
assert req.name == "Max Mustermann"
assert req.is_primary is False
assert req.available_24h is False
assert req.email is None
assert req.phone is None
def test_full_contact(self):
req = ContactCreate(
name="Anna Schmidt",
role="DSB",
email="anna@example.com",
phone="+49 160 12345678",
is_primary=True,
available_24h=True,
)
assert req.role == "DSB"
assert req.is_primary is True
assert req.available_24h is True
def test_serialization(self):
req = ContactCreate(name="Test Kontakt", role="IT-Leiter")
data = req.model_dump(exclude_none=True)
assert data["name"] == "Test Kontakt"
assert data["role"] == "IT-Leiter"
assert "email" not in data
# =============================================================================
# Existing Schema Tests — ContactUpdate
# =============================================================================
class TestContactUpdate:
def test_empty_update(self):
req = ContactUpdate()
data = req.model_dump(exclude_none=True)
assert data == {}
def test_partial_update(self):
req = ContactUpdate(phone="+49 170 9876543", available_24h=True)
data = req.model_dump(exclude_none=True)
assert data == {"phone": "+49 170 9876543", "available_24h": True}
# =============================================================================
# Existing Schema Tests — ScenarioCreate
# =============================================================================
class TestScenarioCreate:
def test_minimal_valid(self):
req = ScenarioCreate(title="Datenpanne")
assert req.title == "Datenpanne"
assert req.severity == "medium"
assert req.is_active is True
assert req.response_steps == []
def test_with_response_steps(self):
steps = ["Schritt 1: Incident identifizieren", "Schritt 2: DSB informieren"]
req = ScenarioCreate(
title="Ransomware-Angriff",
category="system_failure",
severity="critical",
response_steps=steps,
estimated_recovery_time=48,
)
assert req.category == "system_failure"
assert req.severity == "critical"
assert len(req.response_steps) == 2
assert req.estimated_recovery_time == 48
def test_full_serialization(self):
req = ScenarioCreate(
title="Phishing",
category="data_breach",
severity="high",
description="Mitarbeiter wurde Opfer eines Phishing-Angriffs",
)
data = req.model_dump(exclude_none=True)
assert data["severity"] == "high"
assert data["category"] == "data_breach"
# =============================================================================
# Existing Schema Tests — ScenarioUpdate
# =============================================================================
class TestScenarioUpdate:
def test_empty_update(self):
req = ScenarioUpdate()
data = req.model_dump(exclude_none=True)
assert data == {}
def test_severity_update(self):
req = ScenarioUpdate(severity="low")
data = req.model_dump(exclude_none=True)
assert data == {"severity": "low"}
def test_deactivate(self):
req = ScenarioUpdate(is_active=False)
data = req.model_dump(exclude_none=True)
assert data["is_active"] is False
# =============================================================================
# Existing Schema Tests — ChecklistCreate
# =============================================================================
class TestChecklistCreate:
def test_minimal_valid(self):
req = ChecklistCreate(title="DSB benachrichtigen")
assert req.title == "DSB benachrichtigen"
assert req.is_required is True
assert req.order_index == 0
assert req.scenario_id is None
def test_with_scenario_link(self):
req = ChecklistCreate(
title="IT-Team alarmieren",
scenario_id="550e8400-e29b-41d4-a716-446655440000",
order_index=1,
is_required=True,
)
assert req.scenario_id == "550e8400-e29b-41d4-a716-446655440000"
assert req.order_index == 1
# =============================================================================
# Existing Schema Tests — ExerciseCreate
# =============================================================================
class TestExerciseCreate:
def test_minimal_valid(self):
req = ExerciseCreate(title="Jahresübung 2026")
assert req.title == "Jahresübung 2026"
assert req.participants == []
assert req.outcome is None
def test_full_exercise(self):
req = ExerciseCreate(
title="Ransomware-Simulation",
scenario_id="550e8400-e29b-41d4-a716-446655440000",
participants=["Max Mustermann", "Anna Schmidt"],
outcome="passed",
notes="Übung verlief planmäßig",
)
assert req.outcome == "passed"
assert len(req.participants) == 2
assert req.notes == "Übung verlief planmäßig"
# =============================================================================
# New Schema Tests — IncidentCreate / TemplateCreate
# =============================================================================
class TestIncidentCreateSchema:
def test_incident_create_minimal(self):
inc = IncidentCreate(title="Breach")
assert inc.title == "Breach"
assert inc.status == "detected"
assert inc.severity == "medium"
assert inc.estimated_affected_persons == 0
assert inc.art34_required is False
assert inc.affected_data_categories == []
assert inc.measures == []
def test_incident_create_full(self):
inc = IncidentCreate(
title="Big Breach",
description="Ransomware attack",
detected_by="SIEM",
status="assessed",
severity="critical",
affected_data_categories=["personal", "health"],
estimated_affected_persons=1000,
measures=["isolation", "backup restore"],
art34_required=True,
art34_justification="High risk to data subjects",
)
assert inc.severity == "critical"
assert inc.estimated_affected_persons == 1000
assert len(inc.affected_data_categories) == 2
assert inc.art34_required is True
def test_incident_create_serialization_excludes_none(self):
inc = IncidentCreate(title="T")
data = inc.model_dump(exclude_none=True)
assert data["title"] == "T"
assert "art34_justification" not in data
assert "description" not in data
class TestTemplateCreateSchema:
def test_template_create_requires_title_content(self):
t = TemplateCreate(title="T", content="C", type="art33")
assert t.title == "T"
assert t.content == "C"
assert t.type == "art33"
def test_template_create_default_type(self):
t = TemplateCreate(title="T", content="C")
assert t.type == "art33"
def test_template_create_art34_type(self):
t = TemplateCreate(title="Notification Letter", content="Dear...", type="art34")
assert t.type == "art34"
# =============================================================================
# Incidents — GET /notfallplan/incidents
# =============================================================================
class TestListIncidents:
def test_list_incidents_returns_empty_list(self, mock_db):
mock_db.execute.return_value.fetchall.return_value = []
resp = client.get("/notfallplan/incidents")
assert resp.status_code == 200
assert resp.json() == []
def test_list_incidents_returns_one_incident(self, mock_db):
row = make_incident_row()
mock_db.execute.return_value.fetchall.return_value = [row]
resp = client.get("/notfallplan/incidents")
assert resp.status_code == 200
data = resp.json()
assert len(data) == 1
assert data[0]["title"] == "Test Incident"
assert data[0]["status"] == "detected"
assert data[0]["severity"] == "medium"
def test_list_incidents_returns_multiple(self, mock_db):
rows = [
make_incident_row({"id": "id-1", "title": "Incident A"}),
make_incident_row({"id": "id-2", "title": "Incident B"}),
]
mock_db.execute.return_value.fetchall.return_value = rows
resp = client.get("/notfallplan/incidents")
assert resp.status_code == 200
assert len(resp.json()) == 2
def test_list_incidents_filter_by_status(self, mock_db):
mock_db.execute.return_value.fetchall.return_value = []
resp = client.get("/notfallplan/incidents?status=closed")
assert resp.status_code == 200
call_params = mock_db.execute.call_args[0][1]
assert call_params.get("status") == "closed"
def test_list_incidents_filter_by_severity(self, mock_db):
mock_db.execute.return_value.fetchall.return_value = []
resp = client.get("/notfallplan/incidents?severity=high")
assert resp.status_code == 200
call_params = mock_db.execute.call_args[0][1]
assert call_params.get("severity") == "high"
def test_list_incidents_filter_combined(self, mock_db):
mock_db.execute.return_value.fetchall.return_value = []
resp = client.get("/notfallplan/incidents?status=detected&severity=critical")
assert resp.status_code == 200
call_params = mock_db.execute.call_args[0][1]
assert call_params.get("status") == "detected"
assert call_params.get("severity") == "critical"
def test_list_incidents_uses_default_tenant(self, mock_db):
mock_db.execute.return_value.fetchall.return_value = []
resp = client.get("/notfallplan/incidents")
assert resp.status_code == 200
call_params = mock_db.execute.call_args[0][1]
assert call_params.get("tenant_id") == DEFAULT_TENANT
def test_list_incidents_custom_tenant_header(self, mock_db):
mock_db.execute.return_value.fetchall.return_value = []
resp = client.get("/notfallplan/incidents", headers={"X-Tenant-ID": "my-tenant"})
assert resp.status_code == 200
call_params = mock_db.execute.call_args[0][1]
assert call_params.get("tenant_id") == "my-tenant"
def test_list_incidents_all_fields_present(self, mock_db):
row = make_incident_row()
mock_db.execute.return_value.fetchall.return_value = [row]
resp = client.get("/notfallplan/incidents")
item = resp.json()[0]
expected_fields = (
"id", "tenant_id", "title", "description", "detected_at",
"detected_by", "status", "severity", "affected_data_categories",
"estimated_affected_persons", "measures", "art34_required",
"art34_justification", "reported_to_authority_at",
"notified_affected_at", "closed_at", "closed_by",
"lessons_learned", "created_at", "updated_at",
)
for field in expected_fields:
assert field in item, f"Missing field: {field}"
# =============================================================================
# Incidents — POST /notfallplan/incidents
# =============================================================================
class TestCreateIncident:
def test_create_incident_minimal(self, mock_db):
row = make_incident_row()
mock_db.execute.return_value.fetchone.return_value = row
resp = client.post("/notfallplan/incidents", json={"title": "New Incident"})
assert resp.status_code == 201
assert resp.json()["title"] == "Test Incident"
def test_create_incident_full_payload(self, mock_db):
row = make_incident_row({
"title": "Critical Breach",
"description": "Database exposed",
"detected_by": "SOC Team",
"status": "assessed",
"severity": "critical",
"estimated_affected_persons": 500,
})
mock_db.execute.return_value.fetchone.return_value = row
resp = client.post("/notfallplan/incidents", json={
"title": "Critical Breach",
"description": "Database exposed",
"detected_by": "SOC Team",
"status": "assessed",
"severity": "critical",
"estimated_affected_persons": 500,
})
assert resp.status_code == 201
data = resp.json()
assert data["severity"] == "critical"
assert data["estimated_affected_persons"] == 500
def test_create_incident_missing_title_returns_422(self, mock_db):
resp = client.post("/notfallplan/incidents", json={"description": "No title here"})
assert resp.status_code == 422
def test_create_incident_default_status_passed_to_db(self, mock_db):
row = make_incident_row()
mock_db.execute.return_value.fetchone.return_value = row
client.post("/notfallplan/incidents", json={"title": "T"})
call_params = mock_db.execute.call_args[0][1]
assert call_params["status"] == "detected"
assert call_params["severity"] == "medium"
def test_create_incident_commits(self, mock_db):
row = make_incident_row()
mock_db.execute.return_value.fetchone.return_value = row
client.post("/notfallplan/incidents", json={"title": "T"})
mock_db.commit.assert_called_once()
def test_create_incident_with_art34_required(self, mock_db):
row = make_incident_row({"art34_required": True, "art34_justification": "Hohe Risikobewertung"})
mock_db.execute.return_value.fetchone.return_value = row
resp = client.post("/notfallplan/incidents", json={
"title": "High Risk",
"art34_required": True,
"art34_justification": "Hohe Risikobewertung",
})
assert resp.status_code == 201
assert resp.json()["art34_required"] is True
def test_create_incident_passes_tenant_id(self, mock_db):
row = make_incident_row()
mock_db.execute.return_value.fetchone.return_value = row
client.post("/notfallplan/incidents",
json={"title": "T"},
headers={"X-Tenant-ID": "custom-org"})
call_params = mock_db.execute.call_args[0][1]
assert call_params["tenant_id"] == "custom-org"
# =============================================================================
# Incidents — PUT /notfallplan/incidents/{id}
# =============================================================================
class TestUpdateIncident:
def test_update_incident_success(self, mock_db):
existing = MagicMock()
updated_row = make_incident_row({"status": "assessed"})
mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
resp = client.put(f"/notfallplan/incidents/{INCIDENT_ID}", json={"status": "assessed"})
assert resp.status_code == 200
assert resp.json()["status"] == "assessed"
def test_update_incident_not_found_returns_404(self, mock_db):
mock_db.execute.return_value.fetchone.return_value = None
resp = client.put(f"/notfallplan/incidents/{UNKNOWN_ID}", json={"status": "closed"})
assert resp.status_code == 404
def test_update_incident_empty_body_returns_400(self, mock_db):
existing = MagicMock()
mock_db.execute.return_value.fetchone.return_value = existing
resp = client.put(f"/notfallplan/incidents/{INCIDENT_ID}", json={})
assert resp.status_code == 400
def test_update_incident_status_to_reported_auto_sets_timestamp(self, mock_db):
existing = MagicMock()
updated_row = make_incident_row({"status": "reported"})
mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
resp = client.put(f"/notfallplan/incidents/{INCIDENT_ID}", json={"status": "reported"})
assert resp.status_code == 200
call_params = mock_db.execute.call_args[0][1]
assert "reported_to_authority_at" in call_params
def test_update_incident_status_to_closed_auto_sets_closed_at(self, mock_db):
existing = MagicMock()
updated_row = make_incident_row({"status": "closed"})
mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
resp = client.put(f"/notfallplan/incidents/{INCIDENT_ID}", json={"status": "closed"})
assert resp.status_code == 200
call_params = mock_db.execute.call_args[0][1]
assert "closed_at" in call_params
def test_update_incident_lessons_learned(self, mock_db):
existing = MagicMock()
updated_row = make_incident_row({"lessons_learned": "Besseres Monitoring nötig"})
mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
resp = client.put(
f"/notfallplan/incidents/{INCIDENT_ID}",
json={"lessons_learned": "Besseres Monitoring nötig"},
)
assert resp.status_code == 200
assert resp.json()["lessons_learned"] == "Besseres Monitoring nötig"
def test_update_incident_severity(self, mock_db):
existing = MagicMock()
updated_row = make_incident_row({"severity": "high"})
mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
resp = client.put(f"/notfallplan/incidents/{INCIDENT_ID}", json={"severity": "high"})
assert resp.status_code == 200
assert resp.json()["severity"] == "high"
def test_update_incident_commits(self, mock_db):
existing = MagicMock()
updated_row = make_incident_row()
mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
client.put(f"/notfallplan/incidents/{INCIDENT_ID}", json={"severity": "low"})
mock_db.commit.assert_called()
def test_update_incident_always_sets_updated_at(self, mock_db):
existing = MagicMock()
updated_row = make_incident_row()
mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
client.put(f"/notfallplan/incidents/{INCIDENT_ID}", json={"title": "Renamed"})
call_params = mock_db.execute.call_args[0][1]
assert "updated_at" in call_params
# =============================================================================
# Incidents — DELETE /notfallplan/incidents/{id}
# =============================================================================
class TestDeleteIncident:
def test_delete_incident_success_returns_204(self, mock_db):
mock_db.execute.return_value.rowcount = 1
resp = client.delete(f"/notfallplan/incidents/{INCIDENT_ID}")
assert resp.status_code == 204
def test_delete_incident_not_found_returns_404(self, mock_db):
mock_db.execute.return_value.rowcount = 0
resp = client.delete(f"/notfallplan/incidents/{UNKNOWN_ID}")
assert resp.status_code == 404
def test_delete_incident_commits(self, mock_db):
mock_db.execute.return_value.rowcount = 1
client.delete(f"/notfallplan/incidents/{INCIDENT_ID}")
mock_db.commit.assert_called_once()
def test_delete_incident_commits_even_when_not_found(self, mock_db):
# Commit is called before the rowcount check in the implementation
mock_db.execute.return_value.rowcount = 0
client.delete(f"/notfallplan/incidents/{UNKNOWN_ID}")
mock_db.commit.assert_called_once()
def test_delete_incident_passes_tenant_id(self, mock_db):
mock_db.execute.return_value.rowcount = 1
client.delete(
f"/notfallplan/incidents/{INCIDENT_ID}",
headers={"X-Tenant-ID": "acme"},
)
call_params = mock_db.execute.call_args[0][1]
assert call_params["tenant_id"] == "acme"
# =============================================================================
# Templates — GET /notfallplan/templates
# =============================================================================
class TestListTemplates:
def test_list_templates_returns_empty_list(self, mock_db):
mock_db.execute.return_value.fetchall.return_value = []
resp = client.get("/notfallplan/templates")
assert resp.status_code == 200
assert resp.json() == []
def test_list_templates_returns_one(self, mock_db):
row = make_template_row()
mock_db.execute.return_value.fetchall.return_value = [row]
resp = client.get("/notfallplan/templates")
assert resp.status_code == 200
data = resp.json()
assert len(data) == 1
assert data[0]["title"] == "Art. 33 Template"
assert data[0]["type"] == "art33"
def test_list_templates_returns_multiple(self, mock_db):
rows = [
make_template_row({"id": "id-1", "type": "art33", "title": "Meldung Art.33"}),
make_template_row({"id": "id-2", "type": "art34", "title": "Meldung Art.34"}),
]
mock_db.execute.return_value.fetchall.return_value = rows
resp = client.get("/notfallplan/templates")
assert resp.status_code == 200
assert len(resp.json()) == 2
def test_list_templates_filter_by_type(self, mock_db):
mock_db.execute.return_value.fetchall.return_value = []
resp = client.get("/notfallplan/templates?type=art34")
assert resp.status_code == 200
call_params = mock_db.execute.call_args[0][1]
assert call_params.get("type") == "art34"
def test_list_templates_without_type_no_type_param_sent(self, mock_db):
mock_db.execute.return_value.fetchall.return_value = []
resp = client.get("/notfallplan/templates")
assert resp.status_code == 200
call_params = mock_db.execute.call_args[0][1]
assert "type" not in call_params
def test_list_templates_uses_default_tenant(self, mock_db):
mock_db.execute.return_value.fetchall.return_value = []
client.get("/notfallplan/templates")
call_params = mock_db.execute.call_args[0][1]
assert call_params["tenant_id"] == DEFAULT_TENANT
def test_list_templates_custom_tenant_header(self, mock_db):
mock_db.execute.return_value.fetchall.return_value = []
client.get("/notfallplan/templates", headers={"X-Tenant-ID": "acme"})
call_params = mock_db.execute.call_args[0][1]
assert call_params["tenant_id"] == "acme"
def test_list_templates_all_fields_present(self, mock_db):
row = make_template_row()
mock_db.execute.return_value.fetchall.return_value = [row]
resp = client.get("/notfallplan/templates")
item = resp.json()[0]
for field in ("id", "tenant_id", "type", "title", "content", "created_at", "updated_at"):
assert field in item, f"Missing field: {field}"
# =============================================================================
# Templates — POST /notfallplan/templates
# =============================================================================
class TestCreateTemplate:
def test_create_template_success(self, mock_db):
row = make_template_row()
mock_db.execute.return_value.fetchone.return_value = row
resp = client.post("/notfallplan/templates", json={
"title": "Art. 33 Template",
"content": "Sehr geehrte Behoerde...",
"type": "art33",
})
assert resp.status_code == 201
assert resp.json()["title"] == "Art. 33 Template"
def test_create_template_missing_title_returns_422(self, mock_db):
resp = client.post("/notfallplan/templates", json={
"content": "Some content",
"type": "art33",
})
assert resp.status_code == 422
def test_create_template_missing_content_returns_422(self, mock_db):
resp = client.post("/notfallplan/templates", json={
"title": "Template",
"type": "art33",
})
assert resp.status_code == 422
def test_create_template_missing_type_uses_default_art33(self, mock_db):
row = make_template_row()
mock_db.execute.return_value.fetchone.return_value = row
resp = client.post("/notfallplan/templates", json={"title": "T", "content": "C"})
assert resp.status_code == 201
call_params = mock_db.execute.call_args[0][1]
assert call_params["type"] == "art33"
def test_create_template_art34_type(self, mock_db):
row = make_template_row({"type": "art34"})
mock_db.execute.return_value.fetchone.return_value = row
resp = client.post("/notfallplan/templates", json={
"title": "Art. 34 Notification",
"content": "Sehr geehrte Betroffene...",
"type": "art34",
})
assert resp.status_code == 201
assert resp.json()["type"] == "art34"
def test_create_template_commits(self, mock_db):
row = make_template_row()
mock_db.execute.return_value.fetchone.return_value = row
client.post("/notfallplan/templates", json={"title": "T", "content": "C", "type": "art33"})
mock_db.commit.assert_called_once()
def test_create_template_passes_tenant_id(self, mock_db):
row = make_template_row()
mock_db.execute.return_value.fetchone.return_value = row
client.post(
"/notfallplan/templates",
json={"title": "T", "content": "C", "type": "art33"},
headers={"X-Tenant-ID": "my-org"},
)
call_params = mock_db.execute.call_args[0][1]
assert call_params["tenant_id"] == "my-org"
# =============================================================================
# Templates — PUT /notfallplan/templates/{id}
# =============================================================================
class TestUpdateTemplate:
def test_update_template_title_success(self, mock_db):
existing = MagicMock()
updated_row = make_template_row({"title": "Updated Title"})
mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
resp = client.put(f"/notfallplan/templates/{TEMPLATE_ID}", json={"title": "Updated Title"})
assert resp.status_code == 200
assert resp.json()["title"] == "Updated Title"
def test_update_template_not_found_returns_404(self, mock_db):
mock_db.execute.return_value.fetchone.return_value = None
resp = client.put(f"/notfallplan/templates/{UNKNOWN_ID}", json={"title": "X"})
assert resp.status_code == 404
def test_update_template_empty_body_returns_400(self, mock_db):
existing = MagicMock()
mock_db.execute.return_value.fetchone.return_value = existing
resp = client.put(f"/notfallplan/templates/{TEMPLATE_ID}", json={})
assert resp.status_code == 400
def test_update_template_content(self, mock_db):
existing = MagicMock()
updated_row = make_template_row({"content": "New body text"})
mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
resp = client.put(
f"/notfallplan/templates/{TEMPLATE_ID}",
json={"content": "New body text"},
)
assert resp.status_code == 200
assert resp.json()["content"] == "New body text"
def test_update_template_type(self, mock_db):
existing = MagicMock()
updated_row = make_template_row({"type": "internal"})
mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
resp = client.put(f"/notfallplan/templates/{TEMPLATE_ID}", json={"type": "internal"})
assert resp.status_code == 200
def test_update_template_commits(self, mock_db):
existing = MagicMock()
updated_row = make_template_row()
mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
client.put(f"/notfallplan/templates/{TEMPLATE_ID}", json={"title": "New"})
mock_db.commit.assert_called()
def test_update_template_sets_updated_at(self, mock_db):
existing = MagicMock()
updated_row = make_template_row()
mock_db.execute.return_value.fetchone.side_effect = [existing, updated_row]
client.put(f"/notfallplan/templates/{TEMPLATE_ID}", json={"title": "New"})
call_params = mock_db.execute.call_args[0][1]
assert "updated_at" in call_params
# =============================================================================
# Templates — DELETE /notfallplan/templates/{id}
# =============================================================================
class TestDeleteTemplate:
def test_delete_template_success_returns_204(self, mock_db):
mock_db.execute.return_value.rowcount = 1
resp = client.delete(f"/notfallplan/templates/{TEMPLATE_ID}")
assert resp.status_code == 204
def test_delete_template_not_found_returns_404(self, mock_db):
mock_db.execute.return_value.rowcount = 0
resp = client.delete(f"/notfallplan/templates/{UNKNOWN_ID}")
assert resp.status_code == 404
def test_delete_template_commits_on_success(self, mock_db):
mock_db.execute.return_value.rowcount = 1
client.delete(f"/notfallplan/templates/{TEMPLATE_ID}")
mock_db.commit.assert_called_once()
def test_delete_template_commits_even_when_not_found(self, mock_db):
mock_db.execute.return_value.rowcount = 0
client.delete(f"/notfallplan/templates/{UNKNOWN_ID}")
mock_db.commit.assert_called_once()
def test_delete_template_passes_correct_tenant_id(self, mock_db):
mock_db.execute.return_value.rowcount = 1
client.delete(
f"/notfallplan/templates/{TEMPLATE_ID}",
headers={"X-Tenant-ID": "acme"},
)
call_params = mock_db.execute.call_args[0][1]
assert call_params["tenant_id"] == "acme"