Skip to content
File

Blob: tests/worker/protocol/caldav/calendar-report-limits.workers.test.ts

typescript166 lines
1import { describe, expect, it } from "vitest";
2 
3import { createDavFixture, davRequest } from "@tests/worker/helpers/dav";
4import { fetchWorker } from "@tests/worker/helpers/http";
5import { withEnvBinding } from "@tests/worker/helpers/env";
6import { event, reportBody, uid } from "./helpers";
7 
8describe("CalDAV report limits and origins", () => {
9 it("matches category text filters and expands recurring calendar-data", async () => {
10 const fixture = await createDavFixture(["caldav.full"]);
11 const categoryUid = uid();
12 expect(
13 await fetchWorker(
14 davRequest(fixture, "/calendars/default/category.ics", {
15 method: "PUT",
16 body: event({ uid: categoryUid, summary: "Categorized", categories: ["Focus", "Team"] }),
17 }),
18 ),
19 ).toMatchObject({ status: 201 });
20 
21 const categoryQuery = await fetchWorker(
22 davRequest(fixture, "/calendars/default/", {
23 method: "REPORT",
24 headers: { "content-type": "application/xml", depth: "1" },
25 body: reportBody(
26 "calendar-query",
27 '<D:prop><D:getetag/></D:prop><C:filter><C:comp-filter name="VCALENDAR"><C:comp-filter name="VEVENT"><C:prop-filter name="CATEGORIES"><C:text-match collation="i;ascii-casemap">focus</C:text-match></C:prop-filter></C:comp-filter></C:comp-filter></C:filter>',
28 ),
29 }),
30 );
31 expect(categoryQuery.status).toBe(207);
32 expect(await categoryQuery.text()).toContain("/calendars/default/category.ics");
33 
34 const recurrenceUid = uid();
35 const recurring = [
36 "BEGIN:VCALENDAR",
37 "VERSION:2.0",
38 "PRODID:-//dab//tests//EN",
39 "BEGIN:VEVENT",
40 `UID:${recurrenceUid}`,
41 "DTSTAMP:20260101T000000Z",
42 "SUMMARY:Daily",
43 "DTSTART:20260521T120000Z",
44 "DTEND:20260521T130000Z",
45 "RRULE:FREQ=DAILY;COUNT=3",
46 "END:VEVENT",
47 "BEGIN:VEVENT",
48 `UID:${recurrenceUid}`,
49 "DTSTAMP:20260101T000000Z",
50 "RECURRENCE-ID:20260522T120000Z",
51 "SUMMARY:Moved Daily",
52 "DTSTART:20260522T150000Z",
53 "DTEND:20260522T160000Z",
54 "END:VEVENT",
55 "END:VCALENDAR",
56 "",
57 ].join("\r\n");
58 expect(
59 await fetchWorker(
60 davRequest(fixture, "/calendars/default/recurring.ics", {
61 method: "PUT",
62 body: recurring,
63 }),
64 ),
65 ).toMatchObject({ status: 201 });
66 
67 const recurrenceQuery = await fetchWorker(
68 davRequest(fixture, "/calendars/default/", {
69 method: "REPORT",
70 headers: { "content-type": "application/xml", depth: "1" },
71 body: reportBody(
72 "calendar-query",
73 '<D:prop><C:calendar-data><C:expand start="20260521T000000Z" end="20260524T000000Z"/></C:calendar-data></D:prop><C:filter><C:comp-filter name="VCALENDAR"><C:comp-filter name="VEVENT"><C:time-range start="20260523T000000Z" end="20260524T000000Z"/></C:comp-filter></C:comp-filter></C:filter>',
74 ),
75 }),
76 );
77 expect(recurrenceQuery.status).toBe(207);
78 const recurrenceXml = await recurrenceQuery.text();
79 expect(recurrenceXml).toContain("/calendars/default/recurring.ics");
80 expect(recurrenceXml).toContain("RECURRENCE-ID:20260522T120000Z");
81 expect(recurrenceXml).toContain("SUMMARY:Moved Daily");
82 expect(recurrenceXml).not.toContain("RRULE:FREQ=DAILY");
83 });
84 
85 it("applies the configured REPORT result limit to calendar-query and calendar-multiget", async () => {
86 const fixture = await createDavFixture(["caldav.full"]);
87 const firstName = `limited-a-${crypto.randomUUID()}.ics`;
88 const secondName = `limited-b-${crypto.randomUUID()}.ics`;
89 expect(
90 await fetchWorker(
91 davRequest(fixture, `/calendars/default/${firstName}`, {
92 method: "PUT",
93 body: event({ uid: uid(), summary: "Limited A" }),
94 }),
95 ),
96 ).toMatchObject({ status: 201 });
97 expect(
98 await fetchWorker(
99 davRequest(fixture, `/calendars/default/${secondName}`, {
100 method: "PUT",
101 body: event({ uid: uid(), summary: "Limited B" }),
102 }),
103 ),
104 ).toMatchObject({ status: 201 });
105 
106 await withEnvBinding("MAX_REPORT_RESULTS", "1", async () => {
107 // RFC 4791 7.8: when a calendar-query result set exceeds the configured
108 // limit, return a DAV:number-of-matches-within-limits precondition error.
109 const query = await fetchWorker(
110 davRequest(fixture, "/calendars/default/", {
111 method: "REPORT",
112 headers: { "content-type": "application/xml", depth: "1" },
113 body: reportBody(
114 "calendar-query",
115 '<D:prop><D:getetag/></D:prop><C:filter><C:comp-filter name="VCALENDAR"/></C:filter>',
116 ),
117 }),
118 );
119 expect(query.status).toBe(403);
120 await expect(query.text()).resolves.toContain("number-of-matches-within-limits");
121 
122 const multiget = await fetchWorker(
123 davRequest(fixture, "/calendars/default/", {
124 method: "REPORT",
125 headers: { "content-type": "application/xml" },
126 body: reportBody(
127 "calendar-multiget",
128 `<D:prop><D:getetag/></D:prop><D:href>/calendars/default/${firstName}</D:href><D:href>/calendars/default/${secondName}</D:href>`,
129 ),
130 }),
131 );
132 expect(multiget.status).toBe(403);
133 });
134 });
135 
136 it("rejects calendar-multiget absolute hrefs outside the subject origin", async () => {
137 const fixture = await createDavFixture(["caldav.full"]);
138 const name = `absolute-origin-${crypto.randomUUID()}.ics`;
139 const eventUid = uid();
140 expect(
141 await fetchWorker(
142 davRequest(fixture, `/calendars/default/${name}`, {
143 method: "PUT",
144 body: event({ uid: eventUid, summary: "Absolute Origin" }),
145 }),
146 ),
147 ).toMatchObject({ status: 201 });
148 
149 const multiget = await fetchWorker(
150 davRequest(fixture, "/calendars/default/", {
151 method: "REPORT",
152 headers: { "content-type": "application/xml" },
153 body: reportBody(
154 "calendar-multiget",
155 `<D:prop><D:getetag/><C:calendar-data/></D:prop><D:href>https://other.example.test/calendars/default/${name}</D:href>`,
156 ),
157 }),
158 );
159 expect(multiget.status).toBe(207);
160 const xml = await multiget.text();
161 expect(xml).toContain(`https://other.example.test/calendars/default/${name}`);
162 expect(xml).toContain("HTTP/1.1 404 Not Found");
163 expect(xml).not.toContain(eventUid);
164 });
165});