Skip to content
File

Blob: tests/worker/protocol/caldav/helpers.ts

typescript97 lines
1import { expect } from "vitest";
2 
3import { createDavFixture, davRequest } from "@tests/worker/helpers/dav";
4import { fetchWorker } from "@tests/worker/helpers/http";
5 
6type DavFixture = Awaited<ReturnType<typeof createDavFixture>>;
7 
8export function uid(): string {
9 return `${crypto.randomUUID()}@example.test`;
10}
11 
12export function event(input: {
13 uid: string;
14 summary: string;
15 start?: string;
16 end?: string;
17 rrule?: string;
18 categories?: string[];
19}): string {
20 return [
21 "BEGIN:VCALENDAR",
22 "VERSION:2.0",
23 "PRODID:-//dab//tests//EN",
24 "BEGIN:VEVENT",
25 `UID:${input.uid}`,
26 "DTSTAMP:20260101T000000Z",
27 `SUMMARY:${input.summary}`,
28 `DTSTART:${input.start ?? "20260521T120000Z"}`,
29 `DTEND:${input.end ?? "20260521T130000Z"}`,
30 input.categories ? `CATEGORIES:${input.categories.join(",")}` : null,
31 input.rrule ? `RRULE:${input.rrule}` : null,
32 "END:VEVENT",
33 "END:VCALENDAR",
34 "",
35 ]
36 .filter((line): line is string => line !== null)
37 .join("\r\n");
38}
39 
40export function propfindBody(props: string): string {
41 return `<?xml version="1.0"?><D:propfind xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/"><D:prop>${props}</D:prop></D:propfind>`;
42}
43 
44export function mkcolCalendarBody(displayName: string, timezoneIcal?: string): string {
45 return [
46 '<?xml version="1.0"?>',
47 '<D:mkcol xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">',
48 "<D:set><D:prop><D:resourcetype><D:collection/><C:calendar/></D:resourcetype>",
49 `<D:displayname>${displayName}</D:displayname>`,
50 timezoneIcal ? `<C:calendar-timezone>${timezoneIcal}</C:calendar-timezone>` : null,
51 "</D:prop></D:set></D:mkcol>",
52 ]
53 .filter((line): line is string => line !== null)
54 .join("");
55}
56 
57export function reportBody(name: string, body: string): string {
58 return name === "sync-collection"
59 ? `<?xml version="1.0"?><D:sync-collection xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">${body}</D:sync-collection>`
60 : `<?xml version="1.0"?><C:${name} xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">${body}</C:${name}>`;
61}
62 
63export function syncCollectionBody(prevToken: string, props = "<D:getetag/>"): string {
64 return reportBody(
65 "sync-collection",
66 `<D:sync-token>${prevToken}</D:sync-token><D:sync-level>1</D:sync-level><D:prop>${props}</D:prop>`,
67 );
68}
69 
70export async function syncToken(fixture: DavFixture): Promise<string> {
71 const response = await fetchWorker(
72 davRequest(fixture, "/calendars/default/", {
73 method: "REPORT",
74 headers: { "content-type": "application/xml" },
75 body: syncCollectionBody(""),
76 }),
77 );
78 expect(response.status).toBe(207);
79 const token = /<D:sync-token>([^<]+)<\/D:sync-token>/.exec(await response.text())?.[1];
80 if (!token) throw new Error("Missing sync-token in baseline sync-collection response");
81 return token;
82}
83 
84export async function propfindSyncToken(fixture: DavFixture, href = "/calendars/default/") {
85 const response = await fetchWorker(
86 davRequest(fixture, href, {
87 method: "PROPFIND",
88 headers: { depth: "0", "content-type": "application/xml" },
89 body: propfindBody("<D:sync-token/>"),
90 }),
91 );
92 expect(response.status).toBe(207);
93 const token = /<D:sync-token>([^<]+)<\/D:sync-token>/.exec(await response.text())?.[1];
94 if (!token) throw new Error("Missing sync-token in PROPFIND response");
95 return token;
96}