Skip to content
File

Blob: tests/worker/protocol/webdav/files-properties.workers.test.ts

typescript201 lines
1import { env } from "cloudflare:workers";
2import { describe, expect, it } from "vitest";
3 
4import { createDavFixture, davRequest } from "@tests/worker/helpers/dav";
5import { fetchWorker } from "@tests/worker/helpers/http";
6import { emptyIfHeader, filePath, name, storageIdFor } from "./files-helpers";
7 
8describe("WebDAV file properties", () => {
9 it("stores dead properties atomically and rejects protected live property writes", async () => {
10 const fixture = await createDavFixture();
11 const file = `${name("props")}.txt`;
12 const put = await fetchWorker(davRequest(fixture, `/files/${file}`, { method: "PUT", body: "x" }));
13 expect(put.status).toBe(201);
14 
15 const proppatch = await fetchWorker(
16 davRequest(fixture, `/files/${file}`, {
17 method: "PROPPATCH",
18 headers: { "content-type": "application/xml" },
19 body: `<?xml version="1.0"?><D:propertyupdate xmlns:D="DAV:" xmlns:Z="urn:test"><D:set><D:prop><Z:color>blue</Z:color></D:prop></D:set></D:propertyupdate>`,
20 }),
21 );
22 expect(proppatch.status).toBe(207);
23 
24 const protectedPatch = await fetchWorker(
25 davRequest(fixture, `/files/${file}`, {
26 method: "PROPPATCH",
27 headers: { "content-type": "application/xml" },
28 body: `<?xml version="1.0"?><D:propertyupdate xmlns:D="DAV:"><D:set><D:prop><D:getetag>bad</D:getetag></D:prop></D:set></D:propertyupdate>`,
29 }),
30 );
31 expect(protectedPatch.status).toBe(207);
32 expect(await protectedPatch.text()).toContain("HTTP/1.1 403 Forbidden");
33 
34 const computedProtectedPatch = await fetchWorker(
35 davRequest(fixture, `/files/${file}`, {
36 method: "PROPPATCH",
37 headers: { "content-type": "application/xml" },
38 body: `<?xml version="1.0"?><D:propertyupdate xmlns:D="DAV:"><D:set><D:prop><D:sync-token>sync:1</D:sync-token></D:prop></D:set></D:propertyupdate>`,
39 }),
40 );
41 expect(computedProtectedPatch.status).toBe(207);
42 expect(await computedProtectedPatch.text()).toContain("HTTP/1.1 403 Forbidden");
43 
44 const propfind = await fetchWorker(
45 davRequest(fixture, `/files/${file}`, {
46 method: "PROPFIND",
47 headers: { depth: "0", "content-type": "application/xml" },
48 body: `<?xml version="1.0"?><D:propfind xmlns:D="DAV:" xmlns:Z="urn:test"><D:prop><Z:color/><D:getetag/></D:prop></D:propfind>`,
49 }),
50 );
51 const xml = await propfind.text();
52 expect(xml).toContain("<Z:color");
53 expect(xml).toContain("blue");
54 expect(xml).toContain("<D:getetag>");
55 });
56 
57 it("honors PROPFIND allprop include properties", async () => {
58 const fixture = await createDavFixture();
59 const file = `${name("include")}.txt`;
60 expect(await fetchWorker(davRequest(fixture, `/files/${file}`, { method: "PUT", body: "x" }))).toHaveProperty(
61 "status",
62 201,
63 );
64 
65 const propfind = await fetchWorker(
66 davRequest(fixture, `/files/${file}`, {
67 method: "PROPFIND",
68 headers: { depth: "0", "content-type": "application/xml" },
69 body: `<?xml version="1.0"?><D:propfind xmlns:D="DAV:" xmlns:Z="urn:test"><D:allprop/><D:include><Z:included/></D:include></D:propfind>`,
70 }),
71 );
72 expect(propfind.status).toBe(207);
73 const xml = await propfind.text();
74 expect(xml).toContain("included");
75 expect(xml).toContain("urn:test");
76 });
77 
78 it("omits file-only live properties from collection propname responses", async () => {
79 const fixture = await createDavFixture();
80 const directory = name("collection-propname");
81 expect(await fetchWorker(davRequest(fixture, `/files/${directory}/`, { method: "MKCOL" }))).toHaveProperty(
82 "status",
83 201,
84 );
85 const response = await fetchWorker(
86 davRequest(fixture, `/files/${directory}/`, {
87 method: "PROPFIND",
88 headers: { depth: "0", "content-type": "application/xml" },
89 body: `<?xml version="1.0"?><D:propfind xmlns:D="DAV:"><D:propname/></D:propfind>`,
90 }),
91 );
92 expect(response.status).toBe(207);
93 const xml = await response.text();
94 expect(xml).not.toContain("<D:getetag");
95 expect(xml).not.toContain("<D:getcontentlength");
96 expect(xml).not.toContain("<D:getcontenttype");
97 });
98 
99 it("updates parent collection modified state when direct children are created, moved, and deleted", async () => {
100 const fixture = await createDavFixture();
101 const storageId = await storageIdFor(fixture.subjectId);
102 const object = env.FILE_DAV.getByName(storageId);
103 const directory = name("collection-state");
104 const baseNow = 1_800_000_000_000;
105 const path = filePath(`/files/${directory}/`);
106 
107 const createdCollection = await object.mkcol({
108 subjectId: fixture.subjectId,
109 storageId,
110 path,
111 ifHeader: emptyIfHeader,
112 lockTokens: [],
113 nowMs: baseNow,
114 });
115 expect(createdCollection.ok).toBe(true);
116 
117 const begin = await object.beginWrite({
118 subjectId: fixture.subjectId,
119 storageId,
120 path: filePath(`/files/${directory}/child.txt`),
121 contentLength: 5,
122 contentType: "text/plain",
123 contentLanguage: null,
124 ifMatch: null,
125 ifNoneMatch: null,
126 ifHeader: emptyIfHeader,
127 lockTokens: [],
128 nowMs: baseNow + 1_000,
129 maxFileBytes: 100,
130 });
131 expect(begin.ok).toBe(true);
132 if (!begin.ok) throw new Error(begin.message);
133 const committed = await object.commitWrite({
134 subjectId: fixture.subjectId,
135 storageId,
136 uploadId: begin.uploadId,
137 size: 5,
138 r2Etag: null,
139 contentType: "text/plain",
140 contentLanguage: null,
141 nowMs: baseNow + 1_000,
142 });
143 expect(committed.ok).toBe(true);
144 let collection = await object.propfind({
145 subjectId: fixture.subjectId,
146 storageId,
147 path,
148 depth: "0",
149 maxNodes: 1,
150 nowMs: baseNow + 1_000,
151 });
152 expect(collection.ok && collection.nodes[0]?.node.modifiedAtMs).toBe(baseNow + 1_000);
153 
154 const moved = await object.move({
155 subjectId: fixture.subjectId,
156 storageId,
157 from: filePath(`/files/${directory}/child.txt`),
158 to: filePath(`/files/${directory}/renamed.txt`),
159 overwrite: false,
160 ifMatch: null,
161 ifNoneMatch: null,
162 ifHeader: emptyIfHeader,
163 lockTokens: [],
164 nowMs: baseNow + 2_000,
165 maxNodes: 100,
166 });
167 expect(moved.ok).toBe(true);
168 collection = await object.propfind({
169 subjectId: fixture.subjectId,
170 storageId,
171 path,
172 depth: "0",
173 maxNodes: 1,
174 nowMs: baseNow + 2_000,
175 });
176 expect(collection.ok && collection.nodes[0]?.node.modifiedAtMs).toBe(baseNow + 2_000);
177 
178 const deleted = await object.delete({
179 subjectId: fixture.subjectId,
180 storageId,
181 path: filePath(`/files/${directory}/renamed.txt`),
182 ifMatch: null,
183 ifNoneMatch: null,
184 ifHeader: emptyIfHeader,
185 lockTokens: [],
186 nowMs: baseNow + 3_000,
187 maxNodes: 100,
188 });
189 expect(deleted.ok).toBe(true);
190 collection = await object.propfind({
191 subjectId: fixture.subjectId,
192 storageId,
193 path,
194 depth: "0",
195 maxNodes: 1,
196 nowMs: baseNow + 3_000,
197 });
198 expect(collection.ok && collection.nodes[0]?.node.modifiedAtMs).toBe(baseNow + 3_000);
199 });
200});