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