""" 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"}, ] }