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);
});
});