Skip to content

Commit ccae99e

Browse files
authored
Merge pull request #816 from python-caldav/docs/tester-findings-2026-10-23
docs: record 2026-10-23 tester findings
2 parents f2f3407 + 2f3a9a0 commit ccae99e

2 files changed

Lines changed: 48 additions & 18 deletions

File tree

‎caldav/compatibility_hints.py‎

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1523,6 +1523,19 @@ def compare(self, observed):
15231523
}
15241524

15251525
ecloud = nextcloud | {
1526+
## 2026-10-23: MKCALENDAR failed with 403 "Calendar limit reached" (a
1527+
## SabreDAV Forbidden), making the server-tester grade create-calendar
1528+
## unsupported. The account held 27 live calendars, 15 of them empty
1529+
## leftovers with bare-uuid cal_ids from test runs. Calendars sitting in
1530+
## the trashbin count towards the limit too: deleting the leftovers gave
1531+
## room for one more creation only. A DELETE carrying the header
1532+
## "X-NC-CalDAV-No-Trashbin: 1" removes a calendar for good, and also
1533+
## works on one already in the trashbin (it still shows up in a raw
1534+
## depth-1 PROPFIND of the calendar home). With the trashbin purged,
1535+
## the next MKCALENDARs got 429 instead - the calendar-creation rate
1536+
## limit, after a dozen creations within the hour. The same run measured
1537+
## well-known unsupported: /.well-known/caldav redirects to
1538+
## /remote.php/dav/, which does not advertise calendar-access.
15261539
#'search.is-not-defined': {'support': 'unsupported'}, ## observed to work at 4bc0de765a2b53e6f223e0b9ac51c653bac11fb7 (caldav) / 3cae24cf99da1702b851b5a74a9b88c8e5317dad (server checker)
15271540
#'search.text.case-sensitive': {'support': 'unsupported'}, ## observed to work at 4bc0de765a2b53e6f223e0b9ac51c653bac11fb7 (caldav) / 3cae24cf99da1702b851b5a74a9b88c8e5317dad (server checker)
15281541
'delete-calendar.free-namespace': False,
@@ -1616,6 +1629,7 @@ def compare(self, observed):
16161629
'save-load.todo.mixed-calendar': {'support': 'unsupported'},
16171630
'save-load.todo.recurrences.count': {'support': 'unsupported'}, ## This is a new problem?
16181631
'save-load.journal': {'support': 'ungraceful'},
1632+
## One caldav-server-tester run on 2026-10-23 measured this ungraceful.
16191633
'sync-token': {'support': 'fragile'},
16201634
'search.is-not-defined': {'support': 'unsupported'},
16211635
'search.text': {'support': 'unsupported'},
@@ -1736,16 +1750,10 @@ def compare(self, observed):
17361750
## Measured with caldav-server-tester, 2026-10-19.
17371751
"save.no-uid-conflict": {"support": "ungraceful", "behaviour": "refused with a bare 412 when the PUT carries If-None-Match: *"},
17381752
## Writes are asynchronous: a read-back issued immediately after a PUT may
1739-
## 404 or hand back the pre-write copy. That is what separated the two
1740-
## measurement runs - save-load.mutable came out "broken" (modification not
1741-
## reflected after save and reload) in one and "full" in the other, and the
1742-
## timezone probe's load() 404ed on a resource the PUT had just accepted.
1743-
## The delay is what makes the rest of this profile reproducible. Yet the
1744-
## synchronous-write probe itself - one PUT, one direct GET - reads the
1745-
## object back at once (save-load-delay 0, 2026-09-13), so it is too fast
1746-
## to catch reliably: fragile, not unsupported. Four runs on 2026-10-20/21
1747-
## all measured ~1s; it stays fragile until more rounds show that the 0
1748-
## does not come back.
1753+
## 404 or hand back the pre-write copy. However, in some cases the
1754+
## delay is small and varies, sometimes the caldav-server-tester tool will
1755+
## not catch the async behaviour, hence it's rated "fragile" rather than
1756+
## "unsupported".
17491757
"synchronous-write": {"support": "fragile", "delay": 3},
17501758

17511759
## MKCALENDAR works, but a successful one that sets no properties is
@@ -1778,16 +1786,19 @@ def compare(self, observed):
17781786
"support": "unsupported",
17791787
"behaviour": "the restriction is accepted with a 200 ok propstat and then ignored; the collection is VEVENT-only",
17801788
},
1781-
## So whether Bedework stores tasks is unknown rather than unsupported: the
1782-
## server has a task collection type, a CalDAV client just cannot make one.
1783-
## What was measured is that a VTODO does not go into an event calendar;
1784-
## together with the ignored component set that leaves a client nowhere to
1785-
## put a task. The children inherit "unknown". The same goes for journals.
1786-
"save-load.todo": {"support": "unknown"},
1789+
## The caldav-server-tester will yield "unsupported" on tasks and journals.
1790+
## Bedework can store tasks on a tasklist, such task lists have been found
1791+
## in the demo data, but the CalDAV user cannot create a tasklist.
1792+
## The grading is hence set to "fragile". Perhaps Bedework also supports
1793+
## special journal collections, but this is "unknown" as for now.
1794+
"save-load.todo": {
1795+
"support": "fragile",
1796+
"behaviour": "only in a server-provisioned task collection; a client cannot create one, and a VTODO PUT into an event calendar is 403",
1797+
},
17871798
"save-load.todo.mixed-calendar": {"support": "unsupported"},
17881799
"save-load.journal": {"support": "unknown"},
17891800
"save-load.journal.mixed-calendar": {"support": "unsupported"},
1790-
## Not measured either: the tester had no tasks to search for. A Bedework
1801+
## Not measured: the tester had no task collection to search in. A Bedework
17911802
## user with a working `tasks` collection may well see this work.
17921803
"search.time-range.todo": {"support": "unknown"},
17931804

