140 lines
4.9 KiB
JavaScript
140 lines
4.9 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { mkdtemp, mkdir, writeFile, readFile, readdir, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import test from "node:test";
|
|
|
|
import { safeReplaceDir } from "../lib/safe-replace-dir.mjs";
|
|
|
|
// ── Happy path ────────────────────────────────────────────────────────────
|
|
|
|
test("safeReplaceDir copies source content into the target", async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), "safe-replace-copy-"));
|
|
try {
|
|
const safetyRoot = path.join(dir, "root");
|
|
const source = path.join(dir, "source");
|
|
const target = path.join(safetyRoot, "target");
|
|
|
|
await mkdir(source, { recursive: true });
|
|
await writeFile(path.join(source, "file.txt"), "hello");
|
|
await mkdir(safetyRoot, { recursive: true });
|
|
|
|
await safeReplaceDir(source, target, safetyRoot);
|
|
|
|
const content = await readFile(path.join(target, "file.txt"), "utf8");
|
|
assert.equal(content, "hello");
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("safeReplaceDir removes existing content before replacing", async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), "safe-replace-stale-"));
|
|
try {
|
|
const safetyRoot = path.join(dir, "root");
|
|
const source = path.join(dir, "source");
|
|
const target = path.join(safetyRoot, "target");
|
|
|
|
await mkdir(target, { recursive: true });
|
|
await writeFile(path.join(target, "old.txt"), "stale");
|
|
await mkdir(source, { recursive: true });
|
|
await writeFile(path.join(source, "new.txt"), "fresh");
|
|
|
|
await safeReplaceDir(source, target, safetyRoot);
|
|
|
|
const files = await readdir(target);
|
|
assert.deepEqual(files.sort(), ["new.txt"]);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("safeReplaceDir creates target parent directories if they do not exist", async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), "safe-replace-mkdir-"));
|
|
try {
|
|
const safetyRoot = path.join(dir, "root");
|
|
const source = path.join(dir, "source");
|
|
const target = path.join(safetyRoot, "nested", "target");
|
|
|
|
await mkdir(source, { recursive: true });
|
|
await writeFile(path.join(source, "data.txt"), "data");
|
|
await mkdir(safetyRoot, { recursive: true });
|
|
// nested parent does NOT exist yet
|
|
|
|
await safeReplaceDir(source, target, safetyRoot);
|
|
|
|
const content = await readFile(path.join(target, "data.txt"), "utf8");
|
|
assert.equal(content, "data");
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("safeReplaceDir creates deeply nested parent directories (2+ levels missing)", async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), "safe-replace-deep-"));
|
|
try {
|
|
const safetyRoot = path.join(dir, "root");
|
|
const source = path.join(dir, "source");
|
|
// two parent levels (a/b) do NOT exist under safetyRoot
|
|
const target = path.join(safetyRoot, "a", "b", "target");
|
|
|
|
await mkdir(source, { recursive: true });
|
|
await writeFile(path.join(source, "deep.txt"), "deep");
|
|
await mkdir(safetyRoot, { recursive: true });
|
|
// a/ and a/b/ intentionally NOT created
|
|
|
|
await safeReplaceDir(source, target, safetyRoot);
|
|
|
|
const content = await readFile(path.join(target, "deep.txt"), "utf8");
|
|
assert.equal(content, "deep");
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
// ── Safety checks ─────────────────────────────────────────────────────────
|
|
|
|
test("safeReplaceDir refuses when target is outside the safety root", async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), "safe-replace-outside-"));
|
|
try {
|
|
const safetyRoot = path.join(dir, "root");
|
|
const source = path.join(dir, "source");
|
|
const outside = path.join(dir, "outside");
|
|
|
|
await mkdir(source, { recursive: true });
|
|
await mkdir(safetyRoot, { recursive: true });
|
|
|
|
await assert.rejects(
|
|
() => safeReplaceDir(source, outside, safetyRoot),
|
|
/outside safety root/,
|
|
);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("safeReplaceDir refuses when target equals the safety root", async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), "safe-replace-same-"));
|
|
try {
|
|
const safetyRoot = path.join(dir, "root");
|
|
const source = path.join(dir, "source");
|
|
|
|
await mkdir(source, { recursive: true });
|
|
await mkdir(safetyRoot, { recursive: true });
|
|
|
|
await assert.rejects(
|
|
() => safeReplaceDir(source, safetyRoot, safetyRoot),
|
|
/outside safety root/,
|
|
);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("safeReplaceDir refuses an empty target string", async () => {
|
|
await assert.rejects(
|
|
() => safeReplaceDir("/any", "", "/root"),
|
|
/unsafe target/,
|
|
);
|
|
});
|