Skip to content
File

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

typescript81 lines
1import { env } from "cloudflare:workers";
2import { expect } from "vitest";
3 
4import { createControlPlaneDb } from "@/worker/db/d1/client";
5import { getSubjectById } from "@/worker/db/d1/repository";
6import { createDavFixture, davRequest } from "@tests/worker/helpers/dav";
7import { fetchWorker } from "@tests/worker/helpers/http";
8 
9type DavFixture = Awaited<ReturnType<typeof createDavFixture>>;
10 
11export function uid(): string {
12 return `${crypto.randomUUID()}@example.test`;
13}
14 
15export function vcard(input: { uid: string; fn: string; email?: string; tel?: string }): string {
16 return [
17 "BEGIN:VCARD",
18 "VERSION:4.0",
19 `UID:${input.uid}`,
20 `FN:${input.fn}`,
21 `N:${input.fn.split(" ").at(-1) ?? input.fn};${input.fn.split(" ")[0] ?? ""};;;`,
22 input.email ? `EMAIL:${input.email}` : null,
23 input.tel ? `TEL:${input.tel}` : null,
24 "END:VCARD",
25 "",
26 ]
27 .filter((line): line is string => line !== null)
28 .join("\r\n");
29}
30 
31export async function storageIdFor(subjectId: string): Promise<string> {
32 const subject = await getSubjectById(createControlPlaneDb(env.DAV_CONTROL_PLANE), subjectId);
33 if (!subject) throw new Error(`Missing subject ${subjectId}`);
34 return subject.storageId;
35}
36 
37export function propfindBody(props: string): string {
38 return `<?xml version="1.0"?><D:propfind xmlns:D="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/"><D:prop>${props}</D:prop></D:propfind>`;
39}
40 
41export function reportBody(name: string, body: string): string {
42 return name === "sync-collection"
43 ? `<?xml version="1.0"?><D:sync-collection xmlns:D="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">${body}</D:sync-collection>`
44 : `<?xml version="1.0"?><CARD:${name} xmlns:D="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">${body}</CARD:${name}>`;
45}
46 
47export function syncCollectionBody(prevToken: string, props = "<D:getetag/>"): string {
48 return reportBody(
49 "sync-collection",
50 `<D:sync-token>${prevToken}</D:sync-token><D:sync-level>1</D:sync-level><D:prop>${props}</D:prop>`,
51 );
52}
53 
54export async function syncToken(fixture: DavFixture): Promise<string> {
55 const response = await fetchWorker(
56 davRequest(fixture, "/addressbooks/default/", {
57 method: "REPORT",
58 headers: { "content-type": "application/xml" },
59 body: syncCollectionBody(""),
60 }),
61 );
62 expect(response.status).toBe(207);
63 const token = /<D:sync-token>([^<]+)<\/D:sync-token>/.exec(await response.text())?.[1];
64 if (!token) throw new Error("Missing sync-token in baseline sync-collection response");
65 return token;
66}
67 
68export async function propfindSyncToken(fixture: DavFixture, href = "/addressbooks/default/") {
69 const response = await fetchWorker(
70 davRequest(fixture, href, {
71 method: "PROPFIND",
72 headers: { depth: "0", "content-type": "application/xml" },
73 body: propfindBody("<D:sync-token/>"),
74 }),
75 );
76 expect(response.status).toBe(207);
77 const token = /<D:sync-token>([^<]+)<\/D:sync-token>/.exec(await response.text())?.[1];
78 if (!token) throw new Error("Missing sync-token in PROPFIND response");
79 return token;
80}