Consolidate tests (todo §2); merge HTTP static tests, fold 403 render into the live gated route, unify resolveViewPath cases
This commit is contained in:
@@ -5,7 +5,6 @@ import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { after, before, test, type TestContext } from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import * as ejs from "ejs";
|
||||
import { createApp } from "./app.ts";
|
||||
import type { Plugin } from "./plugin.ts";
|
||||
import { contentTypeFor, resolveStaticPath, routePublic } from "./static.ts";
|
||||
@@ -51,7 +50,7 @@ test("renders branding from the menu config into the shell: logo + default theme
|
||||
assert.match(html, /id="theme-dark"\s+checked/); // config default theme reaches the switch
|
||||
});
|
||||
|
||||
test("serves a static file: GET sends body + content-type, HEAD sends headers only", async () => {
|
||||
test("static serving: GET sends body + content-type, HEAD headers only, unsafe paths → 403", async () => {
|
||||
const get = await fetch(base + "/public/css/styles.css");
|
||||
assert.equal(get.status, 200);
|
||||
assert.match(get.headers.get("content-type") ?? "", /text\/css/);
|
||||
@@ -60,6 +59,10 @@ test("serves a static file: GET sends body + content-type, HEAD sends headers on
|
||||
assert.equal(head.status, 200);
|
||||
assert.ok(Number(head.headers.get("content-length")) > 0);
|
||||
assert.equal((await head.text()).length, 0);
|
||||
|
||||
// Encoded traversal and a NUL byte are refused before touching the filesystem.
|
||||
assert.equal((await fetch(base + "/public/..%2f..%2fapp.ts")).status, 403);
|
||||
assert.equal((await fetch(base + "/public/%00")).status, 403);
|
||||
});
|
||||
|
||||
// Production caches compiled templates; rendering must stay correct across repeated requests.
|
||||
@@ -102,13 +105,6 @@ test("renders the 500 HTML page when a handler throws", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// 403 has no first-party route yet (guards land in §4), so assert the template renders.
|
||||
test("renders the 403 error page as HTML", async () => {
|
||||
const html = await ejs.renderFile(join(viewsDir, "403.ejs"), { title: "Forbidden" });
|
||||
assert.match(html, /403/);
|
||||
assert.match(html, /styles\.css/);
|
||||
});
|
||||
|
||||
// A test plugin exercising each RouteResult shape, a path param, and the permission gate.
|
||||
const demoPlugin: Plugin = {
|
||||
apiVersion: "1.0.0",
|
||||
@@ -166,8 +162,12 @@ test("mounts plugin routes: params, html/json/redirect/view results, and the per
|
||||
assert.match(await css.text(), /\.demo/);
|
||||
assert.equal((await fetch(url + "/public/demo/..%2f..%2fplugin.ts")).status, 403); // traversal still blocked
|
||||
|
||||
// gated route with no session → 403
|
||||
assert.equal((await fetch(url + "/demo/secret")).status, 403);
|
||||
// gated route with no session → the rendered 403 page (covers the gate + 403.ejs over HTTP)
|
||||
const denied = await fetch(url + "/demo/secret");
|
||||
assert.equal(denied.status, 403);
|
||||
const deniedBody = await denied.text();
|
||||
assert.match(deniedBody, /403/);
|
||||
assert.match(deniedBody, /styles\.css/);
|
||||
|
||||
// known path + wrong method → 405 with Allow; unknown path → 404
|
||||
const wrong = await fetch(url + "/demo/data", { method: "DELETE" });
|
||||
@@ -199,11 +199,6 @@ test("plugin hooks: onRequest can short-circuit a request and onResponse observe
|
||||
assert.ok(seen.includes("/hooked/ok:handler ran"));
|
||||
});
|
||||
|
||||
test("rejects unsafe static request paths (encoded traversal, NUL) with 403", async () => {
|
||||
assert.equal((await fetch(base + "/public/..%2f..%2fapp.ts")).status, 403);
|
||||
assert.equal((await fetch(base + "/public/%00")).status, 403);
|
||||
});
|
||||
|
||||
test("resolveStaticPath blocks traversal and control chars, allows nested files", () => {
|
||||
assert.equal(resolveStaticPath("/srv/public", "../app.ts"), null);
|
||||
assert.equal(resolveStaticPath("/srv/public", "a\x00b"), null);
|
||||
|
||||
@@ -8,16 +8,13 @@ import { renderPluginView, resolveViewPath } from "./view-resolver.ts";
|
||||
|
||||
const coreViewsDir = join(dirname(fileURLToPath(import.meta.url)), "..", "views");
|
||||
|
||||
test("resolveViewPath resolves names + nested subfolders within the plugin's views dir", () => {
|
||||
test("resolveViewPath resolves names/nested subfolders within the views dir, rejects traversal + control chars", () => {
|
||||
const dir = "/srv/plugins";
|
||||
assert.equal(resolveViewPath(dir, "demo", "page"), "/srv/plugins/demo/views/page.ejs");
|
||||
assert.equal(resolveViewPath(dir, "demo", "shifts/edit"), "/srv/plugins/demo/views/shifts/edit.ejs");
|
||||
assert.equal(resolveViewPath(dir, "demo", "page.ejs"), "/srv/plugins/demo/views/page.ejs"); // extension not doubled
|
||||
});
|
||||
|
||||
test("resolveViewPath rejects traversal and control chars", () => {
|
||||
assert.equal(resolveViewPath("/srv/plugins", "demo", "../../secret"), null);
|
||||
assert.equal(resolveViewPath("/srv/plugins", "demo", "a\x00b"), null);
|
||||
assert.equal(resolveViewPath(dir, "demo", "../../secret"), null); // traversal escapes the dir
|
||||
assert.equal(resolveViewPath(dir, "demo", "a\x00b"), null); // control char
|
||||
});
|
||||
|
||||
test("renderPluginView: a (nested) view includes a core building-block partial and its own partial", async (t: TestContext) => {
|
||||
|
||||
Reference in New Issue
Block a user