chore: remove refactor md (#527)

This commit is contained in:
2025-08-01 16:33:40 -06:00
parent 28ac031ff6
commit 8b644384a2
28 changed files with 3419 additions and 423 deletions

View File

@@ -0,0 +1,392 @@
"""Tests for Daily.co webhook integration."""
import hashlib
import hmac
import json
from datetime import datetime
from unittest.mock import MagicMock, patch
import pytest
from httpx import AsyncClient
from reflector.app import app
from reflector.views.daily import DailyWebhookEvent
class TestDailyWebhookIntegration:
"""Test Daily.co webhook endpoint integration."""
@pytest.fixture
def webhook_secret(self):
"""Test webhook secret."""
return "test-webhook-secret-123"
@pytest.fixture
def mock_room(self):
"""Create a mock room for testing."""
room = MagicMock()
room.id = "test-room-123"
room.name = "Test Room"
room.recording_type = "cloud"
room.platform = "daily"
return room
@pytest.fixture
def mock_meeting(self):
"""Create a mock meeting for testing."""
meeting = MagicMock()
meeting.id = "test-meeting-456"
meeting.room_id = "test-room-123"
meeting.platform = "daily"
meeting.room_name = "test-room-123-abc"
return meeting
def create_webhook_signature(self, payload: bytes, secret: str) -> str:
"""Create HMAC signature for webhook payload."""
return hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
def create_webhook_event(
self, event_type: str, room_name: str = "test-room-123-abc", **kwargs
) -> dict:
"""Create a Daily.co webhook event payload."""
base_event = {
"type": event_type,
"id": f"evt_{event_type.replace('.', '_')}_{int(datetime.utcnow().timestamp())}",
"ts": int(datetime.utcnow().timestamp() * 1000), # milliseconds
"data": {"room": {"name": room_name}, **kwargs},
}
return base_event
@pytest.mark.asyncio
async def test_webhook_participant_joined(
self, webhook_secret, mock_room, mock_meeting
):
"""Test participant joined webhook event."""
event_data = self.create_webhook_event(
"participant.joined",
participant={
"id": "participant-123",
"user_name": "John Doe",
"session_id": "session-456",
},
)
payload = json.dumps(event_data).encode()
signature = self.create_webhook_signature(payload, webhook_secret)
with patch("reflector.views.daily.settings") as mock_settings:
mock_settings.DAILY_WEBHOOK_SECRET = webhook_secret
with patch(
"reflector.db.meetings.meetings_controller.get_by_room_name"
) as mock_get_meeting:
mock_get_meeting.return_value = mock_meeting
with patch(
"reflector.db.meetings.meetings_controller.update_meeting"
) as mock_update:
async with AsyncClient(app=app, base_url="http://test/v1") as ac:
response = await ac.post(
"/daily_webhook",
json=event_data,
headers={"X-Daily-Signature": signature},
)
assert response.status_code == 200
assert response.json() == {"status": "ok"}
# Verify meeting was looked up
mock_get_meeting.assert_called_once_with("test-room-123-abc")
@pytest.mark.asyncio
async def test_webhook_participant_left(
self, webhook_secret, mock_room, mock_meeting
):
"""Test participant left webhook event."""
event_data = self.create_webhook_event(
"participant.left",
participant={
"id": "participant-123",
"user_name": "John Doe",
"session_id": "session-456",
},
)
payload = json.dumps(event_data).encode()
signature = self.create_webhook_signature(payload, webhook_secret)
with patch("reflector.views.daily.settings") as mock_settings:
mock_settings.DAILY_WEBHOOK_SECRET = webhook_secret
with patch(
"reflector.db.meetings.meetings_controller.get_by_room_name"
) as mock_get_meeting:
mock_get_meeting.return_value = mock_meeting
async with AsyncClient(app=app, base_url="http://test/v1") as ac:
response = await ac.post(
"/daily_webhook",
json=event_data,
headers={"X-Daily-Signature": signature},
)
assert response.status_code == 200
assert response.json() == {"status": "ok"}
@pytest.mark.asyncio
async def test_webhook_recording_started(
self, webhook_secret, mock_room, mock_meeting
):
"""Test recording started webhook event."""
event_data = self.create_webhook_event(
"recording.started",
recording={
"id": "recording-789",
"status": "recording",
"start_time": "2025-01-01T10:00:00Z",
},
)
payload = json.dumps(event_data).encode()
signature = self.create_webhook_signature(payload, webhook_secret)
with patch("reflector.views.daily.settings") as mock_settings:
mock_settings.DAILY_WEBHOOK_SECRET = webhook_secret
with patch(
"reflector.db.meetings.meetings_controller.get_by_room_name"
) as mock_get_meeting:
mock_get_meeting.return_value = mock_meeting
with patch(
"reflector.db.meetings.meetings_controller.update_meeting"
) as mock_update:
async with AsyncClient(app=app, base_url="http://test/v1") as ac:
response = await ac.post(
"/daily_webhook",
json=event_data,
headers={"X-Daily-Signature": signature},
)
assert response.status_code == 200
assert response.json() == {"status": "ok"}
@pytest.mark.asyncio
async def test_webhook_recording_ready_triggers_processing(
self, webhook_secret, mock_room, mock_meeting
):
"""Test recording ready webhook triggers audio processing."""
event_data = self.create_webhook_event(
"recording.ready-to-download",
recording={
"id": "recording-789",
"status": "finished",
"download_url": "https://s3.amazonaws.com/bucket/recording.mp4",
"start_time": "2025-01-01T10:00:00Z",
"duration": 1800,
},
)
payload = json.dumps(event_data).encode()
signature = self.create_webhook_signature(payload, webhook_secret)
with patch("reflector.views.daily.settings") as mock_settings:
mock_settings.DAILY_WEBHOOK_SECRET = webhook_secret
with patch(
"reflector.db.meetings.meetings_controller.get_by_room_name"
) as mock_get_meeting:
mock_get_meeting.return_value = mock_meeting
with patch(
"reflector.db.meetings.meetings_controller.update_meeting"
) as mock_update_url:
with patch(
"reflector.worker.tasks.process_recording.delay"
) as mock_process:
async with AsyncClient(
app=app, base_url="http://test/v1"
) as ac:
response = await ac.post(
"/daily_webhook",
json=event_data,
headers={"X-Daily-Signature": signature},
)
assert response.status_code == 200
assert response.json() == {"status": "ok"}
# Verify recording URL was updated
mock_update_url.assert_called_once_with(
mock_meeting.id,
"https://s3.amazonaws.com/bucket/recording.mp4",
)
# Verify processing was triggered
mock_process.assert_called_once_with(mock_meeting.id)
@pytest.mark.asyncio
async def test_webhook_invalid_signature_rejected(self, webhook_secret):
"""Test webhook with invalid signature is rejected."""
event_data = self.create_webhook_event("participant.joined")
with patch("reflector.views.daily.settings") as mock_settings:
mock_settings.DAILY_WEBHOOK_SECRET = webhook_secret
async with AsyncClient(app=app, base_url="http://test/v1") as ac:
response = await ac.post(
"/daily_webhook",
json=event_data,
headers={"X-Daily-Signature": "invalid-signature"},
)
assert response.status_code == 401
assert "Invalid signature" in response.json()["detail"]
@pytest.mark.asyncio
async def test_webhook_missing_signature_rejected(self):
"""Test webhook without signature header is rejected."""
event_data = self.create_webhook_event("participant.joined")
async with AsyncClient(app=app, base_url="http://test/v1") as ac:
response = await ac.post("/daily_webhook", json=event_data)
assert response.status_code == 401
assert "Missing signature" in response.json()["detail"]
@pytest.mark.asyncio
async def test_webhook_meeting_not_found(self, webhook_secret):
"""Test webhook for non-existent meeting."""
event_data = self.create_webhook_event(
"participant.joined", room_name="non-existent-room"
)
payload = json.dumps(event_data).encode()
signature = self.create_webhook_signature(payload, webhook_secret)
with patch("reflector.views.daily.settings") as mock_settings:
mock_settings.DAILY_WEBHOOK_SECRET = webhook_secret
with patch(
"reflector.db.meetings.meetings_controller.get_by_room_name"
) as mock_get_meeting:
mock_get_meeting.return_value = None
async with AsyncClient(app=app, base_url="http://test/v1") as ac:
response = await ac.post(
"/daily_webhook",
json=event_data,
headers={"X-Daily-Signature": signature},
)
assert response.status_code == 404
assert "Meeting not found" in response.json()["detail"]
@pytest.mark.asyncio
async def test_webhook_unknown_event_type(self, webhook_secret, mock_meeting):
"""Test webhook with unknown event type."""
event_data = self.create_webhook_event("unknown.event")
payload = json.dumps(event_data).encode()
signature = self.create_webhook_signature(payload, webhook_secret)
with patch("reflector.views.daily.settings") as mock_settings:
mock_settings.DAILY_WEBHOOK_SECRET = webhook_secret
with patch(
"reflector.db.meetings.meetings_controller.get_by_room_name"
) as mock_get_meeting:
mock_get_meeting.return_value = mock_meeting
async with AsyncClient(app=app, base_url="http://test/v1") as ac:
response = await ac.post(
"/daily_webhook",
json=event_data,
headers={"X-Daily-Signature": signature},
)
# Should still return 200 but log the unknown event
assert response.status_code == 200
assert response.json() == {"status": "ok"}
@pytest.mark.asyncio
async def test_webhook_malformed_json(self, webhook_secret):
"""Test webhook with malformed JSON."""
with patch("reflector.views.daily.settings") as mock_settings:
mock_settings.DAILY_WEBHOOK_SECRET = webhook_secret
async with AsyncClient(app=app, base_url="http://test/v1") as ac:
response = await ac.post(
"/daily_webhook",
content="invalid json",
headers={
"Content-Type": "application/json",
"X-Daily-Signature": "test-signature",
},
)
assert response.status_code == 422 # Validation error
class TestWebhookEventValidation:
"""Test webhook event data validation."""
def test_daily_webhook_event_validation_valid(self):
"""Test valid webhook event passes validation."""
event_data = {
"type": "participant.joined",
"id": "evt_123",
"ts": 1640995200000, # milliseconds
"data": {
"room": {"name": "test-room"},
"participant": {
"id": "participant-123",
"user_name": "John Doe",
"session_id": "session-456",
},
},
}
event = DailyWebhookEvent(**event_data)
assert event.type == "participant.joined"
assert event.data["room"]["name"] == "test-room"
assert event.data["participant"]["id"] == "participant-123"
def test_daily_webhook_event_validation_minimal(self):
"""Test minimal valid webhook event."""
event_data = {
"type": "room.created",
"id": "evt_123",
"ts": 1640995200000,
"data": {"room": {"name": "test-room"}},
}
event = DailyWebhookEvent(**event_data)
assert event.type == "room.created"
assert event.data["room"]["name"] == "test-room"
def test_daily_webhook_event_validation_with_recording(self):
"""Test webhook event with recording data."""
event_data = {
"type": "recording.ready-to-download",
"id": "evt_123",
"ts": 1640995200000,
"data": {
"room": {"name": "test-room"},
"recording": {
"id": "recording-123",
"status": "finished",
"download_url": "https://example.com/recording.mp4",
"start_time": "2025-01-01T10:00:00Z",
"duration": 1800,
},
},
}
event = DailyWebhookEvent(**event_data)
assert event.type == "recording.ready-to-download"
assert event.data["recording"]["id"] == "recording-123"
assert (
event.data["recording"]["download_url"]
== "https://example.com/recording.mp4"
)

View File

@@ -0,0 +1,323 @@
"""Tests for video platform clients."""
from datetime import datetime, timedelta
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from reflector.db.rooms import Room
from reflector.video_platforms.base import MeetingData, VideoPlatformConfig
from reflector.video_platforms.daily import DailyClient
from reflector.video_platforms.mock import MockPlatformClient
from reflector.video_platforms.registry import get_platform_client
from reflector.video_platforms.whereby import WherebyClient
@pytest.fixture
def mock_room():
"""Create a mock room for testing."""
room = MagicMock(spec=Room)
room.id = "test-room-123"
room.name = "Test Room"
room.recording_type = "cloud"
room.platform = "whereby"
return room
@pytest.fixture
def config():
"""Create a test configuration."""
return VideoPlatformConfig(
api_key="test-api-key",
webhook_secret="test-webhook-secret",
subdomain="test-subdomain",
)
class TestPlatformFactory:
"""Test platform client factory."""
def test_create_whereby_client(self, config):
"""Test creating Whereby client."""
client = get_platform_client("whereby", config)
assert isinstance(client, WherebyClient)
def test_create_daily_client(self, config):
"""Test creating Daily.co client."""
client = get_platform_client("daily", config)
assert isinstance(client, DailyClient)
def test_create_mock_client(self, config):
"""Test creating mock client."""
client = get_platform_client("mock", config)
assert isinstance(client, MockPlatformClient)
def test_invalid_platform_raises_error(self, config):
"""Test that invalid platform raises ValueError."""
with pytest.raises(ValueError, match="Unknown platform: invalid"):
get_platform_client("invalid", config)
class TestMockPlatformClient:
"""Test mock platform client implementation."""
@pytest.fixture
def mock_client(self, config):
return MockPlatformClient(config)
@pytest.mark.asyncio
async def test_create_meeting(self, mock_client, mock_room):
"""Test creating a meeting with mock client."""
end_date = datetime.utcnow() + timedelta(hours=1)
meeting_data = await mock_client.create_meeting(
room_name_prefix="test", end_date=end_date, room=mock_room
)
assert isinstance(meeting_data, MeetingData)
assert meeting_data.room_url.startswith("https://mock.video/")
assert meeting_data.host_room_url.startswith("https://mock.video/")
assert meeting_data.room_name.startswith("test")
@pytest.mark.asyncio
async def test_get_room_sessions(self, mock_client):
"""Test getting room sessions."""
# First create a room so it exists
end_date = datetime.utcnow() + timedelta(hours=1)
mock_room = MagicMock()
mock_room.id = "test-room"
meeting = await mock_client.create_meeting("test", end_date, mock_room)
sessions = await mock_client.get_room_sessions(meeting.room_name)
assert isinstance(sessions, dict)
assert "sessions" in sessions
assert len(sessions["sessions"]) == 1
@pytest.mark.asyncio
async def test_delete_room(self, mock_client):
"""Test deleting a room."""
# First create a room so it exists
end_date = datetime.utcnow() + timedelta(hours=1)
mock_room = MagicMock()
mock_room.id = "test-room"
meeting = await mock_client.create_meeting("test", end_date, mock_room)
result = await mock_client.delete_room(meeting.room_name)
assert result is True
def test_verify_webhook_signature_valid(self, mock_client):
"""Test webhook signature verification with valid signature."""
payload = b'{"event": "test"}'
signature = "valid" # Mock accepts "valid" as valid signature
result = mock_client.verify_webhook_signature(payload, signature)
assert result is True
def test_verify_webhook_signature_invalid(self, mock_client):
"""Test webhook signature verification with invalid signature."""
payload = b'{"event": "test"}'
signature = "invalid-signature"
result = mock_client.verify_webhook_signature(payload, signature)
assert result is False
class TestDailyClient:
"""Test Daily.co platform client."""
@pytest.fixture
def daily_client(self, config):
return DailyClient(config)
@pytest.mark.asyncio
async def test_create_meeting_success(self, daily_client, mock_room):
"""Test successful meeting creation."""
end_date = datetime.utcnow() + timedelta(hours=1)
mock_response = {
"url": "https://test.daily.co/test-room-123-abc",
"name": "test-room-123-abc",
"api_created": True,
"privacy": "public",
"config": {"enable_recording": "cloud"},
}
with patch.object(
daily_client, "_make_request", new_callable=AsyncMock
) as mock_request:
mock_request.return_value = mock_response
meeting_data = await daily_client.create_meeting(
room_name_prefix="test", end_date=end_date, room=mock_room
)
assert isinstance(meeting_data, MeetingData)
assert meeting_data.room_url == "https://test.daily.co/test-room-123-abc"
assert (
meeting_data.host_room_url == "https://test.daily.co/test-room-123-abc"
)
assert meeting_data.room_name == "test-room-123-abc"
# Verify request was made with correct parameters
mock_request.assert_called_once()
call_args = mock_request.call_args
assert call_args[0][0] == "POST"
assert "/rooms" in call_args[0][1]
@pytest.mark.asyncio
async def test_get_room_sessions_success(self, daily_client):
"""Test successful room sessions retrieval."""
mock_response = {
"data": [
{
"id": "session-1",
"room_name": "test-room",
"start_time": "2025-01-01T10:00:00Z",
"participants": [],
}
]
}
with patch.object(
daily_client, "_make_request", new_callable=AsyncMock
) as mock_request:
mock_request.return_value = mock_response
sessions = await daily_client.get_room_sessions("test-room")
assert isinstance(sessions, list)
assert len(sessions) == 1
assert sessions[0]["id"] == "session-1"
@pytest.mark.asyncio
async def test_delete_room_success(self, daily_client):
"""Test successful room deletion."""
with patch.object(
daily_client, "_make_request", new_callable=AsyncMock
) as mock_request:
mock_request.return_value = {"deleted": True}
result = await daily_client.delete_room("test-room")
assert result is True
mock_request.assert_called_once_with("DELETE", "/rooms/test-room")
def test_verify_webhook_signature_valid(self, daily_client):
"""Test webhook signature verification with valid HMAC."""
import hashlib
import hmac
payload = b'{"event": "participant.joined"}'
expected_signature = hmac.new(
daily_client.webhook_secret.encode(), payload, hashlib.sha256
).hexdigest()
result = daily_client.verify_webhook_signature(payload, expected_signature)
assert result is True
def test_verify_webhook_signature_invalid(self, daily_client):
"""Test webhook signature verification with invalid HMAC."""
payload = b'{"event": "participant.joined"}'
invalid_signature = "invalid-signature"
result = daily_client.verify_webhook_signature(payload, invalid_signature)
assert result is False
class TestWherebyClient:
"""Test Whereby platform client."""
@pytest.fixture
def whereby_client(self, config):
return WherebyClient(config)
@pytest.mark.asyncio
async def test_create_meeting_delegates_to_whereby_client(
self, whereby_client, mock_room
):
"""Test that create_meeting delegates to existing Whereby client."""
end_date = datetime.utcnow() + timedelta(hours=1)
mock_whereby_response = {
"roomUrl": "https://whereby.com/test-room",
"hostRoomUrl": "https://whereby.com/test-room?host",
"meetingId": "meeting-123",
}
with patch("reflector.video_platforms.whereby.whereby_client") as mock_client:
mock_client.create_meeting.return_value = mock_whereby_response
meeting_data = await whereby_client.create_meeting(
room_name_prefix="test", end_date=end_date, room=mock_room
)
assert isinstance(meeting_data, MeetingData)
assert meeting_data.room_url == "https://whereby.com/test-room"
assert meeting_data.host_room_url == "https://whereby.com/test-room?host"
assert meeting_data.meeting_id == "meeting-123"
@pytest.mark.asyncio
async def test_get_room_sessions_delegates_to_whereby_client(self, whereby_client):
"""Test that get_room_sessions delegates to existing Whereby client."""
mock_sessions = [{"id": "session-1"}]
with patch("reflector.video_platforms.whereby.whereby_client") as mock_client:
mock_client.get_room_sessions.return_value = mock_sessions
sessions = await whereby_client.get_room_sessions("test-room")
assert sessions == mock_sessions
def test_verify_webhook_signature_delegates_to_whereby_client(self, whereby_client):
"""Test that webhook verification delegates to existing Whereby client."""
payload = b'{"event": "test"}'
signature = "test-signature"
with patch("reflector.video_platforms.whereby.whereby_client") as mock_client:
mock_client.verify_webhook_signature.return_value = True
result = whereby_client.verify_webhook_signature(payload, signature)
assert result is True
mock_client.verify_webhook_signature.assert_called_once_with(
payload, signature
)
class TestPlatformIntegration:
"""Integration tests for platform switching."""
@pytest.mark.asyncio
async def test_platform_switching_preserves_interface(self, config, mock_room):
"""Test that different platforms provide consistent interface."""
end_date = datetime.utcnow() + timedelta(hours=1)
# Test with mock platform
mock_client = get_platform_client("mock", config)
mock_meeting = await mock_client.create_meeting("test", end_date, mock_room)
# Test with Daily platform (mocked)
daily_client = get_platform_client("daily", config)
with patch.object(
daily_client, "_make_request", new_callable=AsyncMock
) as mock_request:
mock_request.return_value = {
"url": "https://test.daily.co/test-room",
"name": "test-room",
"api_created": True,
}
daily_meeting = await daily_client.create_meeting(
"test", end_date, mock_room
)
# Both should return MeetingData objects with consistent fields
assert isinstance(mock_meeting, MeetingData)
assert isinstance(daily_meeting, MeetingData)
# Both should have required fields
for meeting in [mock_meeting, daily_meeting]:
assert hasattr(meeting, "room_url")
assert hasattr(meeting, "host_room_url")
assert hasattr(meeting, "room_name")
assert meeting.room_url.startswith("https://")

View File

@@ -0,0 +1 @@
# Test utilities

View File

@@ -0,0 +1,256 @@
"""Utilities for testing video platform functionality."""
from contextlib import asynccontextmanager
from datetime import datetime, timedelta
from typing import Any, Dict, Optional
from unittest.mock import AsyncMock, MagicMock, patch
from reflector.db.rooms import Room
from reflector.video_platforms.base import MeetingData, VideoPlatformConfig
from reflector.video_platforms.factory import create_platform_client
class MockVideoPlatformTestHelper:
"""Helper class for testing with mock video platforms."""
def __init__(self, platform: str = "mock"):
self.platform = platform
self.config = VideoPlatformConfig(
api_key="test-api-key",
webhook_secret="test-webhook-secret",
subdomain="test-subdomain",
)
self.client = create_platform_client(platform, self.config)
def create_mock_room(self, room_id: str = "test-room-123", **kwargs) -> MagicMock:
"""Create a mock room for testing."""
room = MagicMock(spec=Room)
room.id = room_id
room.name = kwargs.get("name", "Test Room")
room.recording_type = kwargs.get("recording_type", "cloud")
room.platform = kwargs.get("platform", self.platform)
return room
async def create_test_meeting(
self, room: Optional[Room] = None, **kwargs
) -> MeetingData:
"""Create a test meeting with default values."""
if room is None:
room = self.create_mock_room()
end_date = kwargs.get("end_date", datetime.utcnow() + timedelta(hours=1))
room_name_prefix = kwargs.get("room_name_prefix", "test")
return await self.client.create_meeting(room_name_prefix, end_date, room)
def create_webhook_event(
self, event_type: str, room_name: str = "test-room-123-abc", **kwargs
) -> Dict[str, Any]:
"""Create a webhook event payload for testing."""
if self.platform == "daily":
return self._create_daily_webhook_event(event_type, room_name, **kwargs)
elif self.platform == "whereby":
return self._create_whereby_webhook_event(event_type, room_name, **kwargs)
else:
return {"type": event_type, "room_name": room_name, **kwargs}
def _create_daily_webhook_event(
self, event_type: str, room_name: str, **kwargs
) -> Dict[str, Any]:
"""Create Daily.co-specific webhook event."""
base_event = {
"type": event_type,
"event_ts": int(datetime.utcnow().timestamp()),
"room": {"name": room_name},
}
if event_type == "participant.joined" or event_type == "participant.left":
base_event["participant"] = kwargs.get(
"participant",
{
"id": "participant-123",
"user_name": "Test User",
"session_id": "session-456",
},
)
if event_type.startswith("recording."):
base_event["recording"] = kwargs.get(
"recording",
{
"id": "recording-789",
"status": "finished" if "ready" in event_type else "recording",
"start_time": "2025-01-01T10:00:00Z",
},
)
if "ready" in event_type:
base_event["recording"]["download_url"] = (
"https://s3.amazonaws.com/bucket/recording.mp4"
)
base_event["recording"]["duration"] = 1800
return base_event
def _create_whereby_webhook_event(
self, event_type: str, room_name: str, **kwargs
) -> Dict[str, Any]:
"""Create Whereby-specific webhook event."""
# Whereby uses different event structure
return {
"event": event_type,
"roomName": room_name,
"timestamp": datetime.utcnow().isoformat(),
**kwargs,
}
def mock_platform_responses(self, platform: str, responses: Dict[str, Any]):
"""Context manager to mock platform API responses."""
if platform == "daily":
return self._mock_daily_responses(responses)
elif platform == "whereby":
return self._mock_whereby_responses(responses)
else:
return self._mock_generic_responses(responses)
@asynccontextmanager
async def _mock_daily_responses(self, responses: Dict[str, Any]):
"""Mock Daily.co API responses."""
with patch(
"reflector.video_platforms.daily.DailyPlatformClient._make_request"
) as mock_request:
mock_request.side_effect = lambda method, url, **kwargs: responses.get(
f"{method} {url}", {}
)
yield mock_request
@asynccontextmanager
async def _mock_whereby_responses(self, responses: Dict[str, Any]):
"""Mock Whereby API responses."""
with patch("reflector.video_platforms.whereby.whereby_client") as mock_client:
for method, response in responses.items():
setattr(mock_client, method, AsyncMock(return_value=response))
yield mock_client
@asynccontextmanager
async def _mock_generic_responses(self, responses: Dict[str, Any]):
"""Mock generic platform responses."""
yield responses
class IntegrationTestScenario:
"""Helper for running integration test scenarios across platforms."""
def __init__(self, platforms: list = None):
self.platforms = platforms or ["mock", "daily", "whereby"]
self.helpers = {
platform: MockVideoPlatformTestHelper(platform)
for platform in self.platforms
}
async def test_meeting_lifecycle(self, room_config: Dict[str, Any] = None):
"""Test complete meeting lifecycle across all platforms."""
results = {}
for platform in self.platforms:
helper = self.helpers[platform]
room = helper.create_mock_room(**(room_config or {}))
# Test meeting creation
meeting = await helper.create_test_meeting(room=room)
assert isinstance(meeting, MeetingData)
assert meeting.room_url.startswith("https://")
# Test room sessions
sessions = await helper.client.get_room_sessions(meeting.room_name)
assert isinstance(sessions, list)
# Test room deletion
deleted = await helper.client.delete_room(meeting.room_name)
assert deleted is True
results[platform] = {
"meeting": meeting,
"sessions": sessions,
"deleted": deleted,
}
return results
def test_webhook_signatures(self, payload: bytes = None):
"""Test webhook signature verification across platforms."""
if payload is None:
payload = b'{"event": "test"}'
results = {}
for platform in self.platforms:
helper = self.helpers[platform]
# Test valid signature
if platform == "mock":
valid_signature = "valid-signature"
else:
import hashlib
import hmac
valid_signature = hmac.new(
helper.config.webhook_secret.encode(), payload, hashlib.sha256
).hexdigest()
valid_result = helper.client.verify_webhook_signature(
payload, valid_signature
)
invalid_result = helper.client.verify_webhook_signature(
payload, "invalid-signature"
)
results[platform] = {"valid": valid_result, "invalid": invalid_result}
return results
def create_test_meeting_data(platform: str = "mock", **overrides) -> MeetingData:
"""Create test meeting data with platform-specific URLs."""
base_data = {"room_name": "test-room-123-abc", "meeting_id": "meeting-456"}
if platform == "daily":
base_data.update(
{
"room_url": "https://test.daily.co/test-room-123-abc",
"host_room_url": "https://test.daily.co/test-room-123-abc",
}
)
elif platform == "whereby":
base_data.update(
{
"room_url": "https://whereby.com/test-room-123-abc",
"host_room_url": "https://whereby.com/test-room-123-abc?host",
}
)
else: # mock
base_data.update(
{
"room_url": "https://mock.daily.co/test-room-123-abc",
"host_room_url": "https://mock.daily.co/test-room-123-abc",
}
)
base_data.update(overrides)
return MeetingData(**base_data)
def assert_meeting_data_valid(meeting_data: MeetingData, platform: str = None):
"""Assert that meeting data is valid for the given platform."""
assert isinstance(meeting_data, MeetingData)
assert meeting_data.room_url.startswith("https://")
assert meeting_data.host_room_url.startswith("https://")
assert isinstance(meeting_data.room_name, str)
assert len(meeting_data.room_name) > 0
if platform == "daily":
assert "daily.co" in meeting_data.room_url
elif platform == "whereby":
assert "whereby.com" in meeting_data.room_url
elif platform == "mock":
assert "mock.daily.co" in meeting_data.room_url