import { describe, expect, it } from "vitest"; import { createDavFixture, davRequest } from "@tests/worker/helpers/dav"; import { fetchWorker } from "@tests/worker/helpers/http"; import { propfindSyncToken, uid, vcard } from "./helpers"; describe("CardDAV address book objects", () => { it("puts, gets, and deletes a vCard object", async () => { const fixture = await createDavFixture(["carddav.full"]); const contactUid = uid(); const href = `/addressbooks/default/alice-${crypto.randomUUID()}.vcf`; const put = await fetchWorker( davRequest(fixture, href, { method: "PUT", headers: { "content-type": "text/vcard" }, body: vcard({ uid: contactUid, fn: "Alice Example", email: "alice@example.test" }), }), ); expect(put.status).toBe(201); expect(put.headers.get("etag")).toMatch(/^"[0-9a-f]+"$/); const get = await fetchWorker(davRequest(fixture, href)); expect(get.status).toBe(200); expect(get.headers.get("content-type")).toBe("text/vcard; charset=utf-8"); const body = await get.text(); expect(body).toContain(`UID:${contactUid}`); expect(body).toContain("FN:Alice Example"); const deleted = await fetchWorker(davRequest(fixture, href, { method: "DELETE" })); expect(deleted.status).toBe(204); expect(await fetchWorker(davRequest(fixture, href))).toMatchObject({ status: 404 }); }); it("rejects malformed vCards and duplicate UIDs", async () => { const fixture = await createDavFixture(["carddav.full"]); const malformed = await fetchWorker( davRequest(fixture, "/addressbooks/default/bad.vcf", { method: "PUT", headers: { "content-type": "text/vcard" }, body: "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:Missing UID\r\nEND:VCARD\r\n", }), ); expect(malformed.status).toBe(400); const contactUid = uid(); const first = await fetchWorker( davRequest(fixture, "/addressbooks/default/one.vcf", { method: "PUT", body: vcard({ uid: contactUid, fn: "One" }), }), ); expect(first.status).toBe(201); const duplicate = await fetchWorker( davRequest(fixture, "/addressbooks/default/two.vcf", { method: "PUT", body: vcard({ uid: contactUid, fn: "Two" }), }), ); expect(duplicate.status).toBe(409); const changedUid = await fetchWorker( davRequest(fixture, "/addressbooks/default/one.vcf", { method: "PUT", body: vcard({ uid: uid(), fn: "Changed UID" }), }), ); expect(changedUid.status).toBe(409); }); it("enforces accepted vCard version requirements", async () => { const fixture = await createDavFixture(["carddav.full"]); const missingFn = await fetchWorker( davRequest(fixture, "/addressbooks/default/missing-fn.vcf", { method: "PUT", body: ["BEGIN:VCARD", "VERSION:4.0", `UID:${uid()}`, "END:VCARD", ""].join("\r\n"), }), ); expect(missingFn.status).toBe(400); const misplacedVersion = await fetchWorker( davRequest(fixture, "/addressbooks/default/misplaced-version.vcf", { method: "PUT", body: ["BEGIN:VCARD", "FN:Wrong Order", "VERSION:4.0", `UID:${uid()}`, "END:VCARD", ""].join("\r\n"), }), ); expect(misplacedVersion.status).toBe(400); const v3 = await fetchWorker( davRequest(fixture, "/addressbooks/default/v3.vcf", { method: "PUT", body: ["BEGIN:VCARD", "VERSION:3.0", `UID:${uid()}`, "FN:Version Three", "END:VCARD", ""].join("\r\n"), }), ); expect(v3.status).toBe(201); }); it("enforces tagged collection sync-token If preconditions on object writes", async () => { const fixture = await createDavFixture(["carddav.full"]); const contactUid = uid(); const initialToken = await propfindSyncToken(fixture); const created = await fetchWorker( davRequest(fixture, "/addressbooks/default/if-sync.vcf", { method: "PUT", headers: { if: ` (<${initialToken}>)` }, body: vcard({ uid: contactUid, fn: "If Sync" }), }), ); expect(created.status).toBe(201); const currentToken = await propfindSyncToken(fixture); const stalePut = await fetchWorker( davRequest(fixture, "/addressbooks/default/if-sync.vcf", { method: "PUT", headers: { if: ` (<${initialToken}>)` }, body: vcard({ uid: contactUid, fn: "If Sync Stale" }), }), ); expect(stalePut.status).toBe(412); const wrongTag = await fetchWorker( davRequest(fixture, "/addressbooks/default/if-sync.vcf", { method: "PUT", headers: { if: ` (<${currentToken}>)` }, body: vcard({ uid: contactUid, fn: "If Sync Wrong Tag" }), }), ); expect(wrongTag.status).toBe(412); const updated = await fetchWorker( davRequest(fixture, "/addressbooks/default/if-sync.vcf", { method: "PUT", headers: { if: ` (<${currentToken}>)` }, body: vcard({ uid: contactUid, fn: "If Sync Updated" }), }), ); expect(updated.status).toBe(204); const staleDelete = await fetchWorker( davRequest(fixture, "/addressbooks/default/if-sync.vcf", { method: "DELETE", headers: { if: ` (<${currentToken}>)` }, }), ); expect(staleDelete.status).toBe(412); }); it("honors HTTP If-Match on CardDAV object DELETE", async () => { const fixture = await createDavFixture(["carddav.full"]); expect( await fetchWorker( davRequest(fixture, "/addressbooks/default/delete-if-match.vcf", { method: "PUT", body: vcard({ uid: uid(), fn: "Delete If-Match" }), }), ), ).toMatchObject({ status: 201 }); const deleted = await fetchWorker( davRequest(fixture, "/addressbooks/default/delete-if-match.vcf", { method: "DELETE", headers: { "if-match": '"definitely-stale"' }, }), ); expect(deleted.status).toBe(412); expect(await fetchWorker(davRequest(fixture, "/addressbooks/default/delete-if-match.vcf"))).toHaveProperty( "status", 200, ); }); it("prevents read-only CardDAV PATs from writing objects", async () => { const fixture = await createDavFixture(["carddav.readonly"]); const put = await fetchWorker( davRequest(fixture, "/addressbooks/default/readonly.vcf", { method: "PUT", body: vcard({ uid: uid(), fn: "No Write" }), }), ); expect(put.status).toBe(403); }); });