MCP iCal Server
by Omar-V2
- mcp-ical
- tests
import time
from datetime import datetime, timedelta
import pytest
from src.mcp_ical.ical import CalendarManager, NoSuchCalendarException
from src.mcp_ical.models import (
CreateEventRequest,
Frequency,
RecurrenceRule,
UpdateEventRequest,
)
@pytest.fixture(scope="session")
def calendar_manager():
"""Create a single CalendarManager instance for all tests."""
return CalendarManager()
@pytest.fixture(scope="session", autouse=True)
def cleanup_calendars_after_tests():
"""Fixture that runs after all tests to ensure calendars are properly cleaned up."""
yield
print("Waiting for iCloud sync before final calendar cleanup...")
time.sleep(5)
# Get a fresh calendar manager
calendar_manager = CalendarManager()
# Clean up any remaining test calendars
for calendar in calendar_manager.list_calendars():
if calendar.title().startswith("test_calendar_"):
calendar_manager._delete_calendar(calendar.uniqueIdentifier())
@pytest.fixture
def test_calendar(calendar_manager):
"""Create an isolated calendar for testing."""
calendar_name = f"test_calendar_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}"
calendar = calendar_manager._create_calendar(calendar_name)
if not calendar:
pytest.fail(f"Failed to create test calendar: {calendar_name}")
yield {"name": calendar_name, "manager": calendar_manager}
try:
calendar_manager._delete_calendar(calendar.uniqueIdentifier())
except Exception as e:
print(f"Failed to cleanup test calendar {calendar_name}: {e}")
@pytest.fixture
def cleanup_events(calendar_manager):
"""Fixture to clean up created events after tests"""
created_events = []
def _add_event(event_id):
created_events.append(event_id)
yield _add_event
# Use the injected calendar_manager instead of creating a new one
for event_id in created_events:
try:
calendar_manager.delete_event(event_id)
except Exception as e:
print(f"Failed to delete event {event_id}: {e}")
@pytest.fixture
def test_event_base():
"""Base event data for testing"""
start_time = datetime.now().replace(microsecond=0) + timedelta(days=1)
end_time = start_time + timedelta(hours=1)
return {
"title": "Test Event",
"start_time": start_time,
"end_time": end_time,
"notes": "Test notes",
"location": "Test location",
}
def test_create_and_get_event(calendar_manager, test_event_base, test_calendar, cleanup_events):
"""Test creating an event and retrieving it"""
# Create event
event = calendar_manager.create_event(
CreateEventRequest(
title=test_event_base["title"],
start_time=test_event_base["start_time"],
end_time=test_event_base["end_time"],
notes=test_event_base["notes"],
location=test_event_base["location"],
calendar_name=test_calendar["name"],
)
)
cleanup_events(event.identifier)
# Verify event was created
retrieved_event = calendar_manager.find_event_by_id(event.identifier)
assert retrieved_event is not None
assert retrieved_event.title == test_event_base["title"]
assert retrieved_event.start_time == test_event_base["start_time"]
assert retrieved_event.end_time == test_event_base["end_time"]
assert retrieved_event.notes == test_event_base["notes"]
assert retrieved_event.location == test_event_base["location"]
assert retrieved_event.calendar_name == test_calendar["name"]
def test_list_events(calendar_manager, test_event_base, test_calendar, cleanup_events):
"""Test listing events"""
# Create first event
event1 = calendar_manager.create_event(
CreateEventRequest(
title="Test Event 1",
start_time=test_event_base["start_time"],
end_time=test_event_base["end_time"],
notes=test_event_base["notes"],
location=test_event_base["location"],
calendar_name=test_calendar["name"],
)
)
cleanup_events(event1.identifier)
# Create second event with offset times
event2 = calendar_manager.create_event(
CreateEventRequest(
title="Test Event 2",
start_time=test_event_base["start_time"] + timedelta(hours=2),
end_time=test_event_base["end_time"] + timedelta(hours=2),
notes=test_event_base["notes"],
location=test_event_base["location"],
calendar_name=test_calendar["name"],
)
)
cleanup_events(event2.identifier)
# List events in the time range
events = calendar_manager.list_events(
start_time=test_event_base["start_time"] - timedelta(hours=1),
end_time=test_event_base["end_time"] + timedelta(hours=3),
)
# Verify both events are in the list
event_ids = [event.identifier for event in events]
assert event1.identifier in event_ids
assert event2.identifier in event_ids
def test_update_event(calendar_manager, test_event_base, test_calendar, cleanup_events):
"""Test updating an event"""
# Create event
event = calendar_manager.create_event(
CreateEventRequest(
title=test_event_base["title"],
start_time=test_event_base["start_time"],
end_time=test_event_base["end_time"],
notes=test_event_base["notes"],
location=test_event_base["location"],
calendar_name=test_calendar["name"],
)
)
# cleanup_events(event.identifier)
# Update event
new_title = "Updated Test Event"
new_location = "Updated Location"
updated_event = calendar_manager.update_event(
event.identifier, UpdateEventRequest(title=new_title, location=new_location)
)
# Verify updates
assert updated_event.title == new_title
assert updated_event.location == new_location
# Verify updates persisted
retrieved_event = calendar_manager.find_event_by_id(event.identifier)
assert retrieved_event.title == new_title
assert retrieved_event.location == new_location
def test_delete_event(calendar_manager, test_event_base, test_calendar):
"""Test deleting an event"""
# Create event
event = calendar_manager.create_event(
CreateEventRequest(
title=test_event_base["title"],
start_time=test_event_base["start_time"],
end_time=test_event_base["end_time"],
notes=test_event_base["notes"],
location=test_event_base["location"],
calendar_name=test_calendar["name"],
)
)
# Delete event
calendar_manager.delete_event(event.identifier)
# Verify event was deleted
retrieved_event = calendar_manager.find_event_by_id(event.identifier)
assert retrieved_event is None
def test_recurring_event(calendar_manager, test_event_base, test_calendar, cleanup_events):
"""Test creating and updating a recurring event"""
# Create recurring event
recurrence_rule = RecurrenceRule(frequency=Frequency.DAILY, interval=1, occurrence_count=3)
event = calendar_manager.create_event(
CreateEventRequest(
title=test_event_base["title"],
start_time=test_event_base["start_time"],
end_time=test_event_base["end_time"],
notes=test_event_base["notes"],
location=test_event_base["location"],
recurrence_rule=recurrence_rule,
calendar_name=test_calendar["name"],
)
)
cleanup_events(event.identifier)
# Update recurring event
new_title = "Updated Recurring Event"
calendar_manager.update_event(event.identifier, UpdateEventRequest(title=new_title))
# List future events to verify update affected all occurrences
events = calendar_manager.list_events(
start_time=test_event_base["start_time"],
end_time=test_event_base["start_time"] + timedelta(days=30),
calendar_name=test_calendar["name"],
)
assert len(events) == 3
for e in events:
assert e.title == new_title
def test_all_day_event_with_reminders(calendar_manager, test_event_base, test_calendar, cleanup_events):
"""Test creating an all-day event with reminders"""
# Request 4 days and 2 days before
requested_offsets = [4 * 24 * 60, 2 * 24 * 60] # [5760, 2880]
event = calendar_manager.create_event(
CreateEventRequest(
title=test_event_base["title"],
start_time=test_event_base["start_time"],
end_time=test_event_base["end_time"],
notes=test_event_base["notes"],
location=test_event_base["location"],
all_day=True,
alarms_minutes_offsets=requested_offsets,
calendar_name=test_calendar["name"],
)
)
cleanup_events(event.identifier)
retrieved_event = calendar_manager.find_event_by_id(event.identifier)
assert retrieved_event.all_day is True
# Verify our explicitly requested alarms are present
# EventKit may add its own default alarm hence asserting presence not equality
actual_alarms = retrieved_event.alarms_minutes_offsets
assert 2880 in actual_alarms, "2 day reminder not found"
assert 5760 in actual_alarms, "4 day reminder not found"
def test_event_across_calendars(calendar_manager, test_event_base, test_calendar, cleanup_events):
"""Test moving an event between calendars"""
# Get available calendars
calendars = calendar_manager.list_calendars()
if len(calendars) < 2:
pytest.skip("Need at least 2 calendars for this test")
# Create event in the Home calendar
event = calendar_manager.create_event(
CreateEventRequest(
title=test_event_base["title"],
start_time=test_event_base["start_time"],
end_time=test_event_base["end_time"],
notes=test_event_base["notes"],
location=test_event_base["location"],
calendar_name="Home",
)
)
cleanup_events(event.identifier)
# Move it to another test calendar
calendar_manager.update_event(event.identifier, UpdateEventRequest(calendar_name=test_calendar["name"]))
# Verify event moved
retrieved_event = calendar_manager.find_event_by_id(event.identifier)
assert retrieved_event.calendar_name == test_calendar["name"]
def test_create_event_uses_default_calendar(calendar_manager, test_event_base, test_calendar, cleanup_events):
"""Test that creating an event without specifying calendar uses the default calendar"""
event = calendar_manager.create_event(
CreateEventRequest(
title=test_event_base["title"],
start_time=test_event_base["start_time"],
end_time=test_event_base["end_time"],
notes=test_event_base["notes"],
location=test_event_base["location"],
calendar_name=test_calendar["name"],
)
)
cleanup_events(event.identifier)
# Get the event and verify it's in the default calendar
retrieved_event = calendar_manager.find_event_by_id(event.identifier)
assert retrieved_event is not None
# The default calendar should be set
assert retrieved_event.calendar_name is not None
def test_create_event_nonexistent_calendar(calendar_manager, test_event_base):
"""Test that creating an event in a non-existent calendar raises NoSuchCalendarException"""
with pytest.raises(NoSuchCalendarException):
calendar_manager.create_event(
CreateEventRequest(
title=test_event_base["title"],
start_time=test_event_base["start_time"],
end_time=test_event_base["end_time"],
notes=test_event_base["notes"],
location=test_event_base["location"],
calendar_name="NonExistentCalendar",
)
)
def test_update_event_nonexistent_calendar(calendar_manager, test_event_base, test_calendar, cleanup_events):
"""Test that updating an event to a non-existent calendar raises NoSuchCalendarException"""
# First create an event
event = calendar_manager.create_event(
CreateEventRequest(
title=test_event_base["title"],
start_time=test_event_base["start_time"],
end_time=test_event_base["end_time"],
calendar_name=test_calendar["name"],
)
)
cleanup_events(event.identifier)
# Try to update it to a non-existent calendar
with pytest.raises(NoSuchCalendarException):
calendar_manager.update_event(event.identifier, UpdateEventRequest(calendar_name="NonExistentCalendar"))
def test_find_nonexistent_event(calendar_manager):
"""Test that finding a non-existent event returns None"""
non_existent_id = "non-existent-event-id"
retrieved_event = calendar_manager.find_event_by_id(non_existent_id)
assert retrieved_event is None
def test_all_day_event_with_same_day_reminders(calendar_manager, test_event_base, test_calendar, cleanup_events):
"""Test creating an all-day event with reminders on the same day"""
# Request reminders 2 hours and 4 hours before
requested_offsets = [2 * 60, 4 * 60] # 120 and 240 minutes
event = calendar_manager.create_event(
CreateEventRequest(
title=test_event_base["title"],
start_time=test_event_base["start_time"],
end_time=test_event_base["end_time"],
all_day=True,
alarms_minutes_offsets=requested_offsets,
calendar_name=test_calendar["name"],
)
)
cleanup_events(event.identifier)
retrieved_event = calendar_manager.find_event_by_id(event.identifier)
assert retrieved_event.all_day is True
actual_alarms = retrieved_event.alarms_minutes_offsets
assert 120 in actual_alarms, "2 hour reminder not found"
assert 240 in actual_alarms, "4 hour reminder not found"
def test_all_day_event_mixed_reminders(calendar_manager, test_event_base, test_calendar, cleanup_events):
"""Test all-day event with mix of same-day and multi-day reminders"""
# Mix of reminders: 2 hours, 1 day, and 3 days before
requested_offsets = [2 * 60, 24 * 60, 3 * 24 * 60] # 120, 1440, and 4320 minutes
event = calendar_manager.create_event(
CreateEventRequest(
title=test_event_base["title"],
start_time=test_event_base["start_time"],
end_time=test_event_base["end_time"],
all_day=True,
alarms_minutes_offsets=requested_offsets,
calendar_name=test_calendar["name"],
)
)
cleanup_events(event.identifier)
retrieved_event = calendar_manager.find_event_by_id(event.identifier)
assert retrieved_event.all_day is True
actual_alarms = sorted(retrieved_event.alarms_minutes_offsets)
assert 120 in actual_alarms, "2 hour reminder not found"
assert 1440 in actual_alarms, "1 day reminder not found"
assert 4320 in actual_alarms, "3 day reminder not found"