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>
398 lines
14 KiB
Python
398 lines
14 KiB
Python
"""
|
|
Tests fuer die Abitur-Docs API
|
|
|
|
Tests fuer:
|
|
- NiBiS Dateinamen-Erkennung
|
|
- Dokumenten-Metadaten-Verwaltung
|
|
- ZIP-Import
|
|
- Status-Workflow
|
|
- Enum-Endpunkte
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import MagicMock, patch
|
|
from datetime import datetime
|
|
|
|
# Import des zu testenden Moduls
|
|
import sys
|
|
import os
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from abitur_docs_api import (
|
|
router,
|
|
AbiturDokument,
|
|
VerarbeitungsStatus,
|
|
DocumentMetadata,
|
|
RecognitionResult,
|
|
Bundesland,
|
|
AbiturFach,
|
|
Anforderungsniveau,
|
|
DokumentTyp,
|
|
parse_nibis_filename,
|
|
documents_db,
|
|
)
|
|
|
|
|
|
class TestNiBiSFilenameRecognition:
|
|
"""Tests fuer die automatische NiBiS Dateinamen-Erkennung."""
|
|
|
|
def test_parse_deutsch_ea_aufgabe(self):
|
|
"""Deutsch eA Aufgabe I sollte erkannt werden."""
|
|
result = parse_nibis_filename("2025_Deutsch_eA_I.pdf")
|
|
assert result.confidence > 0.5
|
|
assert result.extracted.get("jahr") == 2025
|
|
assert result.extracted.get("fach") == "deutsch"
|
|
assert result.extracted.get("niveau") == "eA"
|
|
assert result.extracted.get("aufgaben_nummer") == "I"
|
|
|
|
def test_parse_deutsch_ea_ewh(self):
|
|
"""Deutsch eA Erwartungshorizont sollte erkannt werden."""
|
|
result = parse_nibis_filename("2025_Deutsch_eA_I_EWH.pdf")
|
|
assert result.confidence > 0.5
|
|
assert result.extracted.get("typ") == "erwartungshorizont"
|
|
|
|
def test_parse_englisch_ga(self):
|
|
"""Englisch gA sollte erkannt werden."""
|
|
result = parse_nibis_filename("2025_Englisch_gA_II.pdf")
|
|
assert result.extracted.get("fach") == "englisch"
|
|
assert result.extracted.get("niveau") == "gA"
|
|
assert result.extracted.get("aufgaben_nummer") == "II"
|
|
|
|
def test_parse_mathematik(self):
|
|
"""Mathematik eA sollte erkannt werden."""
|
|
result = parse_nibis_filename("2025_Mathematik_eA_III.pdf")
|
|
assert result.extracted.get("fach") == "mathematik"
|
|
|
|
def test_parse_with_hoerverstehen(self):
|
|
"""Hoerverstehen sollte erkannt werden."""
|
|
result = parse_nibis_filename("2025_Englisch_eA_Hoerverstehen.pdf")
|
|
assert result.extracted.get("typ") == "hoerverstehen"
|
|
|
|
def test_parse_with_sprachmittlung(self):
|
|
"""Sprachmittlung sollte erkannt werden."""
|
|
result = parse_nibis_filename("2025_Spanisch_gA_Sprachmittlung.pdf")
|
|
assert result.extracted.get("typ") == "sprachmittlung"
|
|
|
|
def test_parse_deckblatt(self):
|
|
"""Deckblatt sollte erkannt werden."""
|
|
result = parse_nibis_filename("2025_Geschichte_eA_Deckblatt.pdf")
|
|
assert result.extracted.get("typ") == "deckblatt"
|
|
|
|
def test_parse_unknown_format(self):
|
|
"""Unbekanntes Format sollte niedrige Confidence haben."""
|
|
result = parse_nibis_filename("random_file.pdf")
|
|
assert result.confidence < 0.3
|
|
|
|
def test_parse_year_extraction(self):
|
|
"""Jahr sollte aus dem Dateinamen extrahiert werden."""
|
|
result_2024 = parse_nibis_filename("2024_Deutsch_eA_I.pdf")
|
|
result_2025 = parse_nibis_filename("2025_Deutsch_eA_I.pdf")
|
|
assert result_2024.extracted.get("jahr") == 2024
|
|
assert result_2025.extracted.get("jahr") == 2025
|
|
|
|
def test_parse_case_insensitive(self):
|
|
"""Erkennung sollte case-insensitive sein."""
|
|
result_upper = parse_nibis_filename("2025_DEUTSCH_EA_I.pdf")
|
|
result_lower = parse_nibis_filename("2025_deutsch_ea_i.pdf")
|
|
assert result_upper.extracted.get("fach") == "deutsch"
|
|
assert result_lower.extracted.get("fach") == "deutsch"
|
|
|
|
|
|
class TestVerarbeitungsStatus:
|
|
"""Tests fuer den Dokument-Status-Workflow."""
|
|
|
|
def test_pending_status(self):
|
|
"""Pending Status sollte existieren."""
|
|
assert VerarbeitungsStatus.PENDING.value == "pending"
|
|
|
|
def test_recognized_status(self):
|
|
"""Recognized Status sollte existieren."""
|
|
assert VerarbeitungsStatus.RECOGNIZED.value == "recognized"
|
|
|
|
def test_confirmed_status(self):
|
|
"""Confirmed Status sollte existieren."""
|
|
assert VerarbeitungsStatus.CONFIRMED.value == "confirmed"
|
|
|
|
def test_indexed_status(self):
|
|
"""Indexed Status sollte existieren."""
|
|
assert VerarbeitungsStatus.INDEXED.value == "indexed"
|
|
|
|
def test_error_status(self):
|
|
"""Error Status sollte existieren."""
|
|
assert VerarbeitungsStatus.ERROR.value == "error"
|
|
|
|
def test_status_workflow_order(self):
|
|
"""Status-Workflow sollte die richtige Reihenfolge haben."""
|
|
statuses = list(VerarbeitungsStatus)
|
|
expected_order = [
|
|
VerarbeitungsStatus.PENDING,
|
|
VerarbeitungsStatus.PROCESSING,
|
|
VerarbeitungsStatus.RECOGNIZED,
|
|
VerarbeitungsStatus.CONFIRMED,
|
|
VerarbeitungsStatus.INDEXED,
|
|
VerarbeitungsStatus.ERROR,
|
|
]
|
|
assert statuses == expected_order
|
|
|
|
|
|
class TestBundesland:
|
|
"""Tests fuer die Bundesland-Enumeration."""
|
|
|
|
def test_niedersachsen(self):
|
|
"""Niedersachsen sollte existieren."""
|
|
assert Bundesland.NIEDERSACHSEN.value == "niedersachsen"
|
|
|
|
def test_nrw(self):
|
|
"""Nordrhein-Westfalen sollte existieren."""
|
|
assert Bundesland.NORDRHEIN_WESTFALEN.value == "nordrhein_westfalen"
|
|
|
|
def test_bayern(self):
|
|
"""Bayern sollte existieren."""
|
|
assert Bundesland.BAYERN.value == "bayern"
|
|
|
|
def test_all_bundeslaender_present(self):
|
|
"""Alle wichtigen Bundeslaender sollten vorhanden sein."""
|
|
bundeslaender = [b.value for b in Bundesland]
|
|
assert "niedersachsen" in bundeslaender
|
|
assert "nordrhein_westfalen" in bundeslaender
|
|
assert "bayern" in bundeslaender
|
|
assert "baden_wuerttemberg" in bundeslaender
|
|
|
|
|
|
class TestAbiturFach:
|
|
"""Tests fuer die Fach-Enumeration."""
|
|
|
|
def test_deutsch(self):
|
|
"""Deutsch sollte existieren."""
|
|
assert AbiturFach.DEUTSCH.value == "deutsch"
|
|
|
|
def test_mathematik(self):
|
|
"""Mathematik sollte existieren."""
|
|
assert AbiturFach.MATHEMATIK.value == "mathematik"
|
|
|
|
def test_englisch(self):
|
|
"""Englisch sollte existieren."""
|
|
assert AbiturFach.ENGLISCH.value == "englisch"
|
|
|
|
def test_sprachen_present(self):
|
|
"""Alle Hauptsprachen sollten vorhanden sein."""
|
|
faecher = [f.value for f in AbiturFach]
|
|
assert "deutsch" in faecher
|
|
assert "englisch" in faecher
|
|
assert "franzoesisch" in faecher
|
|
assert "spanisch" in faecher
|
|
|
|
def test_naturwissenschaften_present(self):
|
|
"""Naturwissenschaften sollten vorhanden sein."""
|
|
faecher = [f.value for f in AbiturFach]
|
|
assert "biologie" in faecher
|
|
assert "chemie" in faecher
|
|
assert "physik" in faecher
|
|
|
|
|
|
class TestAnforderungsniveau:
|
|
"""Tests fuer die Anforderungsniveau-Enumeration."""
|
|
|
|
def test_ea(self):
|
|
"""eA (erhoehtes Anforderungsniveau) sollte existieren."""
|
|
assert Anforderungsniveau.EA.value == "eA"
|
|
|
|
def test_ga(self):
|
|
"""gA (grundlegendes Anforderungsniveau) sollte existieren."""
|
|
assert Anforderungsniveau.GA.value == "gA"
|
|
|
|
|
|
class TestDokumentTyp:
|
|
"""Tests fuer die Dokumenttyp-Enumeration."""
|
|
|
|
def test_aufgabe(self):
|
|
"""Aufgabe sollte existieren."""
|
|
assert DokumentTyp.AUFGABE.value == "aufgabe"
|
|
|
|
def test_erwartungshorizont(self):
|
|
"""Erwartungshorizont sollte existieren."""
|
|
assert DokumentTyp.ERWARTUNGSHORIZONT.value == "erwartungshorizont"
|
|
|
|
def test_all_types_present(self):
|
|
"""Alle Dokumenttypen sollten vorhanden sein."""
|
|
typen = [t.value for t in DokumentTyp]
|
|
assert "aufgabe" in typen
|
|
assert "erwartungshorizont" in typen
|
|
assert "deckblatt" in typen
|
|
assert "hoerverstehen" in typen
|
|
assert "sprachmittlung" in typen
|
|
|
|
|
|
class TestDocumentMetadata:
|
|
"""Tests fuer die Dokumenten-Metadaten."""
|
|
|
|
def test_create_metadata(self):
|
|
"""Metadaten sollten erstellt werden koennen."""
|
|
metadata = DocumentMetadata(
|
|
jahr=2025,
|
|
bundesland="niedersachsen",
|
|
fach="deutsch",
|
|
niveau="eA",
|
|
dokument_typ="aufgabe",
|
|
aufgaben_nummer="I"
|
|
)
|
|
assert metadata.jahr == 2025
|
|
assert metadata.fach == "deutsch"
|
|
assert metadata.niveau == "eA"
|
|
|
|
def test_optional_fields(self):
|
|
"""Optionale Felder sollten None sein koennen."""
|
|
metadata = DocumentMetadata(
|
|
jahr=None,
|
|
bundesland=None,
|
|
fach=None,
|
|
niveau=None,
|
|
dokument_typ=None,
|
|
aufgaben_nummer=None
|
|
)
|
|
assert metadata.jahr is None
|
|
assert metadata.fach is None
|
|
|
|
|
|
class TestAbiturDokument:
|
|
"""Tests fuer das AbiturDokument-Modell."""
|
|
|
|
def test_create_document(self):
|
|
"""Ein Dokument sollte erstellt werden koennen."""
|
|
# Use internal AbiturDokument dataclass with correct field names
|
|
doc = AbiturDokument(
|
|
id="doc-123",
|
|
dateiname="doc-123.pdf",
|
|
original_dateiname="2025_Deutsch_eA_I.pdf",
|
|
bundesland=Bundesland.NIEDERSACHSEN,
|
|
fach=AbiturFach.DEUTSCH,
|
|
jahr=2025,
|
|
niveau=Anforderungsniveau.EA,
|
|
typ=DokumentTyp.AUFGABE,
|
|
aufgaben_nummer="I",
|
|
status=VerarbeitungsStatus.PENDING,
|
|
confidence=0.85,
|
|
file_path="/data/docs/doc-123.pdf",
|
|
file_size=1024,
|
|
indexed=False,
|
|
vector_ids=[],
|
|
created_at=datetime.now(),
|
|
updated_at=datetime.now()
|
|
)
|
|
assert doc.id == "doc-123"
|
|
assert doc.status == VerarbeitungsStatus.PENDING
|
|
|
|
def test_document_with_recognition_result(self):
|
|
"""Ein Dokument mit Erkennungsergebnis sollte erstellt werden koennen."""
|
|
# Test that RecognitionResult works with extracted property
|
|
recognition = parse_nibis_filename("2025_Deutsch_eA_I.pdf")
|
|
assert recognition.confidence > 0.5
|
|
assert recognition.extracted.get("fach") == "deutsch"
|
|
|
|
doc = AbiturDokument(
|
|
id="doc-456",
|
|
dateiname="doc-456.pdf",
|
|
original_dateiname="2025_Deutsch_eA_I.pdf",
|
|
bundesland=Bundesland.NIEDERSACHSEN,
|
|
fach=AbiturFach.DEUTSCH,
|
|
jahr=2025,
|
|
niveau=Anforderungsniveau.EA,
|
|
typ=DokumentTyp.AUFGABE,
|
|
aufgaben_nummer="I",
|
|
status=VerarbeitungsStatus.RECOGNIZED,
|
|
confidence=recognition.confidence,
|
|
file_path="/data/docs/doc-456.pdf",
|
|
file_size=1024,
|
|
indexed=False,
|
|
vector_ids=[],
|
|
created_at=datetime.now(),
|
|
updated_at=datetime.now()
|
|
)
|
|
assert doc.confidence > 0.5
|
|
|
|
|
|
class TestRecognitionResult:
|
|
"""Tests fuer das Erkennungsergebnis."""
|
|
|
|
def test_create_recognition_result(self):
|
|
"""Ein Erkennungsergebnis sollte erstellt werden koennen."""
|
|
result = parse_nibis_filename("2025_Deutsch_eA_I.pdf")
|
|
assert result.confidence > 0.5
|
|
assert result.method == "filename_pattern"
|
|
assert result.extracted.get("jahr") == 2025
|
|
assert result.extracted.get("fach") == "deutsch"
|
|
|
|
def test_confidence_range(self):
|
|
"""Confidence sollte zwischen 0 und 1 liegen."""
|
|
result_low = parse_nibis_filename("random_file.pdf")
|
|
result_high = parse_nibis_filename("2025_Deutsch_eA_I_EWH.pdf")
|
|
assert result_low.confidence >= 0.0
|
|
assert result_high.confidence <= 1.0
|
|
|
|
|
|
class TestDocumentsDB:
|
|
"""Tests fuer die In-Memory Datenbank."""
|
|
|
|
def setup_method(self):
|
|
"""Setup vor jedem Test - leere die DB."""
|
|
documents_db.clear()
|
|
|
|
def test_empty_db(self):
|
|
"""Eine leere DB sollte leer sein."""
|
|
assert len(documents_db) == 0
|
|
|
|
def test_add_document_to_db(self):
|
|
"""Ein Dokument sollte zur DB hinzugefuegt werden koennen."""
|
|
doc = AbiturDokument(
|
|
id="test-1",
|
|
dateiname="test-1.pdf",
|
|
original_dateiname="test.pdf",
|
|
bundesland=Bundesland.NIEDERSACHSEN,
|
|
fach=AbiturFach.DEUTSCH,
|
|
jahr=2025,
|
|
niveau=Anforderungsniveau.EA,
|
|
typ=DokumentTyp.AUFGABE,
|
|
aufgaben_nummer="I",
|
|
status=VerarbeitungsStatus.PENDING,
|
|
confidence=0.85,
|
|
file_path="/data/test.pdf",
|
|
file_size=1024,
|
|
indexed=False,
|
|
vector_ids=[],
|
|
created_at=datetime.now(),
|
|
updated_at=datetime.now()
|
|
)
|
|
documents_db["test-1"] = doc
|
|
assert "test-1" in documents_db
|
|
assert documents_db["test-1"].original_dateiname == "test.pdf"
|
|
|
|
|
|
class TestFilenamePatterns:
|
|
"""Tests fuer verschiedene Dateinamen-Muster."""
|
|
|
|
def test_pattern_with_underscore(self):
|
|
"""Unterstrich-Trennzeichen sollten erkannt werden."""
|
|
result = parse_nibis_filename("2025_Biologie_eA_II.pdf")
|
|
assert result.extracted.get("fach") == "biologie"
|
|
|
|
def test_pattern_with_aufgabe_nummer(self):
|
|
"""Roemische Zahlen fuer Aufgaben sollten erkannt werden."""
|
|
for num in ["I", "II", "III", "IV"]:
|
|
result = parse_nibis_filename(f"2025_Deutsch_eA_{num}.pdf")
|
|
assert result.extracted.get("aufgaben_nummer") == num
|
|
|
|
def test_pattern_ewh_suffix(self):
|
|
"""EWH-Suffix sollte als Erwartungshorizont erkannt werden."""
|
|
result = parse_nibis_filename("2025_Deutsch_eA_I_EWH.pdf")
|
|
assert result.extracted.get("typ") == "erwartungshorizont"
|
|
|
|
def test_pattern_without_aufgabe_nummer(self):
|
|
"""Dateien ohne Aufgaben-Nummer sollten auch erkannt werden."""
|
|
result = parse_nibis_filename("2025_Deutsch_eA.pdf")
|
|
assert result.extracted.get("jahr") == 2025
|
|
assert result.extracted.get("fach") == "deutsch"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|