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 36s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 18s
5-Phasen-Migration: Go consent-service Proxies durch native Python/FastAPI ersetzt. Phase 1 — DSR (Betroffenenrechte): 6 Tabellen, 30 Endpoints, Frontend-API umgestellt Phase 2 — E-Mail-Templates: 5 Tabellen, 20 Endpoints, neues Frontend, SDK_STEPS erweitert Phase 3 — Legal Documents Extension: User Consents, Audit Log, Cookie-Kategorien Phase 4 — Banner Consent: Device-Consents, Site-Configs, Kategorien, Vendors Phase 5 — Cleanup: DSR-Proxy aus main.py entfernt, Frontend-URLs aktualisiert 148 neue Tests (50 + 47 + 26 + 25), alle bestanden. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
574 lines
21 KiB
Python
574 lines
21 KiB
Python
"""
|
|
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="<p>Hallo</p>"):
|
|
"""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": "<p>Test</p>"},
|
|
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": "<p>B</p>"},
|
|
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": "<p>Neu</p>"},
|
|
)
|
|
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="<p>Willkommen {{user_name}} bei {{company_name}}</p>")
|
|
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="<p>{{company_name}}</p>")
|
|
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
|