























@@ -20,32 +20,59 @@ function b64(s: string): string {
2020return Buffer.from(s, "utf-8").toString("base64");
2121}
222223+function expectFailure(result: Awaited<ReturnType<typeof handleFileWrite>>, code: string) {
24+expect(result.ok).toBe(false);
25+if (result.ok) {
26+throw new Error("expected file write failure");
27+}
28+expect(result.code).toBe(code);
29+}
30+31+function expectSuccessFields(
32+result: Awaited<ReturnType<typeof handleFileWrite>>,
33+fields: Record<string, unknown>,
34+) {
35+expect(result.ok).toBe(true);
36+if (!result.ok) {
37+throw new Error(`expected ok, got ${result.code}: ${result.message}`);
38+}
39+for (const [key, value] of Object.entries(fields)) {
40+expect(result[key as keyof typeof result]).toEqual(value);
41+}
42+}
43+44+async function expectAccessMissing(target: string) {
45+try {
46+await fs.access(target);
47+} catch (error) {
48+expect((error as NodeJS.ErrnoException).code).toBe("ENOENT");
49+return;
50+}
51+throw new Error(`expected ${target} to be missing`);
52+}
53+2354describe("handleFileWrite — input validation", () => {
2455it("rejects empty / non-string path", async () => {
25-expect(await handleFileWrite({ path: "", contentBase64: b64("x") })).toMatchObject({
26-ok: false,
27-code: "INVALID_PATH",
28-});
56+expectFailure(await handleFileWrite({ path: "", contentBase64: b64("x") }), "INVALID_PATH");
2957});
30583159it("rejects relative paths", async () => {
3260const r = await handleFileWrite({ path: "relative.txt", contentBase64: b64("x") });
33-expect(r).toMatchObject({ ok: false, code: "INVALID_PATH" });
61+expectFailure(r, "INVALID_PATH");
3462});
35633664it("rejects paths with NUL bytes", async () => {
3765const r = await handleFileWrite({ path: "/tmp/foo\0bar", contentBase64: b64("x") });
38-expect(r).toMatchObject({ ok: false, code: "INVALID_PATH" });
66+expectFailure(r, "INVALID_PATH");
3967});
40684169it("requires contentBase64 but allows an empty encoded payload", async () => {
4270const missing = await handleFileWrite({ path: path.join(tmpRoot, "missing.bin") });
43-expect(missing).toMatchObject({ ok: false, code: "INVALID_BASE64" });
71+expectFailure(missing, "INVALID_BASE64");
44724573const target = path.join(tmpRoot, "empty.bin");
4674const empty = await handleFileWrite({ path: target, contentBase64: "" });
47-expect(empty).toMatchObject({
48-ok: true,
75+expectSuccessFields(empty, {
4976size: 0,
5077sha256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
5178});
@@ -91,7 +118,7 @@ describe("handleFileWrite — overwrite policy", () => {
91118contentBase64: b64("after"),
92119overwrite: false,
93120});
94-expect(r).toMatchObject({ ok: false, code: "EXISTS_NO_OVERWRITE" });
121+expectFailure(r, "EXISTS_NO_OVERWRITE");
95122expect(await fs.readFile(target, "utf-8")).toBe("before");
96123});
97124@@ -120,7 +147,7 @@ describe("handleFileWrite — parent directory handling", () => {
120147contentBase64: b64("x"),
121148createParents: false,
122149});
123-expect(r).toMatchObject({ ok: false, code: "PARENT_NOT_FOUND" });
150+expectFailure(r, "PARENT_NOT_FOUND");
124151});
125152126153it("creates missing parents when createParents=true", async () => {
@@ -147,7 +174,7 @@ describe("handleFileWrite — symlink protection", () => {
147174contentBase64: b64("evil"),
148175overwrite: true,
149176});
150-expect(r).toMatchObject({ ok: false, code: "SYMLINK_TARGET_DENIED" });
177+expectFailure(r, "SYMLINK_TARGET_DENIED");
151178// The original file must be unchanged.
152179expect(await fs.readFile(real, "utf-8")).toBe("untouched");
153180});
@@ -169,14 +196,12 @@ describe("handleFileWrite — symlink protection", () => {
169196path: path.join(allowed, "new-file.txt"),
170197contentBase64: b64("payload"),
171198});
172-expect(r).toMatchObject({ ok: false, code: "SYMLINK_REDIRECT" });
199+expectFailure(r, "SYMLINK_REDIRECT");
173200// The error includes the canonical target so the operator can
174201// either update allowWritePaths or set followSymlinks=true.
175202expect(r.ok ? null : r.canonicalPath).toBe(path.join(realDir, "new-file.txt"));
176203// No file was created at the canonical target.
177-await expect(fs.access(path.join(realDir, "new-file.txt"))).rejects.toMatchObject({
178-code: "ENOENT",
179-});
204+await expectAccessMissing(path.join(realDir, "new-file.txt"));
180205// Sentinel must be untouched.
181206expect(await fs.readFile(sentinel, "utf-8")).toBe("DO_NOT_TOUCH");
182207});
@@ -193,11 +218,9 @@ describe("handleFileWrite — symlink protection", () => {
193218createParents: true,
194219});
195220196-expect(r).toMatchObject({ ok: false, code: "SYMLINK_REDIRECT" });
221+expectFailure(r, "SYMLINK_REDIRECT");
197222expect(r.ok ? null : r.canonicalPath).toBe(path.join(realDir, "new", "child.txt"));
198-await expect(fs.access(path.join(realDir, "new"))).rejects.toMatchObject({
199-code: "ENOENT",
200-});
223+await expectAccessMissing(path.join(realDir, "new"));
201224});
202225203226it("follows the parent symlink when followSymlinks=true", async () => {
@@ -230,14 +253,11 @@ describe("handleFileWrite — symlink protection", () => {
230253preflightOnly: true,
231254});
232255233-expect(r).toMatchObject({
234-ok: true,
256+expectSuccessFields(r, {
235257path: path.join(realDir, "new", "child.txt"),
236258size: "payload".length,
237259});
238-await expect(fs.access(path.join(realDir, "new"))).rejects.toMatchObject({
239-code: "ENOENT",
240-});
260+await expectAccessMissing(path.join(realDir, "new"));
241261});
242262243263it("refuses to overwrite a directory", async () => {
@@ -249,7 +269,7 @@ describe("handleFileWrite — symlink protection", () => {
249269contentBase64: b64("x"),
250270overwrite: true,
251271});
252-expect(r).toMatchObject({ ok: false, code: "IS_DIRECTORY" });
272+expectFailure(r, "IS_DIRECTORY");
253273});
254274});
255275@@ -261,9 +281,9 @@ describe("handleFileWrite — integrity check", () => {
261281contentBase64: b64("real-content"),
262282expectedSha256: "0".repeat(64),
263283});
264-expect(r).toMatchObject({ ok: false, code: "INTEGRITY_FAILURE" });
284+expectFailure(r, "INTEGRITY_FAILURE");
265285// The file must never be created on a mismatch.
266-await expect(fs.access(target)).rejects.toMatchObject({ code: "ENOENT" });
286+await expectAccessMissing(target);
267287});
268288269289it("does NOT replace or delete an existing file when overwrite=true and expectedSha256 mismatches", async () => {
@@ -276,7 +296,7 @@ describe("handleFileWrite — integrity check", () => {
276296overwrite: true,
277297expectedSha256: "0".repeat(64),
278298});
279-expect(r).toMatchObject({ ok: false, code: "INTEGRITY_FAILURE" });
299+expectFailure(r, "INTEGRITY_FAILURE");
280300// Critical: the original must survive. A bad caller hash must not
281301// be a primitive for replacing-then-deleting an existing file.
282302expect(await fs.readFile(target, "utf-8")).toBe("ORIGINAL_CONTENT_DO_NOT_TOUCH");
@@ -319,8 +339,8 @@ describe("handleFileWrite — base64 round-trip validation", () => {
319339path: target,
320340contentBase64: "AAA@@@",
321341});
322-expect(r).toMatchObject({ ok: false, code: "INVALID_BASE64" });
323-await expect(fs.access(target)).rejects.toMatchObject({ code: "ENOENT" });
342+expectFailure(r, "INVALID_BASE64");
343+await expectAccessMissing(target);
324344});
325345326346it("accepts standard base64 with and without padding", async () => {
@@ -352,6 +372,6 @@ describe("handleFileWrite — size cap", () => {
352372path: target,
353373contentBase64: big.toString("base64"),
354374});
355-expect(r).toMatchObject({ ok: false, code: "FILE_TOO_LARGE" });
375+expectFailure(r, "FILE_TOO_LARGE");
356376});
357377});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。