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");
});
});
});