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:
528
backend/tests/test_worksheets_api.py
Normal file
528
backend/tests/test_worksheets_api.py
Normal file
@@ -0,0 +1,528 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user