""" Tests for E-Mail-Template routes. Pattern: app.dependency_overrides[get_db] for FastAPI DI. """ import uuid import os import sys from datetime import datetime import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker # Ensure backend dir is on path sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) from classroom_engine.database import Base, get_db from compliance.db.email_template_models import ( EmailTemplateDB, EmailTemplateVersionDB, EmailTemplateApprovalDB, EmailSendLogDB, EmailTemplateSettingsDB, ) from compliance.api.email_template_routes import router as email_template_router # In-memory SQLite for testing SQLALCHEMY_DATABASE_URL = "sqlite:///./test_email_templates.db" engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) TENANT_ID = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e" HEADERS = {"X-Tenant-ID": TENANT_ID} app = FastAPI() app.include_router(email_template_router, prefix="/api/compliance") def override_get_db(): db = TestingSessionLocal() try: yield db finally: db.close() app.dependency_overrides[get_db] = override_get_db client = TestClient(app) @pytest.fixture(autouse=True) def setup_db(): """Create all tables before each test, drop after.""" Base.metadata.create_all(bind=engine) yield Base.metadata.drop_all(bind=engine) # ============================================================================= # Helper # ============================================================================= def _create_template(template_type="welcome", name=None): """Create a template and return the response dict.""" body = {"template_type": template_type} if name: body["name"] = name r = client.post("/api/compliance/email-templates", json=body, headers=HEADERS) assert r.status_code == 200, r.text return r.json() def _create_version(template_id, subject="Test Betreff", body_html="

Hallo

