fix: handle categories, recurrence_rule, attendees, and reminder_minutes in update_event

_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>
This commit is contained in:
Chris Coutinho
2026-02-08 23:15:37 +01:00
parent b3e55d444b
commit da104c59ac
3 changed files with 246 additions and 0 deletions
+42
View File
@@ -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"]
@@ -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
):
+124
View File
@@ -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