AtlatestRepositorysigil-caldav

sigil-caldav / tree / testtest-caldav.sgl

1;;; Tests for CalDAV client library (offline, no live server needed)
2
3(import (sigil test)
4 (sigil string)
5 (sigil time)
6 (sigil caldav ical)
7 (sigil caldav client)
8 (sigil sxml reader))
9
11;; ============================================================
12;; iCalendar DateTime Conversion
13;; ============================================================
15(test-group "ical-datetime"
17 (test "parses UTC datetime"
18 (let ((ts (parse-ical-datetime "20240315T090000Z")))
19 (assert-equal "20240315T090000Z" (format-ical-datetime ts))))
21 (test "parses local datetime in system timezone"
22 (let* ((ts-local (parse-ical-datetime "20240315T090000"))
23 (ts-utc (parse-ical-datetime "20240315T090000Z")))
24 ;; Local time differs from UTC by the offset at that date
25 (assert-equal (- ts-utc (time-utc-offset-at ts-utc)) ts-local)))
27 (test "local datetime uses date-specific offset, not current offset"
28 ;; Parse two dates in different DST periods and verify correct offsets.
29 (let* ((winter-local (parse-ical-datetime "20260115T120000"))
30 (winter-utc (parse-ical-datetime "20260115T120000Z"))
31 (summer-local (parse-ical-datetime "20260715T120000"))
32 (summer-utc (parse-ical-datetime "20260715T120000Z")))
33 (assert-equal (- winter-utc (time-utc-offset-at winter-utc)) winter-local)
34 (assert-equal (- summer-utc (time-utc-offset-at summer-utc)) summer-local)))
36 (test "round-trips midnight"
37 (let ((ts (parse-ical-datetime "20240101T000000Z")))
38 (assert-equal "20240101T000000Z" (format-ical-datetime ts))))
40 (test "round-trips end of day"
41 (let ((ts (parse-ical-datetime "20241231T235959Z")))
42 (assert-equal "20241231T235959Z" (format-ical-datetime ts))))
44 (test "handles leap year date"
45 (let ((ts (parse-ical-datetime "20240229T120000Z")))
46 (assert-equal "20240229T120000Z" (format-ical-datetime ts))))
48 (test "parses date-only"
49 (let ((ts (parse-ical-date "20240315")))
50 (assert-equal "20240315" (format-ical-date ts))))
52 (test "round-trips date-only Jan 1"
53 (let ((ts (parse-ical-date "20240101")))
54 (assert-equal "20240101" (format-ical-date ts))))
56 (test "round-trips date-only Dec 31"
57 (let ((ts (parse-ical-date "20241231")))
58 (assert-equal "20241231" (format-ical-date ts))))
60 (test "known timestamp value"
61 ;; 2024-03-15 09:00:00 UTC = 1710493200
62 (assert-equal 1710493200 (parse-ical-datetime "20240315T090000Z")))
64 (test "epoch is correct"
65 (assert-equal 0 (parse-ical-datetime "19700101T000000Z")))
67 (test "parses datetime with explicit timezone"
68 ;; 11:00 America/New_York on 2026-03-26 is EDT (UTC-4) = 15:00 UTC
69 (let ((ts (parse-ical-datetime "20260326T110000" "America/New_York")))
70 (assert-equal "20260326T150000Z" (format-ical-datetime ts))))
72 (test "parses datetime with different timezone than system"
73 ;; 09:00 America/Los_Angeles on 2026-03-26 is PDT (UTC-7) = 16:00 UTC
74 (let ((ts (parse-ical-datetime "20260326T090000" "America/Los_Angeles")))
75 (assert-equal "20260326T160000Z" (format-ical-datetime ts))))
77 (test "timezone param ignored for UTC datetimes"
78 ;; Z suffix means UTC regardless of any timezone hint
79 (let ((ts (parse-ical-datetime "20260326T150000Z" "America/New_York")))
80 (assert-equal "20260326T150000Z" (format-ical-datetime ts))))
82 (test "timezone handles DST correctly"
83 ;; America/New_York: EST (UTC-5) in January, EDT (UTC-4) in July
84 (let ((winter (parse-ical-datetime "20260115T120000" "America/New_York"))
85 (summer (parse-ical-datetime "20260715T120000" "America/New_York")))
86 ;; Winter: 12:00 EST = 17:00 UTC
87 (assert-equal "20260115T170000Z" (format-ical-datetime winter))
88 ;; Summer: 12:00 EDT = 16:00 UTC
89 (assert-equal "20260715T160000Z" (format-ical-datetime summer))))
91 (test "cross-timezone event parsed correctly via ical-parse"
92 (let* ((ical-text "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:[email protected]\r\nDTSTART;TZID=America/New_York:20260326T110000\r\nDTEND;TZID=America/New_York:20260326T120000\r\nSUMMARY:Cross-TZ Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
93 (events (ical-parse ical-text))
94 (ev (car events)))
95 ;; 11:00 EDT = 15:00 UTC, 12:00 EDT = 16:00 UTC
96 (assert-equal "20260326T150000Z" (format-ical-datetime (ical-event-dtstart ev)))
97 (assert-equal "20260326T160000Z" (format-ical-datetime (ical-event-dtend ev)))
98 (assert-equal "America/New_York" (ical-event-timezone ev)))))
101;; ============================================================
102;; iCalendar Parsing
103;; ============================================================
105(define simple-ical
106 "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:[email protected]\r\nDTSTART:20240315T090000Z\r\nDTEND:20240315T100000Z\r\nSUMMARY:Team Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
108(define detailed-ical
109 "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:[email protected]\r\nDTSTART:20240315T090000Z\r\nDTEND:20240315T100000Z\r\nSUMMARY:Project Review\r\nDESCRIPTION:Quarterly review of all projects\r\nLOCATION:Conference Room B\r\nSTATUS:CONFIRMED\r\nORGANIZER:mailto:[email protected]\r\nATTENDEE:mailto:[email protected]\r\nATTENDEE:mailto:[email protected]\r\nCREATED:20240301T120000Z\r\nLAST-MODIFIED:20240310T080000Z\r\nEND:VEVENT\r\nEND:VCALENDAR")
111(define allday-ical
112 "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:[email protected]\r\nDTSTART;VALUE=DATE:20240315\r\nDTEND;VALUE=DATE:20240316\r\nSUMMARY:Company Holiday\r\nEND:VEVENT\r\nEND:VCALENDAR")
114(define folded-ical
115 "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:[email protected]\r\nDTSTART:20240315T090000Z\r\nDTEND:20240315T100000Z\r\nSUMMARY:This is a very long summary that should be folded across\r\n multiple lines in the iCalendar format\r\nEND:VEVENT\r\nEND:VCALENDAR")
117(define multi-event-ical
118 "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:[email protected]\r\nDTSTART:20240315T090000Z\r\nSUMMARY:Event One\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:[email protected]\r\nDTSTART:20240316T140000Z\r\nSUMMARY:Event Two\r\nEND:VEVENT\r\nEND:VCALENDAR")
120(test-group "ical-parse"
122 (test "parses simple event"
123 (let ((events (ical-parse simple-ical)))
124 (assert-equal 1 (length events))
125 (let ((ev (car events)))
126 (assert-equal "[email protected]" (ical-event-uid ev))
127 (assert-equal "Team Meeting" (ical-event-summary ev))
128 (assert-equal (parse-ical-datetime "20240315T090000Z") (ical-event-dtstart ev))
129 (assert-equal (parse-ical-datetime "20240315T100000Z") (ical-event-dtend ev))
130 (assert-false (ical-event-all-day? ev)))))
132 (test "parses detailed event"
133 (let* ((events (ical-parse detailed-ical))
134 (ev (car events)))
135 (assert-equal "[email protected]" (ical-event-uid ev))
136 (assert-equal "Project Review" (ical-event-summary ev))
137 (assert-equal "Quarterly review of all projects" (ical-event-description ev))
138 (assert-equal "Conference Room B" (ical-event-location ev))
139 (assert-equal "CONFIRMED" (ical-event-status ev))
140 (assert-equal "[email protected]" (ical-event-organizer ev))
141 (assert-equal 2 (length (ical-event-attendees ev)))
142 (assert-equal "[email protected]" (ical-attendee-email (car (ical-event-attendees ev))))
143 (assert-equal "[email protected]" (ical-attendee-email (cadr (ical-event-attendees ev))))
144 (assert-true (ical-event-created ev))
145 (assert-true (ical-event-last-modified ev))))
147 (test "parses all-day event"
148 (let* ((events (ical-parse allday-ical))
149 (ev (car events)))
150 (assert-equal "[email protected]" (ical-event-uid ev))
151 (assert-equal "Company Holiday" (ical-event-summary ev))
152 (assert-true (ical-event-all-day? ev))
153 (assert-equal (parse-ical-date "20240315") (ical-event-dtstart ev))
154 (assert-equal (parse-ical-date "20240316") (ical-event-dtend ev))))
156 (test "handles folded lines"
157 (let* ((events (ical-parse folded-ical))
158 (ev (car events)))
159 (assert-equal "[email protected]" (ical-event-uid ev))
160 (assert-true (> (string-length (ical-event-summary ev)) 50))))
162 (test "parses multiple events"
163 (let ((events (ical-parse multi-event-ical)))
164 (assert-equal 2 (length events))
165 (assert-equal "Event One" (ical-event-summary (car events)))
166 (assert-equal "Event Two" (ical-event-summary (cadr events))))))
169;; ============================================================
170;; iCalendar Generation
171;; ============================================================
173(test-group "ical-generate"
175 (test "generates valid iCalendar text"
176 (let* ((ev (ical-event uid: "gen-test-1"
177 summary: "Test Event"
178 dtstart: (parse-ical-datetime "20240315T090000Z")
179 dtend: (parse-ical-datetime "20240315T100000Z")))
180 (text (ical-generate ev)))
181 (assert-true (string? text))
182 (assert-true (> (string-length text) 0))
183 ;; Check key content lines are present
184 (assert-true (string-contains? text "BEGIN:VCALENDAR"))
185 (assert-true (string-contains? text "VERSION:2.0"))
186 (assert-true (string-contains? text "BEGIN:VEVENT"))
187 (assert-true (string-contains? text "UID:gen-test-1"))
188 (assert-true (string-contains? text "SUMMARY:Test Event"))
189 (assert-true (string-contains? text "DTSTART:20240315T090000Z"))
190 (assert-true (string-contains? text "DTEND:20240315T100000Z"))
191 (assert-true (string-contains? text "END:VEVENT"))
192 (assert-true (string-contains? text "END:VCALENDAR"))))
194 (test "generates all-day event with VALUE=DATE"
195 (let* ((ev (ical-event uid: "allday-gen-1"
196 summary: "Holiday"
197 dtstart: (parse-ical-date "20240315")
198 dtend: (parse-ical-date "20240316")
199 all-day?: #t))
200 (text (ical-generate ev)))
201 (assert-true (string-contains? text "DTSTART;VALUE=DATE:20240315"))
202 (assert-true (string-contains? text "DTEND;VALUE=DATE:20240316"))))
204 (test "includes optional fields when set"
205 (let* ((ev (ical-event uid: "opt-1"
206 summary: "Full Event"
207 dtstart: (parse-ical-datetime "20240315T090000Z")
208 description: "A detailed description"
209 location: "Room 42"
210 status: "CONFIRMED"
211 organizer: "[email protected]"
212 attendees: '("[email protected]")))
213 (text (ical-generate ev)))
214 (assert-true (string-contains? text "DESCRIPTION:A detailed description"))
215 (assert-true (string-contains? text "LOCATION:Room 42"))
216 (assert-true (string-contains? text "STATUS:CONFIRMED"))
217 (assert-true (string-contains? text "ORGANIZER:mailto:[email protected]"))
218 (assert-true (string-contains? text "ATTENDEE:mailto:[email protected]"))))
220 (test "generates attendee with parameters from dict"
221 (let* ((att (ical-attendee "[email protected]" name: "Bob"))
222 (ev (ical-event uid: "att-params-1"
223 summary: "Meeting"
224 dtstart: (parse-ical-datetime "20240315T090000Z")
225 organizer: "[email protected]"
226 attendees: (list att)))
227 (text (ical-generate ev)))
228 ;; Check params are present (line may be folded at 75 chars)
229 (assert-true (string-contains? text "ATTENDEE;CN=Bob;PARTSTAT=NEEDS-ACTION"))
230 (assert-true (string-contains? text "RSVP=TRUE"))
231 ;; Round-trip: verify attendee preserved through parse
232 (let* ((events (ical-parse text))
233 (ev2 (car events))
234 (att2 (car (ical-event-attendees ev2))))
235 (assert-equal "[email protected]" (ical-attendee-email att2)))))
237 (test "generates event with TZID when timezone is set"
238 (let* ((ev (ical-event uid: "tz-test-1"
239 summary: "Local Meeting"
240 dtstart: (parse-ical-datetime "20260323T150000Z")
241 dtend: (parse-ical-datetime "20260323T160000Z")
242 timezone: "Europe/Athens"))
243 (text (ical-generate ev)))
244 (assert-true (string-contains? text "DTSTART;TZID=Europe/Athens:"))
245 (assert-true (string-contains? text "DTEND;TZID=Europe/Athens:"))
246 ;; Should NOT have bare DTSTART: or Z suffix for times
247 (assert-false (string-contains? text "DTSTART:20260323"))
248 (assert-false (string-contains? text "DTEND:20260323"))))
250 (test "generates UTC format when no timezone set"
251 (let* ((ev (ical-event uid: "no-tz-1"
252 summary: "UTC Event"
253 dtstart: (parse-ical-datetime "20260323T150000Z")
254 dtend: (parse-ical-datetime "20260323T160000Z")))
255 (text (ical-generate ev)))
256 (assert-true (string-contains? text "DTSTART:20260323T150000Z"))
257 (assert-true (string-contains? text "DTEND:20260323T160000Z"))))
259 (test "parses TZID from DTSTART params"
260 (let* ((ical-text "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:[email protected]\r\nDTSTART;TZID=Europe/Athens:20260323T170000\r\nDTEND;TZID=Europe/Athens:20260323T180000\r\nSUMMARY:Athens Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
261 (events (ical-parse ical-text))
262 (ev (car events)))
263 (assert-equal "Europe/Athens" (ical-event-timezone ev))
264 (assert-equal "Athens Meeting" (ical-event-summary ev))))
266 (test "round-trip: parse then generate preserves key fields"
267 (let* ((events (ical-parse simple-ical))
268 (ev (car events))
269 (text (ical-generate ev))
270 (events2 (ical-parse text))
271 (ev2 (car events2)))
272 (assert-equal (ical-event-uid ev) (ical-event-uid ev2))
273 (assert-equal (ical-event-summary ev) (ical-event-summary ev2))
274 (assert-equal (ical-event-dtstart ev) (ical-event-dtstart ev2))
275 (assert-equal (ical-event-dtend ev) (ical-event-dtend ev2)))))
278;; ============================================================
279;; UID Generation
280;; ============================================================
282(test-group "generate-event-uid"
284 (test "produces non-empty string"
285 (let ((uid (generate-event-uid)))
286 (assert-true (string? uid))
287 (assert-true (> (string-length uid) 0))))
289 (test "produces unique values"
290 (let ((uid1 (generate-event-uid))
291 (uid2 (generate-event-uid)))
292 (assert-false (equal? uid1 uid2)))))
295;; ============================================================
296;; Recurrence Expansion
297;; ============================================================
299(test-group "ical-expand-recurrence"
301 (test "non-recurring event returns itself"
302 (let ((ev (ical-event uid: "norec"
303 dtstart: 1710493200 dtend: 1710496800)))
304 (let ((expanded (ical-expand-recurrence ev)))
305 (assert-equal 1 (length expanded)))))
307 (test "daily recurrence with COUNT"
308 (let ((ev (ical-event uid: "daily-1"
309 dtstart: 1710493200
310 dtend: 1710496800
311 rrule: "FREQ=DAILY;COUNT=5")))
312 (let ((expanded (ical-expand-recurrence ev)))
313 (assert-equal 5 (length expanded))
314 ;; Each event should be 1 day apart
315 (assert-equal (+ 1710493200 86400)
316 (ical-event-dtstart (cadr expanded))))))
318 (test "weekly recurrence with BYDAY"
319 (let ((ev (ical-event uid: "weekly-1"
320 dtstart: 1710144000 ;; 2024-03-11 Monday 08:00 UTC
321 dtend: 1710147600
322 rrule: "FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=6")))
323 (let ((expanded (ical-expand-recurrence ev)))
324 (assert-equal 6 (length expanded)))))
326 (test "monthly recurrence with COUNT"
327 (let ((ev (ical-event uid: "monthly-1"
328 dtstart: 1710493200
329 dtend: 1710496800
330 rrule: "FREQ=MONTHLY;COUNT=3")))
331 (let ((expanded (ical-expand-recurrence ev)))
332 (assert-equal 3 (length expanded)))))
334 (test "daily recurrence respects date range"
335 (let* ((start 1710493200)
336 (ev (ical-event uid: "range-1"
337 dtstart: start
338 dtend: (+ start 3600)
339 rrule: "FREQ=DAILY;COUNT=30")))
340 (let ((expanded (ical-expand-recurrence ev
341 start: start
342 end: (+ start (* 7 86400)))))
343 ;; Should only include events within the 7-day range
344 (assert-true (<= (length expanded) 7))
345 (assert-true (> (length expanded) 0)))))
347 ;; TZID-aware recurrence: a weekly event tagged with a TZID anchors
348 ;; each occurrence to a fixed wall-clock in that timezone, even when
349 ;; the recurrence walks across a DST boundary. UTC-second arithmetic
350 ;; would drift the wall-clock by 1h after Spring/Fall transitions.
352 (test "weekly TZID=Europe/Athens master in winter, occurrence in DST"
353 ;; Master: 2026-01-13 (Tuesday, EET=UTC+2) at 17:00 Athens.
354 ;; 17:00 EET = 15:00 UTC = 1768316400.
355 ;; Apr 28 2026 (Tuesday, EEST=UTC+3): occurrence wall-clock should
356 ;; stay at 17:00 Athens, i.e. 14:00 UTC = 1777384800.
357 ;; The buggy UTC-naive walker produces 15:00 UTC = 1777388400 (off
358 ;; by +3600 — wall-clock drifts to 18:00 EEST).
359 (let* ((ics (string-append
360 "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\n"
362 "DTSTART;TZID=Europe/Athens:20260113T170000\r\n"
363 "DTEND;TZID=Europe/Athens:20260113T180000\r\n"
364 "SUMMARY:Cross-DST recurrence\r\n"
365 "RRULE:FREQ=WEEKLY;BYDAY=TU\r\n"
366 "END:VEVENT\r\nEND:VCALENDAR"))
367 (master (car (ical-parse ics)))
368 (range-start (parse-ical-datetime "20260428T000000Z"))
369 (range-end (parse-ical-datetime "20260429T000000Z"))
370 (occurrences (ical-expand-recurrence master
371 start: range-start
372 end: range-end)))
373 (assert-equal 1 (length occurrences))
374 (assert-equal "20260428T140000Z"
375 (format-ical-datetime
376 (ical-event-dtstart (car occurrences))))))
378 (test "daily TZID=Europe/Athens spanning DST start"
379 ;; Master: 2026-03-26 (Thursday, EET=UTC+2) at 09:00 Athens.
380 ;; DST starts Sunday 2026-03-29. The Apr 1 (Wednesday) occurrence
381 ;; should be 09:00 EEST = 06:00 UTC, NOT 09:00 EET = 07:00 UTC.
382 (let* ((ics (string-append
383 "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\n"
385 "DTSTART;TZID=Europe/Athens:20260326T090000\r\n"
386 "DTEND;TZID=Europe/Athens:20260326T100000\r\n"
387 "SUMMARY:Daily across DST\r\n"
388 "RRULE:FREQ=DAILY\r\n"
389 "END:VEVENT\r\nEND:VCALENDAR"))
390 (master (car (ical-parse ics)))
391 (range-start (parse-ical-datetime "20260401T000000Z"))
392 (range-end (parse-ical-datetime "20260402T000000Z"))
393 (occurrences (ical-expand-recurrence master
394 start: range-start
395 end: range-end)))
396 (assert-equal 1 (length occurrences))
397 (assert-equal "20260401T060000Z"
398 (format-ical-datetime
399 (ical-event-dtstart (car occurrences))))))
401 (test "monthly TZID=Europe/Athens spanning DST start"
402 ;; Master: 2026-01-15 (Thursday, EET=UTC+2) at 14:30 Athens.
403 ;; April 15 (Wednesday, EEST=UTC+3) — wall-clock 14:30 → 11:30 UTC.
404 (let* ((ics (string-append
405 "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\n"
407 "DTSTART;TZID=Europe/Athens:20260115T143000\r\n"
408 "DTEND;TZID=Europe/Athens:20260115T153000\r\n"
409 "SUMMARY:Monthly across DST\r\n"
410 "RRULE:FREQ=MONTHLY;COUNT=4\r\n"
411 "END:VEVENT\r\nEND:VCALENDAR"))
412 (master (car (ical-parse ics)))
413 (occurrences (ical-expand-recurrence master)))
414 (assert-equal 4 (length occurrences))
415 ;; Months: Jan (EET, 12:30 UTC), Feb (EET), Mar (EET), Apr (EEST, 11:30 UTC)
416 (assert-equal "20260115T123000Z"
417 (format-ical-datetime (ical-event-dtstart (list-ref occurrences 0))))
418 (assert-equal "20260415T113000Z"
419 (format-ical-datetime (ical-event-dtstart (list-ref occurrences 3))))))
421 (test "weekly UTC-anchored event keeps UTC across DST"
422 ;; Sanity: an event with no TZID (UTC-anchored DTSTART) preserves
423 ;; its UTC instant across DST transitions. The wall-clock in any
424 ;; observing tz shifts by 1h, which is correct UTC-anchor behavior.
425 (let* ((ics (string-append
426 "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\n"
428 "DTSTART:20260113T140000Z\r\n"
429 "DTEND:20260113T150000Z\r\n"
430 "SUMMARY:UTC anchor\r\n"
431 "RRULE:FREQ=WEEKLY;BYDAY=TU\r\n"
432 "END:VEVENT\r\nEND:VCALENDAR"))
433 (master (car (ical-parse ics)))
434 (range-start (parse-ical-datetime "20260428T000000Z"))
435 (range-end (parse-ical-datetime "20260429T000000Z"))
436 (occurrences (ical-expand-recurrence master
437 start: range-start
438 end: range-end)))
439 (assert-equal 1 (length occurrences))
440 (assert-equal "20260428T140000Z"
441 (format-ical-datetime
442 (ical-event-dtstart (car occurrences)))))))
445;; ============================================================
446;; Multistatus Response Parsing
447;; ============================================================
449(test-group "parse-multistatus"
451 (test "parses single response"
452 (let* ((xml "<d:multistatus xmlns:d=\"DAV:\"><d:response><d:href>/cal/1.ics</d:href><d:propstat><d:prop><d:displayname>Test Calendar</d:displayname></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat></d:response></d:multistatus>")
453 (sxml (xml->sxml xml))
454 (entries (parse-multistatus sxml)))
455 (assert-equal 1 (length entries))
456 (assert-equal "/cal/1.ics" (dict-ref (car entries) href:))))
458 (test "parses multiple responses"
459 (let* ((xml "<d:multistatus xmlns:d=\"DAV:\"><d:response><d:href>/cal/1.ics</d:href><d:propstat><d:prop><d:displayname>One</d:displayname></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat></d:response><d:response><d:href>/cal/2.ics</d:href><d:propstat><d:prop><d:displayname>Two</d:displayname></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat></d:response></d:multistatus>")
460 (sxml (xml->sxml xml))
461 (entries (parse-multistatus sxml)))
462 (assert-equal 2 (length entries))
463 (assert-equal "/cal/1.ics" (dict-ref (car entries) href:))
464 (assert-equal "/cal/2.ics" (dict-ref (cadr entries) href:))))
466 (test "extracts displayname property"
467 (let* ((xml "<d:multistatus xmlns:d=\"DAV:\"><d:response><d:href>/cal/</d:href><d:propstat><d:prop><d:displayname>Work</d:displayname></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat></d:response></d:multistatus>")
468 (sxml (xml->sxml xml))
469 (entries (parse-multistatus sxml))
470 (entry (car entries)))
471 (assert-equal "Work"
472 (dict-ref entry (string->keyword "d:displayname") #f))))
474 (test "skips 404 propstat"
475 (let* ((xml "<d:multistatus xmlns:d=\"DAV:\"><d:response><d:href>/cal/</d:href><d:propstat><d:prop><d:displayname>Work</d:displayname></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat><d:propstat><d:prop><d:missing-prop/></d:prop><d:status>HTTP/1.1 404 Not Found</d:status></d:propstat></d:response></d:multistatus>")
476 (sxml (xml->sxml xml))
477 (entries (parse-multistatus sxml))
478 (entry (car entries)))
479 ;; displayname from 200 propstat should be present
480 (assert-equal "Work"
481 (dict-ref entry (string->keyword "d:displayname") #f))
482 ;; missing-prop from 404 propstat should not be present
483 (assert-false
484 (dict-ref entry (string->keyword "d:missing-prop") #f)))))
487;; ============================================================
488;; URL Resolution
489;; ============================================================
491(test-group "resolve-url"
493 (test "absolute URL passes through"
494 (assert-equal "https://example.com/path"
495 (resolve-url "https://cal.example.com" "https://example.com/path")))
497 (test "relative path resolves against base"
498 (assert-equal "https://cal.example.com/dav/calendars/"
499 (resolve-url "https://cal.example.com" "/dav/calendars/")))
501 (test "base URL with port"
502 (assert-equal "https://cal.example.com:8443/dav/"
503 (resolve-url "https://cal.example.com:8443" "/dav/"))))
506(run-tests)