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
BreakPilot Dev 19855efacc
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
feat: BreakPilot PWA - Full codebase (clean push without large binaries)
All services: admin-v2, studio-v2, website, ai-compliance-sdk,
consent-service, klausur-service, voice-service, and infrastructure.
Large PDFs and compiled binaries excluded via .gitignore.
2026-02-11 13:25:58 +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