fs-safe/test/root-paths.test.ts
Sarah Fortune ecefc5bd53 test: avoid windows 8.3 short-name diff in realpath assertions
On windows fs.realpathSync and fs.realpath (async) can disagree on
8.3 short-name canonicalization. The github actions windows runner
exposes this: fs.realpathSync returns "C:\Users\RUNNER~1\..."
while fs.realpath returns "C:\Users\runneradmin\...". Tests that
compare a sync helper's output against await fs.realpath fail with
the same path printed in two forms.

Compare against fs.realpathSync (imported as realpathSync from
node:fs) on both sides so the test exercises the same canonical
form regardless of which short-name configuration the runner has.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 16:23:32 -07:00

209 lines
7.0 KiB
TypeScript

import fs from "node:fs/promises";
import { realpathSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { readLocalFileFromRoots, resolveLocalPathFromRootsSync } from "../src/local-roots.js";
import {
pathScope,
resolveExistingPathsWithinRoot,
resolvePathWithinRoot,
resolveStrictExistingPathsWithinRoot,
resolveWritablePathWithinRoot,
} from "../src/root-paths.js";
async function withFixtureRoot<T>(
run: (ctx: { baseDir: string; uploadsDir: string }) => Promise<T>,
): Promise<T> {
const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "fs-root-paths-"));
const uploadsDir = path.join(baseDir, "uploads");
await fs.mkdir(uploadsDir, { recursive: true });
try {
return await run({ baseDir, uploadsDir });
} finally {
await fs.rm(baseDir, { recursive: true, force: true });
}
}
function expectInvalidResult(result: { ok: true; paths: string[] } | { ok: false; error: string }) {
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toContain("uploads directory");
}
}
describe("root path list helpers", () => {
it("creates a root-scoped helper with shorter method names", async () => {
await withFixtureRoot(async ({ uploadsDir }) => {
const uploads = pathScope(uploadsDir, { label: "uploads directory" });
const filePath = path.join(uploadsDir, "ok.txt");
await fs.writeFile(filePath, "ok", "utf8");
await expect(uploads.files(["ok.txt"])).resolves.toEqual({
ok: true,
paths: [await fs.realpath(filePath)],
});
await expect(uploads.writable(" ", { defaultName: "fallback.txt" })).resolves.toEqual({
ok: true,
path: path.join(uploadsDir, "fallback.txt"),
});
});
});
it("accepts existing files under the root", async () => {
await withFixtureRoot(async ({ uploadsDir }) => {
const filePath = path.join(uploadsDir, "ok.txt");
await fs.writeFile(filePath, "ok", "utf8");
const result = await resolveExistingPathsWithinRoot({
rootDir: uploadsDir,
requestedPaths: [filePath],
scopeLabel: "uploads directory",
});
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.paths).toEqual([await fs.realpath(filePath)]);
}
});
});
it("keeps lexical in-root paths when missing fallbacks are allowed", async () => {
await withFixtureRoot(async ({ uploadsDir }) => {
const result = await resolveExistingPathsWithinRoot({
rootDir: uploadsDir,
requestedPaths: ["missing.txt"],
scopeLabel: "uploads directory",
});
expect(result).toEqual({ ok: true, paths: [path.join(uploadsDir, "missing.txt")] });
});
});
it("rejects missing files in strict mode", async () => {
await withFixtureRoot(async ({ uploadsDir }) => {
const result = await resolveStrictExistingPathsWithinRoot({
rootDir: uploadsDir,
requestedPaths: ["missing.txt"],
scopeLabel: "uploads directory",
});
expectInvalidResult(result);
});
});
it.runIf(process.platform !== "win32")("rejects symlink escapes", async () => {
await withFixtureRoot(async ({ baseDir, uploadsDir }) => {
const outsideDir = path.join(baseDir, "outside");
await fs.mkdir(outsideDir, { recursive: true });
await fs.writeFile(path.join(outsideDir, "secret.txt"), "secret", "utf8");
await fs.symlink(outsideDir, path.join(uploadsDir, "alias"));
const result = await resolveExistingPathsWithinRoot({
rootDir: uploadsDir,
requestedPaths: ["alias/secret.txt"],
scopeLabel: "uploads directory",
});
expect(result).toEqual({ ok: false, error: "File is outside uploads directory" });
});
});
it("uses a default file name for blank writable paths", async () => {
await withFixtureRoot(async ({ uploadsDir }) => {
const result = await resolveWritablePathWithinRoot({
rootDir: uploadsDir,
requestedPath: " ",
scopeLabel: "uploads directory",
defaultFileName: "fallback.txt",
});
expect(result).toEqual({ ok: true, path: path.join(uploadsDir, "fallback.txt") });
});
});
it("rejects root-level path aliases", () => {
const result = resolvePathWithinRoot({
rootDir: "/tmp/uploads",
requestedPath: ".",
scopeLabel: "uploads directory",
});
expect(result.ok).toBe(false);
});
});
describe("local roots helpers", () => {
it("reads a regular file from one configured root", async () => {
await withFixtureRoot(async ({ uploadsDir }) => {
const filePath = path.join(uploadsDir, "ok.txt");
await fs.writeFile(filePath, "ok", "utf8");
const result = await readLocalFileFromRoots({
filePath,
roots: [uploadsDir],
label: "media roots",
});
expect(result?.buffer.toString("utf8")).toBe("ok");
expect(result?.realPath).toBe(await fs.realpath(filePath));
expect(result?.root).toBe(await fs.realpath(uploadsDir));
});
});
it.runIf(process.platform !== "win32")("rejects symlink escapes while reading", async () => {
await withFixtureRoot(async ({ baseDir, uploadsDir }) => {
const outsideDir = path.join(baseDir, "outside");
await fs.mkdir(outsideDir, { recursive: true });
await fs.writeFile(path.join(outsideDir, "secret.txt"), "secret", "utf8");
await fs.symlink(outsideDir, path.join(uploadsDir, "alias"));
const result = await readLocalFileFromRoots({
filePath: path.join(uploadsDir, "alias", "secret.txt"),
roots: [uploadsDir],
label: "media roots",
});
expect(result).toBeNull();
});
});
it("resolves existing and missing paths only when canonicalized inside a root", async () => {
await withFixtureRoot(async ({ baseDir, uploadsDir }) => {
const filePath = path.join(uploadsDir, "ok.txt");
const missingPath = path.join(uploadsDir, "new", "later.txt");
const outsidePath = path.join(baseDir, "outside.txt");
await fs.writeFile(filePath, "ok", "utf8");
// Use the sync realpath to compare against resolveLocalPathFromRootsSync.
// On windows fs.realpathSync and fs.realpath (async) sometimes disagree
// on 8.3 short-name canonicalization (e.g. "RUNNER~1" vs "runneradmin").
const uploadsReal = realpathSync(uploadsDir);
expect(
resolveLocalPathFromRootsSync({
filePath,
roots: [uploadsDir],
label: "media roots",
requireFile: true,
}),
).toEqual({ path: realpathSync(filePath), root: uploadsReal });
expect(
resolveLocalPathFromRootsSync({
filePath: missingPath,
roots: [uploadsDir],
label: "media roots",
allowMissing: true,
})?.path,
).toBe(path.join(uploadsReal, "new", "later.txt"));
expect(
resolveLocalPathFromRootsSync({
filePath: outsidePath,
roots: [uploadsDir],
label: "media roots",
allowMissing: true,
}),
).toBeNull();
});
});
});