fs-safe/test/absolute-directory.test.ts
2026-05-07 10:52:57 +01:00

186 lines
6.9 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { ensureAbsoluteDirectory } from "../src/absolute-path.js";
const tempDirs: string[] = [];
async function tempRoot(prefix: string): Promise<string> {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
tempDirs.push(dir);
return dir;
}
afterEach(async () => {
vi.restoreAllMocks();
await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { force: true, recursive: true })));
});
describe("ensureAbsoluteDirectory", () => {
it("safely creates missing absolute directory parents from a real ancestor", async () => {
const root = await fs.realpath(await tempRoot("fs-safe-absolute-dir-"));
const targetDir = path.join(root, "nested", "deeper");
await expect(
ensureAbsoluteDirectory(targetDir, { scopeLabel: "output directory", mode: 0o700 }),
).resolves.toEqual({ ok: true, path: targetDir });
expect((await fs.stat(targetDir)).isDirectory()).toBe(true);
});
it("rejects relative absolute-directory inputs", async () => {
await expect(
ensureAbsoluteDirectory(path.join("..", "..", "..", "escape"), {
scopeLabel: "output directory",
}),
).resolves.toMatchObject({ ok: false, code: "invalid-path" });
});
it("rejects absolute directory creation when the existing target is not a directory", async () => {
const root = await fs.realpath(await tempRoot("fs-safe-absolute-dir-file-"));
const targetPath = path.join(root, "file.txt");
await fs.writeFile(targetPath, "file", "utf8");
await expect(
ensureAbsoluteDirectory(targetPath, { scopeLabel: "output directory" }),
).resolves.toMatchObject({ ok: false, code: "not-file" });
});
it.runIf(process.platform !== "win32")(
"rejects absolute directory creation through symlinked existing segments",
async () => {
const root = await fs.realpath(await tempRoot("fs-safe-absolute-dir-link-"));
const outside = await fs.realpath(await tempRoot("fs-safe-absolute-dir-outside-"));
const linkDir = path.join(root, "link");
await fs.symlink(outside, linkDir);
await expect(
ensureAbsoluteDirectory(path.join(linkDir, "nested"), {
scopeLabel: "output directory",
}),
).resolves.toMatchObject({ ok: false, code: "symlink" });
await expect(fs.readdir(outside)).resolves.toEqual([]);
},
);
it.runIf(process.platform !== "win32")(
"rejects symlinked parents even when the requested suffix already exists",
async () => {
const root = await fs.realpath(await tempRoot("fs-safe-absolute-dir-link-existing-"));
const outside = await fs.realpath(
await tempRoot("fs-safe-absolute-dir-link-existing-outside-"),
);
const existing = path.join(outside, "existing");
const linkDir = path.join(root, "link");
await fs.mkdir(existing);
await fs.symlink(outside, linkDir);
await expect(
ensureAbsoluteDirectory(path.join(linkDir, "existing", "new"), {
scopeLabel: "output directory",
}),
).resolves.toMatchObject({ ok: false, code: "symlink" });
await expect(fs.stat(path.join(existing, "new"))).rejects.toMatchObject({ code: "ENOENT" });
},
);
it("returns a policy failure when an intermediate component is a file", async () => {
const root = await fs.realpath(await tempRoot("fs-safe-absolute-dir-file-component-"));
const filePath = path.join(root, "file");
await fs.writeFile(filePath, "file", "utf8");
await expect(
ensureAbsoluteDirectory(path.join(filePath, "child"), {
scopeLabel: "output directory",
}),
).resolves.toMatchObject({ ok: false, code: "not-file" });
});
it.runIf(process.platform !== "win32")(
"rejects absolute directory creation when an existing parent is swapped before mkdir",
async () => {
const root = await fs.realpath(await tempRoot("fs-safe-absolute-dir-race-"));
const outside = await fs.realpath(await tempRoot("fs-safe-absolute-dir-race-outside-"));
const parentDir = path.join(root, "parent");
const targetDir = path.join(parentDir, "child");
await fs.mkdir(parentDir);
const realLstat = fs.lstat.bind(fs);
let swapped = false;
const lstatSpy = vi.spyOn(fs, "lstat").mockImplementation(async (...args) => {
const candidate = String(args[0]);
if (!swapped && candidate === targetDir) {
swapped = true;
await fs.rename(parentDir, `${parentDir}-real`);
await fs.symlink(outside, parentDir, "dir");
}
return await realLstat(...args);
});
try {
await expect(
ensureAbsoluteDirectory(targetDir, { scopeLabel: "output directory" }),
).resolves.toMatchObject({ ok: false, code: "symlink" });
} finally {
lstatSpy.mockRestore();
}
await expect(fs.stat(path.join(outside, "child"))).rejects.toMatchObject({ code: "ENOENT" });
},
);
it.runIf(process.platform !== "win32")(
"rejects absolute directory creation when the existing target changes before return",
async () => {
const root = await fs.realpath(await tempRoot("fs-safe-absolute-dir-target-race-"));
const outside = await fs.realpath(
await tempRoot("fs-safe-absolute-dir-target-race-outside-"),
);
const targetDir = path.join(root, "target");
await fs.mkdir(targetDir);
const realRealpath = fs.realpath.bind(fs);
let swapped = false;
const realpathSpy = vi.spyOn(fs, "realpath").mockImplementation(async (...args) => {
const candidate = String(args[0]);
if (!swapped && candidate === targetDir) {
swapped = true;
const resolved = await realRealpath(...args);
await fs.rename(targetDir, `${targetDir}-real`);
await fs.symlink(outside, targetDir, "dir");
return resolved;
}
return await realRealpath(...args);
});
try {
await expect(
ensureAbsoluteDirectory(targetDir, { scopeLabel: "output directory" }),
).resolves.toMatchObject({ ok: false, code: "symlink" });
} finally {
realpathSpy.mockRestore();
}
},
);
it("rethrows operational absolute directory creation failures", async () => {
const root = await fs.realpath(await tempRoot("fs-safe-absolute-dir-io-"));
const targetDir = path.join(root, "nested");
const realMkdir = fs.mkdir.bind(fs);
const mkdirSpy = vi.spyOn(fs, "mkdir").mockImplementation(async (...args) => {
if (String(args[0]) === targetDir) {
throw Object.assign(new Error("permission denied"), { code: "EACCES" });
}
return await realMkdir(...args);
});
try {
await expect(
ensureAbsoluteDirectory(targetDir, { scopeLabel: "output directory" }),
).rejects.toMatchObject({ code: "EACCES" });
} finally {
mkdirSpy.mockRestore();
}
});
});