import { test } from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { assertInsideVault } from "./pathSafety.js"; import { VaultEscapeError } from "../errors.js"; function makeTmpVault(): string { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "braindump-vault-")); return dir; } test("normal relative path resolves inside vault", () => { const vault = makeTmpVault(); fs.writeFileSync(path.join(vault, "note.md"), "hello"); const resolved = assertInsideVault(vault, "note.md"); assert.equal(resolved, path.join(vault, "note.md")); }); test("nested new path resolves even if parent doesn't exist yet", () => { const vault = makeTmpVault(); const resolved = assertInsideVault(vault, "projects/foo/bar.md"); assert.equal(resolved, path.join(vault, "projects", "foo", "bar.md")); }); test("../escape is rejected", () => { const vault = makeTmpVault(); assert.throws(() => assertInsideVault(vault, "../escape"), VaultEscapeError); }); test("absolute path is rejected", () => { const vault = makeTmpVault(); assert.throws(() => assertInsideVault(vault, "/etc/passwd"), VaultEscapeError); }); test("embedded .. that escapes is rejected", () => { const vault = makeTmpVault(); assert.throws( () => assertInsideVault(vault, "projects/../../escape"), VaultEscapeError ); }); test("embedded .. that stays inside is allowed", () => { const vault = makeTmpVault(); fs.mkdirSync(path.join(vault, "projects", "foo"), { recursive: true }); const resolved = assertInsideVault(vault, "projects/foo/../bar.md"); assert.equal(resolved, path.join(vault, "projects", "bar.md")); }); test("symlink inside vault pointing outside vault is rejected", () => { const vault = makeTmpVault(); const outside = fs.mkdtempSync(path.join(os.tmpdir(), "braindump-outside-")); fs.writeFileSync(path.join(outside, "secret.txt"), "nope"); fs.symlinkSync(outside, path.join(vault, "escape-link"), "dir"); assert.throws( () => assertInsideVault(vault, "escape-link/secret.txt"), VaultEscapeError ); }); test("writing a NEW file through a symlink that points outside is rejected", () => { const vault = makeTmpVault(); const outside = fs.mkdtempSync(path.join(os.tmpdir(), "braindump-outside-")); fs.symlinkSync(outside, path.join(vault, "escape-link"), "dir"); // outside dir exists but the target file does not — this is the write path. assert.throws( () => assertInsideVault(vault, "escape-link/newfile.md"), VaultEscapeError ); }); test("dangling symlink pointing outside vault is rejected (escape via mkdir -p)", () => { const vault = makeTmpVault(); const outsideBase = fs.mkdtempSync(path.join(os.tmpdir(), "braindump-outside-")); // Symlink target does NOT exist yet — existsSync would treat the link as // absent and wrongly allow it; lstat-based walk must still reject. fs.symlinkSync(path.join(outsideBase, "nonexistent"), path.join(vault, "dangling"), "dir"); assert.throws( () => assertInsideVault(vault, "dangling/pwned.md"), VaultEscapeError ); }); test("symlink to a directory INSIDE the vault is allowed", () => { const vault = makeTmpVault(); fs.mkdirSync(path.join(vault, "real"), { recursive: true }); fs.symlinkSync(path.join(vault, "real"), path.join(vault, "alias"), "dir"); const resolved = assertInsideVault(vault, "alias/note.md"); assert.equal(resolved, path.join(vault, "alias", "note.md")); });