import { env } from "cloudflare:workers"; import { describe, expect, it } from "vitest"; import { createDavFixture, davRequest } from "@tests/worker/helpers/dav"; import { withEnvBinding } from "@tests/worker/helpers/env"; import { fetchWorker } from "@tests/worker/helpers/http"; import { propfindSyncToken, storageIdFor, syncCollectionBody, syncToken, uid, vcard } from "./helpers"; describe("CardDAV sync collection", () => { it("returns requested properties for sync-collection current members without stale replay", async () => { const fixture = await createDavFixture(["carddav.full"]); const contactUid = uid(); expect( await fetchWorker( davRequest(fixture, "/addressbooks/default/sync-props.vcf", { method: "PUT", body: vcard({ uid: contactUid, fn: "Sync Props" }), }), ), ).toMatchObject({ status: 201 }); const initial = await fetchWorker( davRequest(fixture, "/addressbooks/default/", { method: "REPORT", headers: { "content-type": "application/xml" }, body: syncCollectionBody("", ""), }), ); expect(initial.status).toBe(207); const initialXml = await initial.text(); expect(initialXml).toContain("/addressbooks/default/sync-props.vcf"); expect(initialXml).toContain(`UID:${contactUid}`); const token = /([^<]+)<\/D:sync-token>/.exec(initialXml)?.[1]; expect(token).toBeTruthy(); const repeated = await fetchWorker( davRequest(fixture, "/addressbooks/default/", { method: "REPORT", headers: { "content-type": "application/xml" }, body: syncCollectionBody(token!), }), ); expect(repeated.status).toBe(207); expect((await repeated.text()).match(//g)?.length ?? 0).toBe(0); expect( await fetchWorker( davRequest(fixture, "/addressbooks/default/sync-props.vcf", { method: "PUT", body: vcard({ uid: contactUid, fn: "Sync Props Updated" }), }), ), ).toMatchObject({ status: 204 }); const changed = await fetchWorker( davRequest(fixture, "/addressbooks/default/", { method: "REPORT", headers: { "content-type": "application/xml" }, body: syncCollectionBody(token!, ""), }), ); expect(changed.status).toBe(207); const changedXml = await changed.text(); expect(changedXml).toContain("/addressbooks/default/sync-props.vcf"); expect(changedXml).toContain("Sync Props Updated"); const otherBook = `other-${crypto.randomUUID()}`; const storageId = await storageIdFor(fixture.subjectId); const createdBook = await env.CARD_DAV.getByName(storageId).createAddressbook({ subjectId: fixture.subjectId, name: otherBook, displayName: "Other", nowMs: Date.now(), }); expect(createdBook.ok).toBe(true); expect( await fetchWorker( davRequest(fixture, `/addressbooks/${otherBook}/other.vcf`, { method: "PUT", body: vcard({ uid: uid(), fn: "Other Book" }), }), ), ).toMatchObject({ status: 201 }); const propfindToken = await propfindSyncToken(fixture); const syncFromPropfindToken = await fetchWorker( davRequest(fixture, "/addressbooks/default/", { method: "REPORT", headers: { "content-type": "application/xml" }, body: syncCollectionBody(propfindToken), }), ); expect(syncFromPropfindToken.status).toBe(207); expect((await syncFromPropfindToken.text()).match(//g)?.length ?? 0).toBe(0); }); it("accepts sync-collection REPORT with Depth infinity as Depth 0 compatibility", async () => { const fixture = await createDavFixture(["carddav.full"]); const response = await fetchWorker( davRequest(fixture, "/addressbooks/default/", { method: "REPORT", headers: { "content-type": "application/xml", depth: "infinity" }, body: syncCollectionBody(""), }), ); expect(response.status).toBe(207); expect(await response.text()).toContain(""); }); it("reports sync-collection tombstones", async () => { const fixture = await createDavFixture(["carddav.full"]); const baselineToken = await syncToken(fixture); const contactUid = uid(); expect( await fetchWorker( davRequest(fixture, "/addressbooks/default/sync.vcf", { method: "PUT", body: vcard({ uid: contactUid, fn: "Sync Me" }), }), ), ).toMatchObject({ status: 201 }); expect( await fetchWorker(davRequest(fixture, "/addressbooks/default/sync.vcf", { method: "DELETE" })), ).toMatchObject({ status: 204, }); const sync = await fetchWorker( davRequest(fixture, "/addressbooks/default/", { method: "REPORT", headers: { "content-type": "application/xml" }, body: syncCollectionBody(baselineToken), }), ); expect(sync.status).toBe(207); const xml = await sync.text(); expect(xml).not.toContain(`${baselineToken}`); expect(xml).toContain("/addressbooks/default/sync.vcf"); expect(xml).toContain("HTTP/1.1 404 Not Found"); }); it("marks truncated sync-collection responses with a 507 response", async () => { const fixture = await createDavFixture(["carddav.full"]); expect( await fetchWorker( davRequest(fixture, `/addressbooks/default/truncated-a-${crypto.randomUUID()}.vcf`, { method: "PUT", body: vcard({ uid: uid(), fn: "Truncated A" }), }), ), ).toMatchObject({ status: 201 }); expect( await fetchWorker( davRequest(fixture, `/addressbooks/default/truncated-b-${crypto.randomUUID()}.vcf`, { method: "PUT", body: vcard({ uid: uid(), fn: "Truncated B" }), }), ), ).toMatchObject({ status: 201 }); await withEnvBinding("MAX_REPORT_RESULTS", "1", async () => { const sync = await fetchWorker( davRequest(fixture, "/addressbooks/default/", { method: "REPORT", headers: { "content-type": "application/xml" }, body: syncCollectionBody(""), }), ); expect(sync.status).toBe(207); const xml = await sync.text(); expect(xml).toContain("HTTP/1.1 507 Insufficient Storage"); expect(xml).toContain("sync:0"); }); }); });