mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 12:19:06 +00:00
* 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>
408 lines
12 KiB
Python
408 lines
12 KiB
Python
from datetime import datetime, timedelta, timezone
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
from icalendar import Calendar, Event
|
|
|
|
from reflector.db.calendar_events import CalendarEvent, calendar_events_controller
|
|
from reflector.db.rooms import rooms_controller
|
|
|
|
|
|
@pytest.fixture
|
|
async def authenticated_client(client):
|
|
from reflector.app import app
|
|
from reflector.auth import current_user, current_user_optional
|
|
|
|
app.dependency_overrides[current_user] = lambda: {
|
|
"sub": "test-user",
|
|
"email": "test@example.com",
|
|
}
|
|
app.dependency_overrides[current_user_optional] = lambda: {
|
|
"sub": "test-user",
|
|
"email": "test@example.com",
|
|
}
|
|
try:
|
|
yield client
|
|
finally:
|
|
del app.dependency_overrides[current_user]
|
|
del app.dependency_overrides[current_user_optional]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_room_with_ics_fields(authenticated_client):
|
|
client = authenticated_client
|
|
response = await client.post(
|
|
"/rooms",
|
|
json={
|
|
"name": "test-ics-room",
|
|
"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": "",
|
|
"ics_url": "https://calendar.example.com/test.ics",
|
|
"ics_fetch_interval": 600,
|
|
"ics_enabled": True,
|
|
"platform": "daily",
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["name"] == "test-ics-room"
|
|
assert data["ics_url"] == "https://calendar.example.com/test.ics"
|
|
assert data["ics_fetch_interval"] == 600
|
|
assert data["ics_enabled"] is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_room_ics_configuration(authenticated_client):
|
|
client = authenticated_client
|
|
response = await client.post(
|
|
"/rooms",
|
|
json={
|
|
"name": "update-ics-room",
|
|
"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": "",
|
|
"platform": "daily",
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
room_id = response.json()["id"]
|
|
|
|
response = await client.patch(
|
|
f"/rooms/{room_id}",
|
|
json={
|
|
"ics_url": "https://calendar.google.com/updated.ics",
|
|
"ics_fetch_interval": 300,
|
|
"ics_enabled": True,
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["ics_url"] == "https://calendar.google.com/updated.ics"
|
|
assert data["ics_fetch_interval"] == 300
|
|
assert data["ics_enabled"] is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trigger_ics_sync(authenticated_client):
|
|
client = authenticated_client
|
|
room = await rooms_controller.add(
|
|
name="sync-api-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-2nd-participant",
|
|
is_shared=False,
|
|
ics_url="https://calendar.example.com/api.ics",
|
|
ics_enabled=True,
|
|
platform="daily",
|
|
)
|
|
|
|
cal = Calendar()
|
|
event = Event()
|
|
event.add("uid", "api-test-event")
|
|
event.add("summary", "API Test Meeting")
|
|
from reflector.settings import settings
|
|
|
|
event.add("location", f"{settings.UI_BASE_URL}/{room.name}")
|
|
now = datetime.now(timezone.utc)
|
|
event.add("dtstart", now + timedelta(hours=1))
|
|
event.add("dtend", now + timedelta(hours=2))
|
|
cal.add_component(event)
|
|
ics_content = cal.to_ical().decode("utf-8")
|
|
|
|
with patch(
|
|
"reflector.services.ics_sync.ICSFetchService.fetch_ics", new_callable=AsyncMock
|
|
) as mock_fetch:
|
|
mock_fetch.return_value = ics_content
|
|
|
|
response = await client.post(f"/rooms/{room.name}/ics/sync")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "success"
|
|
assert data["events_found"] == 1
|
|
assert data["events_created"] == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trigger_ics_sync_unauthorized(client):
|
|
room = await rooms_controller.add(
|
|
name="sync-unauth-room",
|
|
user_id="owner-123",
|
|
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,
|
|
ics_url="https://calendar.example.com/api.ics",
|
|
ics_enabled=True,
|
|
platform="daily",
|
|
)
|
|
|
|
response = await client.post(f"/rooms/{room.name}/ics/sync")
|
|
assert response.status_code == 403
|
|
assert "Only room owner can trigger ICS sync" in response.json()["detail"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trigger_ics_sync_not_configured(authenticated_client):
|
|
client = authenticated_client
|
|
room = await rooms_controller.add(
|
|
name="sync-not-configured",
|
|
user_id="test-user",
|
|
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,
|
|
ics_enabled=False,
|
|
platform="daily",
|
|
)
|
|
|
|
response = await client.post(f"/rooms/{room.name}/ics/sync")
|
|
assert response.status_code == 400
|
|
assert "ICS not configured" in response.json()["detail"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_ics_status(authenticated_client):
|
|
client = authenticated_client
|
|
room = await rooms_controller.add(
|
|
name="status-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-2nd-participant",
|
|
is_shared=False,
|
|
ics_url="https://calendar.example.com/status.ics",
|
|
ics_enabled=True,
|
|
ics_fetch_interval=300,
|
|
platform="daily",
|
|
)
|
|
|
|
now = datetime.now(timezone.utc)
|
|
await rooms_controller.update(
|
|
room,
|
|
{"ics_last_sync": now, "ics_last_etag": "test-etag"},
|
|
)
|
|
|
|
response = await client.get(f"/rooms/{room.name}/ics/status")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "enabled"
|
|
assert data["last_etag"] == "test-etag"
|
|
assert data["events_count"] == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_ics_status_unauthorized(client):
|
|
room = await rooms_controller.add(
|
|
name="status-unauth",
|
|
user_id="owner-456",
|
|
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,
|
|
ics_url="https://calendar.example.com/status.ics",
|
|
ics_enabled=True,
|
|
platform="daily",
|
|
)
|
|
|
|
response = await client.get(f"/rooms/{room.name}/ics/status")
|
|
assert response.status_code == 403
|
|
assert "Only room owner can view ICS status" in response.json()["detail"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_room_meetings(authenticated_client):
|
|
client = authenticated_client
|
|
room = await rooms_controller.add(
|
|
name="meetings-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-2nd-participant",
|
|
is_shared=False,
|
|
platform="daily",
|
|
)
|
|
|
|
now = datetime.now(timezone.utc)
|
|
event1 = CalendarEvent(
|
|
room_id=room.id,
|
|
ics_uid="meeting-1",
|
|
title="Past Meeting",
|
|
start_time=now - timedelta(hours=2),
|
|
end_time=now - timedelta(hours=1),
|
|
)
|
|
await calendar_events_controller.upsert(event1)
|
|
|
|
event2 = CalendarEvent(
|
|
room_id=room.id,
|
|
ics_uid="meeting-2",
|
|
title="Future Meeting",
|
|
description="Team sync",
|
|
start_time=now + timedelta(hours=1),
|
|
end_time=now + timedelta(hours=2),
|
|
attendees=[{"email": "test@example.com"}],
|
|
)
|
|
await calendar_events_controller.upsert(event2)
|
|
|
|
response = await client.get(f"/rooms/{room.name}/meetings")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data) == 2
|
|
assert data[0]["title"] == "Past Meeting"
|
|
assert data[1]["title"] == "Future Meeting"
|
|
assert data[1]["description"] == "Team sync"
|
|
assert data[1]["attendees"] == [{"email": "test@example.com"}]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_room_meetings_non_owner(client):
|
|
room = await rooms_controller.add(
|
|
name="meetings-privacy",
|
|
user_id="owner-789",
|
|
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="daily",
|
|
)
|
|
|
|
event = CalendarEvent(
|
|
room_id=room.id,
|
|
ics_uid="private-meeting",
|
|
title="Meeting Title",
|
|
description="Sensitive info",
|
|
start_time=datetime.now(timezone.utc) + timedelta(hours=1),
|
|
end_time=datetime.now(timezone.utc) + timedelta(hours=2),
|
|
attendees=[{"email": "private@example.com"}],
|
|
)
|
|
await calendar_events_controller.upsert(event)
|
|
|
|
response = await client.get(f"/rooms/{room.name}/meetings")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data) == 1
|
|
assert data[0]["title"] == "Meeting Title"
|
|
assert data[0]["description"] is None
|
|
assert data[0]["attendees"] is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_upcoming_meetings(authenticated_client):
|
|
client = authenticated_client
|
|
room = await rooms_controller.add(
|
|
name="upcoming-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-2nd-participant",
|
|
is_shared=False,
|
|
platform="daily",
|
|
)
|
|
|
|
now = datetime.now(timezone.utc)
|
|
|
|
past_event = CalendarEvent(
|
|
room_id=room.id,
|
|
ics_uid="past",
|
|
title="Past",
|
|
start_time=now - timedelta(hours=1),
|
|
end_time=now - timedelta(minutes=30),
|
|
)
|
|
await calendar_events_controller.upsert(past_event)
|
|
|
|
soon_event = CalendarEvent(
|
|
room_id=room.id,
|
|
ics_uid="soon",
|
|
title="Soon",
|
|
start_time=now + timedelta(minutes=15),
|
|
end_time=now + timedelta(minutes=45),
|
|
)
|
|
await calendar_events_controller.upsert(soon_event)
|
|
|
|
later_event = CalendarEvent(
|
|
room_id=room.id,
|
|
ics_uid="later",
|
|
title="Later",
|
|
start_time=now + timedelta(hours=2),
|
|
end_time=now + timedelta(hours=3),
|
|
)
|
|
await calendar_events_controller.upsert(later_event)
|
|
|
|
response = await client.get(f"/rooms/{room.name}/meetings/upcoming")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data) == 2
|
|
assert data[0]["title"] == "Soon"
|
|
assert data[1]["title"] == "Later"
|
|
|
|
response = await client.get(
|
|
f"/rooms/{room.name}/meetings/upcoming", params={"minutes_ahead": 180}
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data) == 2
|
|
assert data[0]["title"] == "Soon"
|
|
assert data[1]["title"] == "Later"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_room_not_found_endpoints(client):
|
|
response = await client.post("/rooms/nonexistent/ics/sync")
|
|
assert response.status_code == 404
|
|
|
|
response = await client.get("/rooms/nonexistent/ics/status")
|
|
assert response.status_code == 404
|
|
|
|
response = await client.get("/rooms/nonexistent/meetings")
|
|
assert response.status_code == 404
|
|
|
|
response = await client.get("/rooms/nonexistent/meetings/upcoming")
|
|
assert response.status_code == 404
|