File
Blob: tests/worker/protocol/webdav/locks-refresh.workers.test.ts
| 1 | import { describe, expect, it } from "vitest"; |
| 2 | |
| 3 | import { createDavFixture, davRequest } from "@tests/worker/helpers/dav"; |
| 4 | import { fetchWorker } from "@tests/worker/helpers/http"; |
| 5 | import { lock, name } from "./locks-helpers"; |
| 6 | |
| 7 | describe("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 | }); |