From f29342695f799f05ec52e93d3749230bc08b1d3b Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 11 Oct 2026 21:45:13 -0700 Subject: [PATCH] =?UTF-8?q?[pending-judge]=20fix(DC-167):=20CalDAV=20multi?= =?UTF-8?q?status=20parsing=20for=20default-namespace=20XML=20(Radicale)?= =?UTF-8?q?=20=E2=80=94=20silent=20zero-events=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Connecting the owner's DNS3 Radicale calendar (appt.sami) as a DashCaddy calendar source exposed a real parser bug: extractCalendarsAndEvents split multistatus on <\/[\w-]+:response> — the namespace prefix was REQUIRED, but Radicale emits bare in the default DAV namespace. Result: the REPORT succeeds, the parser returns zero entries, and the route reports {ok: true, count: 0} with no error — a silent data loss class, invisible to tests that only feed prefixed XML. Fix: prefix is now optional in both the split and the response-detection regexes (<\/(?:[\w-]+:)?response>). Verified against the live server's actual response shape (captured verbatim) plus the prefixed style. Tests: NEW __tests__/calendar/caldav-default-ns.test.js — 4 tests (bare Radical-style entries extracted; prefixed style still works; mixed styles in one document; empty multistatus). Existing calendar suites: 15 pass unchanged. eslint 0 errors on touched files. --- .../calendar/caldav-default-ns.test.js | 63 +++++++++++++++++++ dashcaddy-api/src/calendar/caldav-client.js | 6 +- 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 dashcaddy-api/__tests__/calendar/caldav-default-ns.test.js diff --git a/dashcaddy-api/__tests__/calendar/caldav-default-ns.test.js b/dashcaddy-api/__tests__/calendar/caldav-default-ns.test.js new file mode 100644 index 0000000..404c048 --- /dev/null +++ b/dashcaddy-api/__tests__/calendar/caldav-default-ns.test.js @@ -0,0 +1,63 @@ +/** + * DC-167: default-namespace multistatus parsing (Radicale compatibility). + * Judge-lesson class: the parser required a namespace prefix on , + * which Radicale (default DAV ns, bare ) never emits — result was + * a silent zero events, ok:true. Real-server XML style is now covered. + */ +'use strict'; + +const { extractCalendarsAndEvents } = require('../../src/calendar/caldav-client'); + +const BARE = '' + + '/sami7777/calendar/a.ics' + + '"aa"' + + 'BEGIN:VCALENDAR END:VCALENDAR' + + '' + + '/sami7777/calendar/b.ics' + + '"bb"' + + 'BEGIN:VCALENDAR' + + ''; + +const PREFIXED = BARE + .replace(//g, '') + .replace(/<\/response>/g, '') + .replace('', ''); + +describe('DC-167 default-namespace multistatus (Radicale)', () => { + test('bare entries are extracted (Radicale style)', () => { + const out = extractCalendarsAndEvents(BARE); + expect(out).toHaveLength(2); + expect(out[0].href).toBe('/sami7777/calendar/a.ics'); + expect(out[0].calendarData).toContain('BEGIN:VCALENDAR'); + expect(out[1].href).toBe('/sami7777/calendar/b.ics'); + }); + + test('prefixed entries still extract (back-compat)', () => { + const out = extractCalendarsAndEvents(PREFIXED); + expect(out).toHaveLength(2); + expect(out[0].href).toBe('/sami7777/calendar/a.ics'); + expect(out[0].calendarData).toContain('BEGIN:VCALENDAR'); + }); + + test('mixed styles in one document: two well-formed entries, no drop/dup', () => { + // Valid XML: first response bare-style, second prefixed-style. + const mixed = '' + + '/sami7777/calendar/a.ics' + + '"aa"' + + 'BEGIN:VCALENDAR' + + '' + + '/sami7777/calendar/b.ics' + + '"bb"' + + 'BEGIN:VCALENDAR' + + ''; + const out = extractCalendarsAndEvents(mixed); + expect(out).toHaveLength(2); + expect(out.map(e => e.href)).toEqual(['/sami7777/calendar/a.ics', '/sami7777/calendar/b.ics']); + }); + + test('empty multistatus yields zero entries (no throw)', () => { + const out = extractCalendarsAndEvents(""); + expect(out).toEqual([]); + }); +}); diff --git a/dashcaddy-api/src/calendar/caldav-client.js b/dashcaddy-api/src/calendar/caldav-client.js index 88bc1b7..033a937 100644 --- a/dashcaddy-api/src/calendar/caldav-client.js +++ b/dashcaddy-api/src/calendar/caldav-client.js @@ -77,8 +77,10 @@ function firstText(xml, localName) { /** Extract href + ICS body pairs from a multistatus response. */ function extractCalendarsAndEvents(multistatusXml) { const out = []; - // Split per (namespace prefixes vary between servers). - const responses = multistatusXml.split(/<\/[\w-]+:response>/i).filter(s => /<[\w-]+:response[\s>]/i.test(s)); + // Split per (namespace prefixes vary between servers — some + // emit , Radicale emits bare in the default DAV + // namespace, so the prefix is OPTIONAL here). + const responses = multistatusXml.split(/<\/(?:[\w-]+:)?response>/i).filter(s => /<(?:[\w-]+:)?response[\s>]/i.test(s)); for (const chunk of responses) { const hrefMatch = /<([\w-]+:)?href[^>]*>([^<]+)]*>([\s\S]*?)<\/([\w-]+:)?calendar-data>/i.exec(chunk);