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