A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
543 lines
19 KiB
Python
543 lines
19 KiB
Python
"""
|
|
Mail Test API - Test Runner fuer E-Mail Integration (IMAP/SMTP/KI-Analyse)
|
|
Endpoint: /api/admin/mail-tests
|
|
"""
|
|
|
|
from fastapi import APIRouter
|
|
from pydantic import BaseModel
|
|
from typing import List, Optional, Literal
|
|
import httpx
|
|
import asyncio
|
|
import time
|
|
import os
|
|
import socket
|
|
|
|
router = APIRouter(prefix="/api/admin/mail-tests", tags=["Mail Tests"])
|
|
|
|
# ==============================================
|
|
# Models
|
|
# ==============================================
|
|
|
|
class TestResult(BaseModel):
|
|
name: str
|
|
description: str
|
|
expected: str
|
|
actual: str
|
|
status: Literal["passed", "failed", "pending", "skipped"]
|
|
duration_ms: float
|
|
error_message: Optional[str] = None
|
|
|
|
|
|
class TestCategoryResult(BaseModel):
|
|
category: str
|
|
display_name: str
|
|
description: str
|
|
tests: List[TestResult]
|
|
passed: int
|
|
failed: int
|
|
total: int
|
|
|
|
|
|
class FullTestResults(BaseModel):
|
|
categories: List[TestCategoryResult]
|
|
total_passed: int
|
|
total_failed: int
|
|
total_tests: int
|
|
duration_ms: float
|
|
|
|
|
|
# ==============================================
|
|
# Configuration
|
|
# ==============================================
|
|
|
|
MAILPIT_URL = os.getenv("MAILPIT_URL", "http://mailpit:8025")
|
|
SMTP_HOST = os.getenv("SMTP_HOST", "mailpit")
|
|
SMTP_PORT = int(os.getenv("SMTP_PORT", "1025"))
|
|
IMAP_HOST = os.getenv("IMAP_HOST", "mailpit")
|
|
IMAP_PORT = int(os.getenv("IMAP_PORT", "1143"))
|
|
BACKEND_URL = os.getenv("BACKEND_URL", "http://localhost:8000")
|
|
|
|
|
|
# ==============================================
|
|
# Test Implementations
|
|
# ==============================================
|
|
|
|
async def test_smtp_connection() -> TestResult:
|
|
"""Test SMTP Server Connection"""
|
|
start = time.time()
|
|
try:
|
|
# Try to connect to SMTP port
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.settimeout(5)
|
|
result = sock.connect_ex((SMTP_HOST, SMTP_PORT))
|
|
sock.close()
|
|
duration = (time.time() - start) * 1000
|
|
|
|
if result == 0:
|
|
return TestResult(
|
|
name="SMTP Server Verbindung",
|
|
description="Prueft ob der SMTP Server fuer ausgehende E-Mails erreichbar ist",
|
|
expected=f"Verbindung zu {SMTP_HOST}:{SMTP_PORT}",
|
|
actual=f"SMTP Server erreichbar",
|
|
status="passed",
|
|
duration_ms=duration
|
|
)
|
|
else:
|
|
return TestResult(
|
|
name="SMTP Server Verbindung",
|
|
description="Prueft ob der SMTP Server fuer ausgehende E-Mails erreichbar ist",
|
|
expected=f"Verbindung zu {SMTP_HOST}:{SMTP_PORT}",
|
|
actual=f"Verbindung fehlgeschlagen (Code: {result})",
|
|
status="failed",
|
|
duration_ms=duration,
|
|
error_message=f"Socket-Fehler: {result}"
|
|
)
|
|
except Exception as e:
|
|
return TestResult(
|
|
name="SMTP Server Verbindung",
|
|
description="Prueft ob der SMTP Server fuer ausgehende E-Mails erreichbar ist",
|
|
expected=f"Verbindung zu {SMTP_HOST}:{SMTP_PORT}",
|
|
actual=f"Fehler: {str(e)}",
|
|
status="failed",
|
|
duration_ms=(time.time() - start) * 1000,
|
|
error_message=str(e)
|
|
)
|
|
|
|
|
|
async def test_imap_connection() -> TestResult:
|
|
"""Test IMAP Server Connection"""
|
|
start = time.time()
|
|
try:
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.settimeout(5)
|
|
result = sock.connect_ex((IMAP_HOST, IMAP_PORT))
|
|
sock.close()
|
|
duration = (time.time() - start) * 1000
|
|
|
|
if result == 0:
|
|
return TestResult(
|
|
name="IMAP Server Verbindung",
|
|
description="Prueft ob der IMAP Server fuer eingehende E-Mails erreichbar ist",
|
|
expected=f"Verbindung zu {IMAP_HOST}:{IMAP_PORT}",
|
|
actual=f"IMAP Server erreichbar",
|
|
status="passed",
|
|
duration_ms=duration
|
|
)
|
|
else:
|
|
return TestResult(
|
|
name="IMAP Server Verbindung",
|
|
description="Prueft ob der IMAP Server fuer eingehende E-Mails erreichbar ist",
|
|
expected=f"Verbindung zu {IMAP_HOST}:{IMAP_PORT}",
|
|
actual=f"Verbindung fehlgeschlagen",
|
|
status="skipped",
|
|
duration_ms=duration,
|
|
error_message=f"IMAP nicht konfiguriert oder nicht erreichbar"
|
|
)
|
|
except Exception as e:
|
|
return TestResult(
|
|
name="IMAP Server Verbindung",
|
|
description="Prueft ob der IMAP Server fuer eingehende E-Mails erreichbar ist",
|
|
expected=f"Verbindung zu {IMAP_HOST}:{IMAP_PORT}",
|
|
actual=f"Nicht verfuegbar",
|
|
status="skipped",
|
|
duration_ms=(time.time() - start) * 1000,
|
|
error_message=str(e)
|
|
)
|
|
|
|
|
|
async def test_mailpit_api() -> TestResult:
|
|
"""Test Mailpit Web API (Development Mail Catcher)"""
|
|
start = time.time()
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
response = await client.get(f"{MAILPIT_URL}/api/v1/info")
|
|
duration = (time.time() - start) * 1000
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
version = data.get("Version", "unknown")
|
|
return TestResult(
|
|
name="Mailpit Web API",
|
|
description="Prueft ob die Mailpit Test-Oberflaeche verfuegbar ist",
|
|
expected="HTTP 200 mit Version-Info",
|
|
actual=f"Mailpit v{version} aktiv",
|
|
status="passed",
|
|
duration_ms=duration
|
|
)
|
|
else:
|
|
return TestResult(
|
|
name="Mailpit Web API",
|
|
description="Prueft ob die Mailpit Test-Oberflaeche verfuegbar ist",
|
|
expected="HTTP 200 mit Version-Info",
|
|
actual=f"HTTP {response.status_code}",
|
|
status="failed",
|
|
duration_ms=duration,
|
|
error_message=f"Unerwarteter Status: {response.status_code}"
|
|
)
|
|
except Exception as e:
|
|
return TestResult(
|
|
name="Mailpit Web API",
|
|
description="Prueft ob die Mailpit Test-Oberflaeche verfuegbar ist",
|
|
expected="HTTP 200 mit Version-Info",
|
|
actual=f"Fehler: {str(e)}",
|
|
status="failed",
|
|
duration_ms=(time.time() - start) * 1000,
|
|
error_message=str(e)
|
|
)
|
|
|
|
|
|
async def test_mailpit_messages() -> TestResult:
|
|
"""Test Mailpit Message Storage"""
|
|
start = time.time()
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
response = await client.get(f"{MAILPIT_URL}/api/v1/messages")
|
|
duration = (time.time() - start) * 1000
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
total = data.get("total", 0)
|
|
return TestResult(
|
|
name="Mailpit Message Storage",
|
|
description="Prueft ob E-Mails im Entwicklungsmodus abgefangen werden",
|
|
expected="Nachrichtenliste abrufbar",
|
|
actual=f"{total} Nachrichten gespeichert",
|
|
status="passed",
|
|
duration_ms=duration
|
|
)
|
|
else:
|
|
return TestResult(
|
|
name="Mailpit Message Storage",
|
|
description="Prueft ob E-Mails im Entwicklungsmodus abgefangen werden",
|
|
expected="Nachrichtenliste abrufbar",
|
|
actual=f"HTTP {response.status_code}",
|
|
status="failed",
|
|
duration_ms=duration,
|
|
error_message=f"API-Fehler: {response.status_code}"
|
|
)
|
|
except Exception as e:
|
|
return TestResult(
|
|
name="Mailpit Message Storage",
|
|
description="Prueft ob E-Mails im Entwicklungsmodus abgefangen werden",
|
|
expected="Nachrichtenliste abrufbar",
|
|
actual=f"Fehler: {str(e)}",
|
|
status="failed",
|
|
duration_ms=(time.time() - start) * 1000,
|
|
error_message=str(e)
|
|
)
|
|
|
|
|
|
async def test_email_templates_api() -> TestResult:
|
|
"""Test Email Templates API"""
|
|
start = time.time()
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
response = await client.get(f"{BACKEND_URL}/api/email-templates")
|
|
duration = (time.time() - start) * 1000
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
count = len(data) if isinstance(data, list) else data.get("total", 0)
|
|
return TestResult(
|
|
name="E-Mail Templates API",
|
|
description="Prueft ob die E-Mail-Vorlagen-Verwaltung verfuegbar ist",
|
|
expected="Template-Liste abrufbar",
|
|
actual=f"{count} Templates verfuegbar",
|
|
status="passed",
|
|
duration_ms=duration
|
|
)
|
|
else:
|
|
return TestResult(
|
|
name="E-Mail Templates API",
|
|
description="Prueft ob die E-Mail-Vorlagen-Verwaltung verfuegbar ist",
|
|
expected="Template-Liste abrufbar",
|
|
actual=f"HTTP {response.status_code}",
|
|
status="failed",
|
|
duration_ms=duration,
|
|
error_message=f"API nicht verfuegbar"
|
|
)
|
|
except Exception as e:
|
|
return TestResult(
|
|
name="E-Mail Templates API",
|
|
description="Prueft ob die E-Mail-Vorlagen-Verwaltung verfuegbar ist",
|
|
expected="Template-Liste abrufbar",
|
|
actual=f"Fehler: {str(e)}",
|
|
status="failed",
|
|
duration_ms=(time.time() - start) * 1000,
|
|
error_message=str(e)
|
|
)
|
|
|
|
|
|
async def test_email_settings_api() -> TestResult:
|
|
"""Test Email Settings API"""
|
|
start = time.time()
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
response = await client.get(f"{BACKEND_URL}/api/email-templates/settings")
|
|
duration = (time.time() - start) * 1000
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
sender = data.get("sender_email", "nicht konfiguriert")
|
|
return TestResult(
|
|
name="E-Mail Einstellungen",
|
|
description="Prueft ob die globalen E-Mail-Einstellungen (Absender, Logo) verfuegbar sind",
|
|
expected="Einstellungen abrufbar",
|
|
actual=f"Absender: {sender}",
|
|
status="passed",
|
|
duration_ms=duration
|
|
)
|
|
else:
|
|
return TestResult(
|
|
name="E-Mail Einstellungen",
|
|
description="Prueft ob die globalen E-Mail-Einstellungen (Absender, Logo) verfuegbar sind",
|
|
expected="Einstellungen abrufbar",
|
|
actual=f"HTTP {response.status_code}",
|
|
status="failed",
|
|
duration_ms=duration,
|
|
error_message=f"Einstellungen nicht verfuegbar"
|
|
)
|
|
except Exception as e:
|
|
return TestResult(
|
|
name="E-Mail Einstellungen",
|
|
description="Prueft ob die globalen E-Mail-Einstellungen (Absender, Logo) verfuegbar sind",
|
|
expected="Einstellungen abrufbar",
|
|
actual=f"Fehler: {str(e)}",
|
|
status="failed",
|
|
duration_ms=(time.time() - start) * 1000,
|
|
error_message=str(e)
|
|
)
|
|
|
|
|
|
async def test_llm_analysis_endpoint() -> TestResult:
|
|
"""Test LLM Mail Analysis Endpoint"""
|
|
start = time.time()
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
# Check if LLM gateway is enabled
|
|
response = await client.get(f"{BACKEND_URL}/llm/health")
|
|
duration = (time.time() - start) * 1000
|
|
|
|
if response.status_code == 200:
|
|
return TestResult(
|
|
name="KI E-Mail Analyse",
|
|
description="Prueft ob die LLM-basierte E-Mail-Analyse verfuegbar ist",
|
|
expected="LLM Gateway aktiv",
|
|
actual="KI-Analyse bereit",
|
|
status="passed",
|
|
duration_ms=duration
|
|
)
|
|
else:
|
|
return TestResult(
|
|
name="KI E-Mail Analyse",
|
|
description="Prueft ob die LLM-basierte E-Mail-Analyse verfuegbar ist",
|
|
expected="LLM Gateway aktiv",
|
|
actual="LLM Gateway nicht aktiviert",
|
|
status="skipped",
|
|
duration_ms=duration,
|
|
error_message="LLM_GATEWAY_ENABLED=false"
|
|
)
|
|
except Exception as e:
|
|
return TestResult(
|
|
name="KI E-Mail Analyse",
|
|
description="Prueft ob die LLM-basierte E-Mail-Analyse verfuegbar ist",
|
|
expected="LLM Gateway aktiv",
|
|
actual="Nicht verfuegbar",
|
|
status="skipped",
|
|
duration_ms=(time.time() - start) * 1000,
|
|
error_message=str(e)
|
|
)
|
|
|
|
|
|
async def test_gfk_integration() -> TestResult:
|
|
"""Test GFK (Gewaltfreie Kommunikation) Integration"""
|
|
start = time.time()
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
response = await client.get(f"{BACKEND_URL}/v1/communication/health")
|
|
duration = (time.time() - start) * 1000
|
|
|
|
if response.status_code == 200:
|
|
return TestResult(
|
|
name="GFK Integration",
|
|
description="Prueft ob die Gewaltfreie-Kommunikation Analyse fuer Elternbriefe aktiv ist",
|
|
expected="Communication Service verfuegbar",
|
|
actual="GFK-Analyse bereit",
|
|
status="passed",
|
|
duration_ms=duration
|
|
)
|
|
else:
|
|
return TestResult(
|
|
name="GFK Integration",
|
|
description="Prueft ob die Gewaltfreie-Kommunikation Analyse fuer Elternbriefe aktiv ist",
|
|
expected="Communication Service verfuegbar",
|
|
actual=f"HTTP {response.status_code}",
|
|
status="skipped",
|
|
duration_ms=duration,
|
|
error_message="GFK-Service nicht konfiguriert"
|
|
)
|
|
except Exception as e:
|
|
return TestResult(
|
|
name="GFK Integration",
|
|
description="Prueft ob die Gewaltfreie-Kommunikation Analyse fuer Elternbriefe aktiv ist",
|
|
expected="Communication Service verfuegbar",
|
|
actual="Nicht verfuegbar",
|
|
status="skipped",
|
|
duration_ms=(time.time() - start) * 1000,
|
|
error_message=str(e)
|
|
)
|
|
|
|
|
|
# ==============================================
|
|
# Category Runners
|
|
# ==============================================
|
|
|
|
async def run_smtp_tests() -> TestCategoryResult:
|
|
"""Run SMTP-related tests"""
|
|
tests = await asyncio.gather(
|
|
test_smtp_connection(),
|
|
test_mailpit_api(),
|
|
test_mailpit_messages(),
|
|
)
|
|
|
|
passed = sum(1 for t in tests if t.status == "passed")
|
|
failed = sum(1 for t in tests if t.status == "failed")
|
|
|
|
return TestCategoryResult(
|
|
category="smtp",
|
|
display_name="SMTP / Ausgehende E-Mails",
|
|
description="Tests fuer den E-Mail-Versand",
|
|
tests=list(tests),
|
|
passed=passed,
|
|
failed=failed,
|
|
total=len(tests)
|
|
)
|
|
|
|
|
|
async def run_imap_tests() -> TestCategoryResult:
|
|
"""Run IMAP-related tests"""
|
|
tests = await asyncio.gather(
|
|
test_imap_connection(),
|
|
)
|
|
|
|
passed = sum(1 for t in tests if t.status == "passed")
|
|
failed = sum(1 for t in tests if t.status == "failed")
|
|
|
|
return TestCategoryResult(
|
|
category="imap",
|
|
display_name="IMAP / Eingehende E-Mails",
|
|
description="Tests fuer den E-Mail-Empfang",
|
|
tests=list(tests),
|
|
passed=passed,
|
|
failed=failed,
|
|
total=len(tests)
|
|
)
|
|
|
|
|
|
async def run_templates_tests() -> TestCategoryResult:
|
|
"""Run Email Templates tests"""
|
|
tests = await asyncio.gather(
|
|
test_email_templates_api(),
|
|
test_email_settings_api(),
|
|
)
|
|
|
|
passed = sum(1 for t in tests if t.status == "passed")
|
|
failed = sum(1 for t in tests if t.status == "failed")
|
|
|
|
return TestCategoryResult(
|
|
category="templates",
|
|
display_name="E-Mail Templates",
|
|
description="Tests fuer die E-Mail-Vorlagen-Verwaltung",
|
|
tests=list(tests),
|
|
passed=passed,
|
|
failed=failed,
|
|
total=len(tests)
|
|
)
|
|
|
|
|
|
async def run_ai_tests() -> TestCategoryResult:
|
|
"""Run AI Analysis tests"""
|
|
tests = await asyncio.gather(
|
|
test_llm_analysis_endpoint(),
|
|
test_gfk_integration(),
|
|
)
|
|
|
|
passed = sum(1 for t in tests if t.status == "passed")
|
|
failed = sum(1 for t in tests if t.status == "failed")
|
|
|
|
return TestCategoryResult(
|
|
category="ai-analysis",
|
|
display_name="KI-Analyse",
|
|
description="Tests fuer die KI-basierte E-Mail-Analyse",
|
|
tests=list(tests),
|
|
passed=passed,
|
|
failed=failed,
|
|
total=len(tests)
|
|
)
|
|
|
|
|
|
# ==============================================
|
|
# API Endpoints
|
|
# ==============================================
|
|
|
|
@router.post("/{category}", response_model=TestCategoryResult)
|
|
async def run_category_tests(category: str):
|
|
"""Run tests for a specific category"""
|
|
runners = {
|
|
"smtp": run_smtp_tests,
|
|
"imap": run_imap_tests,
|
|
"templates": run_templates_tests,
|
|
"ai-analysis": run_ai_tests,
|
|
}
|
|
|
|
if category not in runners:
|
|
return TestCategoryResult(
|
|
category=category,
|
|
display_name=f"Unbekannt: {category}",
|
|
description="Kategorie nicht gefunden",
|
|
tests=[],
|
|
passed=0,
|
|
failed=0,
|
|
total=0
|
|
)
|
|
|
|
return await runners[category]()
|
|
|
|
|
|
@router.post("/run-all", response_model=FullTestResults)
|
|
async def run_all_tests():
|
|
"""Run all mail tests"""
|
|
start = time.time()
|
|
|
|
categories = await asyncio.gather(
|
|
run_smtp_tests(),
|
|
run_imap_tests(),
|
|
run_templates_tests(),
|
|
run_ai_tests(),
|
|
)
|
|
|
|
total_passed = sum(c.passed for c in categories)
|
|
total_failed = sum(c.failed for c in categories)
|
|
total_tests = sum(c.total for c in categories)
|
|
|
|
return FullTestResults(
|
|
categories=list(categories),
|
|
total_passed=total_passed,
|
|
total_failed=total_failed,
|
|
total_tests=total_tests,
|
|
duration_ms=(time.time() - start) * 1000
|
|
)
|
|
|
|
|
|
@router.get("/categories")
|
|
async def get_categories():
|
|
"""Get available test categories"""
|
|
return {
|
|
"categories": [
|
|
{"id": "smtp", "name": "SMTP", "description": "Ausgehende E-Mails"},
|
|
{"id": "imap", "name": "IMAP", "description": "Eingehende E-Mails"},
|
|
{"id": "templates", "name": "Templates", "description": "E-Mail-Vorlagen"},
|
|
{"id": "ai-analysis", "name": "KI-Analyse", "description": "LLM & GFK"},
|
|
]
|
|
}
|