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>
529 lines
17 KiB
Python
529 lines
17 KiB
Python
"""
|
|
Tests für Worksheets API.
|
|
|
|
Testet alle Content-Generatoren:
|
|
- Multiple Choice
|
|
- Cloze (Lückentext)
|
|
- Mindmap
|
|
- Quiz
|
|
"""
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
# Importiere main app
|
|
import sys
|
|
import os
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from main import app
|
|
|
|
client = TestClient(app)
|
|
|
|
# Sample text für Tests (mind. 50 Zeichen erforderlich)
|
|
SAMPLE_TEXT = """
|
|
Die Photosynthese ist ein biologischer Prozess, bei dem Pflanzen, Algen und einige Bakterien
|
|
Lichtenergie in chemische Energie umwandeln. Dieser Vorgang findet hauptsächlich in den
|
|
Chloroplasten der Pflanzenzellen statt. Dabei werden Kohlendioxid und Wasser unter Verwendung
|
|
von Sonnenlicht in Glucose und Sauerstoff umgewandelt. Die Photosynthese ist fundamental für
|
|
das Leben auf der Erde, da sie Sauerstoff produziert und die Basis der Nahrungskette bildet.
|
|
Pflanzen nutzen die produzierte Glucose als Energiequelle für ihr Wachstum.
|
|
"""
|
|
|
|
SHORT_TEXT = "Zu kurz"
|
|
|
|
|
|
class TestMultipleChoiceGeneration:
|
|
"""Tests für Multiple Choice Generierung."""
|
|
|
|
def test_generate_mc_success(self):
|
|
"""Testet erfolgreiche MC-Generierung."""
|
|
response = client.post(
|
|
"/api/worksheets/generate/multiple-choice",
|
|
json={
|
|
"source_text": SAMPLE_TEXT,
|
|
"num_questions": 3,
|
|
"difficulty": "medium",
|
|
"topic": "Photosynthese",
|
|
"subject": "Biologie"
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert data["content"] is not None
|
|
assert data["content"]["content_type"] == "multiple_choice"
|
|
assert "questions" in data["content"]["data"]
|
|
|
|
def test_generate_mc_with_h5p(self):
|
|
"""Testet H5P-Export für MC."""
|
|
response = client.post(
|
|
"/api/worksheets/generate/multiple-choice",
|
|
json={
|
|
"source_text": SAMPLE_TEXT,
|
|
"num_questions": 2,
|
|
"difficulty": "easy"
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["content"]["h5p_format"] is not None
|
|
assert data["content"]["h5p_format"]["library"] == "H5P.MultiChoice"
|
|
|
|
def test_generate_mc_text_too_short(self):
|
|
"""Testet Fehler bei zu kurzem Text."""
|
|
response = client.post(
|
|
"/api/worksheets/generate/multiple-choice",
|
|
json={
|
|
"source_text": SHORT_TEXT,
|
|
"num_questions": 3
|
|
}
|
|
)
|
|
|
|
# Pydantic Validation sollte fehlschlagen
|
|
assert response.status_code == 422
|
|
|
|
def test_generate_mc_difficulty_levels(self):
|
|
"""Testet verschiedene Schwierigkeitsgrade."""
|
|
for difficulty in ["easy", "medium", "hard"]:
|
|
response = client.post(
|
|
"/api/worksheets/generate/multiple-choice",
|
|
json={
|
|
"source_text": SAMPLE_TEXT,
|
|
"num_questions": 2,
|
|
"difficulty": difficulty
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["content"]["difficulty"] == difficulty
|
|
|
|
|
|
class TestClozeGeneration:
|
|
"""Tests für Lückentext-Generierung."""
|
|
|
|
def test_generate_cloze_fill_in(self):
|
|
"""Testet Fill-in Lückentext."""
|
|
response = client.post(
|
|
"/api/worksheets/generate/cloze",
|
|
json={
|
|
"source_text": SAMPLE_TEXT,
|
|
"num_gaps": 3,
|
|
"cloze_type": "fill_in",
|
|
"topic": "Photosynthese"
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert data["content"]["content_type"] == "cloze"
|
|
assert "gaps" in data["content"]["data"]
|
|
assert "text_with_gaps" in data["content"]["data"]
|
|
|
|
def test_generate_cloze_drag_drop(self):
|
|
"""Testet Drag-Drop Lückentext."""
|
|
response = client.post(
|
|
"/api/worksheets/generate/cloze",
|
|
json={
|
|
"source_text": SAMPLE_TEXT,
|
|
"num_gaps": 4,
|
|
"cloze_type": "drag_drop"
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["content"]["data"]["cloze_type"] == "drag_drop"
|
|
|
|
def test_generate_cloze_dropdown(self):
|
|
"""Testet Dropdown Lückentext."""
|
|
response = client.post(
|
|
"/api/worksheets/generate/cloze",
|
|
json={
|
|
"source_text": SAMPLE_TEXT,
|
|
"num_gaps": 3,
|
|
"cloze_type": "dropdown"
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["content"]["data"]["cloze_type"] == "dropdown"
|
|
|
|
def test_generate_cloze_h5p_format(self):
|
|
"""Testet H5P-Export für Cloze."""
|
|
response = client.post(
|
|
"/api/worksheets/generate/cloze",
|
|
json={
|
|
"source_text": SAMPLE_TEXT,
|
|
"num_gaps": 3
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["content"]["h5p_format"]["library"] == "H5P.Blanks"
|
|
|
|
|
|
class TestMindmapGeneration:
|
|
"""Tests für Mindmap-Generierung."""
|
|
|
|
def test_generate_mindmap_success(self):
|
|
"""Testet erfolgreiche Mindmap-Generierung."""
|
|
response = client.post(
|
|
"/api/worksheets/generate/mindmap",
|
|
json={
|
|
"source_text": SAMPLE_TEXT,
|
|
"topic": "Photosynthese",
|
|
"max_depth": 3
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert data["content"]["content_type"] == "mindmap"
|
|
assert "mindmap" in data["content"]["data"]
|
|
assert "mermaid" in data["content"]["data"]
|
|
assert "json_tree" in data["content"]["data"]
|
|
|
|
def test_generate_mindmap_no_h5p(self):
|
|
"""Testet dass Mindmaps kein H5P-Format haben."""
|
|
response = client.post(
|
|
"/api/worksheets/generate/mindmap",
|
|
json={
|
|
"source_text": SAMPLE_TEXT,
|
|
"max_depth": 2
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["content"]["h5p_format"] is None
|
|
|
|
def test_generate_mindmap_structure(self):
|
|
"""Testet Mindmap-Struktur."""
|
|
response = client.post(
|
|
"/api/worksheets/generate/mindmap",
|
|
json={
|
|
"source_text": SAMPLE_TEXT,
|
|
"max_depth": 3
|
|
}
|
|
)
|
|
|
|
data = response.json()
|
|
mindmap = data["content"]["data"]["mindmap"]
|
|
|
|
assert "root" in mindmap
|
|
assert "title" in mindmap
|
|
assert "total_nodes" in mindmap
|
|
assert mindmap["root"]["level"] == 0
|
|
|
|
|
|
class TestQuizGeneration:
|
|
"""Tests für Quiz-Generierung."""
|
|
|
|
def test_generate_quiz_true_false(self):
|
|
"""Testet True/False Quiz."""
|
|
response = client.post(
|
|
"/api/worksheets/generate/quiz",
|
|
json={
|
|
"source_text": SAMPLE_TEXT,
|
|
"quiz_types": ["true_false"],
|
|
"num_items": 3
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert "questions" in data["content"]["data"]
|
|
# All questions should be of type true_false
|
|
questions = data["content"]["data"]["questions"]
|
|
assert len(questions) > 0
|
|
assert all(q["type"] == "true_false" for q in questions)
|
|
|
|
def test_generate_quiz_matching(self):
|
|
"""Testet Matching Quiz mit definitionsreichem Text."""
|
|
# Text with clear definition patterns for matching extraction
|
|
definition_text = """
|
|
Photosynthese ist der Prozess der Umwandlung von Lichtenergie in chemische Energie.
|
|
Chloroplasten bezeichnet die Zellorganellen, in denen die Photosynthese stattfindet.
|
|
Glucose bedeutet Traubenzucker, ein einfacher Zucker.
|
|
ATP: Adenosintriphosphat, der universelle Energietraeger der Zellen.
|
|
Chlorophyll ist der gruene Farbstoff, der Licht absorbiert.
|
|
"""
|
|
response = client.post(
|
|
"/api/worksheets/generate/quiz",
|
|
json={
|
|
"source_text": definition_text,
|
|
"quiz_types": ["matching"],
|
|
"num_items": 3
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert "questions" in data["content"]["data"]
|
|
# All questions should be of type matching
|
|
questions = data["content"]["data"]["questions"]
|
|
assert len(questions) > 0
|
|
assert all(q["type"] == "matching" for q in questions)
|
|
|
|
def test_generate_quiz_combined(self):
|
|
"""Testet kombiniertes Quiz mit mehreren Typen."""
|
|
response = client.post(
|
|
"/api/worksheets/generate/quiz",
|
|
json={
|
|
"source_text": SAMPLE_TEXT,
|
|
"quiz_types": ["true_false", "matching", "sorting"],
|
|
"num_items": 2,
|
|
"difficulty": "medium"
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
quiz_data = data["content"]["data"]
|
|
|
|
assert "questions" in quiz_data
|
|
assert "quiz_types" in quiz_data
|
|
# Should have questions from multiple types
|
|
question_types = set(q["type"] for q in quiz_data["questions"])
|
|
assert len(question_types) >= 1 # At least one type should have generated questions
|
|
|
|
|
|
class TestBatchGeneration:
|
|
"""Tests für Batch-Generierung."""
|
|
|
|
def test_batch_generate_multiple_types(self):
|
|
"""Testet Generierung mehrerer Content-Typen."""
|
|
response = client.post(
|
|
"/api/worksheets/generate/batch",
|
|
json={
|
|
"source_text": SAMPLE_TEXT,
|
|
"content_types": ["multiple_choice", "cloze"],
|
|
"topic": "Photosynthese",
|
|
"difficulty": "medium"
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert len(data["contents"]) == 2
|
|
|
|
content_types = [c["content_type"] for c in data["contents"]]
|
|
assert "multiple_choice" in content_types
|
|
assert "cloze" in content_types
|
|
|
|
def test_batch_generate_all_types(self):
|
|
"""Testet Generierung aller Content-Typen."""
|
|
response = client.post(
|
|
"/api/worksheets/generate/batch",
|
|
json={
|
|
"source_text": SAMPLE_TEXT,
|
|
"content_types": ["multiple_choice", "cloze", "mindmap", "quiz"],
|
|
"topic": "Test"
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data["contents"]) == 4
|
|
|
|
def test_batch_generate_partial_failure(self):
|
|
"""Testet Batch mit unbekanntem Typ."""
|
|
response = client.post(
|
|
"/api/worksheets/generate/batch",
|
|
json={
|
|
"source_text": SAMPLE_TEXT,
|
|
"content_types": ["multiple_choice", "unknown_type"],
|
|
"topic": "Test"
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert len(data["contents"]) == 1
|
|
assert len(data["errors"]) == 1
|
|
assert "unknown_type" in data["errors"][0]
|
|
|
|
|
|
class TestContentManagement:
|
|
"""Tests für Content-Verwaltung."""
|
|
|
|
def test_get_content(self):
|
|
"""Testet Abrufen von gespeichertem Content."""
|
|
# Erst generieren
|
|
gen_response = client.post(
|
|
"/api/worksheets/generate/multiple-choice",
|
|
json={
|
|
"source_text": SAMPLE_TEXT,
|
|
"num_questions": 2
|
|
}
|
|
)
|
|
content_id = gen_response.json()["content"]["id"]
|
|
|
|
# Dann abrufen
|
|
get_response = client.get(f"/api/worksheets/content/{content_id}")
|
|
|
|
assert get_response.status_code == 200
|
|
data = get_response.json()
|
|
assert data["content"]["id"] == content_id
|
|
|
|
def test_get_content_not_found(self):
|
|
"""Testet Fehler bei nicht vorhandenem Content."""
|
|
response = client.get("/api/worksheets/content/nonexistent-id")
|
|
assert response.status_code == 404
|
|
|
|
def test_get_content_h5p(self):
|
|
"""Testet H5P-Format Abruf."""
|
|
# Erst generieren
|
|
gen_response = client.post(
|
|
"/api/worksheets/generate/multiple-choice",
|
|
json={
|
|
"source_text": SAMPLE_TEXT,
|
|
"num_questions": 2
|
|
}
|
|
)
|
|
content_id = gen_response.json()["content"]["id"]
|
|
|
|
# H5P abrufen
|
|
h5p_response = client.get(f"/api/worksheets/content/{content_id}/h5p")
|
|
|
|
assert h5p_response.status_code == 200
|
|
data = h5p_response.json()
|
|
assert "library" in data
|
|
assert "params" in data
|
|
|
|
def test_delete_content(self):
|
|
"""Testet Löschen von Content."""
|
|
# Erst generieren
|
|
gen_response = client.post(
|
|
"/api/worksheets/generate/cloze",
|
|
json={
|
|
"source_text": SAMPLE_TEXT,
|
|
"num_gaps": 3
|
|
}
|
|
)
|
|
content_id = gen_response.json()["content"]["id"]
|
|
|
|
# Dann löschen
|
|
del_response = client.delete(f"/api/worksheets/content/{content_id}")
|
|
assert del_response.status_code == 200
|
|
assert del_response.json()["status"] == "deleted"
|
|
|
|
# Prüfen dass gelöscht
|
|
get_response = client.get(f"/api/worksheets/content/{content_id}")
|
|
assert get_response.status_code == 404
|
|
|
|
|
|
class TestMetaEndpoints:
|
|
"""Tests für Meta-Endpoints."""
|
|
|
|
def test_list_content_types(self):
|
|
"""Testet Auflistung der Content-Typen."""
|
|
response = client.get("/api/worksheets/types")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "content_types" in data
|
|
assert len(data["content_types"]) == 4
|
|
|
|
type_names = [t["type"] for t in data["content_types"]]
|
|
assert "multiple_choice" in type_names
|
|
assert "cloze" in type_names
|
|
assert "mindmap" in type_names
|
|
assert "quiz" in type_names
|
|
|
|
def test_get_generation_history(self):
|
|
"""Testet Generierungs-Historie."""
|
|
# Erst etwas generieren
|
|
client.post(
|
|
"/api/worksheets/generate/mindmap",
|
|
json={
|
|
"source_text": SAMPLE_TEXT,
|
|
"topic": "History Test"
|
|
}
|
|
)
|
|
|
|
response = client.get("/api/worksheets/history?limit=5")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "total" in data
|
|
assert "contents" in data
|
|
assert data["total"] >= 1
|
|
|
|
|
|
class TestGeneratorIntegration:
|
|
"""Tests für Generator-Integration."""
|
|
|
|
def test_mc_generator_output_format(self):
|
|
"""Testet MC-Generator Output-Format."""
|
|
response = client.post(
|
|
"/api/worksheets/generate/multiple-choice",
|
|
json={
|
|
"source_text": SAMPLE_TEXT,
|
|
"num_questions": 2
|
|
}
|
|
)
|
|
|
|
data = response.json()
|
|
questions = data["content"]["data"]["questions"]
|
|
|
|
for q in questions:
|
|
assert "question" in q
|
|
assert "options" in q
|
|
assert len(q["options"]) == 4
|
|
assert "difficulty" in q
|
|
|
|
# Genau eine richtige Antwort
|
|
correct_count = sum(1 for opt in q["options"] if opt["is_correct"])
|
|
assert correct_count == 1
|
|
|
|
def test_cloze_generator_gap_format(self):
|
|
"""Testet Cloze-Generator Gap-Format."""
|
|
response = client.post(
|
|
"/api/worksheets/generate/cloze",
|
|
json={
|
|
"source_text": SAMPLE_TEXT,
|
|
"num_gaps": 3
|
|
}
|
|
)
|
|
|
|
data = response.json()
|
|
gaps = data["content"]["data"]["gaps"]
|
|
|
|
for gap in gaps:
|
|
assert "position" in gap
|
|
assert "answer" in gap
|
|
assert "alternatives" in gap
|
|
assert "distractors" in gap
|
|
|
|
def test_mindmap_generator_node_format(self):
|
|
"""Testet Mindmap-Generator Node-Format."""
|
|
response = client.post(
|
|
"/api/worksheets/generate/mindmap",
|
|
json={
|
|
"source_text": SAMPLE_TEXT,
|
|
"max_depth": 3
|
|
}
|
|
)
|
|
|
|
data = response.json()
|
|
root = data["content"]["data"]["mindmap"]["root"]
|
|
|
|
assert "id" in root
|
|
assert "label" in root
|
|
assert "level" in root
|
|
assert "children" in root
|
|
assert "color" in root
|