"""Tests for video platform abstraction and Jitsi integration.""" from datetime import datetime, timedelta, timezone from unittest.mock import Mock, patch import pytest from fastapi.testclient import TestClient from reflector.db.rooms import Room from reflector.video_platforms.base import ( MeetingData, VideoPlatformClient, VideoPlatformConfig, ) from reflector.video_platforms.factory import ( create_platform_client, get_platform_config, ) from reflector.video_platforms.jitsi import JitsiClient from reflector.video_platforms.registry import ( get_available_platforms, get_platform_client, register_platform, ) class TestVideoPlatformBase: """Test the video platform base classes and interfaces.""" def test_video_platform_config_creation(self): """Test VideoPlatformConfig can be created with required fields.""" config = VideoPlatformConfig( api_key="test-key", webhook_secret="test-secret", api_url="https://test.example.com", ) assert config.api_key == "test-key" assert config.webhook_secret == "test-secret" assert config.api_url == "https://test.example.com" def test_meeting_data_creation(self): """Test MeetingData can be created with all fields.""" meeting_data = MeetingData( meeting_id="test-123", room_name="test-room", room_url="https://test.com/room", host_room_url="https://test.com/host", platform="jitsi", extra_data={"jwt": "token123"}, ) assert meeting_data.meeting_id == "test-123" assert meeting_data.room_name == "test-room" assert meeting_data.platform == "jitsi" assert meeting_data.extra_data["jwt"] == "token123" class TestJitsiClient: """Test JitsiClient implementation.""" def setup_method(self): """Set up test fixtures.""" self.config = VideoPlatformConfig( api_key="", # Jitsi doesn't use API key webhook_secret="test-webhook-secret", api_url="https://meet.example.com", ) self.client = JitsiClient(self.config) self.test_room = Room( id="test-room-id", name="test-room", user_id="test-user", platform="jitsi" ) @patch("reflector.settings.settings.JITSI_JWT_SECRET", "test-secret-123") @patch("reflector.settings.settings.JITSI_DOMAIN", "meet.example.com") @patch("reflector.settings.settings.JITSI_JWT_ISSUER", "reflector") @patch("reflector.settings.settings.JITSI_JWT_AUDIENCE", "jitsi") def test_jwt_generation(self): """Test JWT token generation with proper payload.""" exp_time = datetime.now(timezone.utc) + timedelta(hours=1) jwt_token = self.client._generate_jwt( room="test-room", moderator=True, exp=exp_time ) # Verify token is generated assert jwt_token is not None assert len(jwt_token) > 50 # JWT tokens are quite long assert jwt_token.count(".") == 2 # JWT has 3 parts separated by dots @patch("reflector.settings.settings.JITSI_JWT_SECRET", None) def test_jwt_generation_without_secret_fails(self): """Test JWT generation fails without secret.""" exp_time = datetime.now(timezone.utc) + timedelta(hours=1) with pytest.raises(ValueError, match="JITSI_JWT_SECRET is required"): self.client._generate_jwt(room="test-room", moderator=False, exp=exp_time) @patch( "reflector.video_platforms.jitsi.client.generate_uuid4", return_value="test-uuid-123", ) @patch("reflector.settings.settings.JITSI_JWT_SECRET", "test-secret-123") @patch("reflector.settings.settings.JITSI_DOMAIN", "meet.example.com") @patch("reflector.settings.settings.JITSI_JWT_ISSUER", "reflector") @patch("reflector.settings.settings.JITSI_JWT_AUDIENCE", "jitsi") async def test_create_meeting(self, mock_uuid): """Test meeting creation with JWT tokens.""" end_date = datetime.now(timezone.utc) + timedelta(hours=2) meeting_data = await self.client.create_meeting( room_name_prefix="test", end_date=end_date, room=self.test_room ) # Verify meeting data structure assert meeting_data.meeting_id == "test-uuid-123" assert meeting_data.platform == "jitsi" assert "reflector-test-room" in meeting_data.room_name assert "meet.example.com" in meeting_data.room_url assert "jwt=" in meeting_data.room_url assert "jwt=" in meeting_data.host_room_url # Verify extra data contains JWT tokens assert "user_jwt" in meeting_data.extra_data assert "host_jwt" in meeting_data.extra_data assert "domain" in meeting_data.extra_data async def test_get_room_sessions(self): """Test room sessions retrieval (mock implementation).""" sessions = await self.client.get_room_sessions("test-room") assert "roomName" in sessions assert "sessions" in sessions assert sessions["roomName"] == "test-room" assert len(sessions["sessions"]) > 0 assert sessions["sessions"][0]["isActive"] is True async def test_delete_room(self): """Test room deletion (no-op for Jitsi).""" result = await self.client.delete_room("test-room") assert result is True async def test_upload_logo(self): """Test logo upload (no-op for Jitsi).""" result = await self.client.upload_logo("test-room", "logo.png") assert result is True def test_verify_webhook_signature_valid(self): """Test webhook signature verification with valid signature.""" body = b'{"event": "test"}' # Generate expected signature import hmac from hashlib import sha256 expected_signature = hmac.new( self.config.webhook_secret.encode(), body, sha256 ).hexdigest() result = self.client.verify_webhook_signature(body, expected_signature) assert result is True def test_verify_webhook_signature_invalid(self): """Test webhook signature verification with invalid signature.""" body = b'{"event": "test"}' invalid_signature = "invalid-signature" result = self.client.verify_webhook_signature(body, invalid_signature) assert result is False def test_verify_webhook_signature_no_secret(self): """Test webhook signature verification without secret.""" config = VideoPlatformConfig( api_key="", webhook_secret="", api_url="https://meet.example.com" ) client = JitsiClient(config) result = client.verify_webhook_signature(b'{"event": "test"}', "signature") assert result is False class TestPlatformRegistry: """Test platform registry functionality.""" def test_platform_registration(self): """Test platform registration and retrieval.""" # Create mock client class class MockClient(VideoPlatformClient): async def create_meeting(self, room_name_prefix, end_date, room): pass async def get_room_sessions(self, room_name): pass async def delete_room(self, room_name): pass async def upload_logo(self, room_name, logo_path): pass def verify_webhook_signature(self, body, signature, timestamp=None): pass # Register mock platform register_platform("test-platform", MockClient) # Verify it's available available = get_available_platforms() assert "test-platform" in available # Test client creation config = VideoPlatformConfig( api_key="test", webhook_secret="test", api_url="test" ) client = get_platform_client("test-platform", config) assert isinstance(client, MockClient) def test_get_unknown_platform_raises_error(self): """Test that requesting unknown platform raises error.""" config = VideoPlatformConfig( api_key="test", webhook_secret="test", api_url="test" ) with pytest.raises(ValueError, match="Unknown video platform: nonexistent"): get_platform_client("nonexistent", config) def test_builtin_platforms_registered(self): """Test that built-in platforms are registered.""" available = get_available_platforms() assert "jitsi" in available class TestPlatformFactory: """Test platform factory functionality.""" @patch("reflector.settings.settings.JITSI_JWT_SECRET", "test-secret") @patch("reflector.settings.settings.JITSI_WEBHOOK_SECRET", "webhook-secret") @patch("reflector.settings.settings.JITSI_DOMAIN", "meet.example.com") def test_get_jitsi_platform_config(self): """Test Jitsi platform configuration.""" config = get_platform_config("jitsi") assert config.api_key == "" # Jitsi uses JWT, no API key assert config.webhook_secret == "webhook-secret" assert config.api_url == "https://meet.example.com" @patch("reflector.settings.settings.WHEREBY_API_KEY", "whereby-key") @patch("reflector.settings.settings.WHEREBY_WEBHOOK_SECRET", "whereby-secret") @patch("reflector.settings.settings.WHEREBY_API_URL", "https://api.whereby.dev") def test_get_whereby_platform_config(self): """Test Whereby platform configuration.""" config = get_platform_config("whereby") assert config.api_key == "whereby-key" assert config.webhook_secret == "whereby-secret" assert config.api_url == "https://api.whereby.dev" def test_get_unknown_platform_config_raises_error(self): """Test that unknown platform config raises error.""" with pytest.raises(ValueError, match="Unknown platform: nonexistent"): get_platform_config("nonexistent") def test_create_platform_client(self): """Test platform client creation via factory.""" with patch( "reflector.video_platforms.factory.get_platform_config" ) as mock_config: mock_config.return_value = VideoPlatformConfig( api_key="", webhook_secret="test-secret", api_url="https://meet.example.com", ) client = create_platform_client("jitsi") assert isinstance(client, JitsiClient) class TestWebhookEndpoints: """Test Jitsi webhook endpoints.""" def setup_method(self): """Set up test client.""" from reflector.app import app self.client = TestClient(app) def test_health_endpoint(self): """Test Jitsi health check endpoint.""" response = self.client.get("/v1/jitsi/health") assert response.status_code == 200 data = response.json() assert data["status"] == "ok" assert data["service"] == "jitsi-webhooks" assert "timestamp" in data assert "webhook_secret_configured" in data @patch("reflector.views.jitsi.verify_jitsi_webhook_signature", return_value=True) @patch("reflector.db.meetings.meetings_controller.get_by_room_name") @patch("reflector.db.meetings.meetings_controller.update_meeting") async def test_jitsi_events_webhook_join(self, mock_update, mock_get, mock_verify): """Test participant join event webhook.""" # Mock meeting mock_meeting = Mock() mock_meeting.id = "test-meeting-id" mock_meeting.num_clients = 1 mock_get.return_value = mock_meeting payload = { "event": "muc-occupant-joined", "room": "test-room", "timestamp": "2025-01-15T10:30:00.000Z", "data": {}, } response = self.client.post( "/v1/jitsi/events", json=payload, headers={"x-jitsi-signature": "valid-signature"}, ) assert response.status_code == 200 data = response.json() assert data["status"] == "ok" assert data["event"] == "muc-occupant-joined" assert data["room"] == "test-room" @patch("reflector.views.jitsi.verify_jitsi_webhook_signature", return_value=False) async def test_jitsi_events_webhook_invalid_signature(self, mock_verify): """Test webhook with invalid signature returns 401.""" payload = { "event": "muc-occupant-joined", "room": "test-room", "timestamp": "2025-01-15T10:30:00.000Z", "data": {}, } response = self.client.post( "/v1/jitsi/events", json=payload, headers={"x-jitsi-signature": "invalid-signature"}, ) assert response.status_code == 401 assert "Invalid webhook signature" in response.text @patch("reflector.views.jitsi.verify_jitsi_webhook_signature", return_value=True) @patch( "reflector.db.meetings.meetings_controller.get_by_room_name", return_value=None ) async def test_jitsi_events_webhook_meeting_not_found(self, mock_get, mock_verify): """Test webhook with nonexistent meeting returns 404.""" payload = { "event": "muc-occupant-joined", "room": "nonexistent-room", "timestamp": "2025-01-15T10:30:00.000Z", "data": {}, } response = self.client.post( "/v1/jitsi/events", json=payload, headers={"x-jitsi-signature": "valid-signature"}, ) assert response.status_code == 404 assert "Meeting not found" in response.text class TestRoomsPlatformIntegration: """Test rooms endpoint integration with platform abstraction.""" def setup_method(self): """Set up test client.""" from reflector.app import app self.client = TestClient(app) @patch("reflector.auth.current_user_optional") @patch("reflector.db.rooms.rooms_controller.add") def test_create_room_with_jitsi_platform(self, mock_add, mock_auth): """Test room creation with Jitsi platform.""" from datetime import datetime, timezone mock_auth.return_value = {"sub": "test-user"} # Create a proper Room object for the mock return from reflector.db.rooms import Room mock_room = Room( id="test-room-id", name="test-jitsi-room", user_id="test-user", created_at=datetime.now(timezone.utc), zulip_auto_post=False, zulip_stream="", zulip_topic="", is_locked=False, room_mode="normal", recording_type="cloud", recording_trigger="automatic-2nd-participant", is_shared=False, platform="jitsi", ) mock_add.return_value = mock_room payload = { "name": "test-jitsi-room", "platform": "jitsi", "zulip_auto_post": False, "zulip_stream": "", "zulip_topic": "", "is_locked": False, "room_mode": "normal", "recording_type": "cloud", "recording_trigger": "automatic-2nd-participant", "is_shared": False, "webhook_url": "", "webhook_secret": "", } response = self.client.post("/v1/rooms", json=payload) # Verify the add method was called with platform parameter mock_add.assert_called_once() call_args = mock_add.call_args assert call_args.kwargs["platform"] == "jitsi" assert call_args.kwargs["name"] == "test-jitsi-room" assert response.status_code == 200 def test_create_meeting_with_jitsi_platform_fallback(self): """Test that meeting creation falls back to whereby when platform client unavailable.""" # This tests the fallback behavior in rooms.py when platform client returns None # The actual platform integration test is covered in the unit tests above # Just verify the endpoint exists and has the right structure # More detailed integration testing would require a full test database setup assert hasattr(self.client.app, "routes") # Find the meeting creation route meeting_routes = [ r for r in self.client.app.routes if hasattr(r, "path") and "meeting" in r.path ] assert len(meeting_routes) > 0