/** * 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 MIXED = '' + '/sami7777/calendar/a.ics' + '"aa"' + 'BEGIN: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'); expect(out[1].href).toBe('/sami7777/calendar/b.ics'); expect(out[1].calendarData).toContain('BEGIN:VCALENDAR'); }); test('mixed styles in one document: two well-formed entries, no drop/dup', () => { 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([]); }); }); describe('DC-167 fixture validity (namespace-aware, fail-closed)', () => { let XMLParser; beforeAll(() => { ({ XMLParser } = require('fast-xml-parser')); // fail-closed: fixture validation is an acceptance criterion — a missing // parser must FAIL this suite, never skip it. expect(typeof XMLParser).toBe('function'); }); const parser = () => new XMLParser({ preserveOrder: true, removeNSPrefix: false, ignoreAttributes: false, processEntities: true }); test('all three fixtures are well-formed and namespace-parseable', () => { for (const xml of [BARE, PREFIXED, MIXED]) { const doc = parser().parse(xml); const ms = doc.find(n => 'multistatus' in n); expect(ms).toBeDefined(); const texts = JSON.stringify(ms); expect(texts).toContain('a.ics'); expect(texts).toContain('b.ics'); } }); test('undeclared d: prefix is REJECTED (scope-aware ns validation)', () => { // Scope-aware namespace validation per XML-NS: walk elements with a // stack of prefix bindings; each prefix in scope must be declared on // that element or an ancestor. Runs against ALL fixtures. const { XMLValidator } = require('fast-xml-parser'); const validateNs = (xml) => { const wf = XMLValidator.validate(xml); if (wf !== true) return { ok: false, reason: 'not well-formed: ' + JSON.stringify(wf.err || wf) }; const stack = [new Map()]; // scope 0: no bindings const tagRe = /<\/(?:([\w.-]+):)?([\w.-]+)>|<(?:([\w.-]+):)?([\w.-]+)([^>]*)>/g; for (const m of xml.matchAll(tagRe)) { const [full, closePrefix, closeLocal, openPrefix, openLocal, attrs] = m; if (closePrefix !== undefined || (openPrefix === undefined && full.startsWith('' }; } return { ok: true }; }; for (const xml of [BARE, PREFIXED, MIXED]) { expect(validateNs(xml)).toEqual({ ok: true }); } const bad = MIXED.replace(' xmlns:d="DAV:"', ''); const res = validateNs(bad); expect(res.ok).toBe(false); expect(res.reason).toMatch(/undeclared prefix/); }); test('mismatched close tag is REJECTED (well-formedness via XMLValidator)', () => { const { XMLValidator } = require('fast-xml-parser'); const bad = BARE.replace('', ''); const v = XMLValidator.validate(bad); expect(v).not.toBe(true); expect(v.err || v).toBeDefined(); }); test('extraction tests consume the exact validated fixtures', () => { // guard against fixture drift: MIXED used above is the same object the // extraction test parses expect(MIXED).toContain(''); expect(MIXED).toContain(''); }); });