Organize src/ into concern folders (http, auth, admin, plugin-host, ui); co-locate tests, move plugin-api barrel into plugin-host, sync docs + AGENTS layout
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import type { Plugin, Route } from "./plugin.ts";
|
||||
import { allowedMethods, isAuthorized, matchRoute } from "./router.ts";
|
||||
|
||||
const noop: Route["handler"] = () => ({ html: "x" });
|
||||
|
||||
// Minimal discovered Plugin — only id + routes matter to the router.
|
||||
function plugin(id: string, routes: Route[]): Plugin {
|
||||
return { apiVersion: "1.0.0", id, routes };
|
||||
}
|
||||
|
||||
test("matchRoute matches method + full path under /<id>, resolves params, HEAD falls back to GET", () => {
|
||||
const plugins = [
|
||||
plugin("scheduling", [
|
||||
{ handler: noop, method: "GET", path: "/shifts" },
|
||||
{ handler: noop, method: "GET", path: "/shifts/:id" },
|
||||
{ handler: noop, method: "POST", path: "/shifts" },
|
||||
]),
|
||||
];
|
||||
assert.equal(matchRoute(plugins, "GET", "/scheduling/shifts")?.route.method, "GET");
|
||||
assert.deepEqual(matchRoute(plugins, "GET", "/scheduling/shifts/42")?.params, { id: "42" });
|
||||
assert.equal(matchRoute(plugins, "POST", "/scheduling/shifts")?.route.method, "POST");
|
||||
// HEAD is answered by the GET route; PUT (no route) and an unknown path miss.
|
||||
assert.equal(matchRoute(plugins, "HEAD", "/scheduling/shifts")?.route.method, "GET");
|
||||
assert.equal(matchRoute(plugins, "PUT", "/scheduling/shifts"), null);
|
||||
assert.equal(matchRoute(plugins, "GET", "/scheduling/missing"), null);
|
||||
});
|
||||
|
||||
test("matchRoute decodes percent-encoded params and rejects malformed encoding", () => {
|
||||
const plugins = [plugin("users", [{ handler: noop, method: "GET", path: "/:id" }])];
|
||||
assert.deepEqual(matchRoute(plugins, "GET", "/users/john%40doe")?.params, { id: "john@doe" });
|
||||
assert.equal(matchRoute(plugins, "GET", "/users/%ZZ"), null);
|
||||
});
|
||||
|
||||
test("matchRoute prefers the most specific (fewest-param) pattern over a param catch-all", () => {
|
||||
const plugins = [
|
||||
plugin("users", [
|
||||
{ handler: noop, method: "GET", path: "/:id" }, // declared first, still loses to the literal
|
||||
{ handler: noop, method: "GET", path: "/new" },
|
||||
]),
|
||||
];
|
||||
assert.equal(matchRoute(plugins, "GET", "/users/new")?.route.path, "/new");
|
||||
assert.equal(matchRoute(plugins, "GET", "/users/123")?.route.path, "/:id");
|
||||
});
|
||||
|
||||
test("allowedMethods lists methods at a path (GET implies HEAD); empty when the path is unknown", () => {
|
||||
const plugins = [
|
||||
plugin("x", [
|
||||
{ handler: noop, method: "GET", path: "/a" },
|
||||
{ handler: noop, method: "POST", path: "/a" },
|
||||
]),
|
||||
];
|
||||
assert.deepEqual(allowedMethods(plugins, "/x/a"), ["GET", "HEAD", "POST"]);
|
||||
assert.deepEqual(allowedMethods(plugins, "/x/missing"), []);
|
||||
});
|
||||
|
||||
test("isAuthorized: open routes pass; gated routes require the role token; public is explicitly open", () => {
|
||||
const open: Route = { handler: noop, method: "GET", path: "/" };
|
||||
const gated: Route = { handler: noop, method: "GET", path: "/", permission: "x:read" };
|
||||
const pub: Route = { handler: noop, method: "GET", path: "/", public: true }; // blessed public alias
|
||||
assert.equal(isAuthorized(open, []), true);
|
||||
assert.equal(isAuthorized(gated, []), false);
|
||||
assert.equal(isAuthorized(gated, ["x:read"]), true);
|
||||
assert.equal(isAuthorized(gated, ["other"]), false);
|
||||
assert.equal(isAuthorized(pub, []), true); // open to anonymous, like omitting permission — but stated outright
|
||||
});
|
||||
Reference in New Issue
Block a user