"): """Create a version for a template and return the response dict.""" r = client.post( f"/api/compliance/email-templates/{template_id}/versions", json={"subject": subject, "body_html": body_html, "version": "1.0"}, headers=HEADERS, ) assert r.status_code == 200, r.text return r.json() # ============================================================================= # Template Types # ============================================================================= class TestTemplateTypes: def test_get_types(self): r = client.get("/api/compliance/email-templates/types") assert r.status_code == 200 types = r.json() assert len(types) == 20 names = [t["type"] for t in types] assert "welcome" in names assert "dsr_receipt" in names assert "breach_notification_authority" in names def test_types_have_variables(self): r = client.get("/api/compliance/email-templates/types") types = r.json() welcome = [t for t in types if t["type"] == "welcome"][0] assert "user_name" in welcome["variables"] assert welcome["category"] == "general" # ============================================================================= # Template CRUD # ============================================================================= class TestCreateTemplate: def test_create_template(self): t = _create_template("welcome") assert t["template_type"] == "welcome" assert t["name"] == "Willkommen" assert t["category"] == "general" assert t["is_active"] is True assert "id" in t def test_create_with_custom_name(self): t = _create_template("welcome", name="Custom Name") assert t["name"] == "Custom Name" def test_create_duplicate_type(self): _create_template("welcome") r = client.post("/api/compliance/email-templates", json={"template_type": "welcome"}, headers=HEADERS) assert r.status_code == 409 def test_create_unknown_type(self): r = client.post("/api/compliance/email-templates", json={"template_type": "nonexistent"}, headers=HEADERS) assert r.status_code == 400 def test_create_with_description(self): r = client.post("/api/compliance/email-templates", json={ "template_type": "dsr_receipt", "description": "DSR Eingangsbestaetigung Template", }, headers=HEADERS) assert r.status_code == 200 assert r.json()["description"] == "DSR Eingangsbestaetigung Template" class TestListTemplates: def test_list_empty(self): r = client.get("/api/compliance/email-templates", headers=HEADERS) assert r.status_code == 200 assert r.json() == [] def test_list_templates(self): _create_template("welcome") _create_template("dsr_receipt") r = client.get("/api/compliance/email-templates", headers=HEADERS) assert r.status_code == 200 assert len(r.json()) == 2 def test_list_by_category(self): _create_template("welcome") # general _create_template("dsr_receipt") # dsr r = client.get("/api/compliance/email-templates?category=dsr", headers=HEADERS) assert r.status_code == 200 data = r.json() assert len(data) == 1 assert data[0]["category"] == "dsr" def test_list_with_latest_version(self): t = _create_template("welcome") _create_version(t["id"], subject="Version 1") r = client.get("/api/compliance/email-templates", headers=HEADERS) data = r.json() assert data[0]["latest_version"] is not None assert data[0]["latest_version"]["subject"] == "Version 1" class TestGetTemplate: def test_get_template(self): t = _create_template("welcome") r = client.get(f"/api/compliance/email-templates/{t['id']}", headers=HEADERS) assert r.status_code == 200 assert r.json()["template_type"] == "welcome" def test_get_not_found(self): fake_id = str(uuid.uuid4()) r = client.get(f"/api/compliance/email-templates/{fake_id}", headers=HEADERS) assert r.status_code == 404 def test_get_invalid_id(self): r = client.get("/api/compliance/email-templates/not-a-uuid", headers=HEADERS) assert r.status_code == 400 # ============================================================================= # Default Content # ============================================================================= class TestDefaultContent: def test_get_default_content(self): r = client.get("/api/compliance/email-templates/default/welcome") assert r.status_code == 200 data = r.json() assert data["template_type"] == "welcome" assert "variables" in data assert "default_subject" in data assert "default_body_html" in data def test_get_default_unknown_type(self): r = client.get("/api/compliance/email-templates/default/nonexistent") assert r.status_code == 404 # ============================================================================= # Initialize Defaults # ============================================================================= class TestInitialize: def test_initialize_defaults(self): r = client.post("/api/compliance/email-templates/initialize", headers=HEADERS) assert r.status_code == 200 data = r.json() assert data["count"] == 20 def test_initialize_idempotent(self): client.post("/api/compliance/email-templates/initialize", headers=HEADERS) r = client.post("/api/compliance/email-templates/initialize", headers=HEADERS) assert r.status_code == 200 assert "already initialized" in r.json()["message"] # ============================================================================= # Version Management # ============================================================================= class TestVersionCreate: def test_create_version_via_path(self): t = _create_template("welcome") v = _create_version(t["id"]) assert v["subject"] == "Test Betreff" assert v["status"] == "draft" assert v["template_id"] == t["id"] def test_create_version_via_query(self): t = _create_template("welcome") r = client.post( f"/api/compliance/email-templates/versions?template_id={t['id']}", json={"subject": "Query-Version", "body_html": "

Test

"}, headers=HEADERS, ) assert r.status_code == 200 assert r.json()["subject"] == "Query-Version" def test_create_version_template_not_found(self): fake_id = str(uuid.uuid4()) r = client.post( f"/api/compliance/email-templates/{fake_id}/versions", json={"subject": "S", "body_html": "

B

"}, headers=HEADERS, ) assert r.status_code == 404 class TestVersionGet: def test_get_versions(self): t = _create_template("welcome") _create_version(t["id"], subject="V1") _create_version(t["id"], subject="V2") r = client.get(f"/api/compliance/email-templates/{t['id']}/versions", headers=HEADERS) assert r.status_code == 200 data = r.json() assert len(data) == 2 def test_get_version_detail(self): t = _create_template("welcome") v = _create_version(t["id"]) r = client.get(f"/api/compliance/email-templates/versions/{v['id']}") assert r.status_code == 200 assert r.json()["subject"] == "Test Betreff" def test_get_version_not_found(self): r = client.get(f"/api/compliance/email-templates/versions/{uuid.uuid4()}") assert r.status_code == 404 class TestVersionUpdate: def test_update_draft(self): t = _create_template("welcome") v = _create_version(t["id"]) r = client.put( f"/api/compliance/email-templates/versions/{v['id']}", json={"subject": "Updated Subject", "body_html": "

Neu

"}, ) assert r.status_code == 200 assert r.json()["subject"] == "Updated Subject" def test_update_non_draft_fails(self): t = _create_template("welcome") v = _create_version(t["id"]) # Submit to review client.post(f"/api/compliance/email-templates/versions/{v['id']}/submit") # Try to update r = client.put( f"/api/compliance/email-templates/versions/{v['id']}", json={"subject": "Should Fail"}, ) assert r.status_code == 400 # ============================================================================= # Approval Workflow # ============================================================================= class TestWorkflow: def test_submit_for_review(self): t = _create_template("welcome") v = _create_version(t["id"]) r = client.post(f"/api/compliance/email-templates/versions/{v['id']}/submit") assert r.status_code == 200 assert r.json()["status"] == "review" assert r.json()["submitted_at"] is not None def test_approve_version(self): t = _create_template("welcome") v = _create_version(t["id"]) client.post(f"/api/compliance/email-templates/versions/{v['id']}/submit") r = client.post(f"/api/compliance/email-templates/versions/{v['id']}/approve") assert r.status_code == 200 assert r.json()["status"] == "approved" def test_reject_version(self): t = _create_template("welcome") v = _create_version(t["id"]) client.post(f"/api/compliance/email-templates/versions/{v['id']}/submit") r = client.post(f"/api/compliance/email-templates/versions/{v['id']}/reject") assert r.status_code == 200 assert r.json()["status"] == "draft" # back to draft def test_publish_version(self): t = _create_template("welcome") v = _create_version(t["id"]) client.post(f"/api/compliance/email-templates/versions/{v['id']}/submit") client.post(f"/api/compliance/email-templates/versions/{v['id']}/approve") r = client.post(f"/api/compliance/email-templates/versions/{v['id']}/publish") assert r.status_code == 200 assert r.json()["status"] == "published" assert r.json()["published_at"] is not None def test_publish_draft_directly(self): """Publishing from draft is allowed (shortcut for admins).""" t = _create_template("welcome") v = _create_version(t["id"]) r = client.post(f"/api/compliance/email-templates/versions/{v['id']}/publish") assert r.status_code == 200 assert r.json()["status"] == "published" def test_submit_non_draft_fails(self): t = _create_template("welcome") v = _create_version(t["id"]) client.post(f"/api/compliance/email-templates/versions/{v['id']}/submit") r = client.post(f"/api/compliance/email-templates/versions/{v['id']}/submit") assert r.status_code == 400 def test_approve_non_review_fails(self): t = _create_template("welcome") v = _create_version(t["id"]) r = client.post(f"/api/compliance/email-templates/versions/{v['id']}/approve") assert r.status_code == 400 def test_full_workflow(self): """Full cycle: create → submit → approve → publish.""" t = _create_template("welcome") v = _create_version(t["id"], subject="Workflow Test") vid = v["id"] # Draft assert v["status"] == "draft" # Submit r = client.post(f"/api/compliance/email-templates/versions/{vid}/submit") assert r.json()["status"] == "review" # Approve r = client.post(f"/api/compliance/email-templates/versions/{vid}/approve") assert r.json()["status"] == "approved" # Publish r = client.post(f"/api/compliance/email-templates/versions/{vid}/publish") assert r.json()["status"] == "published" # ============================================================================= # Preview & Send Test # ============================================================================= class TestPreview: def test_preview_with_variables(self): t = _create_template("welcome") v = _create_version(t["id"], subject="Hallo {{user_name}}", body_html="

Willkommen {{user_name}} bei {{company_name}}

") r = client.post( f"/api/compliance/email-templates/versions/{v['id']}/preview", json={"variables": {"user_name": "Max", "company_name": "ACME"}}, ) assert r.status_code == 200 data = r.json() assert data["subject"] == "Hallo Max" assert "Willkommen Max bei ACME" in data["body_html"] def test_preview_with_defaults(self): t = _create_template("welcome") v = _create_version(t["id"], subject="Hi {{user_name}}", body_html="

{{company_name}}

") r = client.post( f"/api/compliance/email-templates/versions/{v['id']}/preview", json={}, ) assert r.status_code == 200 data = r.json() # Default placeholders assert "[user_name]" in data["subject"] def test_preview_not_found(self): r = client.post( f"/api/compliance/email-templates/versions/{uuid.uuid4()}/preview", json={}, ) assert r.status_code == 404 class TestSendTest: def test_send_test_email(self): t = _create_template("welcome") v = _create_version(t["id"], subject="Test {{user_name}}") r = client.post( f"/api/compliance/email-templates/versions/{v['id']}/send-test", json={"recipient": "test@example.de", "variables": {"user_name": "Max"}}, headers=HEADERS, ) assert r.status_code == 200 data = r.json() assert data["success"] is True assert "test@example.de" in data["message"] def test_send_test_creates_log(self): t = _create_template("welcome") v = _create_version(t["id"], subject="Log Test") client.post( f"/api/compliance/email-templates/versions/{v['id']}/send-test", json={"recipient": "log@example.de"}, headers=HEADERS, ) # Check logs r = client.get("/api/compliance/email-templates/logs", headers=HEADERS) assert r.status_code == 200 logs = r.json()["logs"] assert len(logs) == 1 assert logs[0]["recipient"] == "log@example.de" assert logs[0]["status"] == "test_sent" # ============================================================================= # Settings # ============================================================================= class TestSettings: def test_get_default_settings(self): r = client.get("/api/compliance/email-templates/settings", headers=HEADERS) assert r.status_code == 200 data = r.json() assert data["sender_name"] == "Datenschutzbeauftragter" assert data["primary_color"] == "#4F46E5" def test_update_settings(self): r = client.put( "/api/compliance/email-templates/settings", json={"sender_name": "DSB Max", "company_name": "ACME GmbH", "primary_color": "#FF0000"}, headers=HEADERS, ) assert r.status_code == 200 data = r.json() assert data["sender_name"] == "DSB Max" assert data["company_name"] == "ACME GmbH" assert data["primary_color"] == "#FF0000" def test_update_settings_partial(self): # First create client.put( "/api/compliance/email-templates/settings", json={"sender_name": "DSB", "company_name": "Test"}, headers=HEADERS, ) # Then partial update r = client.put( "/api/compliance/email-templates/settings", json={"company_name": "Neue Firma"}, headers=HEADERS, ) assert r.status_code == 200 data = r.json() assert data["sender_name"] == "DSB" # unchanged assert data["company_name"] == "Neue Firma" # ============================================================================= # Logs # ============================================================================= class TestLogs: def test_logs_empty(self): r = client.get("/api/compliance/email-templates/logs", headers=HEADERS) assert r.status_code == 200 assert r.json()["logs"] == [] assert r.json()["total"] == 0 def test_logs_pagination(self): # Create some logs via send-test t = _create_template("welcome") v = _create_version(t["id"], subject="Pagination") for i in range(5): client.post( f"/api/compliance/email-templates/versions/{v['id']}/send-test", json={"recipient": f"user{i}@example.de"}, headers=HEADERS, ) r = client.get("/api/compliance/email-templates/logs?limit=2&offset=0", headers=HEADERS) data = r.json() assert data["total"] == 5 assert len(data["logs"]) == 2 def test_logs_filter_by_type(self): t1 = _create_template("welcome") t2 = _create_template("dsr_receipt") v1 = _create_version(t1["id"], subject="W") v2 = _create_version(t2["id"], subject="D") client.post( f"/api/compliance/email-templates/versions/{v1['id']}/send-test", json={"recipient": "a@b.de"}, headers=HEADERS, ) client.post( f"/api/compliance/email-templates/versions/{v2['id']}/send-test", json={"recipient": "c@d.de"}, headers=HEADERS, ) r = client.get("/api/compliance/email-templates/logs?template_type=dsr_receipt", headers=HEADERS) data = r.json() assert data["total"] == 1 assert data["logs"][0]["template_type"] == "dsr_receipt" # ============================================================================= # Stats # ============================================================================= class TestStats: def test_stats_empty(self): r = client.get("/api/compliance/email-templates/stats", headers=HEADERS) assert r.status_code == 200 data = r.json() assert data["total"] == 0 assert data["active"] == 0 assert data["published"] == 0 assert data["total_sent"] == 0 def test_stats_with_data(self): t = _create_template("welcome") v = _create_version(t["id"]) # Publish the version client.post(f"/api/compliance/email-templates/versions/{v['id']}/publish") # Send a test client.post( f"/api/compliance/email-templates/versions/{v['id']}/send-test", json={"recipient": "stats@test.de"}, headers=HEADERS, ) r = client.get("/api/compliance/email-templates/stats", headers=HEADERS) data = r.json() assert data["total"] == 1 assert data["active"] == 1 assert data["published"] == 1 assert data["total_sent"] == 1 assert data["by_category"]["general"] == 1