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