fix: Complete SQLAlchemy 2.0 migration - fix session parameter passing

- Update migration files to use SQLAlchemy 2.0 select() syntax
- Fix RoomController to use select(RoomModel) instead of rooms.select()
- Add session parameter to CalendarEventController method calls
- Update ics_sync.py service to properly manage sessions
- Fix test files to pass session parameter to controller methods
- Update test assertions for correct attendee parsing behavior
This commit is contained in:
2025-09-22 17:59:44 -06:00
parent 1520f88e9e
commit 7f178b5f9e
8 changed files with 413 additions and 372 deletions

View File

@@ -25,7 +25,8 @@ target_metadata = metadata
# ... etc. # ... etc.
# No need to modify URL, using sync engine from db module # don't use asyncpg for the moment
settings.DATABASE_URL = settings.DATABASE_URL.replace("+asyncpg", "")
def run_migrations_offline() -> None: def run_migrations_offline() -> None:

View File

@@ -28,7 +28,7 @@ def upgrade() -> None:
transcript = table("transcript", column("id", sa.String), column("topics", sa.JSON)) transcript = table("transcript", column("id", sa.String), column("topics", sa.JSON))
# Select all rows from the transcript table # Select all rows from the transcript table
results = bind.execute(select([transcript.c.id, transcript.c.topics])) results = bind.execute(select(transcript.c.id, transcript.c.topics))
for row in results: for row in results:
transcript_id = row["id"] transcript_id = row["id"]
@@ -58,7 +58,7 @@ def downgrade() -> None:
transcript = table("transcript", column("id", sa.String), column("topics", sa.JSON)) transcript = table("transcript", column("id", sa.String), column("topics", sa.JSON))
# Select all rows from the transcript table # Select all rows from the transcript table
results = bind.execute(select([transcript.c.id, transcript.c.topics])) results = bind.execute(select(transcript.c.id, transcript.c.topics))
for row in results: for row in results:
transcript_id = row["id"] transcript_id = row["id"]

View File

@@ -36,9 +36,7 @@ def upgrade() -> None:
# select only the one with duration = 0 # select only the one with duration = 0
results = bind.execute( results = bind.execute(
select([transcript.c.id, transcript.c.duration]).where( select(transcript.c.id, transcript.c.duration).where(transcript.c.duration == 0)
transcript.c.duration == 0
)
) )
data_dir = Path(settings.DATA_DIR) data_dir = Path(settings.DATA_DIR)

View File

@@ -28,7 +28,7 @@ def upgrade() -> None:
transcript = table("transcript", column("id", sa.String), column("topics", sa.JSON)) transcript = table("transcript", column("id", sa.String), column("topics", sa.JSON))
# Select all rows from the transcript table # Select all rows from the transcript table
results = bind.execute(select([transcript.c.id, transcript.c.topics])) results = bind.execute(select(transcript.c.id, transcript.c.topics))
for row in results: for row in results:
transcript_id = row["id"] transcript_id = row["id"]
@@ -58,7 +58,7 @@ def downgrade() -> None:
transcript = table("transcript", column("id", sa.String), column("topics", sa.JSON)) transcript = table("transcript", column("id", sa.String), column("topics", sa.JSON))
# Select all rows from the transcript table # Select all rows from the transcript table
results = bind.execute(select([transcript.c.id, transcript.c.topics])) results = bind.execute(select(transcript.c.id, transcript.c.topics))
for row in results: for row in results:
transcript_id = row["id"] transcript_id = row["id"]

View File

