""" 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