|
| 1 | +// SPDX-License-Identifier: MPL-2.0 |
| 2 | +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> |
| 3 | +import { assertEquals } from "jsr:@std/assert"; |
| 4 | +import { |
| 5 | + validate, unwrap_path, path_join, sanitize, |
| 6 | + is_within, get_parent, filename, has_extension, |
| 7 | + is_excluded, from_trusted, |
| 8 | + TraversalDetected, |
| 9 | +} from "../src/core/PathHandler.deno.js"; |
| 10 | + |
| 11 | +Deno.test("PathHandler: validate accepts relative paths", () => { |
| 12 | + const p = validate("src/main.affine"); |
| 13 | + assertEquals(p.tag, "Some"); |
| 14 | + assertEquals(unwrap_path(p.value), "src/main.affine"); |
| 15 | +}); |
| 16 | + |
| 17 | +Deno.test("PathHandler: validate rejects absolute paths", () => { |
| 18 | + assertEquals(validate("/etc/passwd").tag, "None"); |
| 19 | +}); |
| 20 | + |
| 21 | +Deno.test("PathHandler: validate rejects path traversal", () => { |
| 22 | + assertEquals(validate("../../etc/passwd").tag, "None"); |
| 23 | +}); |
| 24 | + |
| 25 | +Deno.test("PathHandler: validate rejects embedded traversal", () => { |
| 26 | + assertEquals(validate("src/../../../etc").tag, "None"); |
| 27 | +}); |
| 28 | + |
| 29 | +Deno.test("PathHandler: sanitize removes dangerous characters", () => { |
| 30 | + const clean = sanitize("file<name>.txt"); |
| 31 | + assertEquals(clean.includes("<"), false); |
| 32 | + assertEquals(clean.includes(">"), false); |
| 33 | +}); |
| 34 | + |
| 35 | +Deno.test("PathHandler: sanitize replaces slashes", () => { |
| 36 | + const clean = sanitize("path/to/file"); |
| 37 | + assertEquals(clean.includes("/"), false); |
| 38 | +}); |
| 39 | + |
| 40 | +Deno.test("PathHandler: path_join creates valid joined path", () => { |
| 41 | + const base = from_trusted("docs"); |
| 42 | + const result = path_join(base, ["notes", "file.txt"]); |
| 43 | + assertEquals(result.tag, "Ok"); |
| 44 | + assertEquals(unwrap_path(result.value), "docs/notes/file.txt"); |
| 45 | +}); |
| 46 | + |
| 47 | +Deno.test("PathHandler: path_join rejects traversal in components", () => { |
| 48 | + const base = from_trusted("home"); |
| 49 | + const result = path_join(base, ["..", "..", "etc"]); |
| 50 | + assertEquals(result.tag, "Err"); |
| 51 | + assertEquals(result.error.tag, "TraversalDetected"); |
| 52 | +}); |
| 53 | + |
| 54 | +Deno.test("PathHandler: filename extracts basename", () => { |
| 55 | + const p = from_trusted("docs/reports/file.pdf"); |
| 56 | + assertEquals(filename(p), "file.pdf"); |
| 57 | +}); |
| 58 | + |
| 59 | +Deno.test("PathHandler: filename handles no directory", () => { |
| 60 | + assertEquals(filename(from_trusted("file.txt")), "file.txt"); |
| 61 | +}); |
| 62 | + |
| 63 | +Deno.test("PathHandler: has_extension checks extension", () => { |
| 64 | + const p = from_trusted("src/main.affine"); |
| 65 | + assertEquals(has_extension(p, ".affine"), true); |
| 66 | + assertEquals(has_extension(p, ".js"), false); |
| 67 | +}); |
| 68 | + |
| 69 | +Deno.test("PathHandler: get_parent extracts directory", () => { |
| 70 | + const p = from_trusted("home/user/docs/file.txt"); |
| 71 | + const parent = get_parent(p); |
| 72 | + assertEquals(parent.tag, "Some"); |
| 73 | + assertEquals(unwrap_path(parent.value), "home/user/docs"); |
| 74 | +}); |
| 75 | + |
| 76 | +Deno.test("PathHandler: get_parent returns None for no directory", () => { |
| 77 | + assertEquals(get_parent(from_trusted("file.txt")).tag, "None"); |
| 78 | +}); |
| 79 | + |
| 80 | +Deno.test("PathHandler: is_within checks path containment", () => { |
| 81 | + const p = from_trusted("home/user/docs"); |
| 82 | + const base = from_trusted("home/user"); |
| 83 | + assertEquals(is_within(p, base), true); |
| 84 | +}); |
| 85 | + |
| 86 | +Deno.test("PathHandler: is_within rejects unrelated paths", () => { |
| 87 | + const p = from_trusted("etc/passwd"); |
| 88 | + const base = from_trusted("home/user"); |
| 89 | + assertEquals(is_within(p, base), false); |
| 90 | +}); |
| 91 | + |
| 92 | +Deno.test("PathHandler: is_excluded matches excluded dirs", () => { |
| 93 | + const p = from_trusted("project/node_modules/pkg/index.js"); |
| 94 | + assertEquals(is_excluded(p, ["node_modules", ".git"]), true); |
| 95 | +}); |
| 96 | + |
| 97 | +Deno.test("PathHandler: is_excluded allows non-excluded paths", () => { |
| 98 | + const p = from_trusted("project/src/main.affine"); |
| 99 | + assertEquals(is_excluded(p, ["node_modules", ".git"]), false); |
| 100 | +}); |
0 commit comments