import { describe, expect, it } from "vitest"; import { createDavFixture, davRequest } from "@tests/worker/helpers/dav"; import { fetchWorker } from "@tests/worker/helpers/http"; import { lock, name } from "./locks-helpers"; describe("WebDAV lock scope and moves", () => { it("applies Depth 0 collection locks to membership changes without blocking existing child edits", async () => { const fixture = await createDavFixture(); const directory = name("depth-zero-dir"); const source = `${name("source")}.txt`; expect(await fetchWorker(davRequest(fixture, `/files/${directory}/`, { method: "MKCOL" }))).toHaveProperty( "status", 201, ); expect( await fetchWorker(davRequest(fixture, `/files/${directory}/existing.txt`, { method: "PUT", body: "first" })), ).toHaveProperty("status", 201); expect( await fetchWorker(davRequest(fixture, `/files/${source}`, { method: "PUT", body: "source" })), ).toHaveProperty("status", 201); const token = await lock(fixture, `/files/${directory}/`, "0"); const ifHeader = ` (${token})`; expect( await fetchWorker(davRequest(fixture, `/files/${directory}/created.txt`, { method: "PUT", body: "blocked" })), ).toHaveProperty("status", 423); expect( await fetchWorker(davRequest(fixture, `/files/${directory}/existing.txt`, { method: "PUT", body: "edit" })), ).toHaveProperty("status", 204); expect( await fetchWorker( davRequest(fixture, `/files/${directory}/created.txt`, { method: "PUT", headers: { if: ifHeader }, body: "allowed", }), ), ).toHaveProperty("status", 201); expect( await fetchWorker(davRequest(fixture, `/files/${directory}/child-dir/`, { method: "MKCOL" })), ).toHaveProperty("status", 423); expect( await fetchWorker( davRequest(fixture, `/files/${directory}/child-dir/`, { method: "MKCOL", headers: { if: ifHeader } }), ), ).toHaveProperty("status", 201); expect( await fetchWorker(davRequest(fixture, `/files/${directory}/created.txt`, { method: "DELETE" })), ).toHaveProperty("status", 423); expect( await fetchWorker( davRequest(fixture, `/files/${directory}/created.txt`, { method: "DELETE", headers: { if: ifHeader } }), ), ).toHaveProperty("status", 204); expect( await fetchWorker( davRequest(fixture, `/files/${source}`, { method: "COPY", headers: { destination: `https://${fixture.hostLabel}.dav.example.com/files/${directory}/copied.txt` }, }), ), ).toHaveProperty("status", 423); expect( await fetchWorker( davRequest(fixture, `/files/${source}`, { method: "COPY", headers: { destination: `https://${fixture.hostLabel}.dav.example.com/files/${directory}/copied.txt`, if: ifHeader, }, }), ), ).toHaveProperty("status", 201); const movable = `${name("movable")}.txt`; expect(await fetchWorker(davRequest(fixture, `/files/${movable}`, { method: "PUT", body: "move" }))).toHaveProperty( "status", 201, ); expect( await fetchWorker( davRequest(fixture, `/files/${movable}`, { method: "MOVE", headers: { destination: `https://${fixture.hostLabel}.dav.example.com/files/${directory}/moved.txt` }, }), ), ).toHaveProperty("status", 423); expect( await fetchWorker( davRequest(fixture, `/files/${movable}`, { method: "MOVE", headers: { destination: `https://${fixture.hostLabel}.dav.example.com/files/${directory}/moved.txt`, if: ifHeader, }, }), ), ).toHaveProperty("status", 201); }); it("scopes LOCK refresh and UNLOCK to the request URI and drops direct locks on MOVE", async () => { const fixture = await createDavFixture(); const first = `${name("locked-a")}.txt`; const second = `${name("locked-b")}.txt`; expect(await fetchWorker(davRequest(fixture, `/files/${first}`, { method: "PUT", body: "a" }))).toHaveProperty( "status", 201, ); expect(await fetchWorker(davRequest(fixture, `/files/${second}`, { method: "PUT", body: "b" }))).toHaveProperty( "status", 201, ); const firstToken = await lock(fixture, `/files/${first}`); const secondToken = await lock(fixture, `/files/${second}`); expect( await fetchWorker( davRequest(fixture, `/files/${first}`, { method: "LOCK", headers: { if: `(${secondToken})` } }), ), ).toHaveProperty("status", 412); expect( await fetchWorker( davRequest(fixture, `/files/${first}`, { method: "UNLOCK", headers: { "lock-token": secondToken } }), ), ).toHaveProperty("status", 409); // RFC 4918 7.5: MOVE leaves the source URL unmapped, so direct locks at the // source must be dropped rather than carried to the destination. const moved = `${name("moved-lock")}.txt`; expect( await fetchWorker( davRequest(fixture, `/files/${first}`, { method: "MOVE", headers: { destination: `https://${fixture.hostLabel}.dav.example.com/files/${moved}`, if: `(${firstToken})`, }, }), ), ).toHaveProperty("status", 201); expect( await fetchWorker( davRequest(fixture, `/files/${first}`, { method: "UNLOCK", headers: { "lock-token": firstToken } }), ), ).toHaveProperty("status", 409); // The lock no longer exists at the moved href: UNLOCK there returns 409. expect( await fetchWorker( davRequest(fixture, `/files/${moved}`, { method: "UNLOCK", headers: { "lock-token": firstToken } }), ), ).toHaveProperty("status", 409); }); // DAV-VERIFY-002: lockdiscovery after MOVE must not report a moved lock at // the destination href. it("does not advertise locks on the destination href after MOVE", async () => { const fixture = await createDavFixture(); const file = `${name("move-locked")}.txt`; const moved = `${name("moved-no-lock")}.txt`; expect(await fetchWorker(davRequest(fixture, `/files/${file}`, { method: "PUT", body: "a" }))).toHaveProperty( "status", 201, ); const token = await lock(fixture, `/files/${file}`); expect( await fetchWorker( davRequest(fixture, `/files/${file}`, { method: "MOVE", headers: { destination: `https://${fixture.hostLabel}.dav.example.com/files/${moved}`, if: `(${token})`, }, }), ), ).toHaveProperty("status", 201); const propfind = await fetchWorker( davRequest(fixture, `/files/${moved}`, { method: "PROPFIND", headers: { depth: "0", "content-type": "application/xml" }, body: ``, }), ); expect(propfind.status).toBe(207); expect(await propfind.text()).not.toContain(token.slice(1, -1)); }); it("drops Depth infinity collection locks when the locked collection is moved", async () => { const fixture = await createDavFixture(); const dir = name("move-locked-dir"); const moved = name("moved-locked-dir"); expect(await fetchWorker(davRequest(fixture, `/files/${dir}/`, { method: "MKCOL" }))).toHaveProperty("status", 201); expect( await fetchWorker(davRequest(fixture, `/files/${dir}/child.txt`, { method: "PUT", body: "x" })), ).toHaveProperty("status", 201); const token = await lock(fixture, `/files/${dir}/`, "infinity"); expect( await fetchWorker( davRequest(fixture, `/files/${dir}/`, { method: "MOVE", headers: { destination: `https://${fixture.hostLabel}.dav.example.com/files/${moved}/`, if: `(${token})`, }, }), ), ).toHaveProperty("status", 201); // Writes against descendants of the destination must not require the // original lock token because the lock was dropped, not relocated. const writeAfterMove = await fetchWorker( davRequest(fixture, `/files/${moved}/child.txt`, { method: "PUT", body: "rewritten" }), ); expect(writeAfterMove.status).toBe(204); }); });