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>
509 lines
17 KiB
Python
509 lines
17 KiB
Python
"""
|
|
Unit Tests for Teacher Dashboard API
|
|
Tests for unit assignment and class analytics endpoints
|
|
"""
|
|
import pytest
|
|
from unittest.mock import patch, AsyncMock, MagicMock
|
|
from fastapi.testclient import TestClient
|
|
from datetime import datetime, timedelta
|
|
import uuid
|
|
import sys
|
|
|
|
sys.path.insert(0, '..')
|
|
|
|
from teacher_dashboard_api import (
|
|
router,
|
|
AssignUnitRequest,
|
|
TeacherControlSettings,
|
|
UnitAssignmentStatus,
|
|
)
|
|
from fastapi import FastAPI
|
|
|
|
# Create test app
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
client = TestClient(app)
|
|
|
|
|
|
class TestTeacherAuth:
|
|
"""Test teacher authentication"""
|
|
|
|
def test_dashboard_without_auth_dev_mode(self):
|
|
"""Test dashboard access in dev mode (no auth required)"""
|
|
response = client.get("/api/teacher/dashboard")
|
|
# In dev mode, should return demo teacher
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "teacher" in data
|
|
assert "classes" in data
|
|
|
|
def test_assignments_without_auth_dev_mode(self):
|
|
"""Test assignments list in dev mode"""
|
|
response = client.get("/api/teacher/assignments")
|
|
assert response.status_code == 200
|
|
assert isinstance(response.json(), list)
|
|
|
|
|
|
class TestUnitAssignments:
|
|
"""Test unit assignment endpoints"""
|
|
|
|
def test_create_assignment(self):
|
|
"""Test creating a new unit assignment"""
|
|
request_data = {
|
|
"unit_id": "demo_unit_v1",
|
|
"class_id": "class-5a",
|
|
"settings": {
|
|
"allow_skip": True,
|
|
"allow_replay": True,
|
|
"max_time_per_stop_sec": 90,
|
|
"show_hints": True,
|
|
"require_precheck": True,
|
|
"require_postcheck": True
|
|
}
|
|
}
|
|
|
|
response = client.post("/api/teacher/assignments", json=request_data)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "assignment_id" in data
|
|
assert data["unit_id"] == "demo_unit_v1"
|
|
assert data["class_id"] == "class-5a"
|
|
assert data["status"] == "active"
|
|
assert "settings" in data
|
|
|
|
def test_create_assignment_with_due_date(self):
|
|
"""Test creating assignment with due date"""
|
|
due_date = (datetime.utcnow() + timedelta(days=7)).isoformat()
|
|
request_data = {
|
|
"unit_id": "demo_unit_v1",
|
|
"class_id": "class-6b",
|
|
"due_date": due_date,
|
|
"notes": "Bitte bis naechste Woche fertig"
|
|
}
|
|
|
|
response = client.post("/api/teacher/assignments", json=request_data)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["notes"] == "Bitte bis naechste Woche fertig"
|
|
|
|
def test_create_assignment_minimal(self):
|
|
"""Test creating assignment with minimal data"""
|
|
request_data = {
|
|
"unit_id": "demo_unit_v1",
|
|
"class_id": "class-7a"
|
|
}
|
|
|
|
response = client.post("/api/teacher/assignments", json=request_data)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
# Default settings should be applied
|
|
assert data["settings"]["allow_skip"] == True
|
|
assert data["settings"]["allow_replay"] == True
|
|
|
|
def test_list_assignments(self):
|
|
"""Test listing all assignments"""
|
|
# Create some assignments first
|
|
for i in range(3):
|
|
client.post("/api/teacher/assignments", json={
|
|
"unit_id": f"unit_{i}",
|
|
"class_id": f"class_{i}"
|
|
})
|
|
|
|
response = client.get("/api/teacher/assignments")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert isinstance(data, list)
|
|
assert len(data) >= 3
|
|
|
|
def test_list_assignments_filter_by_class(self):
|
|
"""Test listing assignments filtered by class"""
|
|
# Create assignment for specific class
|
|
client.post("/api/teacher/assignments", json={
|
|
"unit_id": "demo_unit_v1",
|
|
"class_id": "filter-test-class"
|
|
})
|
|
|
|
response = client.get("/api/teacher/assignments?class_id=filter-test-class")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert isinstance(data, list)
|
|
# All returned should be for this class
|
|
for assignment in data:
|
|
assert assignment["class_id"] == "filter-test-class"
|
|
|
|
def test_list_assignments_filter_by_status(self):
|
|
"""Test listing assignments filtered by status"""
|
|
response = client.get("/api/teacher/assignments?status=active")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
for assignment in data:
|
|
assert assignment["status"] == "active"
|
|
|
|
def test_get_single_assignment(self):
|
|
"""Test getting a single assignment"""
|
|
# Create assignment
|
|
create_response = client.post("/api/teacher/assignments", json={
|
|
"unit_id": "demo_unit_v1",
|
|
"class_id": "get-test-class"
|
|
})
|
|
assignment_id = create_response.json()["assignment_id"]
|
|
|
|
# Get it
|
|
response = client.get(f"/api/teacher/assignments/{assignment_id}")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["assignment_id"] == assignment_id
|
|
assert data["unit_id"] == "demo_unit_v1"
|
|
|
|
def test_get_nonexistent_assignment(self):
|
|
"""Test getting non-existent assignment"""
|
|
response = client.get(f"/api/teacher/assignments/{uuid.uuid4()}")
|
|
assert response.status_code == 404
|
|
|
|
def test_update_assignment_settings(self):
|
|
"""Test updating assignment settings"""
|
|
# Create assignment
|
|
create_response = client.post("/api/teacher/assignments", json={
|
|
"unit_id": "demo_unit_v1",
|
|
"class_id": "update-test-class"
|
|
})
|
|
assignment_id = create_response.json()["assignment_id"]
|
|
|
|
# Update settings
|
|
update_response = client.put(
|
|
f"/api/teacher/assignments/{assignment_id}",
|
|
params={
|
|
"allow_skip": False,
|
|
"allow_replay": False
|
|
}
|
|
)
|
|
|
|
# Note: Current implementation uses query params, might need adjustment
|
|
# This test documents expected behavior
|
|
|
|
def test_delete_assignment(self):
|
|
"""Test deleting an assignment"""
|
|
# Create assignment
|
|
create_response = client.post("/api/teacher/assignments", json={
|
|
"unit_id": "demo_unit_v1",
|
|
"class_id": "delete-test-class"
|
|
})
|
|
assignment_id = create_response.json()["assignment_id"]
|
|
|
|
# Delete it
|
|
response = client.delete(f"/api/teacher/assignments/{assignment_id}")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["status"] == "deleted"
|
|
|
|
# Verify it's gone
|
|
get_response = client.get(f"/api/teacher/assignments/{assignment_id}")
|
|
assert get_response.status_code == 404
|
|
|
|
|
|
class TestAssignmentProgress:
|
|
"""Test assignment progress endpoints"""
|
|
|
|
def test_get_assignment_progress(self):
|
|
"""Test getting progress for an assignment"""
|
|
# Create assignment
|
|
create_response = client.post("/api/teacher/assignments", json={
|
|
"unit_id": "demo_unit_v1",
|
|
"class_id": "progress-test-class"
|
|
})
|
|
assignment_id = create_response.json()["assignment_id"]
|
|
|
|
# Get progress
|
|
response = client.get(f"/api/teacher/assignments/{assignment_id}/progress")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "assignment_id" in data
|
|
assert "unit_id" in data
|
|
assert "total_students" in data
|
|
assert "started_count" in data
|
|
assert "completed_count" in data
|
|
assert "avg_completion_rate" in data
|
|
assert "students" in data
|
|
|
|
def test_progress_contains_student_details(self):
|
|
"""Test that progress contains student details"""
|
|
create_response = client.post("/api/teacher/assignments", json={
|
|
"unit_id": "demo_unit_v1",
|
|
"class_id": "progress-details-class"
|
|
})
|
|
assignment_id = create_response.json()["assignment_id"]
|
|
|
|
response = client.get(f"/api/teacher/assignments/{assignment_id}/progress")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert isinstance(data["students"], list)
|
|
# Each student should have these fields
|
|
for student in data["students"]:
|
|
assert "student_id" in student
|
|
assert "student_name" in student
|
|
assert "status" in student
|
|
assert "completion_rate" in student
|
|
|
|
|
|
class TestClassAnalytics:
|
|
"""Test class analytics endpoints"""
|
|
|
|
def test_get_class_analytics(self):
|
|
"""Test getting analytics for a class"""
|
|
response = client.get("/api/teacher/classes/test-class-123/analytics")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "class_id" in data
|
|
assert "total_units_assigned" in data
|
|
assert "units_completed" in data
|
|
assert "avg_completion_rate" in data
|
|
assert "common_misconceptions" in data
|
|
|
|
def test_get_student_progress(self):
|
|
"""Test getting progress for a specific student"""
|
|
response = client.get(f"/api/teacher/students/{uuid.uuid4()}/progress")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "student_id" in data
|
|
|
|
|
|
class TestContentResources:
|
|
"""Test content resource endpoints"""
|
|
|
|
def test_get_assignment_resources(self):
|
|
"""Test getting resources for an assignment"""
|
|
# Create assignment
|
|
create_response = client.post("/api/teacher/assignments", json={
|
|
"unit_id": "demo_unit_v1",
|
|
"class_id": "resources-test-class"
|
|
})
|
|
assignment_id = create_response.json()["assignment_id"]
|
|
|
|
# Get resources
|
|
response = client.get(f"/api/teacher/assignments/{assignment_id}/resources")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert isinstance(data, list)
|
|
assert len(data) >= 1
|
|
|
|
# Check resource structure
|
|
for resource in data:
|
|
assert "resource_type" in resource
|
|
assert "title" in resource
|
|
assert "url" in resource
|
|
assert "unit_id" in resource
|
|
|
|
def test_resources_include_h5p_and_pdf(self):
|
|
"""Test that resources include both H5P and PDF"""
|
|
create_response = client.post("/api/teacher/assignments", json={
|
|
"unit_id": "demo_unit_v1",
|
|
"class_id": "resource-types-class"
|
|
})
|
|
assignment_id = create_response.json()["assignment_id"]
|
|
|
|
response = client.get(f"/api/teacher/assignments/{assignment_id}/resources")
|
|
|
|
data = response.json()
|
|
resource_types = [r["resource_type"] for r in data]
|
|
|
|
assert "h5p" in resource_types
|
|
assert "pdf" in resource_types or "worksheet" in resource_types
|
|
|
|
def test_regenerate_content(self):
|
|
"""Test triggering content regeneration"""
|
|
create_response = client.post("/api/teacher/assignments", json={
|
|
"unit_id": "demo_unit_v1",
|
|
"class_id": "regenerate-test-class"
|
|
})
|
|
assignment_id = create_response.json()["assignment_id"]
|
|
|
|
response = client.post(
|
|
f"/api/teacher/assignments/{assignment_id}/regenerate-content?resource_type=all"
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "queued"
|
|
|
|
|
|
class TestAvailableUnits:
|
|
"""Test available units endpoints"""
|
|
|
|
def test_list_available_units(self):
|
|
"""Test listing available units"""
|
|
response = client.get("/api/teacher/units/available")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert isinstance(data, list)
|
|
assert len(data) >= 1
|
|
|
|
def test_available_units_structure(self):
|
|
"""Test structure of available units"""
|
|
response = client.get("/api/teacher/units/available")
|
|
|
|
data = response.json()
|
|
for unit in data:
|
|
assert "unit_id" in unit
|
|
assert "template" in unit
|
|
assert "grade_band" in unit
|
|
assert "duration_minutes" in unit
|
|
|
|
def test_filter_by_grade(self):
|
|
"""Test filtering available units by grade"""
|
|
response = client.get("/api/teacher/units/available?grade=5")
|
|
|
|
assert response.status_code == 200
|
|
# Should return units appropriate for grade 5
|
|
|
|
def test_filter_by_template(self):
|
|
"""Test filtering available units by template"""
|
|
response = client.get("/api/teacher/units/available?template=flight_path")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
# When database is available, filter should work
|
|
# When using fallback data, filter may not be applied server-side
|
|
# At minimum, verify response structure is correct
|
|
assert isinstance(data, list)
|
|
for unit in data:
|
|
assert "template" in unit
|
|
|
|
|
|
class TestDashboard:
|
|
"""Test dashboard overview endpoint"""
|
|
|
|
def test_get_dashboard(self):
|
|
"""Test getting dashboard overview"""
|
|
response = client.get("/api/teacher/dashboard")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "teacher" in data
|
|
assert "classes" in data
|
|
assert "active_assignments" in data
|
|
assert "alerts" in data
|
|
|
|
def test_dashboard_teacher_info(self):
|
|
"""Test dashboard contains teacher info"""
|
|
response = client.get("/api/teacher/dashboard")
|
|
|
|
data = response.json()
|
|
teacher = data["teacher"]
|
|
assert "id" in teacher
|
|
assert "name" in teacher
|
|
|
|
|
|
class TestHealthEndpoint:
|
|
"""Test health endpoint"""
|
|
|
|
def test_health_check(self):
|
|
"""Test health check endpoint"""
|
|
response = client.get("/api/teacher/health")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "healthy"
|
|
assert data["service"] == "teacher-dashboard"
|
|
|
|
|
|
class TestPydanticModels:
|
|
"""Test Pydantic model validation"""
|
|
|
|
def test_assign_unit_request_validation(self):
|
|
"""Test AssignUnitRequest validation"""
|
|
request = AssignUnitRequest(
|
|
unit_id="test_unit",
|
|
class_id="test_class"
|
|
)
|
|
assert request.unit_id == "test_unit"
|
|
assert request.class_id == "test_class"
|
|
assert request.settings is None
|
|
|
|
def test_teacher_control_settings_defaults(self):
|
|
"""Test TeacherControlSettings default values"""
|
|
settings = TeacherControlSettings()
|
|
|
|
assert settings.allow_skip == True
|
|
assert settings.allow_replay == True
|
|
assert settings.max_time_per_stop_sec == 90
|
|
assert settings.show_hints == True
|
|
assert settings.require_precheck == True
|
|
assert settings.require_postcheck == True
|
|
|
|
def test_teacher_control_settings_custom(self):
|
|
"""Test TeacherControlSettings with custom values"""
|
|
settings = TeacherControlSettings(
|
|
allow_skip=False,
|
|
allow_replay=False,
|
|
max_time_per_stop_sec=120,
|
|
show_hints=False,
|
|
require_precheck=False,
|
|
require_postcheck=True
|
|
)
|
|
|
|
assert settings.allow_skip == False
|
|
assert settings.allow_replay == False
|
|
assert settings.max_time_per_stop_sec == 120
|
|
|
|
def test_unit_assignment_status_enum(self):
|
|
"""Test UnitAssignmentStatus enum values"""
|
|
assert UnitAssignmentStatus.DRAFT == "draft"
|
|
assert UnitAssignmentStatus.ACTIVE == "active"
|
|
assert UnitAssignmentStatus.COMPLETED == "completed"
|
|
assert UnitAssignmentStatus.ARCHIVED == "archived"
|
|
|
|
|
|
class TestEdgeCases:
|
|
"""Test edge cases and error handling"""
|
|
|
|
def test_create_assignment_same_unit_class_twice(self):
|
|
"""Test creating same assignment twice"""
|
|
request_data = {
|
|
"unit_id": "duplicate_test_unit",
|
|
"class_id": "duplicate_test_class"
|
|
}
|
|
|
|
response1 = client.post("/api/teacher/assignments", json=request_data)
|
|
response2 = client.post("/api/teacher/assignments", json=request_data)
|
|
|
|
# Both should succeed (different assignment IDs)
|
|
assert response1.status_code == 200
|
|
assert response2.status_code == 200
|
|
assert response1.json()["assignment_id"] != response2.json()["assignment_id"]
|
|
|
|
def test_progress_for_empty_class(self):
|
|
"""Test getting progress for class with no students"""
|
|
create_response = client.post("/api/teacher/assignments", json={
|
|
"unit_id": "demo_unit_v1",
|
|
"class_id": "empty-class"
|
|
})
|
|
assignment_id = create_response.json()["assignment_id"]
|
|
|
|
response = client.get(f"/api/teacher/assignments/{assignment_id}/progress")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["total_students"] >= 0
|
|
assert isinstance(data["students"], list)
|
|
|
|
def test_analytics_for_class_with_no_assignments(self):
|
|
"""Test analytics for class with no assignments"""
|
|
response = client.get("/api/teacher/classes/nonexistent-class/analytics")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["total_units_assigned"] == 0
|