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>
544 lines
17 KiB
Python
544 lines
17 KiB
Python
"""
|
|
Tests für Correction API.
|
|
|
|
Testet den Korrektur-Workflow:
|
|
- Upload
|
|
- OCR
|
|
- Analyse
|
|
- Export
|
|
"""
|
|
|
|
import pytest
|
|
import io
|
|
from unittest.mock import patch, MagicMock
|
|
from fastapi.testclient import TestClient
|
|
|
|
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)
|
|
|
|
|
|
class TestCorrectionCreate:
|
|
"""Tests für Korrektur-Erstellung."""
|
|
|
|
def test_create_correction_success(self):
|
|
"""Testet erfolgreiche Erstellung."""
|
|
response = client.post(
|
|
"/api/corrections/",
|
|
json={
|
|
"student_id": "student-001",
|
|
"student_name": "Max Mustermann",
|
|
"class_name": "7a",
|
|
"exam_title": "Mathematik Test 1",
|
|
"subject": "Mathematik",
|
|
"max_points": 50.0
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert data["correction"]["student_name"] == "Max Mustermann"
|
|
assert data["correction"]["status"] == "uploaded"
|
|
assert data["correction"]["max_points"] == 50.0
|
|
|
|
def test_create_correction_default_points(self):
|
|
"""Testet Erstellung mit Standard-Punktzahl."""
|
|
response = client.post(
|
|
"/api/corrections/",
|
|
json={
|
|
"student_id": "student-002",
|
|
"student_name": "Anna Schmidt",
|
|
"class_name": "7a",
|
|
"exam_title": "Deutsch Aufsatz",
|
|
"subject": "Deutsch"
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["correction"]["max_points"] == 100.0
|
|
|
|
|
|
class TestCorrectionUpload:
|
|
"""Tests für Datei-Upload."""
|
|
|
|
def _create_correction(self):
|
|
"""Hilfsmethode zum Erstellen einer Korrektur."""
|
|
response = client.post(
|
|
"/api/corrections/",
|
|
json={
|
|
"student_id": "student-test",
|
|
"student_name": "Test Student",
|
|
"class_name": "Test",
|
|
"exam_title": "Test Exam",
|
|
"subject": "Test"
|
|
}
|
|
)
|
|
return response.json()["correction"]["id"]
|
|
|
|
def test_upload_pdf_success(self):
|
|
"""Testet PDF-Upload."""
|
|
correction_id = self._create_correction()
|
|
|
|
# Erstelle Mock-PDF
|
|
pdf_content = b"%PDF-1.4 test content"
|
|
files = {"file": ("test.pdf", io.BytesIO(pdf_content), "application/pdf")}
|
|
|
|
with patch("correction_api._process_ocr"):
|
|
response = client.post(
|
|
f"/api/corrections/{correction_id}/upload",
|
|
files=files
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert data["correction"]["file_path"] is not None
|
|
|
|
def test_upload_image_success(self):
|
|
"""Testet Bild-Upload."""
|
|
correction_id = self._create_correction()
|
|
|
|
# Erstelle Mock-PNG
|
|
png_content = b"\x89PNG\r\n\x1a\n test content"
|
|
files = {"file": ("test.png", io.BytesIO(png_content), "image/png")}
|
|
|
|
with patch("correction_api._process_ocr"):
|
|
response = client.post(
|
|
f"/api/corrections/{correction_id}/upload",
|
|
files=files
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
|
|
def test_upload_invalid_format(self):
|
|
"""Testet Ablehnung ungültiger Formate."""
|
|
correction_id = self._create_correction()
|
|
|
|
files = {"file": ("test.txt", io.BytesIO(b"text"), "text/plain")}
|
|
|
|
response = client.post(
|
|
f"/api/corrections/{correction_id}/upload",
|
|
files=files
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
assert "Ungültiges Dateiformat" in response.json()["detail"]
|
|
|
|
def test_upload_not_found(self):
|
|
"""Testet Upload für nicht existierende Korrektur."""
|
|
files = {"file": ("test.pdf", io.BytesIO(b"content"), "application/pdf")}
|
|
|
|
response = client.post(
|
|
"/api/corrections/nonexistent/upload",
|
|
files=files
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
class TestCorrectionRetrieval:
|
|
"""Tests für Korrektur-Abruf."""
|
|
|
|
def test_get_correction(self):
|
|
"""Testet Abrufen einer Korrektur."""
|
|
# Erstelle Korrektur
|
|
create_response = client.post(
|
|
"/api/corrections/",
|
|
json={
|
|
"student_id": "get-test",
|
|
"student_name": "Get Test",
|
|
"class_name": "Test",
|
|
"exam_title": "Test",
|
|
"subject": "Test"
|
|
}
|
|
)
|
|
correction_id = create_response.json()["correction"]["id"]
|
|
|
|
# Rufe ab
|
|
response = client.get(f"/api/corrections/{correction_id}")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["correction"]["id"] == correction_id
|
|
|
|
def test_get_correction_not_found(self):
|
|
"""Testet Fehler bei nicht vorhandener Korrektur."""
|
|
response = client.get("/api/corrections/nonexistent")
|
|
assert response.status_code == 404
|
|
|
|
def test_list_corrections(self):
|
|
"""Testet Auflisten von Korrekturen."""
|
|
# Erstelle einige Korrekturen
|
|
for i in range(3):
|
|
client.post(
|
|
"/api/corrections/",
|
|
json={
|
|
"student_id": f"list-{i}",
|
|
"student_name": f"Student {i}",
|
|
"class_name": "ListTest",
|
|
"exam_title": "Test",
|
|
"subject": "Test"
|
|
}
|
|
)
|
|
|
|
response = client.get("/api/corrections/?class_name=ListTest")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["total"] >= 3
|
|
|
|
def test_list_corrections_filter_status(self):
|
|
"""Testet Filterung nach Status."""
|
|
response = client.get("/api/corrections/?status=completed")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
# Alle zurückgegebenen Korrekturen sollten "completed" Status haben
|
|
for c in data["corrections"]:
|
|
if c.get("status"): # Falls vorhanden
|
|
assert c["status"] == "completed"
|
|
|
|
|
|
class TestCorrectionAnalysis:
|
|
"""Tests für Korrektur-Analyse."""
|
|
|
|
def _create_correction_with_text(self):
|
|
"""Erstellt Korrektur mit OCR-Text."""
|
|
response = client.post(
|
|
"/api/corrections/",
|
|
json={
|
|
"student_id": "analyze-test",
|
|
"student_name": "Analyze Test",
|
|
"class_name": "Test",
|
|
"exam_title": "Test",
|
|
"subject": "Mathematik",
|
|
"max_points": 100.0
|
|
}
|
|
)
|
|
correction_id = response.json()["correction"]["id"]
|
|
|
|
# Simuliere OCR-Ergebnis durch direktes Setzen
|
|
from correction_api import _corrections, CorrectionStatus
|
|
correction = _corrections[correction_id]
|
|
correction.extracted_text = """
|
|
Aufgabe 1: Die Antwort ist 42.
|
|
|
|
Aufgabe 2: Hier ist meine Lösung für die Gleichung.
|
|
|
|
Aufgabe 3: Das Ergebnis beträgt 15.
|
|
"""
|
|
correction.status = CorrectionStatus.OCR_COMPLETE
|
|
_corrections[correction_id] = correction
|
|
|
|
return correction_id
|
|
|
|
def test_analyze_correction(self):
|
|
"""Testet Analyse einer Korrektur."""
|
|
correction_id = self._create_correction_with_text()
|
|
|
|
response = client.post(
|
|
f"/api/corrections/{correction_id}/analyze"
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert len(data["evaluations"]) > 0
|
|
assert "suggested_grade" in data
|
|
assert "ai_feedback" in data
|
|
|
|
def test_analyze_with_expected_answers(self):
|
|
"""Testet Analyse mit Musterlösung."""
|
|
correction_id = self._create_correction_with_text()
|
|
|
|
expected = {
|
|
"1": "42",
|
|
"2": "Gleichung",
|
|
"3": "15"
|
|
}
|
|
|
|
response = client.post(
|
|
f"/api/corrections/{correction_id}/analyze",
|
|
json=expected
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
# Mit passender Musterlösung sollten einige Antworten korrekt sein
|
|
correct_count = sum(1 for e in data["evaluations"] if e["is_correct"])
|
|
assert correct_count > 0
|
|
|
|
def test_analyze_wrong_status(self):
|
|
"""Testet Analyse bei falschem Status."""
|
|
# Neue Korrektur ohne OCR
|
|
response = client.post(
|
|
"/api/corrections/",
|
|
json={
|
|
"student_id": "wrong-status",
|
|
"student_name": "Wrong Status",
|
|
"class_name": "Test",
|
|
"exam_title": "Test",
|
|
"subject": "Test"
|
|
}
|
|
)
|
|
correction_id = response.json()["correction"]["id"]
|
|
|
|
# Analyse ohne vorherige OCR
|
|
response = client.post(f"/api/corrections/{correction_id}/analyze")
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
|
class TestCorrectionUpdate:
|
|
"""Tests für Korrektur-Aktualisierung."""
|
|
|
|
def test_update_correction(self):
|
|
"""Testet Aktualisierung einer Korrektur."""
|
|
# Erstelle Korrektur
|
|
response = client.post(
|
|
"/api/corrections/",
|
|
json={
|
|
"student_id": "update-test",
|
|
"student_name": "Update Test",
|
|
"class_name": "Test",
|
|
"exam_title": "Test",
|
|
"subject": "Test"
|
|
}
|
|
)
|
|
correction_id = response.json()["correction"]["id"]
|
|
|
|
# Aktualisiere
|
|
response = client.put(
|
|
f"/api/corrections/{correction_id}",
|
|
json={
|
|
"grade": "2",
|
|
"total_points": 85.0,
|
|
"teacher_notes": "Gute Arbeit!"
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["correction"]["grade"] == "2"
|
|
assert data["correction"]["total_points"] == 85.0
|
|
assert data["correction"]["teacher_notes"] == "Gute Arbeit!"
|
|
|
|
def test_complete_correction(self):
|
|
"""Testet Abschluss einer Korrektur."""
|
|
# Erstelle Korrektur
|
|
response = client.post(
|
|
"/api/corrections/",
|
|
json={
|
|
"student_id": "complete-test",
|
|
"student_name": "Complete Test",
|
|
"class_name": "Test",
|
|
"exam_title": "Test",
|
|
"subject": "Test"
|
|
}
|
|
)
|
|
correction_id = response.json()["correction"]["id"]
|
|
|
|
# Schließe ab
|
|
response = client.post(f"/api/corrections/{correction_id}/complete")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["correction"]["status"] == "completed"
|
|
|
|
|
|
class TestCorrectionDelete:
|
|
"""Tests für Korrektur-Löschung."""
|
|
|
|
def test_delete_correction(self):
|
|
"""Testet Löschen einer Korrektur."""
|
|
# Erstelle Korrektur
|
|
response = client.post(
|
|
"/api/corrections/",
|
|
json={
|
|
"student_id": "delete-test",
|
|
"student_name": "Delete Test",
|
|
"class_name": "Test",
|
|
"exam_title": "Test",
|
|
"subject": "Test"
|
|
}
|
|
)
|
|
correction_id = response.json()["correction"]["id"]
|
|
|
|
# Lösche
|
|
response = client.delete(f"/api/corrections/{correction_id}")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["status"] == "deleted"
|
|
|
|
# Prüfe dass gelöscht
|
|
response = client.get(f"/api/corrections/{correction_id}")
|
|
assert response.status_code == 404
|
|
|
|
def test_delete_not_found(self):
|
|
"""Testet Fehler beim Löschen nicht existierender Korrektur."""
|
|
response = client.delete("/api/corrections/nonexistent")
|
|
assert response.status_code == 404
|
|
|
|
|
|
class TestClassSummary:
|
|
"""Tests für Klassen-Zusammenfassung."""
|
|
|
|
def _create_completed_corrections(self, class_name: str, count: int):
|
|
"""Erstellt abgeschlossene Korrekturen für eine Klasse."""
|
|
from correction_api import _corrections, CorrectionStatus
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
for i in range(count):
|
|
response = client.post(
|
|
"/api/corrections/",
|
|
json={
|
|
"student_id": f"summary-{i}",
|
|
"student_name": f"Student {i}",
|
|
"class_name": class_name,
|
|
"exam_title": "Summary Test",
|
|
"subject": "Test",
|
|
"max_points": 100.0
|
|
}
|
|
)
|
|
correction_id = response.json()["correction"]["id"]
|
|
|
|
# Setze als completed mit Punkten
|
|
correction = _corrections[correction_id]
|
|
correction.status = CorrectionStatus.COMPLETED
|
|
correction.total_points = 70 + i * 5 # 70, 75, 80, ...
|
|
correction.percentage = correction.total_points
|
|
correction.grade = str(3 - i // 2) # Verschiedene Noten
|
|
_corrections[correction_id] = correction
|
|
|
|
def test_class_summary(self):
|
|
"""Testet Klassen-Zusammenfassung."""
|
|
class_name = "SummaryTestClass"
|
|
self._create_completed_corrections(class_name, 3)
|
|
|
|
response = client.get(f"/api/corrections/class/{class_name}/summary")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["class_name"] == class_name
|
|
assert data["total_students"] == 3
|
|
assert "average_percentage" in data
|
|
assert "grade_distribution" in data
|
|
assert len(data["corrections"]) == 3
|
|
|
|
def test_class_summary_empty(self):
|
|
"""Testet Zusammenfassung für leere Klasse."""
|
|
response = client.get("/api/corrections/class/EmptyClass/summary")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["total_students"] == 0
|
|
assert data["average_percentage"] == 0
|
|
|
|
|
|
class TestGradeCalculation:
|
|
"""Tests für Notenberechnung."""
|
|
|
|
def test_grade_1(self):
|
|
"""Testet Note 1 (>=92%)."""
|
|
from correction_api import _calculate_grade
|
|
assert _calculate_grade(92) == "1"
|
|
assert _calculate_grade(100) == "1"
|
|
|
|
def test_grade_2(self):
|
|
"""Testet Note 2 (81-91%)."""
|
|
from correction_api import _calculate_grade
|
|
assert _calculate_grade(81) == "2"
|
|
assert _calculate_grade(91) == "2"
|
|
|
|
def test_grade_3(self):
|
|
"""Testet Note 3 (67-80%)."""
|
|
from correction_api import _calculate_grade
|
|
assert _calculate_grade(67) == "3"
|
|
assert _calculate_grade(80) == "3"
|
|
|
|
def test_grade_4(self):
|
|
"""Testet Note 4 (50-66%)."""
|
|
from correction_api import _calculate_grade
|
|
assert _calculate_grade(50) == "4"
|
|
assert _calculate_grade(66) == "4"
|
|
|
|
def test_grade_5(self):
|
|
"""Testet Note 5 (30-49%)."""
|
|
from correction_api import _calculate_grade
|
|
assert _calculate_grade(30) == "5"
|
|
assert _calculate_grade(49) == "5"
|
|
|
|
def test_grade_6(self):
|
|
"""Testet Note 6 (<30%)."""
|
|
from correction_api import _calculate_grade
|
|
assert _calculate_grade(29) == "6"
|
|
assert _calculate_grade(0) == "6"
|
|
|
|
|
|
class TestOCRRetry:
|
|
"""Tests für OCR-Wiederholung."""
|
|
|
|
def test_retry_ocr(self):
|
|
"""Testet OCR-Wiederholung."""
|
|
# Erstelle Korrektur mit Datei
|
|
response = client.post(
|
|
"/api/corrections/",
|
|
json={
|
|
"student_id": "retry-test",
|
|
"student_name": "Retry Test",
|
|
"class_name": "Test",
|
|
"exam_title": "Test",
|
|
"subject": "Test"
|
|
}
|
|
)
|
|
correction_id = response.json()["correction"]["id"]
|
|
|
|
# Setze file_path manuell (simuliert Upload)
|
|
from correction_api import _corrections
|
|
import tempfile
|
|
import os
|
|
|
|
# Erstelle temp file
|
|
fd, path = tempfile.mkstemp(suffix=".pdf")
|
|
os.write(fd, b"%PDF-1.4 test")
|
|
os.close(fd)
|
|
|
|
correction = _corrections[correction_id]
|
|
correction.file_path = path
|
|
_corrections[correction_id] = correction
|
|
|
|
# Retry OCR
|
|
with patch("correction_api._process_ocr"):
|
|
response = client.post(f"/api/corrections/{correction_id}/ocr/retry")
|
|
|
|
assert response.status_code == 200
|
|
|
|
# Cleanup
|
|
os.remove(path)
|
|
|
|
def test_retry_ocr_no_file(self):
|
|
"""Testet Fehler bei OCR-Retry ohne Datei."""
|
|
response = client.post(
|
|
"/api/corrections/",
|
|
json={
|
|
"student_id": "retry-no-file",
|
|
"student_name": "No File",
|
|
"class_name": "Test",
|
|
"exam_title": "Test",
|
|
"subject": "Test"
|
|
}
|
|
)
|
|
correction_id = response.json()["correction"]["id"]
|
|
|
|
response = client.post(f"/api/corrections/{correction_id}/ocr/retry")
|
|
|
|
assert response.status_code == 400
|
|
assert "Keine Datei" in response.json()["detail"]
|