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>
401 lines
13 KiB
Python
401 lines
13 KiB
Python
"""
|
|
Tests für die Certificates API.
|
|
|
|
Testet:
|
|
- CRUD-Operationen für Zeugnisse
|
|
- PDF-Export
|
|
- Workflow (Draft -> Review -> Approved -> Issued)
|
|
- Notenstatistiken
|
|
|
|
Note: Some tests require WeasyPrint which needs system libraries.
|
|
"""
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from unittest.mock import patch, AsyncMock
|
|
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 TestCertificatesAPIImport:
|
|
"""Tests für Certificates API Import."""
|
|
|
|
def test_import_certificates_api(self):
|
|
"""Test that certificates_api can be imported."""
|
|
from certificates_api import router
|
|
assert router is not None
|
|
|
|
def test_import_enums(self):
|
|
"""Test that enums can be imported."""
|
|
from certificates_api import CertificateType, CertificateStatus, BehaviorGrade
|
|
assert CertificateType is not None
|
|
assert CertificateStatus is not None
|
|
assert BehaviorGrade is not None
|
|
|
|
def test_import_models(self):
|
|
"""Test that Pydantic models can be imported."""
|
|
from certificates_api import (
|
|
CertificateCreateRequest,
|
|
CertificateUpdateRequest,
|
|
CertificateResponse,
|
|
SubjectGrade,
|
|
AttendanceInfo
|
|
)
|
|
assert CertificateCreateRequest is not None
|
|
assert SubjectGrade is not None
|
|
|
|
|
|
class TestCertificateTypes:
|
|
"""Tests für Zeugnistypen."""
|
|
|
|
def test_certificate_types_values(self):
|
|
"""Test that all certificate types have correct values."""
|
|
from certificates_api import CertificateType
|
|
|
|
expected_types = ["halbjahr", "jahres", "abschluss", "abgang", "uebergang"]
|
|
actual_types = [t.value for t in CertificateType]
|
|
|
|
for expected in expected_types:
|
|
assert expected in actual_types
|
|
|
|
def test_certificate_status_values(self):
|
|
"""Test that all statuses have correct values."""
|
|
from certificates_api import CertificateStatus
|
|
|
|
expected_statuses = ["draft", "review", "approved", "issued", "archived"]
|
|
actual_statuses = [s.value for s in CertificateStatus]
|
|
|
|
for expected in expected_statuses:
|
|
assert expected in actual_statuses
|
|
|
|
|
|
class TestSubjectGrade:
|
|
"""Tests für SubjectGrade Model."""
|
|
|
|
def test_create_subject_grade(self):
|
|
"""Test creating a subject grade."""
|
|
from certificates_api import SubjectGrade
|
|
|
|
grade = SubjectGrade(
|
|
name="Mathematik",
|
|
grade="2",
|
|
points=11,
|
|
note="Gute Mitarbeit"
|
|
)
|
|
|
|
assert grade.name == "Mathematik"
|
|
assert grade.grade == "2"
|
|
assert grade.points == 11
|
|
|
|
def test_create_subject_grade_minimal(self):
|
|
"""Test creating a minimal subject grade."""
|
|
from certificates_api import SubjectGrade
|
|
|
|
grade = SubjectGrade(name="Deutsch", grade="1")
|
|
|
|
assert grade.name == "Deutsch"
|
|
assert grade.grade == "1"
|
|
assert grade.points is None
|
|
|
|
|
|
class TestAttendanceInfo:
|
|
"""Tests für AttendanceInfo Model."""
|
|
|
|
def test_create_attendance_info(self):
|
|
"""Test creating attendance info."""
|
|
from certificates_api import AttendanceInfo
|
|
|
|
attendance = AttendanceInfo(
|
|
days_absent=10,
|
|
days_excused=8,
|
|
days_unexcused=2
|
|
)
|
|
|
|
assert attendance.days_absent == 10
|
|
assert attendance.days_excused == 8
|
|
assert attendance.days_unexcused == 2
|
|
|
|
def test_default_attendance_values(self):
|
|
"""Test default attendance values."""
|
|
from certificates_api import AttendanceInfo
|
|
|
|
attendance = AttendanceInfo()
|
|
|
|
assert attendance.days_absent == 0
|
|
assert attendance.days_excused == 0
|
|
assert attendance.days_unexcused == 0
|
|
|
|
|
|
class TestCertificateCreateRequest:
|
|
"""Tests für CertificateCreateRequest Model."""
|
|
|
|
def test_create_certificate_request(self):
|
|
"""Test creating a certificate request."""
|
|
from certificates_api import (
|
|
CertificateCreateRequest,
|
|
CertificateType,
|
|
SubjectGrade,
|
|
AttendanceInfo
|
|
)
|
|
|
|
request = CertificateCreateRequest(
|
|
student_id="student-123",
|
|
student_name="Max Mustermann",
|
|
student_birthdate="15.05.2010",
|
|
student_class="5a",
|
|
school_year="2024/2025",
|
|
certificate_type=CertificateType.HALBJAHR,
|
|
subjects=[
|
|
SubjectGrade(name="Deutsch", grade="2"),
|
|
SubjectGrade(name="Mathematik", grade="2"),
|
|
],
|
|
attendance=AttendanceInfo(days_absent=5, days_excused=5),
|
|
class_teacher="Frau Schmidt",
|
|
principal="Herr Direktor"
|
|
)
|
|
|
|
assert request.student_name == "Max Mustermann"
|
|
assert request.certificate_type == CertificateType.HALBJAHR
|
|
assert len(request.subjects) == 2
|
|
|
|
|
|
class TestHelperFunctions:
|
|
"""Tests für Helper-Funktionen."""
|
|
|
|
def test_calculate_average(self):
|
|
"""Test average calculation."""
|
|
from certificates_api import _calculate_average
|
|
|
|
subjects = [
|
|
{"name": "Deutsch", "grade": "2"},
|
|
{"name": "Mathe", "grade": "3"},
|
|
{"name": "Englisch", "grade": "1"}
|
|
]
|
|
|
|
avg = _calculate_average(subjects)
|
|
assert avg == 2.0
|
|
|
|
def test_calculate_average_empty(self):
|
|
"""Test average calculation with empty list."""
|
|
from certificates_api import _calculate_average
|
|
|
|
avg = _calculate_average([])
|
|
assert avg is None
|
|
|
|
def test_calculate_average_non_numeric(self):
|
|
"""Test average calculation with non-numeric grades."""
|
|
from certificates_api import _calculate_average
|
|
|
|
subjects = [
|
|
{"name": "Deutsch", "grade": "A"},
|
|
{"name": "Mathe", "grade": "B"}
|
|
]
|
|
|
|
avg = _calculate_average(subjects)
|
|
assert avg is None
|
|
|
|
def test_get_type_label(self):
|
|
"""Test type label function."""
|
|
from certificates_api import _get_type_label, CertificateType
|
|
|
|
assert "Halbjahres" in _get_type_label(CertificateType.HALBJAHR)
|
|
assert "Jahres" in _get_type_label(CertificateType.JAHRES)
|
|
assert "Abschluss" in _get_type_label(CertificateType.ABSCHLUSS)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not WEASYPRINT_AVAILABLE,
|
|
reason="WeasyPrint not available (requires system libraries)"
|
|
)
|
|
class TestCertificatesAPIEndpoints:
|
|
"""Integration tests für Certificates 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_certificate_data(self):
|
|
"""Sample certificate data for tests."""
|
|
return {
|
|
"student_id": "student-test-123",
|
|
"student_name": "Test Schüler",
|
|
"student_birthdate": "01.01.2012",
|
|
"student_class": "5a",
|
|
"school_year": "2024/2025",
|
|
"certificate_type": "halbjahr",
|
|
"subjects": [
|
|
{"name": "Deutsch", "grade": "2"},
|
|
{"name": "Mathematik", "grade": "3"},
|
|
{"name": "Englisch", "grade": "2"}
|
|
],
|
|
"attendance": {
|
|
"days_absent": 5,
|
|
"days_excused": 4,
|
|
"days_unexcused": 1
|
|
},
|
|
"class_teacher": "Frau Test",
|
|
"principal": "Herr Direktor"
|
|
}
|
|
|
|
def test_create_certificate(self, client, sample_certificate_data):
|
|
"""Test creating a new certificate."""
|
|
if not client:
|
|
pytest.skip("Client not available")
|
|
|
|
response = client.post("/api/certificates/", json=sample_certificate_data)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["student_name"] == sample_certificate_data["student_name"]
|
|
assert data["status"] == "draft"
|
|
assert "id" in data
|
|
assert data["average_grade"] is not None
|
|
|
|
def test_get_certificate(self, client, sample_certificate_data):
|
|
"""Test getting a certificate by ID."""
|
|
if not client:
|
|
pytest.skip("Client not available")
|
|
|
|
# First create a certificate
|
|
create_response = client.post("/api/certificates/", json=sample_certificate_data)
|
|
cert_id = create_response.json()["id"]
|
|
|
|
# Then get it
|
|
response = client.get(f"/api/certificates/{cert_id}")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["id"] == cert_id
|
|
|
|
def test_update_certificate(self, client, sample_certificate_data):
|
|
"""Test updating a certificate."""
|
|
if not client:
|
|
pytest.skip("Client not available")
|
|
|
|
# Create certificate
|
|
create_response = client.post("/api/certificates/", json=sample_certificate_data)
|
|
cert_id = create_response.json()["id"]
|
|
|
|
# Update it
|
|
update_data = {"remarks": "Versetzung in Klasse 6a"}
|
|
response = client.put(f"/api/certificates/{cert_id}", json=update_data)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["remarks"] == "Versetzung in Klasse 6a"
|
|
|
|
def test_delete_certificate(self, client, sample_certificate_data):
|
|
"""Test deleting a certificate."""
|
|
if not client:
|
|
pytest.skip("Client not available")
|
|
|
|
# Create certificate
|
|
create_response = client.post("/api/certificates/", json=sample_certificate_data)
|
|
cert_id = create_response.json()["id"]
|
|
|
|
# Delete it
|
|
response = client.delete(f"/api/certificates/{cert_id}")
|
|
assert response.status_code == 200
|
|
|
|
# Verify it's deleted
|
|
get_response = client.get(f"/api/certificates/{cert_id}")
|
|
assert get_response.status_code == 404
|
|
|
|
def test_export_pdf(self, client, sample_certificate_data):
|
|
"""Test PDF export."""
|
|
if not client:
|
|
pytest.skip("Client not available")
|
|
|
|
# Create certificate
|
|
create_response = client.post("/api/certificates/", json=sample_certificate_data)
|
|
cert_id = create_response.json()["id"]
|
|
|
|
# Export as PDF
|
|
response = client.post(f"/api/certificates/{cert_id}/export-pdf")
|
|
|
|
assert response.status_code == 200
|
|
assert response.headers["content-type"] == "application/pdf"
|
|
assert b"%PDF" in response.content[:10]
|
|
|
|
def test_certificate_workflow(self, client, sample_certificate_data):
|
|
"""Test complete certificate workflow."""
|
|
if not client:
|
|
pytest.skip("Client not available")
|
|
|
|
# 1. Create (draft)
|
|
create_response = client.post("/api/certificates/", json=sample_certificate_data)
|
|
cert_id = create_response.json()["id"]
|
|
assert create_response.json()["status"] == "draft"
|
|
|
|
# 2. Submit for review
|
|
review_response = client.post(f"/api/certificates/{cert_id}/submit-review")
|
|
assert review_response.status_code == 200
|
|
assert review_response.json()["status"] == "review"
|
|
|
|
# 3. Approve
|
|
approve_response = client.post(f"/api/certificates/{cert_id}/approve")
|
|
assert approve_response.status_code == 200
|
|
assert approve_response.json()["status"] == "approved"
|
|
|
|
# 4. Issue
|
|
issue_response = client.post(f"/api/certificates/{cert_id}/issue")
|
|
assert issue_response.status_code == 200
|
|
assert issue_response.json()["status"] == "issued"
|
|
|
|
# 5. Cannot update after issued
|
|
update_response = client.put(f"/api/certificates/{cert_id}", json={"remarks": "Test"})
|
|
assert update_response.status_code == 400
|
|
|
|
def test_get_certificate_types(self, client):
|
|
"""Test getting available certificate types."""
|
|
if not client:
|
|
pytest.skip("Client not available")
|
|
|
|
response = client.get("/api/certificates/types")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "types" in data
|
|
assert len(data["types"]) >= 5
|
|
|
|
def test_get_behavior_grades(self, client):
|
|
"""Test getting available behavior grades."""
|
|
if not client:
|
|
pytest.skip("Client not available")
|
|
|
|
response = client.get("/api/certificates/behavior-grades")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "grades" in data
|
|
assert len(data["grades"]) == 4
|
|
|
|
def test_get_nonexistent_certificate(self, client):
|
|
"""Test getting a certificate that doesn't exist."""
|
|
if not client:
|
|
pytest.skip("Client not available")
|
|
|
|
response = client.get("/api/certificates/nonexistent-id")
|
|
assert response.status_code == 404
|
|
|
|
|
|
# Run tests if executed directly
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|