""" Unit Tests for Meetings API Tests for Jitsi Meet integration endpoints """ import pytest from unittest.mock import patch, AsyncMock, MagicMock from fastapi.testclient import TestClient from datetime import datetime, timedelta # Import the app and router import sys sys.path.insert(0, '..') from meetings_api import ( router, generate_room_name, generate_password, build_jitsi_url, MeetingConfig, CreateMeetingRequest, ScheduleMeetingRequest, TrainingRequest, ParentTeacherRequest, scheduled_meetings, active_meetings, trainings ) from fastapi import FastAPI # Create test app app = FastAPI() app.include_router(router) client = TestClient(app) class TestHelperFunctions: """Test helper functions""" def test_generate_room_name_default_prefix(self): """Test room name generation with default prefix""" room_name = generate_room_name() assert room_name.startswith("meeting-") assert len(room_name) == len("meeting-") + 8 def test_generate_room_name_custom_prefix(self): """Test room name generation with custom prefix""" room_name = generate_room_name("schulung") assert room_name.startswith("schulung-") def test_generate_room_name_unique(self): """Test that room names are unique""" names = [generate_room_name() for _ in range(100)] assert len(set(names)) == 100 def test_generate_password(self): """Test password generation""" password = generate_password() assert len(password) == 8 assert password.isalnum() def test_generate_password_unique(self): """Test that passwords are unique""" passwords = [generate_password() for _ in range(100)] assert len(set(passwords)) == 100 def test_build_jitsi_url_basic(self): """Test basic Jitsi URL building""" url = build_jitsi_url("test-room") assert "localhost:8443/test-room" in url assert "config.prejoinPageEnabled=false" in url assert "config.defaultLanguage=de" in url def test_build_jitsi_url_with_config(self): """Test Jitsi URL with config options""" config = MeetingConfig( start_with_audio_muted=True, start_with_video_muted=True, require_display_name=True ) url = build_jitsi_url("test-room", config) assert "config.startWithAudioMuted=true" in url assert "config.startWithVideoMuted=true" in url assert "config.requireDisplayName=true" in url def test_build_jitsi_url_without_config(self): """Test Jitsi URL without config""" url = build_jitsi_url("test-room", None) assert "localhost:8443/test-room" in url class TestMeetingStatsEndpoint: """Test /stats endpoint""" def test_get_stats_empty(self): """Test stats with no meetings""" # Clear any existing data scheduled_meetings.clear() active_meetings.clear() response = client.get("/api/meetings/stats") assert response.status_code == 200 data = response.json() assert "active" in data assert "scheduled" in data assert "recordings" in data assert "participants" in data def test_get_stats_with_data(self): """Test stats with meetings""" scheduled_meetings.clear() active_meetings.clear() # Add test data scheduled_meetings.append({"room_name": "test", "title": "Test"}) active_meetings.append({"room_name": "active", "title": "Active", "participants": 5}) response = client.get("/api/meetings/stats") assert response.status_code == 200 data = response.json() assert data["scheduled"] == 1 assert data["active"] == 1 assert data["participants"] == 5 class TestActiveMeetingsEndpoint: """Test /active endpoint""" def test_get_active_empty(self): """Test active meetings when empty""" active_meetings.clear() response = client.get("/api/meetings/active") assert response.status_code == 200 assert response.json() == [] def test_get_active_with_meetings(self): """Test active meetings with data""" active_meetings.clear() active_meetings.append({ "room_name": "test-room", "title": "Test Meeting", "participants": 3, "started_at": "2025-12-15T10:00:00" }) response = client.get("/api/meetings/active") assert response.status_code == 200 data = response.json() assert len(data) == 1 assert data[0]["room_name"] == "test-room" assert data[0]["title"] == "Test Meeting" class TestCreateMeetingEndpoint: """Test /create endpoint""" def test_create_quick_meeting(self): """Test creating a quick meeting""" scheduled_meetings.clear() response = client.post("/api/meetings/create", json={ "type": "quick", "title": "Quick Meeting", "duration": 30 }) assert response.status_code == 200 data = response.json() assert "room_name" in data assert data["room_name"].startswith("quick-") assert "join_url" in data def test_create_scheduled_meeting(self): """Test creating a scheduled meeting""" scheduled_meetings.clear() response = client.post("/api/meetings/create", json={ "type": "scheduled", "title": "Scheduled Meeting", "duration": 60, "scheduled_at": "2025-12-20T14:00:00" }) assert response.status_code == 200 data = response.json() assert "room_name" in data assert "join_url" in data def test_create_training_meeting(self): """Test creating a training meeting""" response = client.post("/api/meetings/create", json={ "type": "training", "title": "Training Session", "duration": 120 }) assert response.status_code == 200 data = response.json() assert data["room_name"].startswith("schulung-") def test_create_parent_meeting(self): """Test creating a parent meeting""" response = client.post("/api/meetings/create", json={ "type": "parent", "title": "Elterngespraech", "duration": 30 }) assert response.status_code == 200 data = response.json() assert data["room_name"].startswith("elterngespraech-") def test_create_class_meeting(self): """Test creating a class meeting""" response = client.post("/api/meetings/create", json={ "type": "class", "title": "Klasse 5a", "duration": 45 }) assert response.status_code == 200 data = response.json() assert data["room_name"].startswith("klasse-") def test_create_meeting_with_config(self): """Test creating meeting with custom config""" response = client.post("/api/meetings/create", json={ "type": "quick", "title": "Configured Meeting", "duration": 60, "config": { "enable_lobby": True, "enable_recording": True, "start_with_audio_muted": True } }) assert response.status_code == 200 class TestScheduleMeetingEndpoint: """Test /schedule endpoint""" def test_schedule_meeting(self): """Test scheduling a meeting""" scheduled_meetings.clear() response = client.post("/api/meetings/schedule", json={ "title": "Team Meeting", "scheduled_at": "2025-12-20T14:00:00", "duration": 60, "description": "Weekly team sync" }) assert response.status_code == 200 data = response.json() assert "room_name" in data assert "join_url" in data assert len(scheduled_meetings) == 1 def test_schedule_meeting_with_invites(self): """Test scheduling with invites""" scheduled_meetings.clear() response = client.post("/api/meetings/schedule", json={ "title": "Team Meeting", "scheduled_at": "2025-12-20T14:00:00", "duration": 60, "invites": ["user1@example.com", "user2@example.com"] }) assert response.status_code == 200 class TestTrainingEndpoint: """Test /training endpoint""" def test_create_training(self): """Test creating a training session""" trainings.clear() scheduled_meetings.clear() response = client.post("/api/meetings/training", json={ "title": "Go Grundlagen", "description": "Introduction to Go programming", "scheduled_at": "2025-12-20T10:00:00", "duration": 120, "max_participants": 20, "trainer": "Max Mustermann" }) assert response.status_code == 200 data = response.json() assert "schulung-" in data["room_name"] assert "go-grundlagen" in data["room_name"].lower() assert len(trainings) == 1 def test_create_training_with_config(self): """Test creating training with custom config""" trainings.clear() response = client.post("/api/meetings/training", json={ "title": "Docker Workshop", "scheduled_at": "2025-12-21T14:00:00", "duration": 180, "max_participants": 15, "trainer": "Lisa Schmidt", "config": { "enable_recording": True, "enable_breakout": True } }) assert response.status_code == 200 class TestParentTeacherEndpoint: """Test /parent-teacher endpoint""" def test_create_parent_teacher_meeting(self): """Test creating parent-teacher meeting""" scheduled_meetings.clear() response = client.post("/api/meetings/parent-teacher", json={ "student_name": "Max Müller", "parent_name": "Herr Müller", "parent_email": "mueller@example.com", "scheduled_at": "2025-12-18T15:00:00", "reason": "Halbjahresgespräch", "send_invite": True }) assert response.status_code == 200 data = response.json() assert "elterngespraech-" in data["room_name"] assert "max-m" in data["room_name"].lower() assert "password" in data assert len(data["password"]) == 8 def test_create_parent_teacher_without_email(self): """Test creating without email""" response = client.post("/api/meetings/parent-teacher", json={ "student_name": "Anna Schmidt", "parent_name": "Frau Schmidt", "scheduled_at": "2025-12-19T14:30:00" }) assert response.status_code == 200 class TestScheduledMeetingsEndpoint: """Test /scheduled endpoint""" def test_get_scheduled_empty(self): """Test getting scheduled meetings when empty""" scheduled_meetings.clear() response = client.get("/api/meetings/scheduled") assert response.status_code == 200 assert response.json() == [] def test_get_scheduled_with_data(self): """Test getting scheduled meetings with data""" scheduled_meetings.clear() scheduled_meetings.append({ "room_name": "test-123", "title": "Test Meeting", "scheduled_at": "2025-12-20T10:00:00" }) response = client.get("/api/meetings/scheduled") assert response.status_code == 200 assert len(response.json()) == 1 class TestTrainingsEndpoint: """Test /trainings endpoint""" def test_get_trainings(self): """Test getting training sessions""" trainings.clear() trainings.append({ "room_name": "schulung-test", "title": "Test Training", "trainer": "Test Trainer" }) response = client.get("/api/meetings/trainings") assert response.status_code == 200 assert len(response.json()) == 1 class TestDeleteMeetingEndpoint: """Test DELETE endpoint""" def test_delete_meeting(self): """Test deleting a meeting""" # Clear and add a single meeting scheduled_meetings.clear() scheduled_meetings.append({ "room_name": "to-delete", "title": "Delete Me" }) initial_count = len(scheduled_meetings) response = client.delete("/api/meetings/to-delete") assert response.status_code == 200 assert response.json()["status"] == "deleted" # Check that meeting was removed assert len([m for m in scheduled_meetings if m["room_name"] == "to-delete"]) == 0 def test_delete_nonexistent_meeting(self): """Test deleting a non-existent meeting""" initial_count = len(scheduled_meetings) response = client.delete("/api/meetings/nonexistent") assert response.status_code == 200 # Count should remain the same (nothing was deleted) assert len(scheduled_meetings) == initial_count class TestRecordingsEndpoints: """Test recordings endpoints""" def test_get_recordings(self): """Test getting recordings list""" response = client.get("/api/meetings/recordings") assert response.status_code == 200 data = response.json() assert isinstance(data, list) assert len(data) > 0 def test_get_recording_details(self): """Test getting recording details""" response = client.get("/api/meetings/recordings/docker-basics") assert response.status_code == 200 data = response.json() assert data["id"] == "docker-basics" assert "title" in data assert "download_url" in data def test_download_recording_demo_mode(self): """Test download in demo mode returns 404""" response = client.get("/api/meetings/recordings/test/download") assert response.status_code == 404 def test_delete_recording(self): """Test deleting a recording""" response = client.delete("/api/meetings/recordings/test-recording") assert response.status_code == 200 assert response.json()["status"] == "deleted" class TestHealthEndpoint: """Test health check endpoint""" @patch('meetings_api.httpx.AsyncClient') def test_health_check_jitsi_available(self, mock_client): """Test health check when Jitsi is available""" # Skip this test as it requires async mocking pass def test_health_check_returns_status(self): """Test health check returns expected fields""" response = client.get("/api/meetings/health") assert response.status_code == 200 data = response.json() assert "status" in data assert "jitsi_url" in data assert "jitsi_available" in data assert "scheduled_meetings" in data assert "active_meetings" in data class TestMeetingConfigModel: """Test MeetingConfig model""" def test_default_config(self): """Test default config values""" config = MeetingConfig() assert config.enable_lobby is True assert config.enable_recording is False assert config.start_with_audio_muted is True assert config.start_with_video_muted is False assert config.require_display_name is True assert config.enable_breakout is False def test_custom_config(self): """Test custom config values""" config = MeetingConfig( enable_lobby=False, enable_recording=True, enable_breakout=True ) assert config.enable_lobby is False assert config.enable_recording is True assert config.enable_breakout is True class TestRequestModels: """Test request models""" def test_create_meeting_request_defaults(self): """Test CreateMeetingRequest defaults""" request = CreateMeetingRequest() assert request.type == "quick" assert request.title == "Neues Meeting" assert request.duration == 60 assert request.scheduled_at is None assert request.config is None def test_schedule_meeting_request(self): """Test ScheduleMeetingRequest""" request = ScheduleMeetingRequest( title="Test", scheduled_at="2025-12-20T10:00:00" ) assert request.title == "Test" assert request.duration == 60 def test_training_request(self): """Test TrainingRequest""" request = TrainingRequest( title="Test Training", scheduled_at="2025-12-20T10:00:00", trainer="Trainer" ) assert request.title == "Test Training" assert request.duration == 120 assert request.max_participants == 20 def test_parent_teacher_request(self): """Test ParentTeacherRequest""" request = ParentTeacherRequest( student_name="Max", parent_name="Herr Müller", scheduled_at="2025-12-20T10:00:00" ) assert request.student_name == "Max" assert request.duration == 30 assert request.send_invite is True