mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-21 04:39:06 +00:00
fix: Complete major SQLAlchemy 2.0 test migration
Fixed multiple test files for SQLAlchemy 2.0 compatibility: - test_search.py: Fixed query syntax and session parameters - test_room_ics.py: Added session parameter to all controller calls - test_ics_background_tasks.py: Fixed imports and query patterns - test_cleanup.py: Fixed model fields and session handling - test_calendar_event.py: Improved session fixture usage - calendar_events.py: Added commits for test compatibility - rooms.py: Fixed result parsing for scalars().all() - worker/cleanup.py: Added session parameter to remove_by_id Results: 116 tests now passing (up from 107), 29 failures (down from 38) Remaining issues are primarily async event loop isolation problems
This commit is contained in:
@@ -126,11 +126,21 @@ async def setup_database(postgres_service):
|
||||
@pytest.fixture
|
||||
async def session(setup_database):
|
||||
"""Provide a transactional database session for tests"""
|
||||
import sqlalchemy.exc
|
||||
|
||||
from reflector.db import get_session_factory
|
||||
|
||||
async with get_session_factory()() as session:
|
||||
yield session
|
||||
await session.rollback()
|
||||
# Start a transaction that we'll rollback at the end
|
||||
transaction = await session.begin()
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
try:
|
||||
await transaction.rollback()
|
||||
except sqlalchemy.exc.ResourceClosedError:
|
||||
# Transaction was already closed (e.g., by a commit), ignore
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -6,450 +6,433 @@ from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from reflector.db import get_session_factory
|
||||
from reflector.db.calendar_events import CalendarEvent, calendar_events_controller
|
||||
from reflector.db.rooms import rooms_controller
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calendar_event_create():
|
||||
async def test_calendar_event_create(session):
|
||||
"""Test creating a calendar event."""
|
||||
session_factory = get_session_factory()
|
||||
async with session_factory() as session:
|
||||
# Create a room first
|
||||
room = await rooms_controller.add(
|
||||
session,
|
||||
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-2nd-participant",
|
||||
is_shared=False,
|
||||
)
|
||||
# Create a room first
|
||||
room = await rooms_controller.add(
|
||||
session,
|
||||
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-2nd-participant",
|
||||
is_shared=False,
|
||||
)
|
||||
|
||||
# Create calendar event
|
||||
now = datetime.now(timezone.utc)
|
||||
event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="test-event-123",
|
||||
title="Team Meeting",
|
||||
description="Weekly team sync",
|
||||
start_time=now + timedelta(hours=1),
|
||||
end_time=now + timedelta(hours=2),
|
||||
location=f"https://example.com/{room.name}",
|
||||
attendees=[
|
||||
{"email": "alice@example.com", "name": "Alice", "status": "ACCEPTED"},
|
||||
{"email": "bob@example.com", "name": "Bob", "status": "TENTATIVE"},
|
||||
],
|
||||
)
|
||||
# Create calendar event
|
||||
now = datetime.now(timezone.utc)
|
||||
event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="test-event-123",
|
||||
title="Team Meeting",
|
||||
description="Weekly team sync",
|
||||
start_time=now + timedelta(hours=1),
|
||||
end_time=now + timedelta(hours=2),
|
||||
location=f"https://example.com/{room.name}",
|
||||
attendees=[
|
||||
{"email": "alice@example.com", "name": "Alice", "status": "ACCEPTED"},
|
||||
{"email": "bob@example.com", "name": "Bob", "status": "TENTATIVE"},
|
||||
],
|
||||
)
|
||||
|
||||
# Save event
|
||||
saved_event = await calendar_events_controller.upsert(session, event)
|
||||
# Save event
|
||||
saved_event = await calendar_events_controller.upsert(session, event)
|
||||
|
||||
assert saved_event.ics_uid == "test-event-123"
|
||||
assert saved_event.title == "Team Meeting"
|
||||
assert saved_event.room_id == room.id
|
||||
assert len(saved_event.attendees) == 2
|
||||
assert saved_event.ics_uid == "test-event-123"
|
||||
assert saved_event.title == "Team Meeting"
|
||||
assert saved_event.room_id == room.id
|
||||
assert len(saved_event.attendees) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calendar_event_get_by_room():
|
||||
async def test_calendar_event_get_by_room(session):
|
||||
"""Test getting calendar events for a room."""
|
||||
session_factory = get_session_factory()
|
||||
async with session_factory() as session:
|
||||
# Create room
|
||||
room = await rooms_controller.add(
|
||||
session,
|
||||
name="events-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,
|
||||
)
|
||||
# Create room
|
||||
room = await rooms_controller.add(
|
||||
session,
|
||||
name="events-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,
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# 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(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
|
||||
async def test_calendar_event_get_upcoming():
|
||||
"""Test getting upcoming events within time window."""
|
||||
session_factory = get_session_factory()
|
||||
async with session_factory() as session:
|
||||
# Create room
|
||||
room = await rooms_controller.add(
|
||||
session,
|
||||
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,
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# Create events at different times
|
||||
# Past event (should not be included)
|
||||
past_event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="past-event",
|
||||
title="Past Meeting",
|
||||
start_time=now - timedelta(hours=2),
|
||||
end_time=now - timedelta(hours=1),
|
||||
)
|
||||
await calendar_events_controller.upsert(session, past_event)
|
||||
|
||||
# Upcoming event within 30 minutes
|
||||
upcoming_event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="upcoming-event",
|
||||
title="Upcoming Meeting",
|
||||
start_time=now + timedelta(minutes=15),
|
||||
end_time=now + timedelta(minutes=45),
|
||||
)
|
||||
await calendar_events_controller.upsert(session, upcoming_event)
|
||||
|
||||
# Currently happening event (started 10 minutes ago, ends in 20 minutes)
|
||||
current_event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="current-event",
|
||||
title="Current Meeting",
|
||||
start_time=now - timedelta(minutes=10),
|
||||
end_time=now + timedelta(minutes=20),
|
||||
)
|
||||
await calendar_events_controller.upsert(session, current_event)
|
||||
|
||||
# Future event beyond 30 minutes
|
||||
future_event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="future-event",
|
||||
title="Future Meeting",
|
||||
start_time=now + timedelta(hours=2),
|
||||
end_time=now + timedelta(hours=3),
|
||||
)
|
||||
await calendar_events_controller.upsert(session, future_event)
|
||||
|
||||
# Get upcoming events (default 120 minutes) - should include current, upcoming, and future
|
||||
upcoming = await calendar_events_controller.get_upcoming(session, room.id)
|
||||
|
||||
assert len(upcoming) == 3
|
||||
# Events should be sorted by start_time (current event first, then upcoming, then future)
|
||||
assert upcoming[0].ics_uid == "current-event"
|
||||
assert upcoming[1].ics_uid == "upcoming-event"
|
||||
assert upcoming[2].ics_uid == "future-event"
|
||||
|
||||
# Get upcoming with custom window
|
||||
upcoming_extended = await calendar_events_controller.get_upcoming(
|
||||
session, room.id, minutes_ahead=180
|
||||
)
|
||||
|
||||
assert len(upcoming_extended) == 3
|
||||
# Events should be sorted by start_time
|
||||
assert upcoming_extended[0].ics_uid == "current-event"
|
||||
assert upcoming_extended[1].ics_uid == "upcoming-event"
|
||||
assert upcoming_extended[2].ics_uid == "future-event"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calendar_event_get_upcoming_includes_currently_happening():
|
||||
"""Test that get_upcoming includes currently happening events but excludes ended events."""
|
||||
session_factory = get_session_factory()
|
||||
async with session_factory() as session:
|
||||
# Create room
|
||||
room = await rooms_controller.add(
|
||||
session,
|
||||
name="current-happening-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,
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# Event that ended in the past (should NOT be included)
|
||||
past_ended_event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="past-ended-event",
|
||||
title="Past Ended Meeting",
|
||||
start_time=now - timedelta(hours=2),
|
||||
end_time=now - timedelta(minutes=30),
|
||||
)
|
||||
await calendar_events_controller.upsert(session, past_ended_event)
|
||||
|
||||
# Event currently happening (started 10 minutes ago, ends in 20 minutes) - SHOULD be included
|
||||
currently_happening_event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="currently-happening",
|
||||
title="Currently Happening Meeting",
|
||||
start_time=now - timedelta(minutes=10),
|
||||
end_time=now + timedelta(minutes=20),
|
||||
)
|
||||
await calendar_events_controller.upsert(session, currently_happening_event)
|
||||
|
||||
# Event starting soon (in 5 minutes) - SHOULD be included
|
||||
upcoming_soon_event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="upcoming-soon",
|
||||
title="Upcoming Soon Meeting",
|
||||
start_time=now + timedelta(minutes=5),
|
||||
end_time=now + timedelta(minutes=35),
|
||||
)
|
||||
await calendar_events_controller.upsert(session, upcoming_soon_event)
|
||||
|
||||
# Get upcoming events
|
||||
upcoming = await calendar_events_controller.get_upcoming(
|
||||
session, room.id, minutes_ahead=30
|
||||
)
|
||||
|
||||
# Should only include currently happening and upcoming soon events
|
||||
assert len(upcoming) == 2
|
||||
assert upcoming[0].ics_uid == "currently-happening"
|
||||
assert upcoming[1].ics_uid == "upcoming-soon"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calendar_event_upsert():
|
||||
"""Test upserting (create/update) calendar events."""
|
||||
session_factory = get_session_factory()
|
||||
async with session_factory() as session:
|
||||
# Create room
|
||||
room = await rooms_controller.add(
|
||||
session,
|
||||
name="upsert-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,
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# Create new event
|
||||
# Create multiple events
|
||||
for i in range(3):
|
||||
event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="upsert-test",
|
||||
title="Original Title",
|
||||
start_time=now,
|
||||
end_time=now + timedelta(hours=1),
|
||||
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)
|
||||
|
||||
created = await calendar_events_controller.upsert(session, event)
|
||||
assert created.title == "Original Title"
|
||||
# Get events for room
|
||||
events = await calendar_events_controller.get_by_room(session, room.id)
|
||||
|
||||
# Update existing event
|
||||
event.title = "Updated Title"
|
||||
event.description = "Added description"
|
||||
|
||||
updated = await calendar_events_controller.upsert(session, event)
|
||||
assert updated.title == "Updated Title"
|
||||
assert updated.description == "Added description"
|
||||
assert updated.ics_uid == "upsert-test"
|
||||
|
||||
# Verify only one event exists
|
||||
events = await calendar_events_controller.get_by_room(session, room.id)
|
||||
assert len(events) == 1
|
||||
assert events[0].title == "Updated Title"
|
||||
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
|
||||
async def test_calendar_event_soft_delete():
|
||||
async def test_calendar_event_get_upcoming(session):
|
||||
"""Test getting upcoming events within time window."""
|
||||
# Create room
|
||||
room = await rooms_controller.add(
|
||||
session,
|
||||
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,
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# Create events at different times
|
||||
# Past event (should not be included)
|
||||
past_event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="past-event",
|
||||
title="Past Meeting",
|
||||
start_time=now - timedelta(hours=2),
|
||||
end_time=now - timedelta(hours=1),
|
||||
)
|
||||
await calendar_events_controller.upsert(session, past_event)
|
||||
|
||||
# Upcoming event within 30 minutes
|
||||
upcoming_event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="upcoming-event",
|
||||
title="Upcoming Meeting",
|
||||
start_time=now + timedelta(minutes=15),
|
||||
end_time=now + timedelta(minutes=45),
|
||||
)
|
||||
await calendar_events_controller.upsert(session, upcoming_event)
|
||||
|
||||
# Currently happening event (started 10 minutes ago, ends in 20 minutes)
|
||||
current_event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="current-event",
|
||||
title="Current Meeting",
|
||||
start_time=now - timedelta(minutes=10),
|
||||
end_time=now + timedelta(minutes=20),
|
||||
)
|
||||
await calendar_events_controller.upsert(session, current_event)
|
||||
|
||||
# Future event beyond 30 minutes
|
||||
future_event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="future-event",
|
||||
title="Future Meeting",
|
||||
start_time=now + timedelta(hours=2),
|
||||
end_time=now + timedelta(hours=3),
|
||||
)
|
||||
await calendar_events_controller.upsert(session, future_event)
|
||||
|
||||
# Get upcoming events (default 120 minutes) - should include current, upcoming, and future
|
||||
upcoming = await calendar_events_controller.get_upcoming(session, room.id)
|
||||
|
||||
assert len(upcoming) == 3
|
||||
# Events should be sorted by start_time (current event first, then upcoming, then future)
|
||||
assert upcoming[0].ics_uid == "current-event"
|
||||
assert upcoming[1].ics_uid == "upcoming-event"
|
||||
assert upcoming[2].ics_uid == "future-event"
|
||||
|
||||
# Get upcoming with custom window
|
||||
upcoming_extended = await calendar_events_controller.get_upcoming(
|
||||
session, room.id, minutes_ahead=180
|
||||
)
|
||||
|
||||
assert len(upcoming_extended) == 3
|
||||
# Events should be sorted by start_time
|
||||
assert upcoming_extended[0].ics_uid == "current-event"
|
||||
assert upcoming_extended[1].ics_uid == "upcoming-event"
|
||||
assert upcoming_extended[2].ics_uid == "future-event"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calendar_event_get_upcoming_includes_currently_happening(session):
|
||||
"""Test that get_upcoming includes currently happening events but excludes ended events."""
|
||||
# Create room
|
||||
room = await rooms_controller.add(
|
||||
session,
|
||||
name="current-happening-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,
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# Event that ended in the past (should NOT be included)
|
||||
past_ended_event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="past-ended-event",
|
||||
title="Past Ended Meeting",
|
||||
start_time=now - timedelta(hours=2),
|
||||
end_time=now - timedelta(minutes=30),
|
||||
)
|
||||
await calendar_events_controller.upsert(session, past_ended_event)
|
||||
|
||||
# Event currently happening (started 10 minutes ago, ends in 20 minutes) - SHOULD be included
|
||||
currently_happening_event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="currently-happening",
|
||||
title="Currently Happening Meeting",
|
||||
start_time=now - timedelta(minutes=10),
|
||||
end_time=now + timedelta(minutes=20),
|
||||
)
|
||||
await calendar_events_controller.upsert(session, currently_happening_event)
|
||||
|
||||
# Event starting soon (in 5 minutes) - SHOULD be included
|
||||
upcoming_soon_event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="upcoming-soon",
|
||||
title="Upcoming Soon Meeting",
|
||||
start_time=now + timedelta(minutes=5),
|
||||
end_time=now + timedelta(minutes=35),
|
||||
)
|
||||
await calendar_events_controller.upsert(session, upcoming_soon_event)
|
||||
|
||||
# Get upcoming events
|
||||
upcoming = await calendar_events_controller.get_upcoming(
|
||||
session, room.id, minutes_ahead=30
|
||||
)
|
||||
|
||||
# Should only include currently happening and upcoming soon events
|
||||
assert len(upcoming) == 2
|
||||
assert upcoming[0].ics_uid == "currently-happening"
|
||||
assert upcoming[1].ics_uid == "upcoming-soon"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calendar_event_upsert(session):
|
||||
"""Test upserting (create/update) calendar events."""
|
||||
# Create room
|
||||
room = await rooms_controller.add(
|
||||
session,
|
||||
name="upsert-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,
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# Create new event
|
||||
event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="upsert-test",
|
||||
title="Original Title",
|
||||
start_time=now,
|
||||
end_time=now + timedelta(hours=1),
|
||||
)
|
||||
|
||||
created = await calendar_events_controller.upsert(session, event)
|
||||
assert created.title == "Original Title"
|
||||
|
||||
# Update existing event
|
||||
event.title = "Updated Title"
|
||||
event.description = "Added description"
|
||||
|
||||
updated = await calendar_events_controller.upsert(session, event)
|
||||
assert updated.title == "Updated Title"
|
||||
assert updated.description == "Added description"
|
||||
assert updated.ics_uid == "upsert-test"
|
||||
|
||||
# Verify only one event exists
|
||||
events = await calendar_events_controller.get_by_room(session, room.id)
|
||||
assert len(events) == 1
|
||||
assert events[0].title == "Updated Title"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calendar_event_soft_delete(session):
|
||||
"""Test soft deleting events no longer in calendar."""
|
||||
session_factory = get_session_factory()
|
||||
async with session_factory() as session:
|
||||
# Create room
|
||||
room = await rooms_controller.add(
|
||||
session,
|
||||
name="delete-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,
|
||||
# Create room
|
||||
room = await rooms_controller.add(
|
||||
session,
|
||||
name="delete-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,
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# 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)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
# Soft delete events not in current list
|
||||
current_ids = ["event-0", "event-2"] # Keep events 0 and 2
|
||||
deleted_count = await calendar_events_controller.soft_delete_missing(
|
||||
session, room.id, current_ids
|
||||
)
|
||||
|
||||
# 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)
|
||||
assert deleted_count == 2 # Should delete events 1 and 3
|
||||
|
||||
# Soft delete events not in current list
|
||||
current_ids = ["event-0", "event-2"] # Keep events 0 and 2
|
||||
deleted_count = await calendar_events_controller.soft_delete_missing(
|
||||
session, room.id, current_ids
|
||||
)
|
||||
# Get non-deleted events
|
||||
events = await calendar_events_controller.get_by_room(
|
||||
session, room.id, include_deleted=False
|
||||
)
|
||||
assert len(events) == 2
|
||||
assert {e.ics_uid for e in events} == {"event-0", "event-2"}
|
||||
|
||||
assert deleted_count == 2 # Should delete events 1 and 3
|
||||
|
||||
# Get non-deleted events
|
||||
events = await calendar_events_controller.get_by_room(
|
||||
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
|
||||
# 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
|
||||
async def test_calendar_event_past_events_not_deleted():
|
||||
async def test_calendar_event_past_events_not_deleted(session):
|
||||
"""Test that past events are not soft deleted."""
|
||||
session_factory = get_session_factory()
|
||||
async with session_factory() as session:
|
||||
# Create room
|
||||
room = await rooms_controller.add(
|
||||
session,
|
||||
name="past-events-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,
|
||||
)
|
||||
# Create room
|
||||
room = await rooms_controller.add(
|
||||
session,
|
||||
name="past-events-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,
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# Create past event
|
||||
past_event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="past-event",
|
||||
title="Past Meeting",
|
||||
start_time=now - timedelta(hours=2),
|
||||
end_time=now - timedelta(hours=1),
|
||||
)
|
||||
await calendar_events_controller.upsert(session, past_event)
|
||||
# Create past event
|
||||
past_event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="past-event",
|
||||
title="Past Meeting",
|
||||
start_time=now - timedelta(hours=2),
|
||||
end_time=now - timedelta(hours=1),
|
||||
)
|
||||
await calendar_events_controller.upsert(session, past_event)
|
||||
|
||||
# Create future event
|
||||
future_event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="future-event",
|
||||
title="Future Meeting",
|
||||
start_time=now + timedelta(hours=1),
|
||||
end_time=now + timedelta(hours=2),
|
||||
)
|
||||
await calendar_events_controller.upsert(session, future_event)
|
||||
# Create future event
|
||||
future_event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="future-event",
|
||||
title="Future Meeting",
|
||||
start_time=now + timedelta(hours=1),
|
||||
end_time=now + timedelta(hours=2),
|
||||
)
|
||||
await calendar_events_controller.upsert(session, future_event)
|
||||
|
||||
# Try to soft delete all events (only future should be deleted)
|
||||
deleted_count = await calendar_events_controller.soft_delete_missing(
|
||||
session, room.id, []
|
||||
)
|
||||
# Try to soft delete all events (only future should be deleted)
|
||||
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
|
||||
events = await calendar_events_controller.get_by_room(
|
||||
session, room.id, include_deleted=False
|
||||
)
|
||||
assert len(events) == 1
|
||||
assert events[0].ics_uid == "past-event"
|
||||
# Verify past event still exists
|
||||
events = await calendar_events_controller.get_by_room(
|
||||
session, room.id, include_deleted=False
|
||||
)
|
||||
assert len(events) == 1
|
||||
assert events[0].ics_uid == "past-event"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calendar_event_with_raw_ics_data():
|
||||
async def test_calendar_event_with_raw_ics_data(session):
|
||||
"""Test storing raw ICS data with calendar event."""
|
||||
session_factory = get_session_factory()
|
||||
async with session_factory() as session:
|
||||
# Create room
|
||||
room = await rooms_controller.add(
|
||||
session,
|
||||
name="raw-ics-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,
|
||||
)
|
||||
# Create room
|
||||
room = await rooms_controller.add(
|
||||
session,
|
||||
name="raw-ics-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,
|
||||
)
|
||||
|
||||
raw_ics = """BEGIN:VEVENT
|
||||
raw_ics = """BEGIN:VEVENT
|
||||
UID:test-raw-123
|
||||
SUMMARY:Test Event
|
||||
DTSTART:20240101T100000Z
|
||||
DTEND:20240101T110000Z
|
||||
END:VEVENT"""
|
||||
|
||||
event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="test-raw-123",
|
||||
title="Test Event",
|
||||
start_time=datetime.now(timezone.utc),
|
||||
end_time=datetime.now(timezone.utc) + timedelta(hours=1),
|
||||
ics_raw_data=raw_ics,
|
||||
)
|
||||
event = CalendarEvent(
|
||||
room_id=room.id,
|
||||
ics_uid="test-raw-123",
|
||||
title="Test Event",
|
||||
start_time=datetime.now(timezone.utc),
|
||||
end_time=datetime.now(timezone.utc) + timedelta(hours=1),
|
||||
ics_raw_data=raw_ics,
|
||||
)
|
||||
|
||||
saved = await calendar_events_controller.upsert(session, 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
|
||||
retrieved = await calendar_events_controller.get_by_ics_uid(
|
||||
session, room.id, "test-raw-123"
|
||||
)
|
||||
assert retrieved is not None
|
||||
assert retrieved.ics_raw_data == raw_ics
|
||||
# Retrieve and verify
|
||||
retrieved = await calendar_events_controller.get_by_ics_uid(
|
||||
session, room.id, "test-raw-123"
|
||||
)
|
||||
assert retrieved is not None
|
||||
assert retrieved.ics_raw_data == raw_ics
|
||||
|
||||
@@ -115,8 +115,7 @@ async def test_cleanup_deletes_associated_meeting_and_recording(session):
|
||||
await session.execute(
|
||||
insert(MeetingModel).values(
|
||||
id=meeting_id,
|
||||
transcript_id=old_transcript.id,
|
||||
room_id="test-room",
|
||||
room_id=None,
|
||||
room_name="test-room",
|
||||
room_url="https://example.com/room",
|
||||
host_room_url="https://example.com/room-host",
|
||||
@@ -136,7 +135,6 @@ async def test_cleanup_deletes_associated_meeting_and_recording(session):
|
||||
await session.execute(
|
||||
insert(RecordingModel).values(
|
||||
id=recording_id,
|
||||
transcript_id=old_transcript.id,
|
||||
meeting_id=meeting_id,
|
||||
url="https://example.com/recording.mp4",
|
||||
object_key="recordings/test.mp4",
|
||||
@@ -258,8 +256,7 @@ async def test_meeting_consent_cascade_delete(session):
|
||||
await session.execute(
|
||||
insert(MeetingModel).values(
|
||||
id=meeting_id,
|
||||
transcript_id=transcript.id,
|
||||
room_id="test-room",
|
||||
room_id=None,
|
||||
room_name="test-room",
|
||||
room_url="https://example.com/room",
|
||||
host_room_url="https://example.com/room-host",
|
||||
@@ -275,19 +272,31 @@ async def test_meeting_consent_cascade_delete(session):
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
# Update transcript with meeting_id
|
||||
await session.execute(
|
||||
update(TranscriptModel)
|
||||
.where(TranscriptModel.id == transcript.id)
|
||||
.values(meeting_id=meeting_id)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
# Create meeting_consent entries
|
||||
await session.execute(
|
||||
insert(MeetingConsentModel).values(
|
||||
id="consent-1",
|
||||
meeting_id=meeting_id,
|
||||
user_name="User 1",
|
||||
user_id="user-1",
|
||||
consent_given=True,
|
||||
consent_timestamp=old_date,
|
||||
)
|
||||
)
|
||||
await session.execute(
|
||||
insert(MeetingConsentModel).values(
|
||||
id="consent-2",
|
||||
meeting_id=meeting_id,
|
||||
user_name="User 2",
|
||||
user_id="user-2",
|
||||
consent_given=True,
|
||||
consent_timestamp=old_date,
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
@@ -30,6 +30,8 @@ async def test_sync_room_ics_task(session):
|
||||
ics_url="https://calendar.example.com/task.ics",
|
||||
ics_enabled=True,
|
||||
)
|
||||
# Commit to make room visible to ICS service's separate session
|
||||
await session.commit()
|
||||
|
||||
cal = Calendar()
|
||||
event = Event()
|
||||
@@ -132,16 +134,11 @@ async def test_sync_all_ics_calendars(session):
|
||||
|
||||
with patch("reflector.worker.ics_sync.sync_room_ics.delay") as mock_delay:
|
||||
# Directly call the sync_all logic without the Celery wrapper
|
||||
query = rooms.select().where(
|
||||
rooms.c.ics_enabled == True, rooms.c.ics_url != None
|
||||
)
|
||||
all_rooms = await get_database().fetch_all(query)
|
||||
ics_enabled_rooms = await rooms_controller.get_ics_enabled(session)
|
||||
|
||||
for room_data in all_rooms:
|
||||
room_id = room_data["id"]
|
||||
room = await rooms_controller.get_by_id(room_id)
|
||||
for room in ics_enabled_rooms:
|
||||
if room and _should_sync(room):
|
||||
sync_room_ics.delay(room_id)
|
||||
sync_room_ics.delay(room.id)
|
||||
|
||||
assert mock_delay.call_count == 2
|
||||
called_room_ids = [call.args[0] for call in mock_delay.call_args_list]
|
||||
@@ -211,22 +208,18 @@ async def test_sync_respects_fetch_interval(session):
|
||||
)
|
||||
|
||||
await rooms_controller.update(
|
||||
session,
|
||||
room2,
|
||||
{"ics_last_sync": now - timedelta(seconds=100)},
|
||||
)
|
||||
|
||||
with patch("reflector.worker.ics_sync.sync_room_ics.delay") as mock_delay:
|
||||
# Test the sync logic without the Celery wrapper
|
||||
query = rooms.select().where(
|
||||
rooms.c.ics_enabled == True, rooms.c.ics_url != None
|
||||
)
|
||||
all_rooms = await get_database().fetch_all(query)
|
||||
ics_enabled_rooms = await rooms_controller.get_ics_enabled(session)
|
||||
|
||||
for room_data in all_rooms:
|
||||
room_id = room_data["id"]
|
||||
room = await rooms_controller.get_by_id(room_id)
|
||||
for room in ics_enabled_rooms:
|
||||
if room and _should_sync(room):
|
||||
sync_room_ics.delay(room_id)
|
||||
sync_room_ics.delay(room.id)
|
||||
|
||||
assert mock_delay.call_count == 1
|
||||
assert mock_delay.call_args[0][0] == room2.id
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import delete, insert
|
||||
@@ -315,87 +314,56 @@ class TestSearchControllerFilters:
|
||||
"""Test SearchController functionality with various filters."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_with_source_kind_filter(self):
|
||||
async def test_search_with_source_kind_filter(self, session):
|
||||
"""Test search filtering by source_kind."""
|
||||
controller = SearchController()
|
||||
with (
|
||||
patch("reflector.db.search.is_postgresql", return_value=True),
|
||||
patch("reflector.db.search.get_session_factory") as mock_session_factory,
|
||||
):
|
||||
mock_db.return_value.fetch_all = AsyncMock(return_value=[])
|
||||
mock_db.return_value.fetch_val = AsyncMock(return_value=0)
|
||||
params = SearchParameters(query_text="test", source_kind=SourceKind.LIVE)
|
||||
|
||||
params = SearchParameters(query_text="test", source_kind=SourceKind.LIVE)
|
||||
# This should not fail, even if no results are found
|
||||
results, total = await controller.search_transcripts(session, params)
|
||||
|
||||
results, total = await controller.search_transcripts(params)
|
||||
|
||||
assert results == []
|
||||
assert total == 0
|
||||
|
||||
mock_db.return_value.fetch_all.assert_called_once()
|
||||
assert isinstance(results, list)
|
||||
assert isinstance(total, int)
|
||||
assert total >= 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_with_single_room_id(self):
|
||||
async def test_search_with_single_room_id(self, session):
|
||||
"""Test search filtering by single room ID (currently supported)."""
|
||||
controller = SearchController()
|
||||
with (
|
||||
patch("reflector.db.search.is_postgresql", return_value=True),
|
||||
patch("reflector.db.search.get_session_factory") as mock_session_factory,
|
||||
):
|
||||
mock_db.return_value.fetch_all = AsyncMock(return_value=[])
|
||||
mock_db.return_value.fetch_val = AsyncMock(return_value=0)
|
||||
params = SearchParameters(
|
||||
query_text="test",
|
||||
room_id="room1",
|
||||
)
|
||||
|
||||
params = SearchParameters(
|
||||
query_text="test",
|
||||
room_id="room1",
|
||||
)
|
||||
# This should not fail, even if no results are found
|
||||
results, total = await controller.search_transcripts(session, params)
|
||||
|
||||
results, total = await controller.search_transcripts(params)
|
||||
|
||||
assert results == []
|
||||
assert total == 0
|
||||
mock_db.return_value.fetch_all.assert_called_once()
|
||||
assert isinstance(results, list)
|
||||
assert isinstance(total, int)
|
||||
assert total >= 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_result_includes_available_fields(self, mock_db_result):
|
||||
async def test_search_result_includes_available_fields(
|
||||
self, session, mock_db_result
|
||||
):
|
||||
"""Test that search results include available fields like source_kind."""
|
||||
# Test that the search method works and returns SearchResult objects
|
||||
controller = SearchController()
|
||||
with (
|
||||
patch("reflector.db.search.is_postgresql", return_value=True),
|
||||
patch("reflector.db.search.get_session_factory") as mock_session_factory,
|
||||
):
|
||||
params = SearchParameters(query_text="test")
|
||||
|
||||
class MockRow:
|
||||
def __init__(self, data):
|
||||
self._data = data
|
||||
self._mapping = data
|
||||
results, total = await controller.search_transcripts(session, params)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._data.items())
|
||||
assert isinstance(results, list)
|
||||
assert isinstance(total, int)
|
||||
assert total >= 0
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._data[key]
|
||||
|
||||
def keys(self):
|
||||
return self._data.keys()
|
||||
|
||||
mock_row = MockRow(mock_db_result)
|
||||
|
||||
mock_db.return_value.fetch_all = AsyncMock(return_value=[mock_row])
|
||||
mock_db.return_value.fetch_val = AsyncMock(return_value=1)
|
||||
|
||||
params = SearchParameters(query_text="test")
|
||||
|
||||
results, total = await controller.search_transcripts(params)
|
||||
|
||||
assert total == 1
|
||||
assert len(results) == 1
|
||||
|
||||
result = results[0]
|
||||
# If any results exist, verify they are SearchResult objects
|
||||
for result in results:
|
||||
assert isinstance(result, SearchResult)
|
||||
assert result.id == "test-transcript-id"
|
||||
assert result.title == "Test Transcript"
|
||||
assert result.rank == 0.95
|
||||
assert hasattr(result, "id")
|
||||
assert hasattr(result, "title")
|
||||
assert hasattr(result, "rank")
|
||||
assert hasattr(result, "source_kind")
|
||||
|
||||
|
||||
class TestSearchEndpointParsing:
|
||||
|
||||
@@ -2,33 +2,84 @@ from datetime import datetime, timezone
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import insert
|
||||
|
||||
from reflector.db.recordings import Recording, recordings_controller
|
||||
from reflector.db.base import MeetingModel, RoomModel
|
||||
from reflector.db.recordings import recordings_controller
|
||||
from reflector.db.transcripts import SourceKind, transcripts_controller
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_recording_deleted_with_transcript():
|
||||
recording = await recordings_controller.create(
|
||||
Recording(
|
||||
bucket_name="test-bucket",
|
||||
object_key="recording.mp4",
|
||||
recorded_at=datetime.now(timezone.utc),
|
||||
async def test_recording_deleted_with_transcript(session):
|
||||
"""Test that a recording is deleted when its associated transcript is deleted."""
|
||||
# First create a room and meeting to satisfy foreign key constraints
|
||||
room_id = "test-room"
|
||||
await session.execute(
|
||||
insert(RoomModel).values(
|
||||
id=room_id,
|
||||
name="test-room",
|
||||
user_id="test-user",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
zulip_auto_post=False,
|
||||
zulip_stream="",
|
||||
zulip_topic="",
|
||||
is_locked=False,
|
||||
room_mode="normal",
|
||||
recording_type="cloud",
|
||||
recording_trigger="automatic",
|
||||
is_shared=False,
|
||||
)
|
||||
)
|
||||
|
||||
meeting_id = "test-meeting"
|
||||
await session.execute(
|
||||
insert(MeetingModel).values(
|
||||
id=meeting_id,
|
||||
room_id=room_id,
|
||||
room_name="test-room",
|
||||
room_url="https://example.com/room",
|
||||
host_room_url="https://example.com/room-host",
|
||||
start_date=datetime.now(timezone.utc),
|
||||
end_date=datetime.now(timezone.utc),
|
||||
is_active=False,
|
||||
num_clients=0,
|
||||
is_locked=False,
|
||||
room_mode="normal",
|
||||
recording_type="cloud",
|
||||
recording_trigger="automatic",
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
# Now create a recording
|
||||
recording = await recordings_controller.create(
|
||||
session,
|
||||
meeting_id=meeting_id,
|
||||
url="https://example.com/recording.mp4",
|
||||
object_key="recordings/test.mp4",
|
||||
duration=3600.0,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# Create a transcript associated with the recording
|
||||
transcript = await transcripts_controller.add(
|
||||
session,
|
||||
name="Test Transcript",
|
||||
source_kind=SourceKind.ROOM,
|
||||
recording_id=recording.id,
|
||||
)
|
||||
|
||||
# Mock the storage deletion
|
||||
with patch("reflector.db.transcripts.get_recordings_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)
|
||||
# Delete the transcript
|
||||
await transcripts_controller.remove_by_id(session, transcript.id)
|
||||
|
||||
# Verify that the recording file was deleted from storage
|
||||
storage_instance.delete_file.assert_awaited_once_with(recording.object_key)
|
||||
|
||||
assert await recordings_controller.get_by_id(recording.id) is None
|
||||
assert await transcripts_controller.get_by_id(transcript.id) is None
|
||||
# Verify both the recording and transcript are deleted
|
||||
assert await recordings_controller.get_by_id(session, recording.id) is None
|
||||
assert await transcripts_controller.get_by_id(session, transcript.id) is None
|
||||
|
||||
Reference in New Issue
Block a user