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>
479 lines
18 KiB
Python
479 lines
18 KiB
Python
"""
|
|
Communication Test API - Test Runner fuer Matrix & Jitsi Integration
|
|
Endpoint: /api/admin/communication-tests
|
|
"""
|
|
|
|
from fastapi import APIRouter
|
|
from pydantic import BaseModel
|
|
from typing import List, Optional, Literal
|
|
import httpx
|
|
import asyncio
|
|
import time
|
|
import os
|
|
|
|
router = APIRouter(prefix="/api/admin/communication-tests", tags=["Communication 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
|
|
# ==============================================
|
|
|
|
MATRIX_HOMESERVER = os.getenv("MATRIX_HOMESERVER", "http://matrix:8008")
|
|
JITSI_URL = os.getenv("JITSI_URL", "http://jitsi:8443")
|
|
CONSENT_SERVICE_URL = os.getenv("CONSENT_SERVICE_URL", "http://consent-service:8081")
|
|
|
|
|
|
# ==============================================
|
|
# Test Implementations
|
|
# ==============================================
|
|
|
|
async def test_matrix_health() -> TestResult:
|
|
"""Test Matrix Homeserver Health"""
|
|
start = time.time()
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
# Matrix Federation API health check
|
|
response = await client.get(f"{MATRIX_HOMESERVER}/_matrix/client/versions")
|
|
duration = (time.time() - start) * 1000
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
versions = data.get("versions", [])
|
|
return TestResult(
|
|
name="Matrix Homeserver Health",
|
|
description="Prueft ob der Matrix Synapse Server erreichbar ist",
|
|
expected="HTTP 200 mit Client-Versionen",
|
|
actual=f"HTTP {response.status_code}, Versionen: {', '.join(versions[:3])}",
|
|
status="passed",
|
|
duration_ms=duration
|
|
)
|
|
else:
|
|
return TestResult(
|
|
name="Matrix Homeserver Health",
|
|
description="Prueft ob der Matrix Synapse Server erreichbar ist",
|
|
expected="HTTP 200 mit Client-Versionen",
|
|
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="Matrix Homeserver Health",
|
|
description="Prueft ob der Matrix Synapse Server erreichbar ist",
|
|
expected="HTTP 200 mit Client-Versionen",
|
|
actual=f"Fehler: {str(e)}",
|
|
status="failed",
|
|
duration_ms=(time.time() - start) * 1000,
|
|
error_message=str(e)
|
|
)
|
|
|
|
|
|
async def test_matrix_federation() -> TestResult:
|
|
"""Test Matrix Federation API"""
|
|
start = time.time()
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
response = await client.get(f"{MATRIX_HOMESERVER}/_matrix/federation/v1/version")
|
|
duration = (time.time() - start) * 1000
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
server_name = data.get("server", {}).get("name", "unknown")
|
|
return TestResult(
|
|
name="Matrix Federation API",
|
|
description="Prueft ob die Federation-API fuer Server-zu-Server Kommunikation bereit ist",
|
|
expected="HTTP 200 mit Server-Info",
|
|
actual=f"Server: {server_name}",
|
|
status="passed",
|
|
duration_ms=duration
|
|
)
|
|
else:
|
|
return TestResult(
|
|
name="Matrix Federation API",
|
|
description="Prueft ob die Federation-API fuer Server-zu-Server Kommunikation bereit ist",
|
|
expected="HTTP 200 mit Server-Info",
|
|
actual=f"HTTP {response.status_code}",
|
|
status="skipped",
|
|
duration_ms=duration,
|
|
error_message="Federation nicht aktiviert oder nicht erreichbar"
|
|
)
|
|
except Exception as e:
|
|
return TestResult(
|
|
name="Matrix Federation API",
|
|
description="Prueft ob die Federation-API fuer Server-zu-Server Kommunikation bereit ist",
|
|
expected="HTTP 200 mit Server-Info",
|
|
actual=f"Nicht verfuegbar",
|
|
status="skipped",
|
|
duration_ms=(time.time() - start) * 1000,
|
|
error_message=f"Federation-Endpoint nicht erreichbar: {str(e)}"
|
|
)
|
|
|
|
|
|
async def test_matrix_media_api() -> TestResult:
|
|
"""Test Matrix Media Repository"""
|
|
start = time.time()
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
response = await client.get(f"{MATRIX_HOMESERVER}/_matrix/media/v3/config")
|
|
duration = (time.time() - start) * 1000
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
max_size = data.get("m.upload.size", 0)
|
|
max_mb = max_size / (1024 * 1024) if max_size else "unbegrenzt"
|
|
return TestResult(
|
|
name="Matrix Media Repository",
|
|
description="Prueft ob Datei-Uploads (Bilder, Dokumente) moeglich sind",
|
|
expected="Media-Konfiguration verfuegbar",
|
|
actual=f"Max Upload: {max_mb} MB",
|
|
status="passed",
|
|
duration_ms=duration
|
|
)
|
|
else:
|
|
return TestResult(
|
|
name="Matrix Media Repository",
|
|
description="Prueft ob Datei-Uploads (Bilder, Dokumente) moeglich sind",
|
|
expected="Media-Konfiguration verfuegbar",
|
|
actual=f"HTTP {response.status_code}",
|
|
status="failed",
|
|
duration_ms=duration,
|
|
error_message="Media API nicht verfuegbar"
|
|
)
|
|
except Exception as e:
|
|
return TestResult(
|
|
name="Matrix Media Repository",
|
|
description="Prueft ob Datei-Uploads (Bilder, Dokumente) moeglich sind",
|
|
expected="Media-Konfiguration verfuegbar",
|
|
actual=f"Fehler: {str(e)}",
|
|
status="failed",
|
|
duration_ms=(time.time() - start) * 1000,
|
|
error_message=str(e)
|
|
)
|
|
|
|
|
|
async def test_jitsi_health() -> TestResult:
|
|
"""Test Jitsi Meet Server Health"""
|
|
start = time.time()
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10.0, verify=False) as client:
|
|
# Jitsi health check endpoint
|
|
response = await client.get(f"{JITSI_URL}/http-bind")
|
|
duration = (time.time() - start) * 1000
|
|
|
|
# Jitsi returns various status codes, 200 or 404 means server is up
|
|
if response.status_code in [200, 404, 400]:
|
|
return TestResult(
|
|
name="Jitsi Meet Server",
|
|
description="Prueft ob der Jitsi Video-Konferenz Server erreichbar ist",
|
|
expected="Jitsi Server antwortet",
|
|
actual=f"Server erreichbar (HTTP {response.status_code})",
|
|
status="passed",
|
|
duration_ms=duration
|
|
)
|
|
else:
|
|
return TestResult(
|
|
name="Jitsi Meet Server",
|
|
description="Prueft ob der Jitsi Video-Konferenz Server erreichbar ist",
|
|
expected="Jitsi Server antwortet",
|
|
actual=f"HTTP {response.status_code}",
|
|
status="failed",
|
|
duration_ms=duration,
|
|
error_message=f"Unerwartete Antwort: {response.status_code}"
|
|
)
|
|
except Exception as e:
|
|
return TestResult(
|
|
name="Jitsi Meet Server",
|
|
description="Prueft ob der Jitsi Video-Konferenz Server erreichbar ist",
|
|
expected="Jitsi Server antwortet",
|
|
actual=f"Nicht erreichbar",
|
|
status="failed",
|
|
duration_ms=(time.time() - start) * 1000,
|
|
error_message=str(e)
|
|
)
|
|
|
|
|
|
async def test_jitsi_xmpp() -> TestResult:
|
|
"""Test Jitsi XMPP/Prosody"""
|
|
start = time.time()
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10.0, verify=False) as client:
|
|
# Check if Prosody/XMPP is responding
|
|
response = await client.get(f"{JITSI_URL}/xmpp-websocket",
|
|
headers={"Upgrade": "websocket"})
|
|
duration = (time.time() - start) * 1000
|
|
|
|
# WebSocket upgrade will typically return 400 without proper handshake
|
|
# but that means the endpoint exists
|
|
if response.status_code in [101, 400, 426]:
|
|
return TestResult(
|
|
name="Jitsi XMPP/Prosody",
|
|
description="Prueft ob der XMPP Messaging Server fuer Echtzeit-Kommunikation bereit ist",
|
|
expected="WebSocket-Endpoint verfuegbar",
|
|
actual=f"XMPP-WebSocket aktiv",
|
|
status="passed",
|
|
duration_ms=duration
|
|
)
|
|
else:
|
|
return TestResult(
|
|
name="Jitsi XMPP/Prosody",
|
|
description="Prueft ob der XMPP Messaging Server fuer Echtzeit-Kommunikation bereit ist",
|
|
expected="WebSocket-Endpoint verfuegbar",
|
|
actual=f"HTTP {response.status_code}",
|
|
status="skipped",
|
|
duration_ms=duration,
|
|
error_message="XMPP-WebSocket nicht konfiguriert"
|
|
)
|
|
except Exception as e:
|
|
return TestResult(
|
|
name="Jitsi XMPP/Prosody",
|
|
description="Prueft ob der XMPP Messaging Server fuer Echtzeit-Kommunikation bereit ist",
|
|
expected="WebSocket-Endpoint verfuegbar",
|
|
actual=f"Nicht verfuegbar",
|
|
status="skipped",
|
|
duration_ms=(time.time() - start) * 1000,
|
|
error_message=str(e)
|
|
)
|
|
|
|
|
|
async def test_communication_api_health() -> TestResult:
|
|
"""Test Communication API im Consent Service"""
|
|
start = time.time()
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
response = await client.get(f"{CONSENT_SERVICE_URL}/api/v1/communication/admin/stats")
|
|
duration = (time.time() - start) * 1000
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
return TestResult(
|
|
name="Communication Admin API",
|
|
description="Prueft ob die Communication-Verwaltungs-API im Backend verfuegbar ist",
|
|
expected="HTTP 200 mit Statistiken",
|
|
actual=f"API verfuegbar, Stats geladen",
|
|
status="passed",
|
|
duration_ms=duration
|
|
)
|
|
else:
|
|
return TestResult(
|
|
name="Communication Admin API",
|
|
description="Prueft ob die Communication-Verwaltungs-API im Backend verfuegbar ist",
|
|
expected="HTTP 200 mit Statistiken",
|
|
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="Communication Admin API",
|
|
description="Prueft ob die Communication-Verwaltungs-API im Backend verfuegbar ist",
|
|
expected="HTTP 200 mit Statistiken",
|
|
actual=f"Fehler: {str(e)}",
|
|
status="failed",
|
|
duration_ms=(time.time() - start) * 1000,
|
|
error_message=str(e)
|
|
)
|
|
|
|
|
|
async def test_matrix_rooms_db() -> TestResult:
|
|
"""Test Matrix Rooms Datenbank-Tabelle"""
|
|
start = time.time()
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
# Check if consent service can access matrix_rooms table
|
|
response = await client.get(f"{CONSENT_SERVICE_URL}/health")
|
|
duration = (time.time() - start) * 1000
|
|
|
|
if response.status_code == 200:
|
|
return TestResult(
|
|
name="Matrix Rooms Datenbank",
|
|
description="Prueft ob die matrix_rooms Tabelle fuer Raum-Verwaltung existiert",
|
|
expected="Datenbank-Tabelle verfuegbar",
|
|
actual="Consent Service gesund, DB-Zugriff OK",
|
|
status="passed",
|
|
duration_ms=duration
|
|
)
|
|
else:
|
|
return TestResult(
|
|
name="Matrix Rooms Datenbank",
|
|
description="Prueft ob die matrix_rooms Tabelle fuer Raum-Verwaltung existiert",
|
|
expected="Datenbank-Tabelle verfuegbar",
|
|
actual=f"Service-Fehler: HTTP {response.status_code}",
|
|
status="failed",
|
|
duration_ms=duration,
|
|
error_message="Consent Service nicht gesund"
|
|
)
|
|
except Exception as e:
|
|
return TestResult(
|
|
name="Matrix Rooms Datenbank",
|
|
description="Prueft ob die matrix_rooms Tabelle fuer Raum-Verwaltung existiert",
|
|
expected="Datenbank-Tabelle verfuegbar",
|
|
actual=f"Fehler: {str(e)}",
|
|
status="failed",
|
|
duration_ms=(time.time() - start) * 1000,
|
|
error_message=str(e)
|
|
)
|
|
|
|
|
|
# ==============================================
|
|
# Category Runners
|
|
# ==============================================
|
|
|
|
async def run_matrix_tests() -> TestCategoryResult:
|
|
"""Run all Matrix-related tests"""
|
|
tests = await asyncio.gather(
|
|
test_matrix_health(),
|
|
test_matrix_federation(),
|
|
test_matrix_media_api(),
|
|
test_matrix_rooms_db(),
|
|
)
|
|
|
|
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="matrix",
|
|
display_name="Matrix Messenger",
|
|
description="Tests fuer den dezentralen Matrix Synapse Server",
|
|
tests=list(tests),
|
|
passed=passed,
|
|
failed=failed,
|
|
total=len(tests)
|
|
)
|
|
|
|
|
|
async def run_jitsi_tests() -> TestCategoryResult:
|
|
"""Run all Jitsi-related tests"""
|
|
tests = await asyncio.gather(
|
|
test_jitsi_health(),
|
|
test_jitsi_xmpp(),
|
|
)
|
|
|
|
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="jitsi",
|
|
display_name="Jitsi Video",
|
|
description="Tests fuer den Jitsi Meet Video-Konferenz Server",
|
|
tests=list(tests),
|
|
passed=passed,
|
|
failed=failed,
|
|
total=len(tests)
|
|
)
|
|
|
|
|
|
async def run_api_tests() -> TestCategoryResult:
|
|
"""Run Communication API tests"""
|
|
tests = await asyncio.gather(
|
|
test_communication_api_health(),
|
|
)
|
|
|
|
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="api-health",
|
|
display_name="Communication API",
|
|
description="Tests fuer die Communication-Verwaltungs-Endpunkte",
|
|
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 = {
|
|
"api-health": run_api_tests,
|
|
"matrix": run_matrix_tests,
|
|
"jitsi": run_jitsi_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 communication tests"""
|
|
start = time.time()
|
|
|
|
categories = await asyncio.gather(
|
|
run_api_tests(),
|
|
run_matrix_tests(),
|
|
run_jitsi_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": "api-health", "name": "Communication API", "description": "Backend API Tests"},
|
|
{"id": "matrix", "name": "Matrix Messenger", "description": "Synapse Server Tests"},
|
|
{"id": "jitsi", "name": "Jitsi Video", "description": "Video-Konferenz Tests"},
|
|
]
|
|
}
|