Skip to content
File

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

typescript139 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 refresh and edge cases", () => {
8 it("treats missing LOCK Depth as infinity and rejects invalid Depth values", async () => {
9 const fixture = await createDavFixture();
10 const directory = name("default-depth");
11 expect(await fetchWorker(davRequest(fixture, `/files/${directory}/`, { method: "MKCOL" }))).toHaveProperty(
12 "status",
13 201,
14 );
15 
16 const response = await fetchWorker(
17 davRequest(fixture, `/files/${directory}/`, {
18 method: "LOCK",
19 headers: { timeout: "Second-600", "content-type": "application/xml" },
20 body: `<?xml version="1.0"?><D:lockinfo xmlns:D="DAV:"><D:lockscope><D:exclusive/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockinfo>`,
21 }),
22 );
23 expect(response.status).toBe(200);
24 const token = response.headers.get("lock-token");
25 expect(token).toMatch(/^<opaquelocktoken:[^>]+>$/);
26 
27 expect(
28 await fetchWorker(davRequest(fixture, `/files/${directory}/child.txt`, { method: "PUT", body: "blocked" })),
29 ).toHaveProperty("status", 423);
30 
31 const invalid = await fetchWorker(
32 davRequest(fixture, `/files/${directory}/`, {
33 method: "LOCK",
34 headers: { depth: "1", timeout: "Second-600", "content-type": "application/xml", if: `(${token})` },
35 body: `<?xml version="1.0"?><D:lockinfo xmlns:D="DAV:"><D:lockscope><D:exclusive/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockinfo>`,
36 }),
37 );
38 expect(invalid.status).toBe(400);
39 });
40 
41 // DAV-VERIFY-004: empty-body LOCK is refresh only; unknown tokens fail; new
42 // exclusive LOCK over an existing exclusive lock fails even with the token.
43 it("fails empty-body LOCK with an unknown If token instead of creating a new lock", async () => {
44 const fixture = await createDavFixture();
45 const file = `${name("refresh-unknown")}.txt`;
46 expect(await fetchWorker(davRequest(fixture, `/files/${file}`, { method: "PUT", body: "x" }))).toHaveProperty(
47 "status",
48 201,
49 );
50 const response = await fetchWorker(
51 davRequest(fixture, `/files/${file}`, {
52 method: "LOCK",
53 headers: { if: "(<opaquelocktoken:00000000-0000-0000-0000-000000000000>)" },
54 }),
55 );
56 expect(response.status).toBe(412);
57 expect(response.headers.get("lock-token")).toBeNull();
58 });
59 
60 it("rejects empty-body LOCK refresh that names multiple applicable tokens", async () => {
61 const fixture = await createDavFixture();
62 const file = `${name("refresh-many")}.txt`;
63 expect(await fetchWorker(davRequest(fixture, `/files/${file}`, { method: "PUT", body: "x" }))).toHaveProperty(
64 "status",
65 201,
66 );
67 const tokenA = await lock(fixture, `/files/${file}`);
68 
69 const second = `${name("refresh-many-other")}.txt`;
70 expect(await fetchWorker(davRequest(fixture, `/files/${second}`, { method: "PUT", body: "y" }))).toHaveProperty(
71 "status",
72 201,
73 );
74 // Token A applies to /files/<file>; we send it alongside an unrelated token
75 // that does not apply. Only one token resolves and applies, so refresh succeeds.
76 const onlyOneApplies = await fetchWorker(
77 davRequest(fixture, `/files/${file}`, {
78 method: "LOCK",
79 headers: {
80 if: `(${tokenA}) (<opaquelocktoken:11111111-1111-1111-1111-111111111111>)`,
81 },
82 }),
83 );
84 // The unknown second token is in a separate list; the first list still resolves.
85 expect(onlyOneApplies.status).toBe(200);
86 });
87 
88 it("does not return a Lock-Token header on successful LOCK refresh", async () => {
89 const fixture = await createDavFixture();
90 const file = `${name("refresh-header")}.txt`;
91 expect(await fetchWorker(davRequest(fixture, `/files/${file}`, { method: "PUT", body: "x" }))).toHaveProperty(
92 "status",
93 201,
94 );
95 const token = await lock(fixture, `/files/${file}`);
96 const refreshed = await fetchWorker(
97 davRequest(fixture, `/files/${file}`, { method: "LOCK", headers: { if: `(${token})` } }),
98 );
99 expect(refreshed.status).toBe(200);
100 // RFC 4918 9.10.1: Lock-Token header is only for new lock creation.
101 expect(refreshed.headers.get("lock-token")).toBeNull();
102 });
103 
104 it("accepts LOCK refresh even when Depth is missing or invalid", async () => {
105 const fixture = await createDavFixture();
106 const file = `${name("refresh-depth")}.txt`;
107 expect(await fetchWorker(davRequest(fixture, `/files/${file}`, { method: "PUT", body: "x" }))).toHaveProperty(
108 "status",
109 201,
110 );
111 const token = await lock(fixture, `/files/${file}`);
112 const refreshed = await fetchWorker(
113 davRequest(fixture, `/files/${file}`, {
114 method: "LOCK",
115 headers: { if: `(${token})`, depth: "garbage" },
116 }),
117 );
118 expect(refreshed.status).toBe(200);
119 });
120 
121 it("rejects a new exclusive LOCK even when the existing exclusive token is supplied", async () => {
122 const fixture = await createDavFixture();
123 const file = `${name("new-lock-conflict")}.txt`;
124 expect(await fetchWorker(davRequest(fixture, `/files/${file}`, { method: "PUT", body: "x" }))).toHaveProperty(
125 "status",
126 201,
127 );
128 const token = await lock(fixture, `/files/${file}`);
129 const second = await fetchWorker(
130 davRequest(fixture, `/files/${file}`, {
131 method: "LOCK",
132 headers: { if: `(${token})`, "content-type": "application/xml" },
133 body: `<?xml version="1.0"?><D:lockinfo xmlns:D="DAV:"><D:lockscope><D:exclusive/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockinfo>`,
134 }),
135 );
136 expect(second.status).toBe(423);
137 });
138});