AtlatestRepositorysigil-caldav
sigil-caldav / tree / testtest-caldav.sgl
1
;;; Tests for CalDAV client library (offline, no live server needed)3
(import (sigil test)4
(sigil string)5
(sigil time)6
(sigil caldav ical)7
(sigil caldav client)8
(sigil sxml reader))11
;; ============================================================12
;; iCalendar DateTime Conversion13
;; ============================================================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 date25
(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 = 171049320062
(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 UTC69
(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 UTC74
(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 hint79
(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 July84
(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 UTC87
(assert-equal "20260115T170000Z" (format-ical-datetime winter))88
;; Summer: 12:00 EDT = 16:00 UTC89
(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 UTC96
(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 Parsing103
;; ============================================================105
(define simple-ical106
"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-ical109
"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-ical112
"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-ical115
"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-ical118
"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 Generation171
;; ============================================================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 present184
(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 parse232
(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 times247
(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 Generation280
;; ============================================================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 Expansion297
;; ============================================================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: 1710493200310
dtend: 1710496800311
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 apart315
(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 UTC321
dtend: 1710147600322
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: 1710493200329
dtend: 1710496800330
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: start338
dtend: (+ start 3600)339
rrule: "FREQ=DAILY;COUNT=30")))340
(let ((expanded (ical-expand-recurrence ev341
start: start342
end: (+ start (* 7 86400)))))343
;; Should only include events within the 7-day range344
(assert-true (<= (length expanded) 7))345
(assert-true (> (length expanded) 0)))))347
;; TZID-aware recurrence: a weekly event tagged with a TZID anchors348
;; each occurrence to a fixed wall-clock in that timezone, even when349
;; the recurrence walks across a DST boundary. UTC-second arithmetic350
;; 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 should356
;; stay at 17:00 Athens, i.e. 14:00 UTC = 1777384800.357
;; The buggy UTC-naive walker produces 15:00 UTC = 1777388400 (off358
;; by +3600 — wall-clock drifts to 18:00 EEST).359
(let* ((ics (string-append360
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\n"361
"UID:[email protected]\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 master371
start: range-start372
end: range-end)))373
(assert-equal 1 (length occurrences))374
(assert-equal "20260428T140000Z"375
(format-ical-datetime376
(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) occurrence381
;; should be 09:00 EEST = 06:00 UTC, NOT 09:00 EET = 07:00 UTC.382
(let* ((ics (string-append383
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\n"384
"UID:[email protected]\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 master394
start: range-start395
end: range-end)))396
(assert-equal 1 (length occurrences))397
(assert-equal "20260401T060000Z"398
(format-ical-datetime399
(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-append405
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\n"406
"UID:[email protected]\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) preserves423
;; its UTC instant across DST transitions. The wall-clock in any424
;; observing tz shifts by 1h, which is correct UTC-anchor behavior.425
(let* ((ics (string-append426
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\n"427
"UID:[email protected]\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 master437
start: range-start438
end: range-end)))439
(assert-equal 1 (length occurrences))440
(assert-equal "20260428T140000Z"441
(format-ical-datetime442
(ical-event-dtstart (car occurrences)))))))445
;; ============================================================446
;; Multistatus Response Parsing447
;; ============================================================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 present480
(assert-equal "Work"481
(dict-ref entry (string->keyword "d:displayname") #f))482
;; missing-prop from 404 propstat should not be present483
(assert-false484
(dict-ref entry (string->keyword "d:missing-prop") #f)))))487
;; ============================================================488
;; URL Resolution489
;; ============================================================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)