Skip to content
File

Blob: tests/worker/protocol/carddav/addressbook-objects.workers.test.ts

typescript181 lines
1import { describe, expect, it } from "vitest";
2 
3import { createDavFixture, davRequest } from "@tests/worker/helpers/dav";
4import { fetchWorker } from "@tests/worker/helpers/http";
5import { propfindSyncToken, uid, vcard } from "./helpers";
6 
7describe("CardDAV address book objects", () => {
8 it("puts, gets, and deletes a vCard object", async () => {
9 const fixture = await createDavFixture(["carddav.full"]);
10 const contactUid = uid();
11 const href = `/addressbooks/default/alice-${crypto.randomUUID()}.vcf`;
12 const put = await fetchWorker(
13 davRequest(fixture, href, {
14 method: "PUT",
15 headers: { "content-type": "text/vcard" },
16 body: vcard({ uid: contactUid, fn: "Alice Example", email: "alice@example.test" }),
17 }),
18 );
19 expect(put.status).toBe(201);
20 expect(put.headers.get("etag")).toMatch(/^"[0-9a-f]+"$/);
21 
22 const get = await fetchWorker(davRequest(fixture, href));
23 expect(get.status).toBe(200);
24 expect(get.headers.get("content-type")).toBe("text/vcard; charset=utf-8");
25 const body = await get.text();
26 expect(body).toContain(`UID:${contactUid}`);
27 expect(body).toContain("FN:Alice Example");
28 
29 const deleted = await fetchWorker(davRequest(fixture, href, { method: "DELETE" }));
30 expect(deleted.status).toBe(204);
31 expect(await fetchWorker(davRequest(fixture, href))).toMatchObject({ status: 404 });
32 });
33 
34 it("rejects malformed vCards and duplicate UIDs", async () => {
35 const fixture = await createDavFixture(["carddav.full"]);
36 const malformed = await fetchWorker(
37 davRequest(fixture, "/addressbooks/default/bad.vcf", {
38 method: "PUT",
39 headers: { "content-type": "text/vcard" },
40 body: "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:Missing UID\r\nEND:VCARD\r\n",
41 }),
42 );
43 expect(malformed.status).toBe(400);
44 
45 const contactUid = uid();
46 const first = await fetchWorker(
47 davRequest(fixture, "/addressbooks/default/one.vcf", {
48 method: "PUT",
49 body: vcard({ uid: contactUid, fn: "One" }),
50 }),
51 );
52 expect(first.status).toBe(201);
53 const duplicate = await fetchWorker(
54 davRequest(fixture, "/addressbooks/default/two.vcf", {
55 method: "PUT",
56 body: vcard({ uid: contactUid, fn: "Two" }),
57 }),
58 );
59 expect(duplicate.status).toBe(409);
60 
61 const changedUid = await fetchWorker(
62 davRequest(fixture, "/addressbooks/default/one.vcf", {
63 method: "PUT",
64 body: vcard({ uid: uid(), fn: "Changed UID" }),
65 }),
66 );
67 expect(changedUid.status).toBe(409);
68 });
69 
70 it("enforces accepted vCard version requirements", async () => {
71 const fixture = await createDavFixture(["carddav.full"]);
72 const missingFn = await fetchWorker(
73 davRequest(fixture, "/addressbooks/default/missing-fn.vcf", {
74 method: "PUT",
75 body: ["BEGIN:VCARD", "VERSION:4.0", `UID:${uid()}`, "END:VCARD", ""].join("\r\n"),
76 }),
77 );
78 expect(missingFn.status).toBe(400);
79 
80 const misplacedVersion = await fetchWorker(
81 davRequest(fixture, "/addressbooks/default/misplaced-version.vcf", {
82 method: "PUT",
83 body: ["BEGIN:VCARD", "FN:Wrong Order", "VERSION:4.0", `UID:${uid()}`, "END:VCARD", ""].join("\r\n"),
84 }),
85 );
86 expect(misplacedVersion.status).toBe(400);
87 
88 const v3 = await fetchWorker(
89 davRequest(fixture, "/addressbooks/default/v3.vcf", {
90 method: "PUT",
91 body: ["BEGIN:VCARD", "VERSION:3.0", `UID:${uid()}`, "FN:Version Three", "END:VCARD", ""].join("\r\n"),
92 }),
93 );
94 expect(v3.status).toBe(201);
95 });
96 
97 it("enforces tagged collection sync-token If preconditions on object writes", async () => {
98 const fixture = await createDavFixture(["carddav.full"]);
99 const contactUid = uid();
100 const initialToken = await propfindSyncToken(fixture);
101 const created = await fetchWorker(
102 davRequest(fixture, "/addressbooks/default/if-sync.vcf", {
103 method: "PUT",
104 headers: { if: `</addressbooks/default/> (<${initialToken}>)` },
105 body: vcard({ uid: contactUid, fn: "If Sync" }),
106 }),
107 );
108 expect(created.status).toBe(201);
109 
110 const currentToken = await propfindSyncToken(fixture);
111 const stalePut = await fetchWorker(
112 davRequest(fixture, "/addressbooks/default/if-sync.vcf", {
113 method: "PUT",
114 headers: { if: `</addressbooks/default/> (<${initialToken}>)` },
115 body: vcard({ uid: contactUid, fn: "If Sync Stale" }),
116 }),
117 );
118 expect(stalePut.status).toBe(412);
119 
120 const wrongTag = await fetchWorker(
121 davRequest(fixture, "/addressbooks/default/if-sync.vcf", {
122 method: "PUT",
123 headers: { if: `</addressbooks/not-default/> (<${currentToken}>)` },
124 body: vcard({ uid: contactUid, fn: "If Sync Wrong Tag" }),
125 }),
126 );
127 expect(wrongTag.status).toBe(412);
128 
129 const updated = await fetchWorker(
130 davRequest(fixture, "/addressbooks/default/if-sync.vcf", {
131 method: "PUT",
132 headers: { if: `</addressbooks/default/> (<${currentToken}>)` },
133 body: vcard({ uid: contactUid, fn: "If Sync Updated" }),
134 }),
135 );
136 expect(updated.status).toBe(204);
137 
138 const staleDelete = await fetchWorker(
139 davRequest(fixture, "/addressbooks/default/if-sync.vcf", {
140 method: "DELETE",
141 headers: { if: `</addressbooks/default/> (<${currentToken}>)` },
142 }),
143 );
144 expect(staleDelete.status).toBe(412);
145 });
146 
147 it("honors HTTP If-Match on CardDAV object DELETE", async () => {
148 const fixture = await createDavFixture(["carddav.full"]);
149 expect(
150 await fetchWorker(
151 davRequest(fixture, "/addressbooks/default/delete-if-match.vcf", {
152 method: "PUT",
153 body: vcard({ uid: uid(), fn: "Delete If-Match" }),
154 }),
155 ),
156 ).toMatchObject({ status: 201 });
157 const deleted = await fetchWorker(
158 davRequest(fixture, "/addressbooks/default/delete-if-match.vcf", {
159 method: "DELETE",
160 headers: { "if-match": '"definitely-stale"' },
161 }),
162 );
163 expect(deleted.status).toBe(412);
164 expect(await fetchWorker(davRequest(fixture, "/addressbooks/default/delete-if-match.vcf"))).toHaveProperty(
165 "status",
166 200,
167 );
168 });
169 
170 it("prevents read-only CardDAV PATs from writing objects", async () => {
171 const fixture = await createDavFixture(["carddav.readonly"]);
172 const put = await fetchWorker(
173 davRequest(fixture, "/addressbooks/default/readonly.vcf", {
174 method: "PUT",
175 body: vcard({ uid: uid(), fn: "No Write" }),
176 }),
177 );
178 expect(put.status).toBe(403);
179 });
180});