Skip to content
File

Blob: tests/worker/protocol/webdav/if-header-preconditions.workers.test.ts

typescript169 lines
1import { describe, expect, it } from "vitest";
2 
3import { createDavFixture, davRequest } from "@tests/worker/helpers/dav";
4import { fetchWorker } from "@tests/worker/helpers/http";
5 
6describe("WebDAV If header preconditions", () => {
7 it("rejects false If entity-tag preconditions on mutating WebDAV methods", async () => {
8 const fixture = await createDavFixture();
9 const file = `if-${crypto.randomUUID()}.txt`;
10 const source = `if-source-${crypto.randomUUID()}.txt`;
11 const prop = `if-prop-${crypto.randomUUID()}.txt`;
12 const falseIf = '(["definitely-not-the-current-etag"])';
13 
14 expect(await fetchWorker(davRequest(fixture, `/files/${file}`, { method: "PUT", body: "x" }))).toHaveProperty(
15 "status",
16 201,
17 );
18 expect(await fetchWorker(davRequest(fixture, `/files/${source}`, { method: "PUT", body: "x" }))).toHaveProperty(
19 "status",
20 201,
21 );
22 expect(await fetchWorker(davRequest(fixture, `/files/${prop}`, { method: "PUT", body: "x" }))).toHaveProperty(
23 "status",
24 201,
25 );
26 
27 expect(
28 await fetchWorker(davRequest(fixture, `/files/${file}`, { method: "DELETE", headers: { if: falseIf } })),
29 ).toHaveProperty("status", 412);
30 expect(
31 await fetchWorker(
32 davRequest(fixture, `/files/${source}`, {
33 method: "COPY",
34 headers: { if: falseIf, destination: `https://${fixture.hostLabel}.dav.example.com/files/copied.txt` },
35 }),
36 ),
37 ).toHaveProperty("status", 412);
38 expect(
39 await fetchWorker(
40 davRequest(fixture, `/files/${source}`, {
41 method: "MOVE",
42 headers: { if: falseIf, destination: `https://${fixture.hostLabel}.dav.example.com/files/moved.txt` },
43 }),
44 ),
45 ).toHaveProperty("status", 412);
46 expect(
47 await fetchWorker(
48 davRequest(fixture, `/files/${prop}`, {
49 method: "PROPPATCH",
50 headers: { if: falseIf, "content-type": "application/xml" },
51 body: `<?xml version="1.0"?><D:propertyupdate xmlns:D="DAV:" xmlns:Z="urn:test"><D:set><D:prop><Z:x>y</Z:x></D:prop></D:set></D:propertyupdate>`,
52 }),
53 ),
54 ).toHaveProperty("status", 412);
55 expect(
56 await fetchWorker(davRequest(fixture, `/files/new-if-dir/`, { method: "MKCOL", headers: { if: falseIf } })),
57 ).toHaveProperty("status", 412);
58 });
59 
60 it("honors HTTP If-Match on DELETE, COPY, MOVE, and PROPPATCH", async () => {
61 const fixture = await createDavFixture();
62 const file = `if-match-${crypto.randomUUID()}.txt`;
63 const copySource = `if-match-copy-${crypto.randomUUID()}.txt`;
64 const moveSource = `if-match-move-${crypto.randomUUID()}.txt`;
65 const propSource = `if-match-prop-${crypto.randomUUID()}.txt`;
66 const stale = '"definitely-stale"';
67 for (const name of [file, copySource, moveSource, propSource]) {
68 expect(await fetchWorker(davRequest(fixture, `/files/${name}`, { method: "PUT", body: "x" }))).toHaveProperty(
69 "status",
70 201,
71 );
72 }
73 
74 expect(
75 await fetchWorker(davRequest(fixture, `/files/${file}`, { method: "DELETE", headers: { "if-match": stale } })),
76 ).toHaveProperty("status", 412);
77 expect(await fetchWorker(davRequest(fixture, `/files/${file}`))).toHaveProperty("status", 200);
78 
79 expect(
80 await fetchWorker(
81 davRequest(fixture, `/files/${copySource}`, {
82 method: "COPY",
83 headers: {
84 "if-match": stale,
85 destination: `https://${fixture.hostLabel}.dav.example.com/files/if-match-copy-dest.txt`,
86 },
87 }),
88 ),
89 ).toHaveProperty("status", 412);
90 expect(await fetchWorker(davRequest(fixture, "/files/if-match-copy-dest.txt"))).toHaveProperty("status", 404);
91 
92 expect(
93 await fetchWorker(
94 davRequest(fixture, `/files/${moveSource}`, {
95 method: "MOVE",
96 headers: {
97 "if-match": stale,
98 destination: `https://${fixture.hostLabel}.dav.example.com/files/if-match-move-dest.txt`,
99 },
100 }),
101 ),
102 ).toHaveProperty("status", 412);
103 expect(await fetchWorker(davRequest(fixture, `/files/${moveSource}`))).toHaveProperty("status", 200);
104 
105 expect(
106 await fetchWorker(
107 davRequest(fixture, `/files/${propSource}`, {
108 method: "PROPPATCH",
109 headers: { "if-match": stale, "content-type": "application/xml" },
110 body: `<?xml version="1.0"?><D:propertyupdate xmlns:D="DAV:" xmlns:Z="urn:test"><D:set><D:prop><Z:x>y</Z:x></D:prop></D:set></D:propertyupdate>`,
111 }),
112 ),
113 ).toHaveProperty("status", 412);
114 });
115 
116 it("applies tagged If entity-tag conditions to the tagged resource", async () => {
117 const fixture = await createDavFixture();
118 const target = `tag-target-${crypto.randomUUID()}.txt`;
119 const tagged = `tagged-${crypto.randomUUID()}.txt`;
120 expect(
121 await fetchWorker(davRequest(fixture, `/files/${target}`, { method: "PUT", body: "target" })),
122 ).toHaveProperty("status", 201);
123 const taggedPut = await fetchWorker(davRequest(fixture, `/files/${tagged}`, { method: "PUT", body: "tagged" }));
124 expect(taggedPut.status).toBe(201);
125 const etag = taggedPut.headers.get("etag");
126 expect(etag).toMatch(/^"[0-9a-f]+"$/);
127 
128 const response = await fetchWorker(
129 davRequest(fixture, `/files/${target}`, {
130 method: "PROPPATCH",
131 headers: {
132 if: `<https://${fixture.hostLabel}.dav.example.com/files/${tagged}> ([${etag}])`,
133 "content-type": "application/xml",
134 },
135 body: `<?xml version="1.0"?><D:propertyupdate xmlns:D="DAV:" xmlns:Z="urn:test"><D:set><D:prop><Z:x>y</Z:x></D:prop></D:set></D:propertyupdate>`,
136 }),
137 );
138 expect(response.status).toBe(207);
139 });
140 
141 it("rejects false If preconditions on LOCK refresh", async () => {
142 const fixture = await createDavFixture();
143 const file = `refresh-${crypto.randomUUID()}.txt`;
144 expect(await fetchWorker(davRequest(fixture, `/files/${file}`, { method: "PUT", body: "x" }))).toHaveProperty(
145 "status",
146 201,
147 );
148 const locked = await fetchWorker(
149 davRequest(fixture, `/files/${file}`, {
150 method: "LOCK",
151 headers: { depth: "0", timeout: "Second-600", "content-type": "application/xml" },
152 body: `<?xml version="1.0"?><D:lockinfo xmlns:D="DAV:"><D:lockscope><D:exclusive/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockinfo>`,
153 }),
154 );
155 expect(locked.status).toBe(200);
156 const token = locked.headers.get("lock-token");
157 expect(token).toMatch(/^<opaquelocktoken:[^>]+>$/);
158 
159 expect(
160 await fetchWorker(
161 davRequest(fixture, `/files/${file}`, {
162 method: "LOCK",
163 headers: { if: `(${token} ["definitely-not-the-current-etag"])` },
164 }),
165 ),
166 ).toHaveProperty("status", 412);
167 });
168});