fix: Restore all files lost during destructive rebase
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>
This commit is contained in:
360
backend/tests/test_letters_api.py
Normal file
360
backend/tests/test_letters_api.py
Normal file
@@ -0,0 +1,360 @@
|
||||
"""
|
||||
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"])
|
||||
Reference in New Issue
Block a user