@@ -2060,6 +2071,8 @@ def compare(self, observed):
20602071
## .delete() degrades to wiping the objects, so a cal_id is never freed for
20612072
## reuse and a later MKCALENDAR at the same id gets 405.
20622073
'delete-calendar': False,
2074+
## sync-token (fragile, inherited from davical) and sync-token.delete
2075+
## both came out full in a server-tester run on 2026-10-23.
20632076
## (scheduling.mailbox.inbox-delivery used to be pinned False here. It was
20642077
## added in a bulk hints commit with no per-server evidence, and it cannot be
20652078
## probed on the configured server - the checker needs a second user account,
@@ -2107,6 +2120,8 @@ def compare(self, observed):
21072120
},
21082121
## includes-implicit.todo has been observed as both supported and unsupported
21092122
## across different test runs. Other includes-implicit children are unsupported.
2123+
## A run on 2026-10-23 measured event, infinite-scope, todo and todo.pending
2124+
## all unsupported.
21102125
## Marking the parent as fragile to avoid cascading derivation issues.
21112126
"search.recurrences.includes-implicit": {
21122127
"support": "fragile"
@@ -2194,6 +2209,7 @@ def compare(self, observed):
21942209
"search.time-range.alarm": {'support': 'unsupported'},
21952210
"search.text": { "support": "unsupported", "behaviour": "a text search ignores the filter and returns all elements" },
21962211
"search.recurrences.expanded.todo": { "support": "unsupported" },
2212+
## 2026-10-23: one server-tester run measured this full.
21972213
"search.recurrences.expanded.event": { "support": "fragile" },
21982214
'search.recurrences.includes-implicit.todo': {'support': 'unsupported'},
21992215
'principal-search': {'support': 'ungraceful'},
@@ -2551,6 +2567,8 @@ def compare(self, observed):
25512567
## 2026-10-11: two server-tester runs half an hour apart; in the first
25522568
## every MKCALENDAR/MKCOL attempt failed, in the second calendars were
25532569
## created (at the requested URL) and deleted on the first attempt.
2570+
## 2026-10-23: one run created calendars fine (set-displayname and
2571+
## stable-url full too).
25542572
'create-calendar': {'support': 'fragile'},
25552573
'create-calendar.auto': {'support': 'unsupported'},
25562574
'create-calendar.with-supported-component-types': {'support': 'unsupported'},
@@ -2582,7 +2600,9 @@ def compare(self, observed):
25822600
## but worked 2026-10-11
25832601
'search.time-range.open.end': {'support': 'fragile'},
25842602
## Open-start searches used to work (no hint, so full), but were
2585-
## rejected with 403 Forbidden 2026-10-11
2603+
## rejected with 403 Forbidden 2026-10-11. 2026-10-23: ungraceful
2604+
## again, while open.end, open.start.duration, comp-type.optional and
2605+
## includes-implicit.infinite-scope all came out full.
25862606
'search.time-range.open.start': {'support': 'fragile'},
25872607
## Probed 2026-10-20
25882608
"save-load.todo.uncomplete": {

‎docs/design/TODO.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,13 @@ iCloud leaves a moved instance (`RECURRENCE-ID` 2026-10-31, `DTSTART` 2026-10-17
6262
* Workaround when `by-moved-dt` is unsupported: an extra query for VEVENTs that have a `RECURRENCE-ID`. In CalDAV that is an empty `prop-filter` (RFC 4791 section 9.7.2; there is no `is-defined` element). `search()` decides `def` client-side and leaves it out of the server query, so the filter must be sent server-side (`post_filter=False`, or a raw query), or the extra query fetches every event. Filter the results client-side and merge them in, deduplicated on the resource URL, or on UID and `RECURRENCE-ID` when the search expands. The `_TIME_RANGE_MARGIN` widening can't do it, as an instance may be moved any distance. The tester check should probe the empty prop-filter on iCloud directly (`search.is-not-defined` being `fragile` there says little about it); else fall back to fetching all recurring events.
6363
* Cost: without a time range, the extra query downloads every event with overrides in the calendar, on every event time-range search, and that grows without bound. Bound it (a range covering the RRULE horizon) or cache it, and make it opt-out. Report it to Apple too.
6464
* The reporter (Home Assistant, https://github.com/home-assistant/core/issues/179125) offered to test.
65+
66+
## Purging deleted calendars from the Nextcloud trashbin
67+
68+
On Nextcloud a deleted calendar moves to the trashbin, keeps its cal_id occupied (`delete-calendar.free-namespace` unsupported with the trashbin on, as on ecloud) and still counts towards the per-user calendar limit: on ecloud, 2026-10-23, MKCALENDAR failed with 403 "Calendar limit reached" at 27 live calendars plus those in the trashbin. A DELETE carrying the header `X-NC-CalDAV-No-Trashbin: 1` removes a calendar for good, and also works on one already in the trashbin (it still shows in a raw depth-1 PROPFIND of the calendar home, though `get_calendars()` hides it). Support it in the library: a compatibility feature (e.g. a `behaviour` on `delete-calendar`, or a new `delete-calendar.purge`) telling `Calendar.delete()` to send the header, maybe a `purge=True` argument, and a caldav-server-tester probe for it. That would also make `free-namespace` hold for Nextcloud when purging.
69+
70+
## A recognisable cal_id for every test calendar
71+
72+
The tests and the caldav-server-tester leave calendars behind, and on servers that cap the number of calendars per principal (ecloud, see above) that eventually blocks all creation. On 2026-10-23 ecloud held 15 empty calendars with bare-uuid cal_ids and the uuid as display name; which code made them was not found. Every calendar the tests or the tester create should carry a cal_id prefix, like the caldav-server-tester's `csc_` UIDs, so a cleanup step can delete leftovers safely - with the purge from the section above where the server has a trashbin.
73+
* caldav tests: there is no common cal_id prefix on every calendar, and no sweep. Several tests create calendars by name only (`test_caldav.py` "CalConfigA"/"CalConfigB", "XandikosMetaCal"/"RadicaleMetaCal", `test_get_calendars_sections.py`, `test_examples.py`), so `make_calendar()` gives them a bare uuid. Give all of them a `pythoncaldav-test` prefixed cal_id and add a session-level sweep.
74+
* caldav-server-tester: has `PROBE_CALENDAR_PREFIXES` and a sweep matching cal_id or display name, but `CheckDuplicateUID` creates its second calendar by name only, so its cal_id is a bare uuid that the sweep cannot recognise when the server drops the display name. Pass a prefixed cal_id. Its sweep also only moves calendars to the Nextcloud trashbin.

0 commit comments

Comments
 (0)