import { env } from "cloudflare:workers";
import { describe, expect, it } from "vitest";
import { createDavFixture, davRequest } from "@tests/worker/helpers/dav";
import { fetchWorker } from "@tests/worker/helpers/http";
import { emptyIfHeader, filePath, name, storageIdFor } from "./files-helpers";
describe("WebDAV file properties", () => {
it("stores dead properties atomically and rejects protected live property writes", async () => {
const fixture = await createDavFixture();
const file = `${name("props")}.txt`;
const put = await fetchWorker(davRequest(fixture, `/files/${file}`, { method: "PUT", body: "x" }));
expect(put.status).toBe(201);
const proppatch = await fetchWorker(
davRequest(fixture, `/files/${file}`, {
method: "PROPPATCH",
headers: { "content-type": "application/xml" },
body: `blue`,
}),
);
expect(proppatch.status).toBe(207);
const protectedPatch = await fetchWorker(
davRequest(fixture, `/files/${file}`, {
method: "PROPPATCH",
headers: { "content-type": "application/xml" },
body: `bad`,
}),
);
expect(protectedPatch.status).toBe(207);
expect(await protectedPatch.text()).toContain("HTTP/1.1 403 Forbidden");
const computedProtectedPatch = await fetchWorker(
davRequest(fixture, `/files/${file}`, {
method: "PROPPATCH",
headers: { "content-type": "application/xml" },
body: `sync:1`,
}),
);
expect(computedProtectedPatch.status).toBe(207);
expect(await computedProtectedPatch.text()).toContain("HTTP/1.1 403 Forbidden");
const propfind = await fetchWorker(
davRequest(fixture, `/files/${file}`, {
method: "PROPFIND",
headers: { depth: "0", "content-type": "application/xml" },
body: ``,
}),
);
const xml = await propfind.text();
expect(xml).toContain("");
});
it("honors PROPFIND allprop include properties", async () => {
const fixture = await createDavFixture();
const file = `${name("include")}.txt`;
expect(await fetchWorker(davRequest(fixture, `/files/${file}`, { method: "PUT", body: "x" }))).toHaveProperty(
"status",
201,
);
const propfind = await fetchWorker(
davRequest(fixture, `/files/${file}`, {
method: "PROPFIND",
headers: { depth: "0", "content-type": "application/xml" },
body: ``,
}),
);
expect(propfind.status).toBe(207);
const xml = await propfind.text();
expect(xml).toContain("included");
expect(xml).toContain("urn:test");
});
it("omits file-only live properties from collection propname responses", async () => {
const fixture = await createDavFixture();
const directory = name("collection-propname");
expect(await fetchWorker(davRequest(fixture, `/files/${directory}/`, { method: "MKCOL" }))).toHaveProperty(
"status",
201,
);
const response = await fetchWorker(
davRequest(fixture, `/files/${directory}/`, {
method: "PROPFIND",
headers: { depth: "0", "content-type": "application/xml" },
body: ``,
}),
);
expect(response.status).toBe(207);
const xml = await response.text();
expect(xml).not.toContain(" {
const fixture = await createDavFixture();
const storageId = await storageIdFor(fixture.subjectId);
const object = env.FILE_DAV.getByName(storageId);
const directory = name("collection-state");
const baseNow = 1_800_000_000_000;
const path = filePath(`/files/${directory}/`);
const createdCollection = await object.mkcol({
subjectId: fixture.subjectId,
storageId,
path,
ifHeader: emptyIfHeader,
lockTokens: [],
nowMs: baseNow,
});
expect(createdCollection.ok).toBe(true);
const begin = await object.beginWrite({
subjectId: fixture.subjectId,
storageId,
path: filePath(`/files/${directory}/child.txt`),
contentLength: 5,
contentType: "text/plain",
contentLanguage: null,
ifMatch: null,
ifNoneMatch: null,
ifHeader: emptyIfHeader,
lockTokens: [],
nowMs: baseNow + 1_000,
maxFileBytes: 100,
});
expect(begin.ok).toBe(true);
if (!begin.ok) throw new Error(begin.message);
const committed = await object.commitWrite({
subjectId: fixture.subjectId,
storageId,
uploadId: begin.uploadId,
size: 5,
r2Etag: null,
contentType: "text/plain",
contentLanguage: null,
nowMs: baseNow + 1_000,
});
expect(committed.ok).toBe(true);
let collection = await object.propfind({
subjectId: fixture.subjectId,
storageId,
path,
depth: "0",
maxNodes: 1,
nowMs: baseNow + 1_000,
});
expect(collection.ok && collection.nodes[0]?.node.modifiedAtMs).toBe(baseNow + 1_000);
const moved = await object.move({
subjectId: fixture.subjectId,
storageId,
from: filePath(`/files/${directory}/child.txt`),
to: filePath(`/files/${directory}/renamed.txt`),
overwrite: false,
ifMatch: null,
ifNoneMatch: null,
ifHeader: emptyIfHeader,
lockTokens: [],
nowMs: baseNow + 2_000,
maxNodes: 100,
});
expect(moved.ok).toBe(true);
collection = await object.propfind({
subjectId: fixture.subjectId,
storageId,
path,
depth: "0",
maxNodes: 1,
nowMs: baseNow + 2_000,
});
expect(collection.ok && collection.nodes[0]?.node.modifiedAtMs).toBe(baseNow + 2_000);
const deleted = await object.delete({
subjectId: fixture.subjectId,
storageId,
path: filePath(`/files/${directory}/renamed.txt`),
ifMatch: null,
ifNoneMatch: null,
ifHeader: emptyIfHeader,
lockTokens: [],
nowMs: baseNow + 3_000,
maxNodes: 100,
});
expect(deleted.ok).toBe(true);
collection = await object.propfind({
subjectId: fixture.subjectId,
storageId,
path,
depth: "0",
maxNodes: 1,
nowMs: baseNow + 3_000,
});
expect(collection.ok && collection.nodes[0]?.node.modifiedAtMs).toBe(baseNow + 3_000);
});
});