"""Integration tests for Calendar CalDAV operations.
Note: These tests use the shared temporary_calendar fixture from conftest.py
which reuses a session-scoped calendar to avoid Nextcloud rate limiting issues.
Each test cleans up its own events/todos but shares the same calendar.
"""
import logging
import uuid
from datetime import datetime, timedelta
import pytest
from httpx import HTTPStatusError
from nextcloud_mcp_server.client import NextcloudClient
logger = logging.getLogger(__name__)
# Mark all tests in this module as integration tests
pytestmark = pytest.mark.integration
@pytest.fixture
async def temporary_event(nc_client: NextcloudClient, temporary_calendar: str):
"""Create a temporary event for testing and clean up afterward.
Uses the shared temporary_calendar fixture from conftest.py which reuses
a session-scoped calendar to avoid Nextcloud rate limiting.
"""
event_uid = None
calendar_name = temporary_calendar
# Create a test event
tomorrow = datetime.now() + timedelta(days=1)
event_data = {
"title": f"Test Event {uuid.uuid4().hex[:8]}",
"start_datetime": tomorrow.strftime("%Y-%m-%dT14:00:00"),
"end_datetime": tomorrow.strftime("%Y-%m-%dT15:00:00"),
"description": "Test event created by integration tests",
"location": "Test Location",
"categories": "testing",
"status": "CONFIRMED",
"priority": 5,
}
try:
logger.info(f"Creating temporary event in calendar: {calendar_name}")
result = await nc_client.calendar.create_event(calendar_name, event_data)
event_uid = result.get("uid")
if not event_uid:
pytest.fail("Failed to create temporary event")
logger.info(f"Created temporary event with UID: {event_uid}")
yield {"uid": event_uid, "calendar_name": calendar_name, "data": event_data}
finally:
# Cleanup
if event_uid:
try:
logger.info(f"Cleaning up temporary event: {event_uid}")
await nc_client.calendar.delete_event(calendar_name, event_uid)
logger.info(f"Successfully deleted temporary event: {event_uid}")
except HTTPStatusError as e:
if e.response.status_code != 404:
logger.error(f"Error deleting temporary event {event_uid}: {e}")
except Exception as e:
logger.error(
f"Unexpected error deleting temporary event {event_uid}: {e}"
)
async def test_list_calendars(nc_client: NextcloudClient):
"""Test listing available calendars."""
calendars = await nc_client.calendar.list_calendars()
assert isinstance(calendars, list)
if not calendars:
pytest.skip("No calendars available - Calendar app may not be enabled")
logger.info(f"Found {len(calendars)} calendars")
# Check structure of calendars
for calendar in calendars:
assert "name" in calendar
assert "display_name" in calendar
assert "href" in calendar
# Optional fields
assert "description" in calendar
assert "color" in calendar
logger.info(f"Calendar: {calendar['name']} - {calendar['display_name']}")
async def test_create_and_delete_event(
nc_client: NextcloudClient, temporary_calendar: str
):
"""Test creating and deleting a basic event."""
calendar_name = temporary_calendar
# Create event
tomorrow = datetime.now() + timedelta(days=1)
event_data = {
"title": "Integration Test Event",
"start_datetime": tomorrow.strftime("%Y-%m-%dT10:00:00"),
"end_datetime": tomorrow.strftime("%Y-%m-%dT11:00:00"),
"description": "Test event for integration testing",
"location": "Test Room",
"categories": "testing,integration",
"status": "CONFIRMED",
"priority": 3,
}
try:
result = await nc_client.calendar.create_event(calendar_name, event_data)
assert "uid" in result
assert result["status_code"] in [200, 201, 204]
event_uid = result["uid"]
logger.info(f"Created event with UID: {event_uid}")
# Verify event was created by retrieving it
retrieved_event, etag = await nc_client.calendar.get_event(
calendar_name, event_uid
)
assert retrieved_event["uid"] == event_uid
assert retrieved_event["title"] == "Integration Test Event"
assert retrieved_event["location"] == "Test Room"
# Delete event
delete_result = await nc_client.calendar.delete_event(calendar_name, event_uid)
assert delete_result["status_code"] in [200, 204, 404]
logger.info(f"Successfully deleted event: {event_uid}")
except Exception as e:
logger.error(f"Test failed: {e}")
raise
async def test_create_all_day_event(
nc_client: NextcloudClient, temporary_calendar: str
):
"""Test creating an all-day event."""
calendar_name = temporary_calendar
tomorrow = datetime.now() + timedelta(days=1)
event_data = {
"title": "All Day Test Event",
"start_datetime": tomorrow.strftime("%Y-%m-%d"),
"all_day": True,
"description": "Test all-day event",
"categories": "testing",
}
try:
result = await nc_client.calendar.create_event(calendar_name, event_data)
event_uid = result["uid"]
logger.info(f"Created all-day event with UID: {event_uid}")
# Verify event
retrieved_event, _ = await nc_client.calendar.get_event(
calendar_name, event_uid
)
assert retrieved_event["title"] == "All Day Test Event"
assert retrieved_event.get("all_day") is True
# Cleanup
await nc_client.calendar.delete_event(calendar_name, event_uid)
except Exception as e:
logger.error(f"All-day event test failed: {e}")
raise
async def test_create_recurring_event(
nc_client: NextcloudClient, temporary_calendar: str
):
"""Test creating a recurring event."""
calendar_name = temporary_calendar
tomorrow = datetime.now() + timedelta(days=1)
event_data = {
"title": "Weekly Recurring Test",
"start_datetime": tomorrow.strftime("%Y-%m-%dT14:00:00"),
"end_datetime": tomorrow.strftime("%Y-%m-%dT15:00:00"),
"description": "Test recurring event",
"recurring": True,
"recurrence_rule": "FREQ=WEEKLY;BYDAY=MO,WE,FR",
"reminder_minutes": 30,
}
try:
result = await nc_client.calendar.create_event(calendar_name, event_data)
event_uid = result["uid"]
logger.info(f"Created recurring event with UID: {event_uid}")
# Verify event
retrieved_event, _ = await nc_client.calendar.get_event(
calendar_name, event_uid
)
assert retrieved_event["title"] == "Weekly Recurring Test"
assert retrieved_event.get("recurring") is True
# Cleanup
await nc_client.calendar.delete_event(calendar_name, event_uid)
except Exception as e:
logger.error(f"Recurring event test failed: {e}")
raise
async def test_list_events_in_range(nc_client: NextcloudClient, temporary_event: dict):
"""Test listing events within a date range."""
calendar_name = temporary_event["calendar_name"]
# Get events for the next week
start_datetime = datetime.now()
end_datetime = datetime.now() + timedelta(days=7)
events = await nc_client.calendar.get_calendar_events(
calendar_name=calendar_name,
start_datetime=start_datetime,
end_datetime=end_datetime,
limit=50,
)
assert isinstance(events, list)
logger.info(f"Found {len(events)} events in date range")
# Our temporary event should be in the list
event_uids = [event.get("uid") for event in events]
assert temporary_event["uid"] in event_uids
# Check event structure
for event in events:
assert "uid" in event
assert "title" in event
assert "start_datetime" in event
async def test_update_event(nc_client: NextcloudClient, temporary_event: dict):
"""Test updating an existing event."""
calendar_name = temporary_event["calendar_name"]
event_uid = temporary_event["uid"]
# Update event data
updated_data = {
"title": "Updated Test Event Title",
"description": "Updated description for test event",
"location": "Updated Location",
"priority": 1, # High priority
}
try:
result = await nc_client.calendar.update_event(
calendar_name, event_uid, updated_data
)
assert result["uid"] == event_uid
# Verify updates
updated_event, _ = await nc_client.calendar.get_event(calendar_name, event_uid)
assert updated_event["title"] == "Updated Test Event Title"
assert updated_event["description"] == "Updated description for test event"
assert updated_event["location"] == "Updated Location"
assert updated_event["priority"] == 1
logger.info(f"Successfully updated event: {event_uid}")
except Exception as e:
logger.error(f"Event update test failed: {e}")
raise
async def test_create_event_with_attendees(
nc_client: NextcloudClient, temporary_calendar: str
):
"""Test creating an event with attendees."""
calendar_name = temporary_calendar
tomorrow = datetime.now() + timedelta(days=1)
event_data = {
"title": "Meeting with Attendees",
"start_datetime": tomorrow.strftime("%Y-%m-%dT16:00:00"),
"end_datetime": tomorrow.strftime("%Y-%m-%dT17:00:00"),
"description": "Test meeting with multiple attendees",
"location": "Conference Room A",
"attendees": "test1@example.com,test2@example.com",
"reminder_minutes": 15,
"status": "TENTATIVE",
}
try:
result = await nc_client.calendar.create_event(calendar_name, event_data)
event_uid = result["uid"]
logger.info(f"Created event with attendees, UID: {event_uid}")
# Verify event
retrieved_event, _ = await nc_client.calendar.get_event(
calendar_name, event_uid
)
assert retrieved_event["title"] == "Meeting with Attendees"
assert "test1@example.com" in retrieved_event.get("attendees", "")
assert retrieved_event["status"] == "TENTATIVE"
# Cleanup
await nc_client.calendar.delete_event(calendar_name, event_uid)
except Exception as e:
logger.error(f"Event with attendees test failed: {e}")
raise
async def test_get_nonexistent_event(
nc_client: NextcloudClient, temporary_calendar: str
):
"""Test retrieving a non-existent event."""
calendar_name = temporary_calendar
fake_uid = f"nonexistent-{uuid.uuid4()}"
# caldav library raises generic Exception for missing events, not HTTPStatusError
with pytest.raises(Exception, match="not found"):
await nc_client.calendar.get_event(calendar_name, fake_uid)
logger.info(f"Correctly raised exception for nonexistent event: {fake_uid}")
async def test_delete_nonexistent_event(
nc_client: NextcloudClient, temporary_calendar: str
):
"""Test deleting a non-existent event."""
calendar_name = temporary_calendar
fake_uid = f"nonexistent-{uuid.uuid4()}"
result = await nc_client.calendar.delete_event(calendar_name, fake_uid)
assert result["status_code"] == 404
logger.info(f"Correctly got 404 for deleting nonexistent event: {fake_uid}")
async def test_event_with_url_and_categories(
nc_client: NextcloudClient, temporary_calendar: str
):
"""Test creating an event with URL and multiple categories."""
calendar_name = temporary_calendar
tomorrow = datetime.now() + timedelta(days=1)
event_data = {
"title": "Event with URL and Categories",
"start_datetime": tomorrow.strftime("%Y-%m-%dT09:00:00"),
"end_datetime": tomorrow.strftime("%Y-%m-%dT10:30:00"),
"description": "Test event with additional metadata",
"categories": "work,meeting,important,quarterly",
"url": "https://zoom.us/j/123456789",
"privacy": "PRIVATE",
"priority": 2,
}
try:
result = await nc_client.calendar.create_event(calendar_name, event_data)
event_uid = result["uid"]
logger.info(f"Created event with metadata, UID: {event_uid}")
# Verify event
retrieved_event, _ = await nc_client.calendar.get_event(
calendar_name, event_uid
)
assert retrieved_event["title"] == "Event with URL and Categories"
assert "work" in retrieved_event.get("categories", "")
assert "important" in retrieved_event.get("categories", "")
assert retrieved_event.get("url") == "https://zoom.us/j/123456789"
assert retrieved_event.get("privacy") == "PRIVATE"
assert retrieved_event.get("priority") == 2
# Cleanup
await nc_client.calendar.delete_event(calendar_name, event_uid)
except Exception as e:
logger.error(f"Event with metadata test failed: {e}")
raise
async def test_calendar_operations_error_handling(
nc_client: NextcloudClient,
):
"""Test error handling for calendar operations."""
# Test with non-existent calendar
fake_calendar = f"nonexistent_calendar_{uuid.uuid4().hex}"
# caldav library returns empty list for non-existent calendars, doesn't raise
# Testing that it doesn't crash and returns empty results
events = await nc_client.calendar.get_calendar_events(fake_calendar)
assert isinstance(events, list)
# Empty list is expected for non-existent calendar
assert len(events) == 0
logger.info("Error handling tests completed successfully")