This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/tests/test_teacher_dashboard_api.py
Benjamin Admin 21a844cb8a 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>
2026-02-09 09:51:32 +01:00

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