"""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"