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);
});
});