File
Blob: tests/worker/protocol/webdav/locks-basic.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 basics", () => { |
| 8 | it("denies writes without the active lock token and allows writes with it", async () => { |
| 9 | const fixture = await createDavFixture(); |
| 10 | const file = `${name("locked")}.txt`; |
| 11 | expect(await fetchWorker(davRequest(fixture, `/files/${file}`, { method: "PUT", body: "first" }))).toHaveProperty( |
| 12 | "status", |
| 13 | 201, |
| 14 | ); |
| 15 | const token = await lock(fixture, `/files/${file}`); |
| 16 | |
| 17 | const denied = await fetchWorker(davRequest(fixture, `/files/${file}`, { method: "PUT", body: "blocked" })); |
| 18 | expect(denied.status).toBe(423); |
| 19 | |
| 20 | const allowed = await fetchWorker( |
| 21 | davRequest(fixture, `/files/${file}`, { |
| 22 | method: "PUT", |
| 23 | headers: { if: `<https://${fixture.hostLabel}.dav.example.com/files/${file}> (${token})` }, |
| 24 | body: "second", |
| 25 | }), |
| 26 | ); |
| 27 | expect(allowed.status).toBe(204); |
| 28 | |
| 29 | const unlock = await fetchWorker( |
| 30 | davRequest(fixture, `/files/${file}`, { method: "UNLOCK", headers: { "lock-token": token } }), |
| 31 | ); |
| 32 | expect(unlock.status).toBe(204); |
| 33 | }); |
| 34 | |
| 35 | it("applies Depth infinity collection locks to descendants and supports refresh", async () => { |
| 36 | const fixture = await createDavFixture(); |
| 37 | const directory = name("locked-dir"); |
| 38 | expect(await fetchWorker(davRequest(fixture, `/files/${directory}/`, { method: "MKCOL" }))).toHaveProperty( |
| 39 | "status", |
| 40 | 201, |
| 41 | ); |
| 42 | const token = await lock(fixture, `/files/${directory}/`, "infinity"); |
| 43 | |
| 44 | const denied = await fetchWorker( |
| 45 | davRequest(fixture, `/files/${directory}/child.txt`, { method: "PUT", body: "x" }), |
| 46 | ); |
| 47 | expect(denied.status).toBe(423); |
| 48 | |
| 49 | const refresh = await fetchWorker( |
| 50 | davRequest(fixture, `/files/${directory}/`, { |
| 51 | method: "LOCK", |
| 52 | headers: { if: `(${token})`, timeout: "Second-120" }, |
| 53 | }), |
| 54 | ); |
| 55 | expect(refresh.status).toBe(200); |
| 56 | |
| 57 | const allowed = await fetchWorker( |
| 58 | davRequest(fixture, `/files/${directory}/child.txt`, { |
| 59 | method: "PUT", |
| 60 | headers: { if: `<https://${fixture.hostLabel}.dav.example.com/files/${directory}/> (${token})` }, |
| 61 | body: "x", |
| 62 | }), |
| 63 | ); |
| 64 | expect(allowed.status).toBe(201); |
| 65 | }); |
| 66 | |
| 67 | it("creates a readable zero-byte resource for LOCK on an unmapped file URL", async () => { |
| 68 | const fixture = await createDavFixture(); |
| 69 | const file = `${name("unmapped-lock")}.txt`; |
| 70 | const response = await fetchWorker( |
| 71 | davRequest(fixture, `/files/${file}`, { |
| 72 | method: "LOCK", |
| 73 | headers: { depth: "0", timeout: "Second-600", "content-type": "application/xml" }, |
| 74 | body: `<?xml version="1.0"?><D:lockinfo xmlns:D="DAV:"><D:lockscope><D:exclusive/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockinfo>`, |
| 75 | }), |
| 76 | ); |
| 77 | |
| 78 | expect(response.status).toBe(201); |
| 79 | const token = response.headers.get("lock-token"); |
| 80 | expect(token).toMatch(/^<opaquelocktoken:[^>]+>$/); |
| 81 | |
| 82 | const get = await fetchWorker(davRequest(fixture, `/files/${file}`)); |
| 83 | expect(get.status).toBe(200); |
| 84 | expect(get.headers.get("content-length")).toBe("0"); |
| 85 | await expect(get.arrayBuffer()).resolves.toHaveProperty("byteLength", 0); |
| 86 | |
| 87 | const head = await fetchWorker(davRequest(fixture, `/files/${file}`, { method: "HEAD" })); |
| 88 | expect(head.status).toBe(200); |
| 89 | expect(head.headers.get("content-length")).toBe("0"); |
| 90 | |
| 91 | const put = await fetchWorker( |
| 92 | davRequest(fixture, `/files/${file}`, { |
| 93 | method: "PUT", |
| 94 | headers: { if: `(${token})` }, |
| 95 | body: "replaced", |
| 96 | }), |
| 97 | ); |
| 98 | expect(put.status).toBe(204); |
| 99 | await expect((await fetchWorker(davRequest(fixture, `/files/${file}`))).text()).resolves.toBe("replaced"); |
| 100 | }); |
| 101 | |
| 102 | it("includes inherited Depth infinity locks in lockdiscovery", async () => { |
| 103 | const fixture = await createDavFixture(); |
| 104 | const directory = name("lockdiscovery"); |
| 105 | expect(await fetchWorker(davRequest(fixture, `/files/${directory}/`, { method: "MKCOL" }))).toHaveProperty( |
| 106 | "status", |
| 107 | 201, |
| 108 | ); |
| 109 | expect( |
| 110 | await fetchWorker(davRequest(fixture, `/files/${directory}/child.txt`, { method: "PUT", body: "x" })), |
| 111 | ).toHaveProperty("status", 201); |
| 112 | const token = await lock(fixture, `/files/${directory}/`, "infinity"); |
| 113 | |
| 114 | const propfind = await fetchWorker( |
| 115 | davRequest(fixture, `/files/${directory}/child.txt`, { |
| 116 | method: "PROPFIND", |
| 117 | headers: { depth: "0", "content-type": "application/xml" }, |
| 118 | body: `<?xml version="1.0"?><D:propfind xmlns:D="DAV:"><D:prop><D:lockdiscovery/></D:prop></D:propfind>`, |
| 119 | }), |
| 120 | ); |
| 121 | expect(propfind.status).toBe(207); |
| 122 | expect(await propfind.text()).toContain(token.slice(1, -1)); |
| 123 | }); |
| 124 | }); |