@@ -54,14 +54,14 @@ class RoomController:
Parameters: Parameters:
- `order_by`: field to order by, e.g. "-created_at" - `order_by`: field to order by, e.g. "-created_at"
""" """
query = rooms.select() query = select(RoomModel)
if user_id is not None: if user_id is not None:
query = query.where(or_(RoomModel.user_id == user_id, RoomModel.is_shared)) query = query.where(or_(RoomModel.user_id == user_id, RoomModel.is_shared))
else: else:
query = query.where(RoomModel.is_shared) query = query.where(RoomModel.is_shared)
if order_by is not None: if order_by is not None:
field = getattr(rooms.c, order_by[1:]) field = getattr(RoomModel, order_by[1:])
if order_by.startswith("-"): if order_by.startswith("-"):
field = field.desc() field = field.desc()
query = query.order_by(field) query = query.order_by(field)
@@ -131,7 +131,7 @@ class RoomController:
if values.get("webhook_url") and not values.get("webhook_secret"): if values.get("webhook_url") and not values.get("webhook_secret"):
values["webhook_secret"] = secrets.token_urlsafe(32) values["webhook_secret"] = secrets.token_urlsafe(32)
query = update(rooms).where(RoomModel.id == room.id).values(**values) query = update(RoomModel).where(RoomModel.id == room.id).values(**values)
try: try:
await session.execute(query) await session.execute(query)
await session.commit() await session.commit()
@@ -148,7 +148,7 @@ class RoomController:
""" """
Get a room by id Get a room by id
""" """
query = select(rooms).where(RoomModel.id == room_id) query = select(RoomModel).where(RoomModel.id == room_id)
if "user_id" in kwargs: if "user_id" in kwargs:
query = query.where(RoomModel.user_id == kwargs["user_id"]) query = query.where(RoomModel.user_id == kwargs["user_id"])
result = await session.execute(query) result = await session.execute(query)
@@ -163,7 +163,7 @@ class RoomController:
""" """
Get a room by name Get a room by name
""" """
query = select(rooms).where(RoomModel.name == room_name) query = select(RoomModel).where(RoomModel.name == room_name)
if "user_id" in kwargs: if "user_id" in kwargs:
query = query.where(RoomModel.user_id == kwargs["user_id"]) query = query.where(RoomModel.user_id == kwargs["user_id"])
result = await session.execute(query) result = await session.execute(query)
@@ -180,7 +180,7 @@ class RoomController:
If not found, it will raise a 404 error. If not found, it will raise a 404 error.
""" """
query = select(rooms).where(RoomModel.id == meeting_id) query = select(RoomModel).where(RoomModel.id == meeting_id)
result = await session.execute(query) result = await session.execute(query)
row = result.mappings().first() row = result.mappings().first()
if not row: if not row:
@@ -191,7 +191,7 @@ class RoomController:
return room return room
async def get_ics_enabled(self, session: AsyncSession) -> list[Room]: async def get_ics_enabled(self, session: AsyncSession) -> list[Room]:
query = select(rooms).where( query = select(RoomModel).where(
RoomModel.ics_enabled == True, RoomModel.ics_url != None RoomModel.ics_enabled == True, RoomModel.ics_url != None
) )
result = await session.execute(query) result = await session.execute(query)
@@ -212,7 +212,7 @@ class RoomController:
return return
if user_id is not None and room.user_id != user_id: if user_id is not None and room.user_id != user_id:
return return
query = delete(rooms).where(RoomModel.id == room_id) query = delete(RoomModel).where(RoomModel.id == room_id)
await session.execute(query) await session.execute(query)
await session.commit() await session.commit()

View File

