mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
121 lines
4.3 KiB
TypeScript
121 lines
4.3 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { access, chmod, readFile, readdir, rm } from "node:fs/promises";
|
|
import { dirname, join } from "node:path";
|
|
import { Readable } from "node:stream";
|
|
import { expect, it, vi } from "vitest";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
const observation = vi.hoisted(() => ({
|
|
synced: [] as string[],
|
|
refuseSync: undefined as string | undefined,
|
|
}));
|
|
vi.mock("node:fs/promises", async (importOriginal) => {
|
|
const filesystem = await importOriginal<typeof import("node:fs/promises")>();
|
|
return {
|
|
...filesystem,
|
|
open: async (...args: Parameters<typeof filesystem.open>) => {
|
|
const handle = await filesystem.open(...args);
|
|
const sync = handle.sync.bind(handle);
|
|
handle.sync = async () => {
|
|
const path = String(args[0]);
|
|
if (path === observation.refuseSync)
|
|
throw new Error("controlled nested directory sync failure");
|
|
await sync();
|
|
observation.synced.push(path);
|
|
};
|
|
return handle;
|
|
},
|
|
};
|
|
});
|
|
import { SafeOutputTree } from "../../../src/artifacts/SafeOutputTree.js";
|
|
|
|
it.skipIf(process.platform === "win32")(
|
|
"syncs nested directory entries before their parents and refuses publication when one cannot sync",
|
|
async () => {
|
|
const root = await createTestTempDirectory("rea-tree-directory-sync-");
|
|
const bytes = Buffer.from("published bytes");
|
|
for (const fail of [false, true]) {
|
|
const output = join(root, fail ? "failed" : "success");
|
|
const tree = await SafeOutputTree.create(output);
|
|
try {
|
|
await tree.write("a/b/payload.txt", Readable.from([bytes]), {
|
|
bytes: bytes.length,
|
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
});
|
|
observation.synced.length = 0;
|
|
observation.refuseSync = fail ? join(output, "a") : undefined;
|
|
if (fail) {
|
|
await expect(tree.commit()).rejects.toMatchObject({
|
|
message: "Could not durably sync extraction output",
|
|
cause: { message: "controlled nested directory sync failure" },
|
|
});
|
|
expect(tree.published).toBe(false);
|
|
expect(observation.synced).toEqual([join(output, "a", "b")]);
|
|
observation.refuseSync = undefined;
|
|
expect(await tree.rollback()).toEqual({
|
|
status: "complete",
|
|
residualPaths: [],
|
|
});
|
|
await expect(access(output)).rejects.toMatchObject({
|
|
code: "ENOENT",
|
|
});
|
|
} else {
|
|
await tree.commit();
|
|
expect(tree.published).toBe(true);
|
|
expect(observation.synced).toEqual([
|
|
join(output, "a", "b"),
|
|
join(output, "a"),
|
|
output,
|
|
dirname(output),
|
|
]);
|
|
expect(
|
|
Buffer.compare(
|
|
await readFile(join(output, "a", "b", "payload.txt")),
|
|
bytes,
|
|
),
|
|
).toBe(0);
|
|
}
|
|
} finally {
|
|
observation.refuseSync = undefined;
|
|
await tree.closeHandles();
|
|
}
|
|
}
|
|
},
|
|
);
|
|
|
|
it
|
|
.skipIf(process.platform === "win32" || process.getuid?.() === 0)
|
|
.each([false, true])(
|
|
"resumes rollback after root removal denial without readmitting a closed file (failed write: %s)",
|
|
async (failedWrite) => {
|
|
const root = await createTestTempDirectory("rea-tree-removal-retry-");
|
|
const output = join(root, "output");
|
|
const tree = await SafeOutputTree.create(output);
|
|
const bytes = Buffer.from("x");
|
|
try {
|
|
const write = tree.write("partial.txt", Readable.from([bytes]), {
|
|
bytes: failedWrite ? 2 : 1,
|
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
});
|
|
if (failedWrite)
|
|
await expect(write).rejects.toMatchObject({ reason: "integrity" });
|
|
else await write;
|
|
await chmod(root, 0o500);
|
|
await expect(tree.rollback()).rejects.toMatchObject({
|
|
cause: { code: "EACCES", syscall: "rmdir" },
|
|
});
|
|
expect(await readdir(output)).toEqual([]);
|
|
await chmod(root, 0o700);
|
|
expect(await tree.rollback()).toEqual({
|
|
status: "complete",
|
|
residualPaths: [],
|
|
});
|
|
await expect(access(output)).rejects.toMatchObject({ code: "ENOENT" });
|
|
} finally {
|
|
await chmod(root, 0o700);
|
|
await tree.closeHandles();
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
},
|
|
);
|