mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 20:29:06 +00:00
feat: daily.co support as alternative to whereby (#691)
* llm instructions * vibe dailyco * vibe dailyco * doc update (vibe) * dont show recording ui on call * stub processor (vibe) * stub processor (vibe) self-review * stub processor (vibe) self-review * chore(main): release 0.14.0 (#670) * Add multitrack pipeline * Mixdown audio tracks * Mixdown with pyav filter graph * Trigger multitrack processing for daily recordings * apply platform from envs in priority: non-dry * Use explicit track keys for processing * Align tracks of a multitrack recording * Generate waveforms for the mixed audio * Emit multriack pipeline events * Fix multitrack pipeline track alignment * dailico docs * Enable multitrack reprocessing * modal temp files uniform names, cleanup. remove llm temporary docs * docs cleanup * dont proceed with raw recordings if any of the downloads fail * dry transcription pipelines * remove is_miltitrack * comments * explicit dailyco room name * docs * remove stub data/method * frontend daily/whereby code self-review (no-mistake) * frontend daily/whereby code self-review (no-mistakes) * frontend daily/whereby code self-review (no-mistakes) * consent cleanup for multitrack (no-mistakes) * llm fun * remove extra comments * fix tests * merge migrations * Store participant names * Get participants by meeting session id * pop back main branch migration * s3 paddington (no-mistakes) * comment * pr comments * pr comments * pr comments * platform / meeting cleanup * Use participant names in summary generation * platform assignment to meeting at controller level * pr comment * room playform properly default none * room playform properly default none * restore migration lost * streaming WIP * extract storage / use common storage / proper env vars for storage * fix mocks tests * remove fall back * streaming for multifile * cenrtal storage abstraction (no-mistakes) * remove dead code / vars * Set participant user id for authenticated users * whereby recording name parsing fix * whereby recording name parsing fix * more file stream * storage dry + tests * remove homemade boto3 streaming and use proper boto * update migration guide * webhook creation script - print uuid --------- Co-authored-by: Igor Loskutov <igor.loskutoff@gmail.com> Co-authored-by: Mathieu Virbel <mat@meltingrocks.com> Co-authored-by: Sergey Mankovsky <sergey@monadical.com>
This commit is contained in:
@@ -5,6 +5,18 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from reflector.schemas.platform import WHEREBY_PLATFORM
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def register_mock_platform():
|
||||
from mocks.mock_platform import MockPlatformClient
|
||||
|
||||
from reflector.video_platforms.registry import register_platform
|
||||
|
||||
register_platform(WHEREBY_PLATFORM, MockPlatformClient)
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def settings_configuration():
|
||||
|
||||
0
server/tests/mocks/__init__.py
Normal file
0
server/tests/mocks/__init__.py
Normal file
112
server/tests/mocks/mock_platform.py
Normal file
112
server/tests/mocks/mock_platform.py
Normal file
@@ -0,0 +1,112 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Literal, Optional
|
||||
|
||||
from reflector.db.rooms import Room
|
||||
from reflector.video_platforms.base import (
|
||||
ROOM_PREFIX_SEPARATOR,
|
||||
MeetingData,
|
||||
VideoPlatformClient,
|
||||
VideoPlatformConfig,
|
||||
)
|
||||
|
||||
MockPlatform = Literal["mock"]
|
||||
|
||||
|
||||
class MockPlatformClient(VideoPlatformClient):
|
||||
PLATFORM_NAME: MockPlatform = "mock"
|
||||
|
||||
def __init__(self, config: VideoPlatformConfig):
|
||||
super().__init__(config)
|
||||
self._rooms: Dict[str, Dict[str, Any]] = {}
|
||||
self._webhook_calls: list[Dict[str, Any]] = []
|
||||
|
||||
async def create_meeting(
|
||||
self, room_name_prefix: str, end_date: datetime, room: Room
|
||||
) -> MeetingData:
|
||||
meeting_id = str(uuid.uuid4())
|
||||
room_name = f"{room_name_prefix}{ROOM_PREFIX_SEPARATOR}{meeting_id[:8]}"
|
||||
room_url = f"https://mock.video/{room_name}"
|
||||
host_room_url = f"{room_url}?host=true"
|
||||
|
||||
self._rooms[room_name] = {
|
||||
"id": meeting_id,
|
||||
"name": room_name,
|
||||
"url": room_url,
|
||||
"host_url": host_room_url,
|
||||
"end_date": end_date,
|
||||
"room": room,
|
||||
"participants": [],
|
||||
"is_active": True,
|
||||
}
|
||||
|
||||
return MeetingData.model_construct(
|
||||
meeting_id=meeting_id,
|
||||
room_name=room_name,
|
||||
room_url=room_url,
|
||||
host_room_url=host_room_url,
|
||||
platform="whereby",
|
||||
extra_data={"mock": True},
|
||||
)
|
||||
|
||||
async def get_room_sessions(self, room_name: str) -> Dict[str, Any]:
|
||||
if room_name not in self._rooms:
|
||||
return {"error": "Room not found"}
|
||||
|
||||
room_data = self._rooms[room_name]
|
||||
return {
|
||||
"roomName": room_name,
|
||||
"sessions": [
|
||||
{
|
||||
"sessionId": room_data["id"],
|
||||
"startTime": datetime.utcnow().isoformat(),
|
||||
"participants": room_data["participants"],
|
||||
"isActive": room_data["is_active"],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
async def delete_room(self, room_name: str) -> bool:
|
||||
if room_name in self._rooms:
|
||||
self._rooms[room_name]["is_active"] = False
|
||||
return True
|
||||
return False
|
||||
|
||||
async def upload_logo(self, room_name: str, logo_path: str) -> bool:
|
||||
if room_name in self._rooms:
|
||||
self._rooms[room_name]["logo_path"] = logo_path
|
||||
return True
|
||||
return False
|
||||
|
||||
def verify_webhook_signature(
|
||||
self, body: bytes, signature: str, timestamp: Optional[str] = None
|
||||
) -> bool:
|
||||
return signature == "valid"
|
||||
|
||||
def add_participant(
|
||||
self, room_name: str, participant_id: str, participant_name: str
|
||||
):
|
||||
if room_name in self._rooms:
|
||||
self._rooms[room_name]["participants"].append(
|
||||
{
|
||||
"id": participant_id,
|
||||
"name": participant_name,
|
||||
"joined_at": datetime.utcnow().isoformat(),
|
||||
}
|
||||
)
|
||||
|
||||
def trigger_webhook(self, event_type: str, data: Dict[str, Any]):
|
||||
self._webhook_calls.append(
|
||||
{
|
||||
"type": event_type,
|
||||
"data": data,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
}
|
||||
)
|
||||
|
||||
def get_webhook_calls(self) -> list[Dict[str, Any]]:
|
||||
return self._webhook_calls.copy()
|
||||
|
||||
def clear_data(self):
|
||||
self._rooms.clear()
|
||||
self._webhook_calls.clear()
|
||||
@@ -139,14 +139,10 @@ async def test_cleanup_deletes_associated_meeting_and_recording():
|
||||
mock_settings.PUBLIC_DATA_RETENTION_DAYS = 7
|
||||
|
||||
# Mock storage deletion
|
||||
with patch("reflector.db.transcripts.get_transcripts_storage") as mock_storage:
|
||||
with patch("reflector.worker.cleanup.get_transcripts_storage") as mock_storage:
|
||||
mock_storage.return_value.delete_file = AsyncMock()
|
||||
with patch(
|
||||
"reflector.worker.cleanup.get_recordings_storage"
|
||||
) as mock_rec_storage:
|
||||
mock_rec_storage.return_value.delete_file = AsyncMock()
|
||||
|
||||
result = await cleanup_old_public_data()
|
||||
result = await cleanup_old_public_data()
|
||||
|
||||
# Check results
|
||||
assert result["transcripts_deleted"] == 1
|
||||
|
||||
330
server/tests/test_consent_multitrack.py
Normal file
330
server/tests/test_consent_multitrack.py
Normal file
@@ -0,0 +1,330 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from reflector.db.meetings import (
|
||||
MeetingConsent,
|
||||
meeting_consent_controller,
|
||||
meetings_controller,
|
||||
)
|
||||
from reflector.db.recordings import Recording, recordings_controller
|
||||
from reflector.db.rooms import rooms_controller
|
||||
from reflector.db.transcripts import SourceKind, transcripts_controller
|
||||
from reflector.pipelines.main_live_pipeline import cleanup_consent
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_consent_cleanup_deletes_multitrack_files():
|
||||
room = await rooms_controller.add(
|
||||
name="Test Room",
|
||||
user_id="test-user",
|
||||
zulip_auto_post=False,
|
||||
zulip_stream="",
|
||||
zulip_topic="",
|
||||
is_locked=False,
|
||||
room_mode="normal",
|
||||
recording_type="cloud",
|
||||
recording_trigger="automatic",
|
||||
is_shared=False,
|
||||
platform="daily",
|
||||
)
|
||||
|
||||
# Create meeting
|
||||
meeting = await meetings_controller.create(
|
||||
id="test-multitrack-meeting",
|
||||
room_name="test-room-20250101120000",
|
||||
room_url="https://test.daily.co/test-room",
|
||||
host_room_url="https://test.daily.co/test-room",
|
||||
start_date=datetime.now(timezone.utc),
|
||||
end_date=datetime.now(timezone.utc),
|
||||
room=room,
|
||||
)
|
||||
|
||||
track_keys = [
|
||||
"recordings/test-room-20250101120000/track-0.webm",
|
||||
"recordings/test-room-20250101120000/track-1.webm",
|
||||
"recordings/test-room-20250101120000/track-2.webm",
|
||||
]
|
||||
recording = await recordings_controller.create(
|
||||
Recording(
|
||||
bucket_name="test-bucket",
|
||||
object_key="recordings/test-room-20250101120000", # Folder path
|
||||
recorded_at=datetime.now(timezone.utc),
|
||||
meeting_id=meeting.id,
|
||||
track_keys=track_keys,
|
||||
)
|
||||
)
|
||||
|
||||
# Create transcript
|
||||
transcript = await transcripts_controller.add(
|
||||
name="Test Multitrack Transcript",
|
||||
source_kind=SourceKind.ROOM,
|
||||
recording_id=recording.id,
|
||||
meeting_id=meeting.id,
|
||||
)
|
||||
|
||||
# Add consent denial
|
||||
await meeting_consent_controller.upsert(
|
||||
MeetingConsent(
|
||||
meeting_id=meeting.id,
|
||||
user_id="test-user",
|
||||
consent_given=False,
|
||||
consent_timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
)
|
||||
|
||||
# Mock get_transcripts_storage (master credentials with bucket override)
|
||||
with patch(
|
||||
"reflector.pipelines.main_live_pipeline.get_transcripts_storage"
|
||||
) as mock_get_transcripts_storage:
|
||||
mock_master_storage = MagicMock()
|
||||
mock_master_storage.delete_file = AsyncMock()
|
||||
mock_get_transcripts_storage.return_value = mock_master_storage
|
||||
|
||||
await cleanup_consent(transcript_id=transcript.id)
|
||||
|
||||
# Verify master storage was used with bucket override for all track keys
|
||||
assert mock_master_storage.delete_file.call_count == 3
|
||||
deleted_keys = []
|
||||
for call_args in mock_master_storage.delete_file.call_args_list:
|
||||
key = call_args[0][0]
|
||||
bucket_kwarg = call_args[1].get("bucket")
|
||||
deleted_keys.append(key)
|
||||
assert bucket_kwarg == "test-bucket" # Verify bucket override!
|
||||
assert set(deleted_keys) == set(track_keys)
|
||||
|
||||
updated_transcript = await transcripts_controller.get_by_id(transcript.id)
|
||||
assert updated_transcript.audio_deleted is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_consent_cleanup_handles_missing_track_keys():
|
||||
room = await rooms_controller.add(
|
||||
name="Test Room 2",
|
||||
user_id="test-user",
|
||||
zulip_auto_post=False,
|
||||
zulip_stream="",
|
||||
zulip_topic="",
|
||||
is_locked=False,
|
||||
room_mode="normal",
|
||||
recording_type="cloud",
|
||||
recording_trigger="automatic",
|
||||
is_shared=False,
|
||||
platform="daily",
|
||||
)
|
||||
|
||||
# Create meeting
|
||||
meeting = await meetings_controller.create(
|
||||
id="test-multitrack-meeting-2",
|
||||
room_name="test-room-20250101120001",
|
||||
room_url="https://test.daily.co/test-room-2",
|
||||
host_room_url="https://test.daily.co/test-room-2",
|
||||
start_date=datetime.now(timezone.utc),
|
||||
end_date=datetime.now(timezone.utc),
|
||||
room=room,
|
||||
)
|
||||
|
||||
recording = await recordings_controller.create(
|
||||
Recording(
|
||||
bucket_name="test-bucket",
|
||||
object_key="recordings/old-style-recording.mp4",
|
||||
recorded_at=datetime.now(timezone.utc),
|
||||
meeting_id=meeting.id,
|
||||
track_keys=None,
|
||||
)
|
||||
)
|
||||
|
||||
transcript = await transcripts_controller.add(
|
||||
name="Test Old-Style Transcript",
|
||||
source_kind=SourceKind.ROOM,
|
||||
recording_id=recording.id,
|
||||
meeting_id=meeting.id,
|
||||
)
|
||||
|
||||
# Add consent denial
|
||||
await meeting_consent_controller.upsert(
|
||||
MeetingConsent(
|
||||
meeting_id=meeting.id,
|
||||
user_id="test-user-2",
|
||||
consent_given=False,
|
||||
consent_timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
)
|
||||
|
||||
# Mock get_transcripts_storage (master credentials with bucket override)
|
||||
with patch(
|
||||
"reflector.pipelines.main_live_pipeline.get_transcripts_storage"
|
||||
) as mock_get_transcripts_storage:
|
||||
mock_master_storage = MagicMock()
|
||||
mock_master_storage.delete_file = AsyncMock()
|
||||
mock_get_transcripts_storage.return_value = mock_master_storage
|
||||
|
||||
await cleanup_consent(transcript_id=transcript.id)
|
||||
|
||||
# Verify master storage was used with bucket override
|
||||
assert mock_master_storage.delete_file.call_count == 1
|
||||
call_args = mock_master_storage.delete_file.call_args
|
||||
assert call_args[0][0] == recording.object_key
|
||||
assert call_args[1].get("bucket") == "test-bucket" # Verify bucket override!
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_consent_cleanup_empty_track_keys_falls_back():
|
||||
room = await rooms_controller.add(
|
||||
name="Test Room 3",
|
||||
user_id="test-user",
|
||||
zulip_auto_post=False,
|
||||
zulip_stream="",
|
||||
zulip_topic="",
|
||||
is_locked=False,
|
||||
room_mode="normal",
|
||||
recording_type="cloud",
|
||||
recording_trigger="automatic",
|
||||
is_shared=False,
|
||||
platform="daily",
|
||||
)
|
||||
|
||||
# Create meeting
|
||||
meeting = await meetings_controller.create(
|
||||
id="test-multitrack-meeting-3",
|
||||
room_name="test-room-20250101120002",
|
||||
room_url="https://test.daily.co/test-room-3",
|
||||
host_room_url="https://test.daily.co/test-room-3",
|
||||
start_date=datetime.now(timezone.utc),
|
||||
end_date=datetime.now(timezone.utc),
|
||||
room=room,
|
||||
)
|
||||
|
||||
recording = await recordings_controller.create(
|
||||
Recording(
|
||||
bucket_name="test-bucket",
|
||||
object_key="recordings/fallback-recording.mp4",
|
||||
recorded_at=datetime.now(timezone.utc),
|
||||
meeting_id=meeting.id,
|
||||
track_keys=[],
|
||||
)
|
||||
)
|
||||
|
||||
transcript = await transcripts_controller.add(
|
||||
name="Test Empty Track Keys Transcript",
|
||||
source_kind=SourceKind.ROOM,
|
||||
recording_id=recording.id,
|
||||
meeting_id=meeting.id,
|
||||
)
|
||||
|
||||
# Add consent denial
|
||||
await meeting_consent_controller.upsert(
|
||||
MeetingConsent(
|
||||
meeting_id=meeting.id,
|
||||
user_id="test-user-3",
|
||||
consent_given=False,
|
||||
consent_timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
)
|
||||
|
||||
# Mock get_transcripts_storage (master credentials with bucket override)
|
||||
with patch(
|
||||
"reflector.pipelines.main_live_pipeline.get_transcripts_storage"
|
||||
) as mock_get_transcripts_storage:
|
||||
mock_master_storage = MagicMock()
|
||||
mock_master_storage.delete_file = AsyncMock()
|
||||
mock_get_transcripts_storage.return_value = mock_master_storage
|
||||
|
||||
# Run cleanup
|
||||
await cleanup_consent(transcript_id=transcript.id)
|
||||
|
||||
# Verify master storage was used with bucket override
|
||||
assert mock_master_storage.delete_file.call_count == 1
|
||||
call_args = mock_master_storage.delete_file.call_args
|
||||
assert call_args[0][0] == recording.object_key
|
||||
assert call_args[1].get("bucket") == "test-bucket" # Verify bucket override!
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_consent_cleanup_partial_failure_doesnt_mark_deleted():
|
||||
room = await rooms_controller.add(
|
||||
name="Test Room 4",
|
||||
user_id="test-user",
|
||||
zulip_auto_post=False,
|
||||
zulip_stream="",
|
||||
zulip_topic="",
|
||||
is_locked=False,
|
||||
room_mode="normal",
|
||||
recording_type="cloud",
|
||||
recording_trigger="automatic",
|
||||
is_shared=False,
|
||||
platform="daily",
|
||||
)
|
||||
|
||||
# Create meeting
|
||||
meeting = await meetings_controller.create(
|
||||
id="test-multitrack-meeting-4",
|
||||
room_name="test-room-20250101120003",
|
||||
room_url="https://test.daily.co/test-room-4",
|
||||
host_room_url="https://test.daily.co/test-room-4",
|
||||
start_date=datetime.now(timezone.utc),
|
||||
end_date=datetime.now(timezone.utc),
|
||||
room=room,
|
||||
)
|
||||
|
||||
track_keys = [
|
||||
"recordings/test-room-20250101120003/track-0.webm",
|
||||
"recordings/test-room-20250101120003/track-1.webm",
|
||||
"recordings/test-room-20250101120003/track-2.webm",
|
||||
]
|
||||
recording = await recordings_controller.create(
|
||||
Recording(
|
||||
bucket_name="test-bucket",
|
||||
object_key="recordings/test-room-20250101120003",
|
||||
recorded_at=datetime.now(timezone.utc),
|
||||
meeting_id=meeting.id,
|
||||
track_keys=track_keys,
|
||||
)
|
||||
)
|
||||
|
||||
# Create transcript
|
||||
transcript = await transcripts_controller.add(
|
||||
name="Test Partial Failure Transcript",
|
||||
source_kind=SourceKind.ROOM,
|
||||
recording_id=recording.id,
|
||||
meeting_id=meeting.id,
|
||||
)
|
||||
|
||||
# Add consent denial
|
||||
await meeting_consent_controller.upsert(
|
||||
MeetingConsent(
|
||||
meeting_id=meeting.id,
|
||||
user_id="test-user-4",
|
||||
consent_given=False,
|
||||
consent_timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
)
|
||||
|
||||
# Mock get_transcripts_storage (master credentials with bucket override) with partial failure
|
||||
with patch(
|
||||
"reflector.pipelines.main_live_pipeline.get_transcripts_storage"
|
||||
) as mock_get_transcripts_storage:
|
||||
mock_master_storage = MagicMock()
|
||||
|
||||
call_count = 0
|
||||
|
||||
async def delete_side_effect(key, bucket=None):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 2:
|
||||
raise Exception("S3 deletion failed")
|
||||
|
||||
mock_master_storage.delete_file = AsyncMock(side_effect=delete_side_effect)
|
||||
mock_get_transcripts_storage.return_value = mock_master_storage
|
||||
|
||||
await cleanup_consent(transcript_id=transcript.id)
|
||||
|
||||
# Verify master storage was called with bucket override
|
||||
assert mock_master_storage.delete_file.call_count == 3
|
||||
|
||||
updated_transcript = await transcripts_controller.get_by_id(transcript.id)
|
||||
assert (
|
||||
updated_transcript.audio_deleted is None
|
||||
or updated_transcript.audio_deleted is False
|
||||
)
|
||||
@@ -127,18 +127,27 @@ async def mock_storage():
|
||||
from reflector.storage.base import Storage
|
||||
|
||||
class TestStorage(Storage):
|
||||
async def _put_file(self, path, data):
|
||||
async def _put_file(self, path, data, bucket=None):
|
||||
return None
|
||||
|
||||
async def _get_file_url(self, path):
|
||||
async def _get_file_url(
|
||||
self,
|
||||
path,
|
||||
operation: str = "get_object",
|
||||
expires_in: int = 3600,
|
||||
bucket=None,
|
||||
):
|
||||
return f"http://test-storage/{path}"
|
||||
|
||||
async def _get_file(self, path):
|
||||
async def _get_file(self, path, bucket=None):
|
||||
return b"test_audio_data"
|
||||
|
||||
async def _delete_file(self, path):
|
||||
async def _delete_file(self, path, bucket=None):
|
||||
return None
|
||||
|
||||
async def _stream_to_fileobj(self, path, fileobj, bucket=None):
|
||||
fileobj.write(b"test_audio_data")
|
||||
|
||||
storage = TestStorage()
|
||||
# Add mock tracking for verification
|
||||
storage._put_file = AsyncMock(side_effect=storage._put_file)
|
||||
@@ -181,7 +190,7 @@ async def mock_waveform_processor():
|
||||
async def mock_topic_detector():
|
||||
"""Mock TranscriptTopicDetectorProcessor"""
|
||||
with patch(
|
||||
"reflector.pipelines.main_file_pipeline.TranscriptTopicDetectorProcessor"
|
||||
"reflector.pipelines.topic_processing.TranscriptTopicDetectorProcessor"
|
||||
) as mock_topic_class:
|
||||
mock_topic = AsyncMock()
|
||||
mock_topic.set_pipeline = MagicMock()
|
||||
@@ -218,7 +227,7 @@ async def mock_topic_detector():
|
||||
async def mock_title_processor():
|
||||
"""Mock TranscriptFinalTitleProcessor"""
|
||||
with patch(
|
||||
"reflector.pipelines.main_file_pipeline.TranscriptFinalTitleProcessor"
|
||||
"reflector.pipelines.topic_processing.TranscriptFinalTitleProcessor"
|
||||
) as mock_title_class:
|
||||
mock_title = AsyncMock()
|
||||
mock_title.set_pipeline = MagicMock()
|
||||
@@ -247,7 +256,7 @@ async def mock_title_processor():
|
||||
async def mock_summary_processor():
|
||||
"""Mock TranscriptFinalSummaryProcessor"""
|
||||
with patch(
|
||||
"reflector.pipelines.main_file_pipeline.TranscriptFinalSummaryProcessor"
|
||||
"reflector.pipelines.topic_processing.TranscriptFinalSummaryProcessor"
|
||||
) as mock_summary_class:
|
||||
mock_summary = AsyncMock()
|
||||
mock_summary.set_pipeline = MagicMock()
|
||||
|
||||
@@ -48,6 +48,7 @@ async def test_create_room_with_ics_fields(authenticated_client):
|
||||
"ics_url": "https://calendar.example.com/test.ics",
|
||||
"ics_fetch_interval": 600,
|
||||
"ics_enabled": True,
|
||||
"platform": "daily",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@@ -75,6 +76,7 @@ async def test_update_room_ics_configuration(authenticated_client):
|
||||
"is_shared": False,
|
||||
"webhook_url": "",
|
||||
"webhook_secret": "",
|
||||
"platform": "daily",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@@ -111,6 +113,7 @@ async def test_trigger_ics_sync(authenticated_client):
|
||||
is_shared=False,
|
||||
ics_url="https://calendar.example.com/api.ics",
|
||||
ics_enabled=True,
|
||||
platform="daily",
|
||||
)
|
||||
|
||||
cal = Calendar()
|
||||
@@ -154,6 +157,7 @@ async def test_trigger_ics_sync_unauthorized(client):
|
||||
is_shared=False,
|
||||
ics_url="https://calendar.example.com/api.ics",
|
||||
ics_enabled=True,
|
||||
platform="daily",
|
||||
)
|
||||
|
||||
response = await client.post(f"/rooms/{room.name}/ics/sync")
|
||||
@@ -176,6 +180,7 @@ async def test_trigger_ics_sync_not_configured(authenticated_client):
|
||||
recording_trigger="automatic-2nd-participant",
|
||||
is_shared=False,
|
||||
ics_enabled=False,
|
||||
platform="daily",
|
||||
)
|
||||
|
||||
response = await client.post(f"/rooms/{room.name}/ics/sync")
|
||||
@@ -200,6 +205,7 @@ async def test_get_ics_status(authenticated_client):
|
||||
ics_url="https://calendar.example.com/status.ics",
|
||||
ics_enabled=True,
|
||||
ics_fetch_interval=300,
|
||||
platform="daily",
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
@@ -231,6 +237,7 @@ async def test_get_ics_status_unauthorized(client):
|
||||
is_shared=False,
|
||||
ics_url="https://calendar.example.com/status.ics",
|
||||
ics_enabled=True,
|
||||
platform="daily",
|
||||
)
|
||||
|
||||
response = await client.get(f"/rooms/{room.name}/ics/status")
|
||||
@@ -252,6 +259,7 @@ async def test_list_room_meetings(authenticated_client):
|
||||
recording_type="cloud",
|
||||
recording_trigger="automatic-2nd-participant",
|
||||
is_shared=False,
|
||||
platform="daily",
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
@@ -298,6 +306,7 @@ async def test_list_room_meetings_non_owner(client):
|
||||
recording_type="cloud",
|
||||
recording_trigger="automatic-2nd-participant",
|
||||
is_shared=False,
|
||||
platform="daily",
|
||||
)
|
||||
|
||||
event = CalendarEvent(
|
||||
@@ -334,6 +343,7 @@ async def test_list_upcoming_meetings(authenticated_client):
|
||||
recording_type="cloud",
|
||||
recording_trigger="automatic-2nd-participant",
|
||||
is_shared=False,
|
||||
platform="daily",
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
321
server/tests/test_storage.py
Normal file
321
server/tests/test_storage.py
Normal file
@@ -0,0 +1,321 @@
|
||||
"""Tests for storage abstraction layer."""
|
||||
|
||||
import io
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
from reflector.storage.base import StoragePermissionError
|
||||
from reflector.storage.storage_aws import AwsStorage
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aws_storage_stream_to_fileobj():
|
||||
"""Test that AWS storage can stream directly to a file object without loading into memory."""
|
||||
# Setup
|
||||
storage = AwsStorage(
|
||||
aws_bucket_name="test-bucket",
|
||||
aws_region="us-east-1",
|
||||
aws_access_key_id="test-key",
|
||||
aws_secret_access_key="test-secret",
|
||||
)
|
||||
|
||||
# Mock download_fileobj to write data
|
||||
async def mock_download(Bucket, Key, Fileobj, **kwargs):
|
||||
Fileobj.write(b"chunk1chunk2")
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.download_fileobj = AsyncMock(side_effect=mock_download)
|
||||
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
||||
mock_client.__aexit__ = AsyncMock(return_value=None)
|
||||
|
||||
# Patch the session client
|
||||
with patch.object(storage.session, "client", return_value=mock_client):
|
||||
# Create a file-like object to stream to
|
||||
output = io.BytesIO()
|
||||
|
||||
# Act - stream to file object
|
||||
await storage.stream_to_fileobj("test-file.mp4", output, bucket="test-bucket")
|
||||
|
||||
# Assert
|
||||
mock_client.download_fileobj.assert_called_once_with(
|
||||
Bucket="test-bucket", Key="test-file.mp4", Fileobj=output
|
||||
)
|
||||
|
||||
# Check that data was written to output
|
||||
output.seek(0)
|
||||
assert output.read() == b"chunk1chunk2"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aws_storage_stream_to_fileobj_with_folder():
|
||||
"""Test streaming with folder prefix in bucket name."""
|
||||
storage = AwsStorage(
|
||||
aws_bucket_name="test-bucket/recordings",
|
||||
aws_region="us-east-1",
|
||||
aws_access_key_id="test-key",
|
||||
aws_secret_access_key="test-secret",
|
||||
)
|
||||
|
||||
async def mock_download(Bucket, Key, Fileobj, **kwargs):
|
||||
Fileobj.write(b"data")
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.download_fileobj = AsyncMock(side_effect=mock_download)
|
||||
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
||||
mock_client.__aexit__ = AsyncMock(return_value=None)
|
||||
|
||||
with patch.object(storage.session, "client", return_value=mock_client):
|
||||
output = io.BytesIO()
|
||||
await storage.stream_to_fileobj("file.mp4", output, bucket="other-bucket")
|
||||
|
||||
# Should use folder prefix from instance config
|
||||
mock_client.download_fileobj.assert_called_once_with(
|
||||
Bucket="other-bucket", Key="recordings/file.mp4", Fileobj=output
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_storage_base_class_stream_to_fileobj():
|
||||
"""Test that base Storage class has stream_to_fileobj method."""
|
||||
from reflector.storage.base import Storage
|
||||
|
||||
# Verify method exists in base class
|
||||
assert hasattr(Storage, "stream_to_fileobj")
|
||||
|
||||
# Create a mock storage instance
|
||||
storage = MagicMock(spec=Storage)
|
||||
storage.stream_to_fileobj = AsyncMock()
|
||||
|
||||
# Should be callable
|
||||
await storage.stream_to_fileobj("file.mp4", io.BytesIO())
|
||||
storage.stream_to_fileobj.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aws_storage_stream_uses_download_fileobj():
|
||||
"""Test that download_fileobj is called correctly."""
|
||||
storage = AwsStorage(
|
||||
aws_bucket_name="test-bucket",
|
||||
aws_region="us-east-1",
|
||||
aws_access_key_id="test-key",
|
||||
aws_secret_access_key="test-secret",
|
||||
)
|
||||
|
||||
async def mock_download(Bucket, Key, Fileobj, **kwargs):
|
||||
Fileobj.write(b"data")
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.download_fileobj = AsyncMock(side_effect=mock_download)
|
||||
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
||||
mock_client.__aexit__ = AsyncMock(return_value=None)
|
||||
|
||||
with patch.object(storage.session, "client", return_value=mock_client):
|
||||
output = io.BytesIO()
|
||||
await storage.stream_to_fileobj("test.mp4", output)
|
||||
|
||||
# Verify download_fileobj was called with correct parameters
|
||||
mock_client.download_fileobj.assert_called_once_with(
|
||||
Bucket="test-bucket", Key="test.mp4", Fileobj=output
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aws_storage_handles_access_denied_error():
|
||||
"""Test that AccessDenied errors are caught and wrapped in StoragePermissionError."""
|
||||
storage = AwsStorage(
|
||||
aws_bucket_name="test-bucket",
|
||||
aws_region="us-east-1",
|
||||
aws_access_key_id="test-key",
|
||||
aws_secret_access_key="test-secret",
|
||||
)
|
||||
|
||||
# Mock ClientError with AccessDenied
|
||||
error_response = {"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}
|
||||
mock_client = AsyncMock()
|
||||
mock_client.put_object = AsyncMock(
|
||||
side_effect=ClientError(error_response, "PutObject")
|
||||
)
|
||||
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
||||
mock_client.__aexit__ = AsyncMock(return_value=None)
|
||||
|
||||
with patch.object(storage.session, "client", return_value=mock_client):
|
||||
with pytest.raises(StoragePermissionError) as exc_info:
|
||||
await storage.put_file("test.txt", b"data")
|
||||
|
||||
# Verify error message contains expected information
|
||||
error_msg = str(exc_info.value)
|
||||
assert "AccessDenied" in error_msg
|
||||
assert "default bucket 'test-bucket'" in error_msg
|
||||
assert "S3 upload failed" in error_msg
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aws_storage_handles_no_such_bucket_error():
|
||||
"""Test that NoSuchBucket errors are caught and wrapped in StoragePermissionError."""
|
||||
storage = AwsStorage(
|
||||
aws_bucket_name="test-bucket",
|
||||
aws_region="us-east-1",
|
||||
aws_access_key_id="test-key",
|
||||
aws_secret_access_key="test-secret",
|
||||
)
|
||||
|
||||
# Mock ClientError with NoSuchBucket
|
||||
error_response = {
|
||||
"Error": {
|
||||
"Code": "NoSuchBucket",
|
||||
"Message": "The specified bucket does not exist",
|
||||
}
|
||||
}
|
||||
mock_client = AsyncMock()
|
||||
mock_client.delete_object = AsyncMock(
|
||||
side_effect=ClientError(error_response, "DeleteObject")
|
||||
)
|
||||
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
||||
mock_client.__aexit__ = AsyncMock(return_value=None)
|
||||
|
||||
with patch.object(storage.session, "client", return_value=mock_client):
|
||||
with pytest.raises(StoragePermissionError) as exc_info:
|
||||
await storage.delete_file("test.txt")
|
||||
|
||||
# Verify error message contains expected information
|
||||
error_msg = str(exc_info.value)
|
||||
assert "NoSuchBucket" in error_msg
|
||||
assert "default bucket 'test-bucket'" in error_msg
|
||||
assert "S3 delete failed" in error_msg
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aws_storage_error_message_with_bucket_override():
|
||||
"""Test that error messages correctly show overridden bucket."""
|
||||
storage = AwsStorage(
|
||||
aws_bucket_name="default-bucket",
|
||||
aws_region="us-east-1",
|
||||
aws_access_key_id="test-key",
|
||||
aws_secret_access_key="test-secret",
|
||||
)
|
||||
|
||||
# Mock ClientError with AccessDenied
|
||||
error_response = {"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}
|
||||
mock_client = AsyncMock()
|
||||
mock_client.get_object = AsyncMock(
|
||||
side_effect=ClientError(error_response, "GetObject")
|
||||
)
|
||||
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
||||
mock_client.__aexit__ = AsyncMock(return_value=None)
|
||||
|
||||
with patch.object(storage.session, "client", return_value=mock_client):
|
||||
with pytest.raises(StoragePermissionError) as exc_info:
|
||||
await storage.get_file("test.txt", bucket="override-bucket")
|
||||
|
||||
# Verify error message shows overridden bucket, not default
|
||||
error_msg = str(exc_info.value)
|
||||
assert "overridden bucket 'override-bucket'" in error_msg
|
||||
assert "default-bucket" not in error_msg
|
||||
assert "S3 download failed" in error_msg
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aws_storage_reraises_non_handled_errors():
|
||||
"""Test that non-AccessDenied/NoSuchBucket errors are re-raised as-is."""
|
||||
storage = AwsStorage(
|
||||
aws_bucket_name="test-bucket",
|
||||
aws_region="us-east-1",
|
||||
aws_access_key_id="test-key",
|
||||
aws_secret_access_key="test-secret",
|
||||
)
|
||||
|
||||
# Mock ClientError with different error code
|
||||
error_response = {
|
||||
"Error": {"Code": "InternalError", "Message": "Internal Server Error"}
|
||||
}
|
||||
mock_client = AsyncMock()
|
||||
mock_client.put_object = AsyncMock(
|
||||
side_effect=ClientError(error_response, "PutObject")
|
||||
)
|
||||
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
||||
mock_client.__aexit__ = AsyncMock(return_value=None)
|
||||
|
||||
with patch.object(storage.session, "client", return_value=mock_client):
|
||||
# Should raise ClientError, not StoragePermissionError
|
||||
with pytest.raises(ClientError) as exc_info:
|
||||
await storage.put_file("test.txt", b"data")
|
||||
|
||||
# Verify it's the original ClientError
|
||||
assert exc_info.value.response["Error"]["Code"] == "InternalError"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aws_storage_presign_url_handles_errors():
|
||||
"""Test that presigned URL generation handles permission errors."""
|
||||
storage = AwsStorage(
|
||||
aws_bucket_name="test-bucket",
|
||||
aws_region="us-east-1",
|
||||
aws_access_key_id="test-key",
|
||||
aws_secret_access_key="test-secret",
|
||||
)
|
||||
|
||||
# Mock ClientError with AccessDenied during presign operation
|
||||
error_response = {"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}
|
||||
mock_client = AsyncMock()
|
||||
mock_client.generate_presigned_url = AsyncMock(
|
||||
side_effect=ClientError(error_response, "GeneratePresignedUrl")
|
||||
)
|
||||
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
||||
mock_client.__aexit__ = AsyncMock(return_value=None)
|
||||
|
||||
with patch.object(storage.session, "client", return_value=mock_client):
|
||||
with pytest.raises(StoragePermissionError) as exc_info:
|
||||
await storage.get_file_url("test.txt")
|
||||
|
||||
# Verify error message
|
||||
error_msg = str(exc_info.value)
|
||||
assert "S3 presign failed" in error_msg
|
||||
assert "AccessDenied" in error_msg
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aws_storage_list_objects_handles_errors():
|
||||
"""Test that list_objects handles permission errors."""
|
||||
storage = AwsStorage(
|
||||
aws_bucket_name="test-bucket",
|
||||
aws_region="us-east-1",
|
||||
aws_access_key_id="test-key",
|
||||
aws_secret_access_key="test-secret",
|
||||
)
|
||||
|
||||
# Mock ClientError during list operation
|
||||
error_response = {"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}
|
||||
mock_paginator = MagicMock()
|
||||
|
||||
async def mock_paginate(*args, **kwargs):
|
||||
raise ClientError(error_response, "ListObjectsV2")
|
||||
yield # Make it an async generator
|
||||
|
||||
mock_paginator.paginate = mock_paginate
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.get_paginator = MagicMock(return_value=mock_paginator)
|
||||
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
||||
mock_client.__aexit__ = AsyncMock(return_value=None)
|
||||
|
||||
with patch.object(storage.session, "client", return_value=mock_client):
|
||||
with pytest.raises(StoragePermissionError) as exc_info:
|
||||
await storage.list_objects(prefix="test/")
|
||||
|
||||
error_msg = str(exc_info.value)
|
||||
assert "S3 list_objects failed" in error_msg
|
||||
assert "AccessDenied" in error_msg
|
||||
|
||||
|
||||
def test_aws_storage_constructor_rejects_mixed_auth():
|
||||
"""Test that constructor rejects both role_arn and access keys."""
|
||||
with pytest.raises(ValueError, match="cannot use both.*role_arn.*access keys"):
|
||||
AwsStorage(
|
||||
aws_bucket_name="test-bucket",
|
||||
aws_region="us-east-1",
|
||||
aws_access_key_id="test-key",
|
||||
aws_secret_access_key="test-secret",
|
||||
aws_role_arn="arn:aws:iam::123456789012:role/test-role",
|
||||
)
|
||||
@@ -22,13 +22,16 @@ async def test_recording_deleted_with_transcript():
|
||||
recording_id=recording.id,
|
||||
)
|
||||
|
||||
with patch("reflector.db.transcripts.get_recordings_storage") as mock_get_storage:
|
||||
with patch("reflector.db.transcripts.get_transcripts_storage") as mock_get_storage:
|
||||
storage_instance = mock_get_storage.return_value
|
||||
storage_instance.delete_file = AsyncMock()
|
||||
|
||||
await transcripts_controller.remove_by_id(transcript.id)
|
||||
|
||||
storage_instance.delete_file.assert_awaited_once_with(recording.object_key)
|
||||
# Should be called with bucket override
|
||||
storage_instance.delete_file.assert_awaited_once_with(
|
||||
recording.object_key, bucket=recording.bucket_name
|
||||
)
|
||||
|
||||
assert await recordings_controller.get_by_id(recording.id) is None
|
||||
assert await transcripts_controller.get_by_id(transcript.id) is None
|
||||
|
||||
17
server/tests/test_utils_daily.py
Normal file
17
server/tests/test_utils_daily.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import pytest
|
||||
|
||||
from reflector.utils.daily import extract_base_room_name
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"daily_room_name,expected",
|
||||
[
|
||||
("daily-20251020193458", "daily"),
|
||||
("daily-2-20251020193458", "daily-2"),
|
||||
("my-room-name-20251020193458", "my-room-name"),
|
||||
("room-with-numbers-123-20251020193458", "room-with-numbers-123"),
|
||||
("x-20251020193458", "x"),
|
||||
],
|
||||
)
|
||||
def test_extract_base_room_name(daily_room_name, expected):
|
||||
assert extract_base_room_name(daily_room_name) == expected
|
||||
63
server/tests/test_utils_url.py
Normal file
63
server/tests/test_utils_url.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""Tests for URL utility functions."""
|
||||
|
||||
from reflector.utils.url import add_query_param
|
||||
|
||||
|
||||
class TestAddQueryParam:
|
||||
"""Test the add_query_param function."""
|
||||
|
||||
def test_add_param_to_url_without_query(self):
|
||||
"""Should add query param with ? to URL without existing params."""
|
||||
url = "https://example.com/room"
|
||||
result = add_query_param(url, "t", "token123")
|
||||
assert result == "https://example.com/room?t=token123"
|
||||
|
||||
def test_add_param_to_url_with_existing_query(self):
|
||||
"""Should add query param with & to URL with existing params."""
|
||||
url = "https://example.com/room?existing=param"
|
||||
result = add_query_param(url, "t", "token123")
|
||||
assert result == "https://example.com/room?existing=param&t=token123"
|
||||
|
||||
def test_add_param_to_url_with_multiple_existing_params(self):
|
||||
"""Should add query param to URL with multiple existing params."""
|
||||
url = "https://example.com/room?param1=value1¶m2=value2"
|
||||
result = add_query_param(url, "t", "token123")
|
||||
assert (
|
||||
result == "https://example.com/room?param1=value1¶m2=value2&t=token123"
|
||||
)
|
||||
|
||||
def test_add_param_with_special_characters(self):
|
||||
"""Should properly encode special characters in param value."""
|
||||
url = "https://example.com/room"
|
||||
result = add_query_param(url, "name", "hello world")
|
||||
assert result == "https://example.com/room?name=hello+world"
|
||||
|
||||
def test_add_param_to_url_with_fragment(self):
|
||||
"""Should preserve URL fragment when adding query param."""
|
||||
url = "https://example.com/room#section"
|
||||
result = add_query_param(url, "t", "token123")
|
||||
assert result == "https://example.com/room?t=token123#section"
|
||||
|
||||
def test_add_param_to_url_with_query_and_fragment(self):
|
||||
"""Should preserve fragment when adding param to URL with existing query."""
|
||||
url = "https://example.com/room?existing=param#section"
|
||||
result = add_query_param(url, "t", "token123")
|
||||
assert result == "https://example.com/room?existing=param&t=token123#section"
|
||||
|
||||
def test_add_param_overwrites_existing_param(self):
|
||||
"""Should overwrite existing param with same name."""
|
||||
url = "https://example.com/room?t=oldtoken"
|
||||
result = add_query_param(url, "t", "newtoken")
|
||||
assert result == "https://example.com/room?t=newtoken"
|
||||
|
||||
def test_url_without_scheme(self):
|
||||
"""Should handle URLs without scheme (relative URLs)."""
|
||||
url = "/room/path"
|
||||
result = add_query_param(url, "t", "token123")
|
||||
assert result == "/room/path?t=token123"
|
||||
|
||||
def test_empty_url(self):
|
||||
"""Should handle empty URL."""
|
||||
url = ""
|
||||
result = add_query_param(url, "t", "token123")
|
||||
assert result == "?t=token123"
|
||||
58
server/tests/test_video_platforms_factory.py
Normal file
58
server/tests/test_video_platforms_factory.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""Tests for video_platforms.factory module."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from reflector.video_platforms.factory import get_platform
|
||||
|
||||
|
||||
class TestGetPlatformF:
|
||||
"""Test suite for get_platform function."""
|
||||
|
||||
@patch("reflector.video_platforms.factory.settings")
|
||||
def test_with_room_platform(self, mock_settings):
|
||||
"""When room_platform provided, should return room_platform."""
|
||||
mock_settings.DEFAULT_VIDEO_PLATFORM = "whereby"
|
||||
|
||||
# Should return the room's platform when provided
|
||||
assert get_platform(room_platform="daily") == "daily"
|
||||
assert get_platform(room_platform="whereby") == "whereby"
|
||||
|
||||
@patch("reflector.video_platforms.factory.settings")
|
||||
def test_without_room_platform_uses_default(self, mock_settings):
|
||||
"""When no room_platform, should return DEFAULT_VIDEO_PLATFORM."""
|
||||
mock_settings.DEFAULT_VIDEO_PLATFORM = "whereby"
|
||||
|
||||
# Should return default when room_platform is None
|
||||
assert get_platform(room_platform=None) == "whereby"
|
||||
|
||||
@patch("reflector.video_platforms.factory.settings")
|
||||
def test_with_daily_default(self, mock_settings):
|
||||
"""When DEFAULT_VIDEO_PLATFORM is 'daily', should return 'daily' when no room_platform."""
|
||||
mock_settings.DEFAULT_VIDEO_PLATFORM = "daily"
|
||||
|
||||
# Should return default 'daily' when room_platform is None
|
||||
assert get_platform(room_platform=None) == "daily"
|
||||
|
||||
@patch("reflector.video_platforms.factory.settings")
|
||||
def test_no_room_id_provided(self, mock_settings):
|
||||
"""Should work correctly even when room_id is not provided."""
|
||||
mock_settings.DEFAULT_VIDEO_PLATFORM = "whereby"
|
||||
|
||||
# Should use room_platform when provided
|
||||
assert get_platform(room_platform="daily") == "daily"
|
||||
|
||||
# Should use default when room_platform not provided
|
||||
assert get_platform(room_platform=None) == "whereby"
|
||||
|
||||
@patch("reflector.video_platforms.factory.settings")
|
||||
def test_room_platform_always_takes_precedence(self, mock_settings):
|
||||
"""room_platform should always be used when provided."""
|
||||
mock_settings.DEFAULT_VIDEO_PLATFORM = "whereby"
|
||||
|
||||
# room_platform should take precedence over default
|
||||
assert get_platform(room_platform="daily") == "daily"
|
||||
assert get_platform(room_platform="whereby") == "whereby"
|
||||
|
||||
# Different default shouldn't matter when room_platform provided
|
||||
mock_settings.DEFAULT_VIDEO_PLATFORM = "daily"
|
||||
assert get_platform(room_platform="whereby") == "whereby"
|
||||
Reference in New Issue
Block a user