File
Blob: tests/worker/protocol/carddav/helpers.ts
| 1 | import { env } from "cloudflare:workers"; |
| 2 | import { expect } from "vitest"; |
| 3 | |
| 4 | import { createControlPlaneDb } from "@/worker/db/d1/client"; |
| 5 | import { getSubjectById } from "@/worker/db/d1/repository"; |
| 6 | import { createDavFixture, davRequest } from "@tests/worker/helpers/dav"; |
| 7 | import { fetchWorker } from "@tests/worker/helpers/http"; |
| 8 | |
| 9 | type DavFixture = Awaited<ReturnType<typeof createDavFixture>>; |
| 10 | |
| 11 | export function uid(): string { |
| 12 | return `${crypto.randomUUID()}@example.test`; |
| 13 | } |
| 14 | |
| 15 | export 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 | |
| 31 | export 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 | |
| 37 | export 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 | |
| 41 | export 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 | |
| 47 | export 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 | |
| 54 | export 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 | |
| 68 | export 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 | } |