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>
361 lines
12 KiB
Python
361 lines
12 KiB
Python
"""
|
|
Tests für die Letters API.
|
|
|
|
Testet:
|
|
- CRUD-Operationen für Elternbriefe
|
|
- PDF-Export
|
|
- GFK-Verbesserungsvorschläge
|
|
|
|
Note: Some tests require WeasyPrint which needs system libraries.
|
|
"""
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from unittest.mock import patch, AsyncMock, MagicMock
|
|
import sys
|
|
import os
|
|
|
|
# Add parent directory to path
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
# Check if WeasyPrint is available (required for PDF endpoints)
|
|
try:
|
|
import weasyprint
|
|
WEASYPRINT_AVAILABLE = True
|
|
except (ImportError, OSError):
|
|
WEASYPRINT_AVAILABLE = False
|
|
|
|
|
|
class TestLettersAPIImport:
|
|
"""Tests für Letters API Import."""
|
|
|
|
def test_import_letters_api(self):
|
|
"""Test that letters_api can be imported."""
|
|
from letters_api import router
|
|
assert router is not None
|
|
|
|
def test_import_enums(self):
|
|
"""Test that enums can be imported."""
|
|
from letters_api import LetterType, LetterTone, LetterStatus
|
|
assert LetterType is not None
|
|
assert LetterTone is not None
|
|
assert LetterStatus is not None
|
|
|
|
def test_import_models(self):
|
|
"""Test that Pydantic models can be imported."""
|
|
from letters_api import (
|
|
LetterCreateRequest,
|
|
LetterUpdateRequest,
|
|
LetterResponse,
|
|
LetterListResponse,
|
|
ExportPDFRequest,
|
|
ImproveRequest,
|
|
ImproveResponse
|
|
)
|
|
assert LetterCreateRequest is not None
|
|
assert LetterResponse is not None
|
|
|
|
|
|
class TestLetterTypes:
|
|
"""Tests für Brieftypen."""
|
|
|
|
def test_letter_types_values(self):
|
|
"""Test that all letter types have correct values."""
|
|
from letters_api import LetterType
|
|
|
|
expected_types = ["general", "halbjahr", "fehlzeiten", "elternabend", "lob", "custom"]
|
|
actual_types = [t.value for t in LetterType]
|
|
|
|
for expected in expected_types:
|
|
assert expected in actual_types
|
|
|
|
def test_letter_tones_values(self):
|
|
"""Test that all tones have correct values."""
|
|
from letters_api import LetterTone
|
|
|
|
expected_tones = ["formal", "professional", "warm", "concerned", "appreciative"]
|
|
actual_tones = [t.value for t in LetterTone]
|
|
|
|
for expected in expected_tones:
|
|
assert expected in actual_tones
|
|
|
|
|
|
class TestLetterCreateRequest:
|
|
"""Tests für LetterCreateRequest Model."""
|
|
|
|
def test_create_minimal_request(self):
|
|
"""Test creating a request with minimal required fields."""
|
|
from letters_api import LetterCreateRequest
|
|
|
|
request = LetterCreateRequest(
|
|
recipient_name="Familie Müller",
|
|
recipient_address="Musterstraße 1, 12345 Musterstadt",
|
|
student_name="Max Müller",
|
|
student_class="5a",
|
|
subject="Einladung Elternabend",
|
|
content="Sehr geehrte Familie Müller...",
|
|
teacher_name="Frau Schmidt"
|
|
)
|
|
|
|
assert request.recipient_name == "Familie Müller"
|
|
assert request.student_name == "Max Müller"
|
|
assert request.teacher_name == "Frau Schmidt"
|
|
|
|
def test_create_full_request(self):
|
|
"""Test creating a request with all fields."""
|
|
from letters_api import LetterCreateRequest, LetterType, LetterTone, SchoolInfoModel
|
|
|
|
school_info = SchoolInfoModel(
|
|
name="Musterschule",
|
|
address="Schulweg 1, 12345 Musterstadt",
|
|
phone="0123-456789",
|
|
email="info@musterschule.de"
|
|
)
|
|
|
|
request = LetterCreateRequest(
|
|
recipient_name="Familie Müller",
|
|
recipient_address="Musterstraße 1, 12345 Musterstadt",
|
|
student_name="Max Müller",
|
|
student_class="5a",
|
|
subject="Einladung Elternabend",
|
|
content="Sehr geehrte Familie Müller...",
|
|
teacher_name="Frau Schmidt",
|
|
teacher_title="Klassenlehrerin",
|
|
letter_type=LetterType.ELTERNABEND,
|
|
tone=LetterTone.PROFESSIONAL,
|
|
school_info=school_info,
|
|
gfk_principles_applied=["Beobachtung", "Bitte"]
|
|
)
|
|
|
|
assert request.letter_type == LetterType.ELTERNABEND
|
|
assert request.tone == LetterTone.PROFESSIONAL
|
|
assert request.school_info.name == "Musterschule"
|
|
|
|
|
|
class TestHelperFunctions:
|
|
"""Tests für Helper-Funktionen."""
|
|
|
|
def test_get_type_label(self):
|
|
"""Test type label function."""
|
|
from letters_api import _get_type_label, LetterType
|
|
|
|
assert "Einladung" in _get_type_label(LetterType.ELTERNABEND)
|
|
assert "Fehlzeiten" in _get_type_label(LetterType.FEHLZEITEN)
|
|
assert "Positives" in _get_type_label(LetterType.LOB)
|
|
|
|
def test_get_tone_label(self):
|
|
"""Test tone label function."""
|
|
from letters_api import _get_tone_label, LetterTone
|
|
|
|
assert "förmlich" in _get_tone_label(LetterTone.FORMAL)
|
|
assert "Professionell" in _get_tone_label(LetterTone.PROFESSIONAL)
|
|
assert "Warmherzig" in _get_tone_label(LetterTone.WARM)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not WEASYPRINT_AVAILABLE,
|
|
reason="WeasyPrint not available (requires system libraries)"
|
|
)
|
|
class TestLettersAPIEndpoints:
|
|
"""Integration tests für Letters API Endpoints."""
|
|
|
|
@pytest.fixture
|
|
def client(self):
|
|
"""Create test client."""
|
|
try:
|
|
from main import app
|
|
return TestClient(app)
|
|
except ImportError:
|
|
pytest.skip("main.py not available for testing")
|
|
|
|
@pytest.fixture
|
|
def sample_letter_data(self):
|
|
"""Sample letter data for tests."""
|
|
return {
|
|
"recipient_name": "Familie Test",
|
|
"recipient_address": "Teststraße 1\n12345 Teststadt",
|
|
"student_name": "Test Kind",
|
|
"student_class": "5a",
|
|
"subject": "Testbrief",
|
|
"content": "Dies ist ein Testbrief.",
|
|
"teacher_name": "Herr Test",
|
|
"letter_type": "general",
|
|
"tone": "professional"
|
|
}
|
|
|
|
def test_create_letter(self, client, sample_letter_data):
|
|
"""Test creating a new letter."""
|
|
if not client:
|
|
pytest.skip("Client not available")
|
|
|
|
response = client.post("/api/letters/", json=sample_letter_data)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["recipient_name"] == sample_letter_data["recipient_name"]
|
|
assert data["student_name"] == sample_letter_data["student_name"]
|
|
assert data["status"] == "draft"
|
|
assert "id" in data
|
|
|
|
def test_get_letter(self, client, sample_letter_data):
|
|
"""Test getting a letter by ID."""
|
|
if not client:
|
|
pytest.skip("Client not available")
|
|
|
|
# First create a letter
|
|
create_response = client.post("/api/letters/", json=sample_letter_data)
|
|
letter_id = create_response.json()["id"]
|
|
|
|
# Then get it
|
|
response = client.get(f"/api/letters/{letter_id}")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["id"] == letter_id
|
|
|
|
def test_update_letter(self, client, sample_letter_data):
|
|
"""Test updating a letter."""
|
|
if not client:
|
|
pytest.skip("Client not available")
|
|
|
|
# Create letter
|
|
create_response = client.post("/api/letters/", json=sample_letter_data)
|
|
letter_id = create_response.json()["id"]
|
|
|
|
# Update it
|
|
update_data = {"subject": "Aktualisierter Betreff"}
|
|
response = client.put(f"/api/letters/{letter_id}", json=update_data)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["subject"] == "Aktualisierter Betreff"
|
|
|
|
def test_delete_letter(self, client, sample_letter_data):
|
|
"""Test deleting a letter."""
|
|
if not client:
|
|
pytest.skip("Client not available")
|
|
|
|
# Create letter
|
|
create_response = client.post("/api/letters/", json=sample_letter_data)
|
|
letter_id = create_response.json()["id"]
|
|
|
|
# Delete it
|
|
response = client.delete(f"/api/letters/{letter_id}")
|
|
assert response.status_code == 200
|
|
|
|
# Verify it's deleted
|
|
get_response = client.get(f"/api/letters/{letter_id}")
|
|
assert get_response.status_code == 404
|
|
|
|
def test_list_letters(self, client, sample_letter_data):
|
|
"""Test listing letters."""
|
|
if not client:
|
|
pytest.skip("Client not available")
|
|
|
|
# Create a letter
|
|
client.post("/api/letters/", json=sample_letter_data)
|
|
|
|
# List all
|
|
response = client.get("/api/letters/")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "letters" in data
|
|
assert "total" in data
|
|
assert isinstance(data["letters"], list)
|
|
|
|
def test_get_letter_types(self, client):
|
|
"""Test getting available letter types."""
|
|
if not client:
|
|
pytest.skip("Client not available")
|
|
|
|
response = client.get("/api/letters/types")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "types" in data
|
|
assert len(data["types"]) > 0
|
|
|
|
def test_get_letter_tones(self, client):
|
|
"""Test getting available tones."""
|
|
if not client:
|
|
pytest.skip("Client not available")
|
|
|
|
response = client.get("/api/letters/tones")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "tones" in data
|
|
assert len(data["tones"]) > 0
|
|
|
|
def test_export_pdf(self, client, sample_letter_data):
|
|
"""Test PDF export."""
|
|
if not client:
|
|
pytest.skip("Client not available")
|
|
|
|
# Create letter
|
|
create_response = client.post("/api/letters/", json=sample_letter_data)
|
|
letter_id = create_response.json()["id"]
|
|
|
|
# Export as PDF
|
|
response = client.post(f"/api/letters/{letter_id}/export-pdf")
|
|
|
|
assert response.status_code == 200
|
|
assert response.headers["content-type"] == "application/pdf"
|
|
assert b"%PDF" in response.content[:10]
|
|
|
|
def test_export_pdf_direct(self, client, sample_letter_data):
|
|
"""Test direct PDF export without saving."""
|
|
if not client:
|
|
pytest.skip("Client not available")
|
|
|
|
export_data = {"letter_data": sample_letter_data}
|
|
response = client.post("/api/letters/export-pdf", json=export_data)
|
|
|
|
assert response.status_code == 200
|
|
assert response.headers["content-type"] == "application/pdf"
|
|
|
|
def test_get_nonexistent_letter(self, client):
|
|
"""Test getting a letter that doesn't exist."""
|
|
if not client:
|
|
pytest.skip("Client not available")
|
|
|
|
response = client.get("/api/letters/nonexistent-id")
|
|
assert response.status_code == 404
|
|
|
|
|
|
class TestLetterImprove:
|
|
"""Tests für GFK-Verbesserungsvorschläge."""
|
|
|
|
def test_improve_request_model(self):
|
|
"""Test ImproveRequest model."""
|
|
from letters_api import ImproveRequest
|
|
|
|
request = ImproveRequest(
|
|
content="Der Schüler macht nie seine Hausaufgaben.",
|
|
communication_type="behavior",
|
|
tone="concerned"
|
|
)
|
|
|
|
assert request.content == "Der Schüler macht nie seine Hausaufgaben."
|
|
assert request.communication_type == "behavior"
|
|
|
|
def test_improve_response_model(self):
|
|
"""Test ImproveResponse model."""
|
|
from letters_api import ImproveResponse
|
|
|
|
response = ImproveResponse(
|
|
improved_content="Ich habe beobachtet, dass die Hausaufgaben...",
|
|
changes=["'nie' durch konkretes Datum ersetzt", "Ich-Botschaft verwendet"],
|
|
gfk_score=0.85,
|
|
gfk_principles_applied=["Beobachtung", "Gefühl", "Bedürfnis"]
|
|
)
|
|
|
|
assert response.gfk_score == 0.85
|
|
assert "Beobachtung" in response.gfk_principles_applied
|
|
|
|
|
|
# Run tests if executed directly
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|