da104c59ac
_merge_ical_properties() only handled a subset of event fields, silently dropping categories, recurrence_rule, attendees, and reminder_minutes during updates. These fields were fully supported by _create_ical_event() and accepted by the MCP tool, but never applied. Closes #544 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
650 lines
23 KiB
Python
650 lines
23 KiB
Python
"""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_update_event_extended_fields(
|
|
nc_client: NextcloudClient, temporary_calendar: str
|
|
):
|
|
"""Test updating categories, recurrence_rule, attendees, and reminder_minutes."""
|
|
calendar_name = temporary_calendar
|
|
|
|
tomorrow = datetime.now() + timedelta(days=1)
|
|
event_data = {
|
|
"title": "Extended Fields Update Test",
|
|
"start_datetime": tomorrow.strftime("%Y-%m-%dT10:00:00"),
|
|
"end_datetime": tomorrow.strftime("%Y-%m-%dT11:00:00"),
|
|
"description": "Base event for extended-field update test",
|
|
}
|
|
|
|
event_uid = None
|
|
try:
|
|
result = await nc_client.calendar.create_event(calendar_name, event_data)
|
|
event_uid = result["uid"]
|
|
logger.info(f"Created base event for extended fields test: {event_uid}")
|
|
|
|
# --- Phase 1: Set all four extended fields ---
|
|
updated_data = {
|
|
"categories": "work,meeting",
|
|
"recurrence_rule": "FREQ=WEEKLY;COUNT=4",
|
|
"attendees": "alice@example.com,bob@example.com",
|
|
"reminder_minutes": 15,
|
|
}
|
|
await nc_client.calendar.update_event(calendar_name, event_uid, updated_data)
|
|
|
|
retrieved, _ = await nc_client.calendar.get_event(calendar_name, event_uid)
|
|
|
|
# Verify categories
|
|
assert "work" in retrieved.get("categories", "")
|
|
assert "meeting" in retrieved.get("categories", "")
|
|
|
|
# Verify recurrence rule
|
|
assert retrieved.get("recurring") is True
|
|
assert "WEEKLY" in retrieved.get("recurrence_rule", "")
|
|
|
|
# Verify attendees
|
|
attendees = retrieved.get("attendees", "")
|
|
assert "alice@example.com" in attendees
|
|
assert "bob@example.com" in attendees
|
|
|
|
logger.info("Phase 1 passed: all extended fields set correctly")
|
|
|
|
# --- Phase 2: Clear all four extended fields ---
|
|
cleared_data = {
|
|
"categories": "",
|
|
"recurrence_rule": "",
|
|
"attendees": "",
|
|
"reminder_minutes": 0,
|
|
}
|
|
await nc_client.calendar.update_event(calendar_name, event_uid, cleared_data)
|
|
|
|
cleared, _ = await nc_client.calendar.get_event(calendar_name, event_uid)
|
|
|
|
# Verify categories cleared
|
|
assert not cleared.get("categories")
|
|
|
|
# Verify recurrence cleared
|
|
assert cleared.get("recurring") is not True
|
|
assert not cleared.get("recurrence_rule")
|
|
|
|
# Verify attendees cleared
|
|
assert not cleared.get("attendees")
|
|
|
|
logger.info("Phase 2 passed: all extended fields cleared correctly")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Extended fields update test failed: {e}")
|
|
raise
|
|
finally:
|
|
if event_uid:
|
|
try:
|
|
await nc_client.calendar.delete_event(calendar_name, event_uid)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
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_list_events_date_range_filtering(
|
|
nc_client: NextcloudClient, temporary_calendar: str
|
|
):
|
|
"""Test that date range filtering actually excludes events outside the range.
|
|
|
|
Reproduces GH-538: get_calendar_events() accepted date range parameters
|
|
but returned events from the entire calendar history, ignoring date filters.
|
|
"""
|
|
calendar_name = temporary_calendar
|
|
past_uid = None
|
|
future_uid = None
|
|
|
|
try:
|
|
# Create Event A: 30 days in the past
|
|
past_date = datetime.now() - timedelta(days=30)
|
|
past_event_data = {
|
|
"title": f"Past Event {uuid.uuid4().hex[:8]}",
|
|
"start_datetime": past_date.strftime("%Y-%m-%dT10:00:00"),
|
|
"end_datetime": past_date.strftime("%Y-%m-%dT11:00:00"),
|
|
"description": "Event in the past for date range test",
|
|
}
|
|
result_past = await nc_client.calendar.create_event(
|
|
calendar_name, past_event_data
|
|
)
|
|
past_uid = result_past["uid"]
|
|
logger.info(f"Created past event: {past_uid}")
|
|
|
|
# Create Event B: 1 day in the future
|
|
future_date = datetime.now() + timedelta(days=1)
|
|
future_event_data = {
|
|
"title": f"Future Event {uuid.uuid4().hex[:8]}",
|
|
"start_datetime": future_date.strftime("%Y-%m-%dT14:00:00"),
|
|
"end_datetime": future_date.strftime("%Y-%m-%dT15:00:00"),
|
|
"description": "Event in the future for date range test",
|
|
}
|
|
result_future = await nc_client.calendar.create_event(
|
|
calendar_name, future_event_data
|
|
)
|
|
future_uid = result_future["uid"]
|
|
logger.info(f"Created future event: {future_uid}")
|
|
|
|
# Query with date range: today → 7 days ahead
|
|
now = datetime.now()
|
|
week_ahead = now + timedelta(days=7)
|
|
|
|
events = await nc_client.calendar.get_calendar_events(
|
|
calendar_name=calendar_name,
|
|
start_datetime=now,
|
|
end_datetime=week_ahead,
|
|
limit=50,
|
|
)
|
|
|
|
event_uids = [e["uid"] for e in events]
|
|
|
|
# Future event (tomorrow) SHOULD be in results
|
|
assert future_uid in event_uids, (
|
|
f"Future event {future_uid} should be in date-filtered results"
|
|
)
|
|
|
|
# Past event (30 days ago) should NOT be in results
|
|
assert past_uid not in event_uids, (
|
|
f"Past event {past_uid} should be excluded by date range filter "
|
|
f"(GH-538: date range was being ignored)"
|
|
)
|
|
|
|
logger.info(
|
|
f"Date range filtering works: {len(events)} events returned, "
|
|
f"past event correctly excluded"
|
|
)
|
|
|
|
finally:
|
|
# Cleanup both events
|
|
for uid in [past_uid, future_uid]:
|
|
if uid:
|
|
try:
|
|
await nc_client.calendar.delete_event(calendar_name, uid)
|
|
except Exception as e:
|
|
logger.warning(f"Cleanup failed for event {uid}: {e}")
|
|
|
|
|
|
async def test_recurring_event_date_range_expansion(
|
|
nc_client: NextcloudClient, temporary_calendar: str
|
|
):
|
|
"""Test that recurring events are expanded into individual occurrences.
|
|
|
|
When querying with a date range, a recurring event should return one
|
|
event dict per occurrence within the range, each with the correct
|
|
start_datetime for that occurrence (not the original master event date).
|
|
|
|
This is a follow-up to GH-538: the time-range filter correctly selected
|
|
recurring events, but returned the master event with its original DTSTART
|
|
instead of expanding occurrences.
|
|
"""
|
|
calendar_name = temporary_calendar
|
|
event_uid = None
|
|
|
|
try:
|
|
# Create a daily recurring event starting 7 days ago
|
|
start = datetime.now() - timedelta(days=7)
|
|
event_data = {
|
|
"title": f"Daily Recurrence {uuid.uuid4().hex[:8]}",
|
|
"start_datetime": start.strftime("%Y-%m-%dT09:00:00"),
|
|
"end_datetime": start.strftime("%Y-%m-%dT10:00:00"),
|
|
"description": "Daily recurring event for expansion test",
|
|
"recurring": True,
|
|
"recurrence_rule": "FREQ=DAILY",
|
|
}
|
|
result = await nc_client.calendar.create_event(calendar_name, event_data)
|
|
event_uid = result["uid"]
|
|
logger.info(f"Created daily recurring event: {event_uid}")
|
|
|
|
# Query with date range: today → 3 days ahead
|
|
query_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
|
query_end = query_start + timedelta(days=3)
|
|
|
|
events = await nc_client.calendar.get_calendar_events(
|
|
calendar_name=calendar_name,
|
|
start_datetime=query_start,
|
|
end_datetime=query_end,
|
|
limit=50,
|
|
)
|
|
|
|
# Filter to only our recurring event (calendar may have others)
|
|
our_events = [e for e in events if e["uid"] == event_uid]
|
|
|
|
# Should have multiple occurrences (one per day in the range)
|
|
assert len(our_events) >= 2, (
|
|
f"Expected multiple expanded occurrences, got {len(our_events)}. "
|
|
f"Expansion may not be working."
|
|
)
|
|
|
|
# Each occurrence should have a different start_datetime
|
|
start_dates = [e["start_datetime"] for e in our_events]
|
|
assert len(set(start_dates)) == len(our_events), (
|
|
f"Each occurrence should have a unique start_datetime, got: {start_dates}"
|
|
)
|
|
|
|
# No start_datetime should fall outside the queried range
|
|
for e in our_events:
|
|
event_start = datetime.fromisoformat(e["start_datetime"])
|
|
# Remove timezone info for comparison if present
|
|
if event_start.tzinfo is not None:
|
|
event_start = event_start.replace(tzinfo=None)
|
|
assert event_start >= query_start - timedelta(hours=1), (
|
|
f"Occurrence {e['start_datetime']} is before query start {query_start}"
|
|
)
|
|
assert event_start < query_end + timedelta(hours=1), (
|
|
f"Occurrence {e['start_datetime']} is after query end {query_end}"
|
|
)
|
|
|
|
# Expanded occurrences should NOT have recurrence rules
|
|
# (server strips RRULE when expanding)
|
|
for e in our_events:
|
|
assert not e.get("recurring"), (
|
|
"Expanded occurrence should not have recurring=True, "
|
|
"RRULE should be stripped by server-side expansion"
|
|
)
|
|
|
|
logger.info(
|
|
f"Recurring event expansion works: {len(our_events)} occurrences "
|
|
f"returned with unique start dates"
|
|
)
|
|
|
|
finally:
|
|
if event_uid:
|
|
try:
|
|
await nc_client.calendar.delete_event(calendar_name, event_uid)
|
|
except Exception as e:
|
|
logger.warning(f"Cleanup failed for recurring event {event_uid}: {e}")
|
|
|
|
|
|
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")
|