diff --git a/nextcloud_mcp_server/client/calendar.py b/nextcloud_mcp_server/client/calendar.py index dbc0b7a..2d36b28 100644 --- a/nextcloud_mcp_server/client/calendar.py +++ b/nextcloud_mcp_server/client/calendar.py @@ -812,6 +812,48 @@ class CalendarClient: if "url" in event_data: component["URL"] = event_data["url"] + # Handle categories + if "categories" in event_data: + categories_str = event_data["categories"] + if categories_str: + component["CATEGORIES"] = categories_str.split(",") + elif "CATEGORIES" in component: + del component["CATEGORIES"] + + # Handle recurrence rule + if "recurrence_rule" in event_data: + rrule_str = event_data["recurrence_rule"] + if rrule_str: + component["RRULE"] = vRecur.from_ical(rrule_str) + elif "RRULE" in component: + del component["RRULE"] + + # Handle attendees + if "attendees" in event_data: + attendees_str = event_data["attendees"] + # Remove all existing attendees first + while "ATTENDEE" in component: + del component["ATTENDEE"] + if attendees_str: + for email in attendees_str.split(","): + if email.strip(): + component.add("attendee", f"mailto:{email.strip()}") + + # Handle reminder (VALARM) + if "reminder_minutes" in event_data: + component.subcomponents = [ + sub + for sub in component.subcomponents + if sub.name != "VALARM" + ] + minutes = event_data["reminder_minutes"] + if minutes > 0: + alarm = Alarm() + alarm.add("action", "DISPLAY") + alarm.add("description", "Event reminder") + alarm.add("trigger", dt.timedelta(minutes=-minutes)) + component.add_component(alarm) + # Handle dates if "start_datetime" in event_data: start_str = event_data["start_datetime"] diff --git a/tests/client/calendar/test_calendar_operations.py b/tests/client/calendar/test_calendar_operations.py index 5a9df16..9e780ae 100644 --- a/tests/client/calendar/test_calendar_operations.py +++ b/tests/client/calendar/test_calendar_operations.py @@ -273,6 +273,86 @@ async def test_update_event(nc_client: NextcloudClient, temporary_event: dict): 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 ): diff --git a/tests/server/test_calendar_events_mcp.py b/tests/server/test_calendar_events_mcp.py new file mode 100644 index 0000000..1f4e218 --- /dev/null +++ b/tests/server/test_calendar_events_mcp.py @@ -0,0 +1,124 @@ +"""Integration tests for Calendar VEVENT update MCP tools - extended fields.""" + +import json +import logging +from datetime import datetime, timedelta + +import pytest +from mcp import ClientSession + +from nextcloud_mcp_server.client import NextcloudClient + +logger = logging.getLogger(__name__) +pytestmark = pytest.mark.integration + + +async def test_mcp_update_event_extended_fields( + nc_mcp_client: ClientSession, nc_client: NextcloudClient, temporary_calendar: str +): + """Test updating categories, recurrence_rule, attendees, and reminder_minutes via MCP.""" + + calendar_name = temporary_calendar + event_uid = None + + try: + # 1. Create a base event via MCP + tomorrow = datetime.now() + timedelta(days=1) + create_result = await nc_mcp_client.call_tool( + "nc_calendar_create_event", + { + "calendar_name": calendar_name, + "title": "Extended Fields MCP Test", + "start_datetime": tomorrow.strftime("%Y-%m-%dT14:00:00"), + "end_datetime": tomorrow.strftime("%Y-%m-%dT15:00:00"), + "description": "Base event for MCP extended-field update test", + }, + ) + assert create_result.isError is False, ( + f"MCP event creation failed: {create_result.content}" + ) + + result_data = json.loads(create_result.content[0].text) + event_uid = result_data["uid"] + logger.info(f"Created base event via MCP: {event_uid}") + + # 2. Update with all four extended fields via MCP + update_result = await nc_mcp_client.call_tool( + "nc_calendar_update_event", + { + "calendar_name": calendar_name, + "event_uid": event_uid, + "categories": "work,meeting", + "recurrence_rule": "FREQ=WEEKLY;COUNT=4", + "attendees": "alice@example.com,bob@example.com", + "reminder_minutes": 15, + }, + ) + assert update_result.isError is False, ( + f"MCP event update failed: {update_result.content}" + ) + + # 3. Verify via direct client + event, _ = await nc_client.calendar.get_event(calendar_name, event_uid) + + # Categories + assert "work" in event.get("categories", ""), ( + f"Expected 'work' in categories, got: {event.get('categories')}" + ) + assert "meeting" in event.get("categories", ""), ( + f"Expected 'meeting' in categories, got: {event.get('categories')}" + ) + + # Recurrence + assert event.get("recurring") is True, "Expected event to be recurring" + assert "WEEKLY" in event.get("recurrence_rule", ""), ( + f"Expected WEEKLY in rrule, got: {event.get('recurrence_rule')}" + ) + + # Attendees + attendees = event.get("attendees", "") + assert "alice@example.com" in attendees, ( + f"Expected alice in attendees, got: {attendees}" + ) + assert "bob@example.com" in attendees, ( + f"Expected bob in attendees, got: {attendees}" + ) + + logger.info("MCP extended fields update verified successfully") + + # 4. Clear all four fields via MCP + clear_result = await nc_mcp_client.call_tool( + "nc_calendar_update_event", + { + "calendar_name": calendar_name, + "event_uid": event_uid, + "categories": "", + "recurrence_rule": "", + "attendees": "", + "reminder_minutes": 0, + }, + ) + assert clear_result.isError is False, ( + f"MCP event clear failed: {clear_result.content}" + ) + + # 5. Verify fields cleared + cleared, _ = await nc_client.calendar.get_event(calendar_name, event_uid) + assert not cleared.get("categories"), ( + f"Expected categories cleared, got: {cleared.get('categories')}" + ) + assert cleared.get("recurring") is not True, ( + f"Expected recurring cleared, got: {cleared.get('recurring')}" + ) + assert not cleared.get("attendees"), ( + f"Expected attendees cleared, got: {cleared.get('attendees')}" + ) + + logger.info("MCP extended fields clear verified successfully") + + finally: + if event_uid: + try: + await nc_client.calendar.delete_event(calendar_name, event_uid) + except Exception: + pass