Skip to content
File

Blob: tests/worker/protocol/webdav/locks-scope-move.workers.test.ts

typescript223 lines
1import { describe, expect, it } from "vitest";
2 
3import { createDavFixture, davRequest } from "@tests/worker/helpers/dav";
4import { fetchWorker } from "@tests/worker/helpers/http";
5import { lock, name } from "./locks-helpers";
6 
7describe("WebDAV lock scope and moves", () => {
8 it("applies Depth 0 collection locks to membership changes without blocking existing child edits", async () => {
9 const fixture = await createDavFixture();
10 const directory = name("depth-zero-dir");
11 const source = `${name("source")}.txt`;
12 expect(await fetchWorker(davRequest(fixture, `/files/${directory}/`, { method: "MKCOL" }))).toHaveProperty(
13 "status",
14 201,
15 );
16 expect(
17 await fetchWorker(davRequest(fixture, `/files/${directory}/existing.txt`, { method: "PUT", body: "first" })),
18 ).toHaveProperty("status", 201);
19 expect(
20 await fetchWorker(davRequest(fixture, `/files/${source}`, { method: "PUT", body: "source" })),
21 ).toHaveProperty("status", 201);
22 const token = await lock(fixture, `/files/${directory}/`, "0");
23 const ifHeader = `<https://${fixture.hostLabel}.dav.example.com/files/${directory}/> (${token})`;
24 
25 expect(
26 await fetchWorker(davRequest(fixture, `/files/${directory}/created.txt`, { method: "PUT", body: "blocked" })),
27 ).toHaveProperty("status", 423);
28 expect(
29 await fetchWorker(davRequest(fixture, `/files/${directory}/existing.txt`, { method: "PUT", body: "edit" })),
30 ).toHaveProperty("status", 204);
31 expect(
32 await fetchWorker(
33 davRequest(fixture, `/files/${directory}/created.txt`, {
34 method: "PUT",
35 headers: { if: ifHeader },
36 body: "allowed",
37 }),
38 ),
39 ).toHaveProperty("status", 201);
40 
41 expect(
42 await fetchWorker(davRequest(fixture, `/files/${directory}/child-dir/`, { method: "MKCOL" })),
43 ).toHaveProperty("status", 423);
44 expect(
45 await fetchWorker(
46 davRequest(fixture, `/files/${directory}/child-dir/`, { method: "MKCOL", headers: { if: ifHeader } }),
47 ),
48 ).toHaveProperty("status", 201);
49 
50 expect(
51 await fetchWorker(davRequest(fixture, `/files/${directory}/created.txt`, { method: "DELETE" })),
52 ).toHaveProperty("status", 423);
53 expect(
54 await fetchWorker(
55 davRequest(fixture, `/files/${directory}/created.txt`, { method: "DELETE", headers: { if: ifHeader } }),
56 ),
57 ).toHaveProperty("status", 204);
58 
59 expect(
60 await fetchWorker(
61 davRequest(fixture, `/files/${source}`, {
62 method: "COPY",
63 headers: { destination: `https://${fixture.hostLabel}.dav.example.com/files/${directory}/copied.txt` },
64 }),
65 ),
66 ).toHaveProperty("status", 423);
67 expect(
68 await fetchWorker(
69 davRequest(fixture, `/files/${source}`, {
70 method: "COPY",
71 headers: {
72 destination: `https://${fixture.hostLabel}.dav.example.com/files/${directory}/copied.txt`,
73 if: ifHeader,
74 },
75 }),
76 ),
77 ).toHaveProperty("status", 201);
78 
79 const movable = `${name("movable")}.txt`;
80 expect(await fetchWorker(davRequest(fixture, `/files/${movable}`, { method: "PUT", body: "move" }))).toHaveProperty(
81 "status",
82 201,
83 );
84 expect(
85 await fetchWorker(
86 davRequest(fixture, `/files/${movable}`, {
87 method: "MOVE",
88 headers: { destination: `https://${fixture.hostLabel}.dav.example.com/files/${directory}/moved.txt` },
89 }),
90 ),
91 ).toHaveProperty("status", 423);
92 expect(
93 await fetchWorker(
94 davRequest(fixture, `/files/${movable}`, {
95 method: "MOVE",
96 headers: {
97 destination: `https://${fixture.hostLabel}.dav.example.com/files/${directory}/moved.txt`,
98 if: ifHeader,
99 },
100 }),
101 ),
102 ).toHaveProperty("status", 201);
103 });
104 
105 it("scopes LOCK refresh and UNLOCK to the request URI and drops direct locks on MOVE", async () => {
106 const fixture = await createDavFixture();
107 const first = `${name("locked-a")}.txt`;
108 const second = `${name("locked-b")}.txt`;
109 expect(await fetchWorker(davRequest(fixture, `/files/${first}`, { method: "PUT", body: "a" }))).toHaveProperty(
110 "status",
111 201,
112 );
113 expect(await fetchWorker(davRequest(fixture, `/files/${second}`, { method: "PUT", body: "b" }))).toHaveProperty(
114 "status",
115 201,
116 );
117 const firstToken = await lock(fixture, `/files/${first}`);
118 const secondToken = await lock(fixture, `/files/${second}`);
119 
120 expect(
121 await fetchWorker(
122 davRequest(fixture, `/files/${first}`, { method: "LOCK", headers: { if: `(${secondToken})` } }),
123 ),
124 ).toHaveProperty("status", 412);
125 expect(
126 await fetchWorker(
127 davRequest(fixture, `/files/${first}`, { method: "UNLOCK", headers: { "lock-token": secondToken } }),
128 ),
129 ).toHaveProperty("status", 409);
130 
131 // RFC 4918 7.5: MOVE leaves the source URL unmapped, so direct locks at the
132 // source must be dropped rather than carried to the destination.
133 const moved = `${name("moved-lock")}.txt`;
134 expect(
135 await fetchWorker(
136 davRequest(fixture, `/files/${first}`, {
137 method: "MOVE",
138 headers: {
139 destination: `https://${fixture.hostLabel}.dav.example.com/files/${moved}`,
140 if: `(${firstToken})`,
141 },
142 }),
143 ),
144 ).toHaveProperty("status", 201);
145 expect(
146 await fetchWorker(
147 davRequest(fixture, `/files/${first}`, { method: "UNLOCK", headers: { "lock-token": firstToken } }),
148 ),
149 ).toHaveProperty("status", 409);
150 // The lock no longer exists at the moved href: UNLOCK there returns 409.
151 expect(
152 await fetchWorker(
153 davRequest(fixture, `/files/${moved}`, { method: "UNLOCK", headers: { "lock-token": firstToken } }),
154 ),
155 ).toHaveProperty("status", 409);
156 });
157 
158 // DAV-VERIFY-002: lockdiscovery after MOVE must not report a moved lock at
159 // the destination href.
160 it("does not advertise locks on the destination href after MOVE", async () => {
161 const fixture = await createDavFixture();
162 const file = `${name("move-locked")}.txt`;
163 const moved = `${name("moved-no-lock")}.txt`;
164 expect(await fetchWorker(davRequest(fixture, `/files/${file}`, { method: "PUT", body: "a" }))).toHaveProperty(
165 "status",
166 201,
167 );
168 const token = await lock(fixture, `/files/${file}`);
169 
170 expect(
171 await fetchWorker(
172 davRequest(fixture, `/files/${file}`, {
173 method: "MOVE",
174 headers: {
175 destination: `https://${fixture.hostLabel}.dav.example.com/files/${moved}`,
176 if: `(${token})`,
177 },
178 }),
179 ),
180 ).toHaveProperty("status", 201);
181 
182 const propfind = await fetchWorker(
183 davRequest(fixture, `/files/${moved}`, {
184 method: "PROPFIND",
185 headers: { depth: "0", "content-type": "application/xml" },
186 body: `<?xml version="1.0"?><D:propfind xmlns:D="DAV:"><D:prop><D:lockdiscovery/></D:prop></D:propfind>`,
187 }),
188 );
189 expect(propfind.status).toBe(207);
190 expect(await propfind.text()).not.toContain(token.slice(1, -1));
191 });
192 
193 it("drops Depth infinity collection locks when the locked collection is moved", async () => {
194 const fixture = await createDavFixture();
195 const dir = name("move-locked-dir");
196 const moved = name("moved-locked-dir");
197 expect(await fetchWorker(davRequest(fixture, `/files/${dir}/`, { method: "MKCOL" }))).toHaveProperty("status", 201);
198 expect(
199 await fetchWorker(davRequest(fixture, `/files/${dir}/child.txt`, { method: "PUT", body: "x" })),
200 ).toHaveProperty("status", 201);
201 const token = await lock(fixture, `/files/${dir}/`, "infinity");
202 
203 expect(
204 await fetchWorker(
205 davRequest(fixture, `/files/${dir}/`, {
206 method: "MOVE",
207 headers: {
208 destination: `https://${fixture.hostLabel}.dav.example.com/files/${moved}/`,
209 if: `(${token})`,
210 },
211 }),
212 ),
213 ).toHaveProperty("status", 201);
214 
215 // Writes against descendants of the destination must not require the
216 // original lock token because the lock was dropped, not relocated.
217 const writeAfterMove = await fetchWorker(
218 davRequest(fixture, `/files/${moved}/child.txt`, { method: "PUT", body: "rewritten" }),
219 );
220 expect(writeAfterMove.status).toBe(204);
221 });
222});