Skip to content

Commit 018a055

Browse files
fix(jmap): guard create_event against malformed server response; preserve RELATED=END in alarm round-trip
1 parent b2968a9 commit 018a055

5 files changed

Lines changed: 44 additions & 1 deletion

File tree

caldav/jmap/async_client.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,11 @@ async def create_event(self, calendar_id: str, ical_str: str) -> str:
175175
created, _, _, not_created, _, _ = parse_event_set(resp_args)
176176
if "new-0" in not_created:
177177
self._raise_set_error(session, not_created["new-0"])
178+
if "new-0" not in created:
179+
raise JMAPMethodError(
180+
url=session.api_url,
181+
reason="CalendarEvent/set response missing created entry for new-0",
182+
)
178183
return created["new-0"]["id"]
179184

180185
raise JMAPMethodError(url=session.api_url, reason="No CalendarEvent/set response")

caldav/jmap/client.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,11 @@ def create_event(self, calendar_id: str, ical_str: str) -> str:
254254
created, _, _, not_created, _, _ = parse_event_set(resp_args)
255255
if "new-0" in not_created:
256256
self._raise_set_error(session, not_created["new-0"])
257+
if "new-0" not in created:
258+
raise JMAPMethodError(
259+
url=session.api_url,
260+
reason="CalendarEvent/set response missing created entry for new-0",
261+
)
257262
return created["new-0"]["id"]
258263

259264
raise JMAPMethodError(url=session.api_url, reason="No CalendarEvent/set response")

caldav/jmap/convert/ical_to_jscal.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,8 @@ def _valarm_to_alert(alarm) -> tuple[str, dict]:
230230
if isinstance(trigger_val, timedelta):
231231
# Relative trigger — convert to SignedDuration string
232232
alert["trigger"] = _timedelta_to_duration(trigger_val)
233+
if str(trigger_prop.params.get("RELATED", "START")).upper() == "END":
234+
alert["relativeTo"] = "end"
233235
elif isinstance(trigger_val, datetime):
234236
# Absolute trigger — UTCDateTime string
235237
alert["trigger"] = trigger_val.strftime("%Y-%m-%dT%H:%M:%SZ")

caldav/jmap/convert/jscal_to_ical.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,10 @@ def _alert_to_valarm(alert: dict) -> icalendar.Alarm:
243243
else:
244244
try:
245245
td = _duration_to_timedelta(trigger_str)
246-
alarm.add("trigger", td)
246+
trigger = icalendar.vDuration(td)
247+
if alert.get("relativeTo") == "end":
248+
trigger.params["RELATED"] = "END"
249+
alarm.add("trigger", trigger)
247250
except ValueError:
248251
alarm.add("trigger", timedelta(0))
249252
else:

tests/test_jmap_unit.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1172,6 +1172,20 @@ def test_valarm_absolute(self):
11721172
alert = next(iter(result["alerts"].values()))
11731173
assert alert["trigger"].endswith("Z")
11741174

1175+
def test_valarm_related_end(self):
1176+
ical = _make_ical(
1177+
"DTSTART:20240615T100000Z\r\n"
1178+
"SUMMARY:End Alarm Event\r\n"
1179+
"BEGIN:VALARM\r\n"
1180+
"ACTION:DISPLAY\r\n"
1181+
"TRIGGER;RELATED=END:-PT5M\r\n"
1182+
"END:VALARM\r\n"
1183+
)
1184+
result = ical_to_jscal(ical)
1185+
alert = next(iter(result["alerts"].values()))
1186+
assert alert["trigger"] == "-PT5M"
1187+
assert alert.get("relativeTo") == "end"
1188+
11751189
def test_organizer_attendee(self):
11761190
ical = _make_ical(
11771191
"DTSTART:20240615T100000Z\r\n"
@@ -1367,6 +1381,14 @@ def test_alert_relative(self):
13671381
assert "BEGIN:VALARM" in result
13681382
assert "TRIGGER:-PT15M" in result
13691383

1384+
def test_alert_related_end(self):
1385+
jscal = _minimal_jscal(
1386+
alerts={"al1": {"trigger": "-PT5M", "action": "display", "relativeTo": "end"}}
1387+
)
1388+
result = jscal_to_ical(jscal)
1389+
assert "RELATED=END" in result
1390+
assert "-PT5M" in result
1391+
13701392
def test_participants_organizer(self):
13711393
jscal = _minimal_jscal(
13721394
participants={
@@ -1542,6 +1564,12 @@ def test_create_event_raises_on_failure(self, monkeypatch):
15421564
client.create_event("cal1", self._MINIMAL_ICAL)
15431565
assert exc_info.value.error_type == "invalidArguments"
15441566

1567+
def test_create_event_raises_on_malformed_response(self, monkeypatch):
1568+
resp = self._set_response(created={}, notCreated={})
1569+
client = _make_client_with_mocked_session(monkeypatch, resp)
1570+
with pytest.raises(JMAPMethodError):
1571+
client.create_event("cal1", self._MINIMAL_ICAL)
1572+
15451573
def test_create_event_passes_calendar_id(self, monkeypatch):
15461574
resp = self._set_response(created={"new-0": {"id": "sv-2"}})
15471575
client, captured = self._capturing_client(monkeypatch, resp)

0 commit comments

Comments
 (0)