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