import { describe, expect, it } from "vitest"; import { createDavFixture, davRequest } from "@tests/worker/helpers/dav"; import { fetchWorker } from "@tests/worker/helpers/http"; import { event, propfindSyncToken, reportBody, uid } from "./helpers"; describe("CalDAV calendar objects", () => { it("puts, gets, and deletes an iCalendar object", async () => { const fixture = await createDavFixture(["caldav.full"]); const eventUid = uid(); const put = await fetchWorker( davRequest(fixture, "/calendars/default/meeting.ics", { method: "PUT", headers: { "content-type": "text/calendar" }, body: event({ uid: eventUid, summary: "Planning" }), }), ); expect(put.status).toBe(201); expect(put.headers.get("etag")).toMatch(/^"[0-9a-f]+"$/); const get = await fetchWorker(davRequest(fixture, "/calendars/default/meeting.ics")); expect(get.status).toBe(200); expect(get.headers.get("content-type")).toBe("text/calendar; charset=utf-8"); const body = await get.text(); expect(body).toContain(`UID:${eventUid}`); expect(body).toContain("SUMMARY:Planning"); const deleted = await fetchWorker(davRequest(fixture, "/calendars/default/meeting.ics", { method: "DELETE" })); expect(deleted.status).toBe(204); expect(await fetchWorker(davRequest(fixture, "/calendars/default/meeting.ics"))).toMatchObject({ status: 404 }); }); it("stores and time-range queries IANA TZID calendar objects", async () => { const fixture = await createDavFixture(["caldav.full"]); const eventUid = uid(); const body = [ "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//dab//tests//EN", "BEGIN:VEVENT", `UID:${eventUid}`, "DTSTAMP:20260101T000000Z", "SUMMARY:TZID Query", "DTSTART;TZID=America/Los_Angeles:20260521T090000", "DTEND;TZID=America/Los_Angeles:20260521T100000", "END:VEVENT", "END:VCALENDAR", "", ].join("\r\n"); const put = await fetchWorker( davRequest(fixture, "/calendars/default/tzid.ics", { method: "PUT", headers: { "content-type": "text/calendar" }, body, }), ); expect(put.status).toBe(201); const get = await fetchWorker(davRequest(fixture, "/calendars/default/tzid.ics")); expect(get.status).toBe(200); await expect(get.text()).resolves.toContain("DTSTART;TZID=America/Los_Angeles:20260521T090000"); const query = await fetchWorker( davRequest(fixture, "/calendars/default/", { method: "REPORT", headers: { "content-type": "application/xml", depth: "1" }, body: reportBody( "calendar-query", '', ), }), ); expect(query.status).toBe(207); expect(await query.text()).toContain("/calendars/default/tzid.ics"); }); it("rejects malformed iCalendar objects, duplicate UIDs, and excessive recurrence", async () => { const fixture = await createDavFixture(["caldav.full"]); const malformed = await fetchWorker( davRequest(fixture, "/calendars/default/bad.ics", { method: "PUT", body: "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nEND:VCALENDAR\r\n", }), ); expect(malformed.status).toBe(400); const eventUid = uid(); expect( await fetchWorker( davRequest(fixture, "/calendars/default/one.ics", { method: "PUT", body: event({ uid: eventUid, summary: "One" }), }), ), ).toMatchObject({ status: 201 }); expect( await fetchWorker( davRequest(fixture, "/calendars/default/two.ics", { method: "PUT", body: event({ uid: eventUid, summary: "Two" }), }), ), ).toMatchObject({ status: 409 }); const recurrence = await fetchWorker( davRequest(fixture, "/calendars/default/recurs.ics", { method: "PUT", body: event({ uid: uid(), summary: "Too many", rrule: "FREQ=DAILY;COUNT=10001" }), }), ); expect(recurrence.status).toBe(400); await expect(recurrence.text()).resolves.toContain("maximum instances"); expect( await fetchWorker( davRequest(fixture, "/calendars/default/unbounded.ics", { method: "PUT", body: event({ uid: uid(), summary: "Open ended", rrule: "FREQ=MONTHLY" }), }), ), ).toMatchObject({ status: 201 }); }); it("enforces tagged collection sync-token If preconditions on object writes", async () => { const fixture = await createDavFixture(["caldav.full"]); const eventUid = uid(); const initialToken = await propfindSyncToken(fixture); const created = await fetchWorker( davRequest(fixture, "/calendars/default/if-sync.ics", { method: "PUT", headers: { if: ` (<${initialToken}>)` }, body: event({ uid: eventUid, summary: "If Sync" }), }), ); expect(created.status).toBe(201); const currentToken = await propfindSyncToken(fixture); const stalePut = await fetchWorker( davRequest(fixture, "/calendars/default/if-sync.ics", { method: "PUT", headers: { if: ` (<${initialToken}>)` }, body: event({ uid: eventUid, summary: "If Sync Stale" }), }), ); expect(stalePut.status).toBe(412); const wrongTag = await fetchWorker( davRequest(fixture, "/calendars/default/if-sync.ics", { method: "PUT", headers: { if: ` (<${currentToken}>)` }, body: event({ uid: eventUid, summary: "If Sync Wrong Tag" }), }), ); expect(wrongTag.status).toBe(412); const updated = await fetchWorker( davRequest(fixture, "/calendars/default/if-sync.ics", { method: "PUT", headers: { if: ` (<${currentToken}>)` }, body: event({ uid: eventUid, summary: "If Sync Updated" }), }), ); expect(updated.status).toBe(204); const staleDelete = await fetchWorker( davRequest(fixture, "/calendars/default/if-sync.ics", { method: "DELETE", headers: { if: ` (<${currentToken}>)` }, }), ); expect(staleDelete.status).toBe(412); }); it("honors HTTP If-Match on CalDAV object DELETE", async () => { const fixture = await createDavFixture(["caldav.full"]); expect( await fetchWorker( davRequest(fixture, "/calendars/default/delete-if-match.ics", { method: "PUT", body: event({ uid: uid(), summary: "Delete If-Match" }), }), ), ).toMatchObject({ status: 201 }); const deleted = await fetchWorker( davRequest(fixture, "/calendars/default/delete-if-match.ics", { method: "DELETE", headers: { "if-match": '"definitely-stale"' }, }), ); expect(deleted.status).toBe(412); expect(await fetchWorker(davRequest(fixture, "/calendars/default/delete-if-match.ics"))).toHaveProperty( "status", 200, ); }); it("rejects encoded slashes and control characters in calendar object paths", async () => { const fixture = await createDavFixture(["caldav.full"]); const encodedSlash = await fetchWorker(davRequest(fixture, "/calendars/default%2Fevil/item.ics")); expect(encodedSlash.status).toBe(404); const controlCharacter = await fetchWorker(davRequest(fixture, "/calendars/default/%00.ics")); expect(controlCharacter.status).toBe(404); }); it("prevents read-only CalDAV PATs from writing objects", async () => { const fixture = await createDavFixture(["caldav.readonly"]); const put = await fetchWorker( davRequest(fixture, "/calendars/default/readonly.ics", { method: "PUT", body: event({ uid: uid(), summary: "No Write" }), }), ); expect(put.status).toBe(403); }); });