import { describe, expect, it } from "vitest";
import { createDavFixture, davRequest } from "@tests/worker/helpers/dav";
import { fetchWorker } from "@tests/worker/helpers/http";
import { event, propfindBody, reportBody, uid } from "./helpers";
describe("CalDAV reports", () => {
it("supports calendar-query and calendar-multiget", async () => {
const fixture = await createDavFixture(["caldav.full"]);
const aliceUid = uid();
const bobUid = uid();
expect(
await fetchWorker(
davRequest(fixture, "/calendars/default/alice.ics", {
method: "PUT",
body: event({ uid: aliceUid, summary: "Alice Query", start: "20260521T120000Z", end: "20260521T130000Z" }),
}),
),
).toMatchObject({ status: 201 });
expect(
await fetchWorker(
davRequest(fixture, "/calendars/default/bob.ics", {
method: "PUT",
body: event({ uid: bobUid, summary: "Bob Query", start: "20270621T120000Z", end: "20270621T130000Z" }),
}),
),
).toMatchObject({ status: 201 });
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);
const queryXml = await query.text();
expect(queryXml).toContain("/calendars/default/alice.ics");
expect(queryXml).not.toContain("/calendars/default/bob.ics");
expect(queryXml).toContain(`UID:${aliceUid}`);
const multiget = await fetchWorker(
davRequest(fixture, "/calendars/default/", {
method: "REPORT",
headers: { "content-type": "application/xml", depth: "1" },
body: reportBody(
"calendar-multiget",
"/calendars/default/bob.ics/calendars/default/missing.ics",
),
}),
);
expect(multiget.status).toBe(207);
const multigetXml = await multiget.text();
expect((multigetXml.match(//g) ?? []).length).toBe(2);
expect(multigetXml).toContain("/calendars/default/bob.ics");
expect(multigetXml).toContain(`UID:${bobUid}`);
expect(multigetXml).toContain("/calendars/default/missing.ics");
expect(multigetXml).toContain("HTTP/1.1 404 Not Found");
});
it("does not return calendar-data from PROPFIND but still returns it from REPORT", async () => {
const fixture = await createDavFixture(["caldav.full"]);
const eventUid = uid();
expect(
await fetchWorker(
davRequest(fixture, "/calendars/default/no-propfind-data.ics", {
method: "PUT",
body: event({ uid: eventUid, summary: "No PROPFIND Data" }),
}),
),
).toMatchObject({ status: 201 });
const propfind = await fetchWorker(
davRequest(fixture, "/calendars/default/no-propfind-data.ics", {
method: "PROPFIND",
headers: { depth: "0", "content-type": "application/xml" },
body: propfindBody(""),
}),
);
expect(propfind.status).toBe(207);
expect(await propfind.text()).toContain("HTTP/1.1 404 Not Found");
const report = await fetchWorker(
davRequest(fixture, "/calendars/default/", {
method: "REPORT",
headers: { "content-type": "application/xml", depth: "1" },
body: reportBody(
"calendar-query",
'',
),
}),
);
expect(report.status).toBe(207);
expect(await report.text()).toContain(`UID:${eventUid}`);
});
it("uses CALDAV:timezone for floating time-range matching", async () => {
const fixture = await createDavFixture(["caldav.full"]);
const floatingUid = uid();
expect(
await fetchWorker(
davRequest(fixture, "/calendars/default/floating.ics", {
method: "PUT",
body: event({
uid: floatingUid,
summary: "Floating",
start: "20260521T090000",
end: "20260521T100000",
}),
}),
),
).toMatchObject({ status: 201 });
const filter =
'';
const withoutTimezone = await fetchWorker(
davRequest(fixture, "/calendars/default/", {
method: "REPORT",
headers: { "content-type": "application/xml", depth: "1" },
body: reportBody("calendar-query", filter),
}),
);
expect(withoutTimezone.status).toBe(207);
expect(await withoutTimezone.text()).not.toContain("/calendars/default/floating.ics");
const timezone = ["BEGIN:VTIMEZONE", "TZID:America/Los_Angeles", "END:VTIMEZONE"].join("\r\n");
const withTimezone = await fetchWorker(
davRequest(fixture, "/calendars/default/", {
method: "REPORT",
headers: { "content-type": "application/xml", depth: "1" },
body: reportBody("calendar-query", `${timezone}${filter}`),
}),
);
expect(withTimezone.status).toBe(207);
expect(await withTimezone.text()).toContain("/calendars/default/floating.ics");
});
it("rejects REPORT elements from the wrong namespace", async () => {
const fixture = await createDavFixture(["caldav.full"]);
const response = await fetchWorker(
davRequest(fixture, "/calendars/default/", {
method: "REPORT",
headers: { "content-type": "application/xml", depth: "1" },
body: '',
}),
);
expect(response.status).toBe(400);
});
it("treats missing calendar-query Depth as 0 and Depth 1 as child scope", async () => {
const fixture = await createDavFixture(["caldav.full"]);
expect(
await fetchWorker(
davRequest(fixture, "/calendars/default/depth.ics", {
method: "PUT",
body: event({ uid: uid(), summary: "Depth Query" }),
}),
),
).toMatchObject({ status: 201 });
const body = reportBody(
"calendar-query",
'',
);
const depth0 = await fetchWorker(
davRequest(fixture, "/calendars/default/", {
method: "REPORT",
headers: { "content-type": "application/xml" },
body,
}),
);
expect(depth0.status).toBe(207);
expect(await depth0.text()).not.toContain("/calendars/default/depth.ics");
const depth1 = await fetchWorker(
davRequest(fixture, "/calendars/default/", {
method: "REPORT",
headers: { "content-type": "application/xml", depth: "1" },
body,
}),
);
expect(depth1.status).toBe(207);
expect(await depth1.text()).toContain("/calendars/default/depth.ics");
});
it("rejects unsupported CalDAV filters and invalid time ranges", async () => {
const fixture = await createDavFixture(["caldav.full"]);
const unsupportedParamFilter = await fetchWorker(
davRequest(fixture, "/calendars/default/", {
method: "REPORT",
headers: { "content-type": "application/xml" },
body: reportBody(
"calendar-query",
'',
),
}),
);
expect(unsupportedParamFilter.status).toBe(400);
await expect(unsupportedParamFilter.text()).resolves.toContain("Unsupported CalDAV param-filter");
const unsupportedComponent = await fetchWorker(
davRequest(fixture, "/calendars/default/", {
method: "REPORT",
headers: { "content-type": "application/xml" },
body: reportBody(
"calendar-query",
'',
),
}),
);
expect(unsupportedComponent.status).toBe(400);
await expect(unsupportedComponent.text()).resolves.toContain("Unsupported CalDAV comp-filter");
const invalidTimeRange = await fetchWorker(
davRequest(fixture, "/calendars/default/", {
method: "REPORT",
headers: { "content-type": "application/xml" },
body: reportBody(
"calendar-query",
'',
),
}),
);
expect(invalidTimeRange.status).toBe(400);
await expect(invalidTimeRange.text()).resolves.toContain("end must be after start");
});
});