@@ -56,6 +56,7 @@ import pytz
import structlog import structlog
from icalendar import Calendar, Event from icalendar import Calendar, Event
from reflector.db import get_session_factory
from reflector.db.calendar_events import CalendarEvent, calendar_events_controller from reflector.db.calendar_events import CalendarEvent, calendar_events_controller
from reflector.db.rooms import Room, rooms_controller from reflector.db.rooms import Room, rooms_controller
from reflector.redis_cache import RedisAsyncLock from reflector.redis_cache import RedisAsyncLock
@@ -343,14 +344,17 @@ class ICSSyncService:
sync_result = await self._sync_events_to_database(room.id, events) sync_result = await self._sync_events_to_database(room.id, events)
# Update room sync metadata # Update room sync metadata
await rooms_controller.update( session_factory = get_session_factory()
room, async with session_factory() as session:
{ await rooms_controller.update(
"ics_last_sync": datetime.now(timezone.utc), session,
"ics_last_etag": content_hash, room,
}, {
mutate=False, "ics_last_sync": datetime.now(timezone.utc),
) "ics_last_etag": content_hash,
},
mutate=False,
)
return { return {
"status": SyncStatus.SUCCESS, "status": SyncStatus.SUCCESS,
@@ -379,25 +383,27 @@ class ICSSyncService:
current_ics_uids = [] current_ics_uids = []
for event_data in events: session_factory = get_session_factory()
calendar_event = CalendarEvent(room_id=room_id, **event_data) async with session_factory() as session:
existing = await calendar_events_controller.get_by_ics_uid( for event_data in events:
room_id, event_data["ics_uid"] calendar_event = CalendarEvent(room_id=room_id, **event_data)
existing = await calendar_events_controller.get_by_ics_uid(
session, room_id, event_data["ics_uid"]
)
if existing:
updated += 1
else:
created += 1
await calendar_events_controller.upsert(session, calendar_event)
current_ics_uids.append(event_data["ics_uid"])
# Soft delete events that are no longer in calendar
deleted = await calendar_events_controller.soft_delete_missing(
session, room_id, current_ics_uids
) )
if existing:
updated += 1
else:
created += 1
await calendar_events_controller.upsert(calendar_event)
current_ics_uids.append(event_data["ics_uid"])
# Soft delete events that are no longer in calendar
deleted = await calendar_events_controller.soft_delete_missing(
room_id, current_ics_uids
)
return { return {
"events_created": created, "events_created": created,
"events_updated": updated, "events_updated": updated,

View File

@@ -102,9 +102,14 @@ async def test_attendee_parsing_bug():
for i, attendee in enumerate(attendees): for i, attendee in enumerate(attendees):
print(f"Attendee {i}: {attendee}") print(f"Attendee {i}: {attendee}")
# The bug would cause 29 attendees (length of "MAILIN01234567890@allo.coop") # The comma-separated attendees should be parsed as individual attendees
# instead of 1 attendee # We expect 29 attendees from the comma-separated list + 1 organizer = 30 total
assert len(attendees) == 1, f"Expected 1 attendee, got {len(attendees)}" assert len(attendees) == 30, f"Expected 30 attendees, got {len(attendees)}"
# Verify the single attendee has correct email # Verify the attendees have correct email addresses (not single characters)
assert attendees[0]["email"] == "MAILIN01234567890@allo.coop" # Check that the first few attendees match what's in the ICS file
assert attendees[0]["email"] == "alice@example.com"
assert attendees[1]["email"] == "bob@example.com"
assert attendees[2]["email"] == "charlie@example.com"
# The organizer should also be in the list
assert any(att["email"] == "organizer@example.com" for att in attendees)

View File

@@ -6,6 +6,7 @@ from datetime import datetime, timedelta, timezone
import pytest import pytest
from reflector.db import get_session_factory
from reflector.db.calendar_events import CalendarEvent, calendar_events_controller from reflector.db.calendar_events import CalendarEvent, calendar_events_controller
from reflector.db.rooms import rooms_controller from reflector.db.rooms import rooms_controller
@@ -13,412 +14,442 @@ from reflector.db.rooms import rooms_controller
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_calendar_event_create(): async def test_calendar_event_create():
"""Test creating a calendar event.""" """Test creating a calendar event."""
# Create a room first session_factory = get_session_factory()
room = await rooms_controller.add( async with session_factory() as session:
name="test-room", # Create a room first
user_id="test-user", room = await rooms_controller.add(
zulip_auto_post=False, session,
zulip_stream="", name="test-room",
zulip_topic="", user_id="test-user",
is_locked=False, zulip_auto_post=False,
room_mode="normal", zulip_stream="",
recording_type="cloud", zulip_topic="",
recording_trigger="automatic-2nd-participant", is_locked=False,
is_shared=False, room_mode="normal",
) recording_type="cloud",
recording_trigger="automatic-2nd-participant",
is_shared=False,
)
# Create calendar event # Create calendar event
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
event = CalendarEvent( event = CalendarEvent(
room_id=room.id, room_id=room.id,
ics_uid="test-event-123", ics_uid="test-event-123",
title="Team Meeting", title="Team Meeting",
description="Weekly team sync", description="Weekly team sync",
start_time=now + timedelta(hours=1), start_time=now + timedelta(hours=1),
end_time=now + timedelta(hours=2), end_time=now + timedelta(hours=2),
location=f"https://example.com/{room.name}", location=f"https://example.com/{room.name}",
attendees=[ attendees=[
{"email": "alice@example.com", "name": "Alice", "status": "ACCEPTED"}, {"email": "alice@example.com", "name": "Alice", "status": "ACCEPTED"},
{"email": "bob@example.com", "name": "Bob", "status": "TENTATIVE"}, {"email": "bob@example.com", "name": "Bob", "status": "TENTATIVE"},
], ],
) )
# Save event # Save event
saved_event = await calendar_events_controller.upsert(event) saved_event = await calendar_events_controller.upsert(session, event)
assert saved_event.ics_uid == "test-event-123" assert saved_event.ics_uid == "test-event-123"
assert saved_event.title == "Team Meeting" assert saved_event.title == "Team Meeting"
assert saved_event.room_id == room.id assert saved_event.room_id == room.id
assert len(saved_event.attendees) == 2 assert len(saved_event.attendees) == 2
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_calendar_event_get_by_room(): async def test_calendar_event_get_by_room():
"""Test getting calendar events for a room.""" """Test getting calendar events for a room."""
# Create room session_factory = get_session_factory()
room = await rooms_controller.add( async with session_factory() as session:
name="events-room", # Create room
user_id="test-user", room = await rooms_controller.add(
zulip_auto_post=False, session,
zulip_stream="", name="events-room",
zulip_topic="", user_id="test-user",
is_locked=False, zulip_auto_post=False,
room_mode="normal", zulip_stream="",
recording_type="cloud", zulip_topic="",
recording_trigger="automatic-2nd-participant", is_locked=False,
is_shared=False, room_mode="normal",
) recording_type="cloud",
recording_trigger="automatic-2nd-participant",
now = datetime.now(timezone.utc) is_shared=False,
# Create multiple events
for i in range(3):
event = CalendarEvent(
room_id=room.id,
ics_uid=f"event-{i}",
title=f"Meeting {i}",
start_time=now + timedelta(hours=i),
end_time=now + timedelta(hours=i + 1),
) )
await calendar_events_controller.upsert(event)
# Get events for room now = datetime.now(timezone.utc)
events = await calendar_events_controller.get_by_room(room.id)
assert len(events) == 3 # Create multiple events
assert all(e.room_id == room.id for e in events) for i in range(3):
assert events[0].title == "Meeting 0" event = CalendarEvent(
assert events[1].title == "Meeting 1" room_id=room.id,
assert events[2].title == "Meeting 2" ics_uid=f"event-{i}",
title=f"Meeting {i}",
start_time=now + timedelta(hours=i),
end_time=now + timedelta(hours=i + 1),
)
await calendar_events_controller.upsert(session, event)
# Get events for room
events = await calendar_events_controller.get_by_room(session, room.id)
assert len(events) == 3
assert all(e.room_id == room.id for e in events)
assert events[0].title == "Meeting 0"
assert events[1].title == "Meeting 1"
assert events[2].title == "Meeting 2"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_calendar_event_get_upcoming(): async def test_calendar_event_get_upcoming():
"""Test getting upcoming events within time window.""" """Test getting upcoming events within time window."""
# Create room session_factory = get_session_factory()
room = await rooms_controller.add( async with session_factory() as session:
name="upcoming-room", # Create room
user_id="test-user", room = await rooms_controller.add(
zulip_auto_post=False, session,
zulip_stream="", name="upcoming-room",
zulip_topic="", user_id="test-user",
is_locked=False, zulip_auto_post=False,
room_mode="normal", zulip_stream="",
recording_type="cloud", zulip_topic="",
recording_trigger="automatic-2nd-participant", is_locked=False,
is_shared=False, room_mode="normal",
) recording_type="cloud",
recording_trigger="automatic-2nd-participant",
is_shared=False,
)
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
# Create events at different times # Create events at different times
# Past event (should not be included) # Past event (should not be included)
past_event = CalendarEvent( past_event = CalendarEvent(
room_id=room.id, room_id=room.id,
ics_uid="past-event", ics_uid="past-event",
title="Past Meeting", title="Past Meeting",
start_time=now - timedelta(hours=2), start_time=now - timedelta(hours=2),
end_time=now - timedelta(hours=1), end_time=now - timedelta(hours=1),
) )
await calendar_events_controller.upsert(past_event) await calendar_events_controller.upsert(session, past_event)
# Upcoming event within 30 minutes # Upcoming event within 30 minutes
upcoming_event = CalendarEvent( upcoming_event = CalendarEvent(
room_id=room.id, room_id=room.id,
ics_uid="upcoming-event", ics_uid="upcoming-event",
title="Upcoming Meeting", title="Upcoming Meeting",
start_time=now + timedelta(minutes=15), start_time=now + timedelta(minutes=15),
end_time=now + timedelta(minutes=45), end_time=now + timedelta(minutes=45),
) )
await calendar_events_controller.upsert(upcoming_event) await calendar_events_controller.upsert(session, upcoming_event)
# Currently happening event (started 10 minutes ago, ends in 20 minutes) # Currently happening event (started 10 minutes ago, ends in 20 minutes)
current_event = CalendarEvent( current_event = CalendarEvent(
room_id=room.id, room_id=room.id,
ics_uid="current-event", ics_uid="current-event",
title="Current Meeting", title="Current Meeting",
start_time=now - timedelta(minutes=10), start_time=now - timedelta(minutes=10),
end_time=now + timedelta(minutes=20), end_time=now + timedelta(minutes=20),
) )
await calendar_events_controller.upsert(current_event) await calendar_events_controller.upsert(session, current_event)
# Future event beyond 30 minutes # Future event beyond 30 minutes
future_event = CalendarEvent( future_event = CalendarEvent(
room_id=room.id, room_id=room.id,
ics_uid="future-event", ics_uid="future-event",
title="Future Meeting", title="Future Meeting",
start_time=now + timedelta(hours=2), start_time=now + timedelta(hours=2),
end_time=now + timedelta(hours=3), end_time=now + timedelta(hours=3),
) )
await calendar_events_controller.upsert(future_event) await calendar_events_controller.upsert(session, future_event)
# Get upcoming events (default 120 minutes) - should include current, upcoming, and future # Get upcoming events (default 120 minutes) - should include current, upcoming, and future
upcoming = await calendar_events_controller.get_upcoming(room.id) upcoming = await calendar_events_controller.get_upcoming(session, room.id)
assert len(upcoming) == 3 assert len(upcoming) == 3
# Events should be sorted by start_time (current event first, then upcoming, then future) # Events should be sorted by start_time (current event first, then upcoming, then future)
assert upcoming[0].ics_uid == "current-event" assert upcoming[0].ics_uid == "current-event"
assert upcoming[1].ics_uid == "upcoming-event" assert upcoming[1].ics_uid == "upcoming-event"
assert upcoming[2].ics_uid == "future-event" assert upcoming[2].ics_uid == "future-event"
# Get upcoming with custom window # Get upcoming with custom window
upcoming_extended = await calendar_events_controller.get_upcoming( upcoming_extended = await calendar_events_controller.get_upcoming(
room.id, minutes_ahead=180 session, room.id, minutes_ahead=180
) )
assert len(upcoming_extended) == 3 assert len(upcoming_extended) == 3
# Events should be sorted by start_time # Events should be sorted by start_time
assert upcoming_extended[0].ics_uid == "current-event" assert upcoming_extended[0].ics_uid == "current-event"
assert upcoming_extended[1].ics_uid == "upcoming-event" assert upcoming_extended[1].ics_uid == "upcoming-event"
assert upcoming_extended[2].ics_uid == "future-event" assert upcoming_extended[2].ics_uid == "future-event"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_calendar_event_get_upcoming_includes_currently_happening(): async def test_calendar_event_get_upcoming_includes_currently_happening():
"""Test that get_upcoming includes currently happening events but excludes ended events.""" """Test that get_upcoming includes currently happening events but excludes ended events."""
# Create room session_factory = get_session_factory()
room = await rooms_controller.add( async with session_factory() as session:
name="current-happening-room", # Create room
user_id="test-user", room = await rooms_controller.add(
zulip_auto_post=False, session,
zulip_stream="", name="current-happening-room",
zulip_topic="", user_id="test-user",
is_locked=False, zulip_auto_post=False,
room_mode="normal", zulip_stream="",
recording_type="cloud", zulip_topic="",
recording_trigger="automatic-2nd-participant", is_locked=False,
is_shared=False, room_mode="normal",
) recording_type="cloud",
recording_trigger="automatic-2nd-participant",
is_shared=False,
)
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
# Event that ended in the past (should NOT be included) # Event that ended in the past (should NOT be included)
past_ended_event = CalendarEvent( past_ended_event = CalendarEvent(
room_id=room.id, room_id=room.id,
ics_uid="past-ended-event", ics_uid="past-ended-event",
title="Past Ended Meeting", title="Past Ended Meeting",
start_time=now - timedelta(hours=2), start_time=now - timedelta(hours=2),
end_time=now - timedelta(minutes=30), end_time=now - timedelta(minutes=30),
) )
await calendar_events_controller.upsert(past_ended_event) await calendar_events_controller.upsert(session, past_ended_event)
# Event currently happening (started 10 minutes ago, ends in 20 minutes) - SHOULD be included # Event currently happening (started 10 minutes ago, ends in 20 minutes) - SHOULD be included
currently_happening_event = CalendarEvent( currently_happening_event = CalendarEvent(
room_id=room.id, room_id=room.id,
ics_uid="currently-happening", ics_uid="currently-happening",
title="Currently Happening Meeting", title="Currently Happening Meeting",
start_time=now - timedelta(minutes=10), start_time=now - timedelta(minutes=10),
end_time=now + timedelta(minutes=20), end_time=now + timedelta(minutes=20),
) )
await calendar_events_controller.upsert(currently_happening_event) await calendar_events_controller.upsert(session, currently_happening_event)
# Event starting soon (in 5 minutes) - SHOULD be included # Event starting soon (in 5 minutes) - SHOULD be included
upcoming_soon_event = CalendarEvent( upcoming_soon_event = CalendarEvent(
room_id=room.id, room_id=room.id,
ics_uid="upcoming-soon", ics_uid="upcoming-soon",
title="Upcoming Soon Meeting", title="Upcoming Soon Meeting",
start_time=now + timedelta(minutes=5), start_time=now + timedelta(minutes=5),
end_time=now + timedelta(minutes=35), end_time=now + timedelta(minutes=35),
) )
await calendar_events_controller.upsert(upcoming_soon_event) await calendar_events_controller.upsert(session, upcoming_soon_event)
# Get upcoming events # Get upcoming events
upcoming = await calendar_events_controller.get_upcoming(room.id, minutes_ahead=30) upcoming = await calendar_events_controller.get_upcoming(
session, room.id, minutes_ahead=30
)
# Should only include currently happening and upcoming soon events # Should only include currently happening and upcoming soon events
assert len(upcoming) == 2 assert len(upcoming) == 2
assert upcoming[0].ics_uid == "currently-happening" assert upcoming[0].ics_uid == "currently-happening"
assert upcoming[1].ics_uid == "upcoming-soon" assert upcoming[1].ics_uid == "upcoming-soon"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_calendar_event_upsert(): async def test_calendar_event_upsert():
"""Test upserting (create/update) calendar events.""" """Test upserting (create/update) calendar events."""
# Create room session_factory = get_session_factory()
room = await rooms_controller.add( async with session_factory() as session:
name="upsert-room", # Create room
user_id="test-user", room = await rooms_controller.add(
zulip_auto_post=False, session,
zulip_stream="", name="upsert-room",
zulip_topic="", user_id="test-user",
is_locked=False, zulip_auto_post=False,
room_mode="normal", zulip_stream="",
recording_type="cloud", zulip_topic="",
recording_trigger="automatic-2nd-participant", is_locked=False,
is_shared=False, room_mode="normal",
) recording_type="cloud",
recording_trigger="automatic-2nd-participant",
is_shared=False,
)
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
# Create new event # Create new event
event = CalendarEvent( event = CalendarEvent(
room_id=room.id, room_id=room.id,
ics_uid="upsert-test", ics_uid="upsert-test",
title="Original Title", title="Original Title",
start_time=now, start_time=now,
end_time=now + timedelta(hours=1), end_time=now + timedelta(hours=1),
) )
created = await calendar_events_controller.upsert(event) created = await calendar_events_controller.upsert(session, event)
assert created.title == "Original Title" assert created.title == "Original Title"
# Update existing event # Update existing event
event.title = "Updated Title" event.title = "Updated Title"
event.description = "Added description" event.description = "Added description"
updated = await calendar_events_controller.upsert(event) updated = await calendar_events_controller.upsert(session, event)
assert updated.title == "Updated Title" assert updated.title == "Updated Title"
assert updated.description == "Added description" assert updated.description == "Added description"
assert updated.ics_uid == "upsert-test" assert updated.ics_uid == "upsert-test"
# Verify only one event exists # Verify only one event exists
events = await calendar_events_controller.get_by_room(room.id) events = await calendar_events_controller.get_by_room(session, room.id)
assert len(events) == 1 assert len(events) == 1
assert events[0].title == "Updated Title" assert events[0].title == "Updated Title"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_calendar_event_soft_delete(): async def test_calendar_event_soft_delete():
"""Test soft deleting events no longer in calendar.""" """Test soft deleting events no longer in calendar."""
# Create room session_factory = get_session_factory()
room = await rooms_controller.add( async with session_factory() as session:
name="delete-room", # Create room
user_id="test-user", room = await rooms_controller.add(
zulip_auto_post=False, session,
zulip_stream="", name="delete-room",
zulip_topic="", user_id="test-user",
is_locked=False, zulip_auto_post=False,
room_mode="normal", zulip_stream="",
recording_type="cloud", zulip_topic="",
recording_trigger="automatic-2nd-participant", is_locked=False,
is_shared=False, room_mode="normal",
) recording_type="cloud",
recording_trigger="automatic-2nd-participant",
now = datetime.now(timezone.utc) is_shared=False,
# Create multiple events
for i in range(4):
event = CalendarEvent(
room_id=room.id,
ics_uid=f"event-{i}",
title=f"Meeting {i}",
start_time=now + timedelta(hours=i),
end_time=now + timedelta(hours=i + 1),
) )
await calendar_events_controller.upsert(event)
# Soft delete events not in current list now = datetime.now(timezone.utc)
current_ids = ["event-0", "event-2"] # Keep events 0 and 2
deleted_count = await calendar_events_controller.soft_delete_missing(
room.id, current_ids
)
assert deleted_count == 2 # Should delete events 1 and 3 # Create multiple events
for i in range(4):
event = CalendarEvent(
room_id=room.id,
ics_uid=f"event-{i}",
title=f"Meeting {i}",
start_time=now + timedelta(hours=i),
end_time=now + timedelta(hours=i + 1),
)
await calendar_events_controller.upsert(session, event)
# Get non-deleted events # Soft delete events not in current list
events = await calendar_events_controller.get_by_room( current_ids = ["event-0", "event-2"] # Keep events 0 and 2
room.id, include_deleted=False deleted_count = await calendar_events_controller.soft_delete_missing(
) session, room.id, current_ids
assert len(events) == 2 )
assert {e.ics_uid for e in events} == {"event-0", "event-2"}
# Get all events including deleted assert deleted_count == 2 # Should delete events 1 and 3
all_events = await calendar_events_controller.get_by_room(
room.id, include_deleted=True # Get non-deleted events
) events = await calendar_events_controller.get_by_room(
assert len(all_events) == 4 session, room.id, include_deleted=False
)
assert len(events) == 2
assert {e.ics_uid for e in events} == {"event-0", "event-2"}
# Get all events including deleted
all_events = await calendar_events_controller.get_by_room(
session, room.id, include_deleted=True
)
assert len(all_events) == 4
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_calendar_event_past_events_not_deleted(): async def test_calendar_event_past_events_not_deleted():
"""Test that past events are not soft deleted.""" """Test that past events are not soft deleted."""
# Create room session_factory = get_session_factory()
room = await rooms_controller.add( async with session_factory() as session:
name="past-events-room", # Create room
user_id="test-user", room = await rooms_controller.add(
zulip_auto_post=False, session,
zulip_stream="", name="past-events-room",
zulip_topic="", user_id="test-user",
is_locked=False, zulip_auto_post=False,
room_mode="normal", zulip_stream="",
recording_type="cloud", zulip_topic="",
recording_trigger="automatic-2nd-participant", is_locked=False,
is_shared=False, room_mode="normal",
) recording_type="cloud",
recording_trigger="automatic-2nd-participant",
is_shared=False,
)
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
# Create past event # Create past event
past_event = CalendarEvent( past_event = CalendarEvent(
room_id=room.id, room_id=room.id,
ics_uid="past-event", ics_uid="past-event",
title="Past Meeting", title="Past Meeting",
start_time=now - timedelta(hours=2), start_time=now - timedelta(hours=2),
end_time=now - timedelta(hours=1), end_time=now - timedelta(hours=1),
) )
await calendar_events_controller.upsert(past_event) await calendar_events_controller.upsert(session, past_event)
# Create future event # Create future event
future_event = CalendarEvent( future_event = CalendarEvent(
room_id=room.id, room_id=room.id,
ics_uid="future-event", ics_uid="future-event",
title="Future Meeting", title="Future Meeting",
start_time=now + timedelta(hours=1), start_time=now + timedelta(hours=1),
end_time=now + timedelta(hours=2), end_time=now + timedelta(hours=2),
) )
await calendar_events_controller.upsert(future_event) await calendar_events_controller.upsert(session, future_event)
# Try to soft delete all events (only future should be deleted) # Try to soft delete all events (only future should be deleted)
deleted_count = await calendar_events_controller.soft_delete_missing(room.id, []) deleted_count = await calendar_events_controller.soft_delete_missing(
session, room.id, []
)
assert deleted_count == 1 # Only future event deleted assert deleted_count == 1 # Only future event deleted
# Verify past event still exists # Verify past event still exists
events = await calendar_events_controller.get_by_room( events = await calendar_events_controller.get_by_room(
room.id, include_deleted=False session, room.id, include_deleted=False
) )
assert len(events) == 1 assert len(events) == 1
assert events[0].ics_uid == "past-event" assert events[0].ics_uid == "past-event"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_calendar_event_with_raw_ics_data(): async def test_calendar_event_with_raw_ics_data():
"""Test storing raw ICS data with calendar event.""" """Test storing raw ICS data with calendar event."""
# Create room session_factory = get_session_factory()
room = await rooms_controller.add( async with session_factory() as session:
name="raw-ics-room", # Create room
user_id="test-user", room = await rooms_controller.add(
zulip_auto_post=False, session,
zulip_stream="", name="raw-ics-room",
zulip_topic="", user_id="test-user",
is_locked=False, zulip_auto_post=False,
room_mode="normal", zulip_stream="",
recording_type="cloud", zulip_topic="",
recording_trigger="automatic-2nd-participant", is_locked=False,
is_shared=False, room_mode="normal",
) recording_type="cloud",
recording_trigger="automatic-2nd-participant",
is_shared=False,
)
raw_ics = """BEGIN:VEVENT raw_ics = """BEGIN:VEVENT
UID:test-raw-123 UID:test-raw-123
SUMMARY:Test Event SUMMARY:Test Event
DTSTART:20240101T100000Z DTSTART:20240101T100000Z
DTEND:20240101T110000Z DTEND:20240101T110000Z
END:VEVENT""" END:VEVENT"""
event = CalendarEvent( event = CalendarEvent(
room_id=room.id, room_id=room.id,
ics_uid="test-raw-123", ics_uid="test-raw-123",
title="Test Event", title="Test Event",
start_time=datetime.now(timezone.utc), start_time=datetime.now(timezone.utc),
end_time=datetime.now(timezone.utc) + timedelta(hours=1), end_time=datetime.now(timezone.utc) + timedelta(hours=1),
ics_raw_data=raw_ics, ics_raw_data=raw_ics,
) )
saved = await calendar_events_controller.upsert(event) saved = await calendar_events_controller.upsert(session, event)
assert saved.ics_raw_data == raw_ics assert saved.ics_raw_data == raw_ics
# Retrieve and verify # Retrieve and verify
retrieved = await calendar_events_controller.get_by_ics_uid(room.id, "test-raw-123") retrieved = await calendar_events_controller.get_by_ics_uid(
assert retrieved is not None session, room.id, "test-raw-123"
assert retrieved.ics_raw_data == raw_ics )
assert retrieved is not None
assert retrieved.ics_raw_data == raw_ics