Files
plainpages/src/view-resolver.test.ts

45 lines
2.3 KiB
TypeScript

import assert from "node:assert/strict";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { test, type TestContext } from "node:test";
import { fileURLToPath } from "node:url";
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", () => {
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);
});
test("renderPluginView: a (nested) view includes a core building-block partial and its own partial", async (t: TestContext) => {
const pluginsDir = mkdtempSync(join(tmpdir(), "pp-views-"));
t.after(() => rmSync(pluginsDir, { force: true, recursive: true }));
const views = join(pluginsDir, "demo", "views");
mkdirSync(join(views, "partials"), { recursive: true });
mkdirSync(join(views, "sub"), { recursive: true });
writeFileSync(join(views, "partials", "local.ejs"), "<span class=local><%= who %></span>");
writeFileSync(
join(views, "sub", "page.ejs"),
`<%- include("partials/theme-switch") %><%- include("partials/local", { who }) %>`,
);
const render = renderPluginView({ cache: false, coreViewsDir, pluginsDir });
const html = await render("demo", "sub/page", { who: "Plug" });
assert.match(html, /role="radiogroup"/); // core partial, resolved from coreViewsDir
assert.match(html, /<span class=local>Plug<\/span>/); // the plugin's own partial, with data
});
test("renderPluginView throws on an out-of-bounds view name", async () => {
const render = renderPluginView({ cache: false, coreViewsDir, pluginsDir: "/srv/plugins" });
await assert.rejects(render("demo", "../../etc/passwd", {}), /invalid view name/);
});