Move test-only tooling to src/test-tooling, split positive, warn on undeclared gated permissions
CI / full-gate (push) Successful in 4m19s
CI / full-gate (push) Successful in 4m19s
This commit is contained in:
@@ -129,6 +129,30 @@ test("findConflicts: duplicate nav id is an error, a shared permission name only
|
||||
assert.ok(permissionDup.some((c) => c.kind === "permission" && c.level === "warn"));
|
||||
});
|
||||
|
||||
const noopHandler = () => {};
|
||||
|
||||
test("findConflicts: a route or nav node gating on a permission no plugin declares warns once per plugin and name", () => {
|
||||
const gated = findConflicts([
|
||||
p({ id: "a", nav: [{ children: [{ id: "a:x", label: "X", permission: "ghost:read" }], id: "a:root", label: "A", session: true }], routes: [
|
||||
{ handler: noopHandler, method: "GET", path: "/x", permission: "ghost:read" },
|
||||
{ handler: noopHandler, method: "GET", path: "/y", permission: "other:write" },
|
||||
] }),
|
||||
p({ id: "b", permissions: [{ name: "other:write" }] }),
|
||||
]);
|
||||
const messages = gated.filter((c) => c.kind === "permission").map((c) => c.message);
|
||||
assert.equal(messages.length, 1);
|
||||
assert.match(messages[0] ?? "", /"ghost:read"/);
|
||||
assert.deepEqual(gated[0]?.plugins, ["a"]);
|
||||
assert.equal(gated[0]?.level, "warn");
|
||||
});
|
||||
|
||||
test("findConflicts: a gate on a permission another plugin declares, or on none, stays quiet", () => {
|
||||
assert.deepEqual(findConflicts([
|
||||
p({ id: "a", permissions: [{ name: "a:read" }], routes: [{ handler: noopHandler, method: "GET", path: "/x", permission: "a:read" }] }),
|
||||
p({ id: "b", routes: [{ handler: noopHandler, method: "GET", path: "/y", permission: "a:read" }, { handler: noopHandler, method: "GET", path: "/z", session: true }] }),
|
||||
]), []);
|
||||
});
|
||||
|
||||
test("findConflicts: each single slot (`home`/`dashboard`) may have one owner — two is a loud error", () => {
|
||||
const handler = () => ({ html: "x" });
|
||||
const homeDup = findConflicts([p({ id: "a", home: handler }), p({ id: "b", home: handler })]);
|
||||
|
||||
@@ -218,6 +218,8 @@ export function findConflicts(plugins: Plugin[]): PluginConflict[] {
|
||||
if (owners.length > 1) out.push({ kind: "permission", level: "warn", message: `permission "${name}" declared by ${uniq(owners).length} plugins; pick a more specific "<resource>" unless shared on purpose`, plugins: uniq(owners) });
|
||||
});
|
||||
|
||||
out.push(...undeclaredGates(plugins));
|
||||
|
||||
// Both the id's dashes and the key's camel humps become underscores, so plugin "a-b" key "c" and
|
||||
// plugin "a" key "bC" name one variable — one plugin would silently read the other's value.
|
||||
collect(plugins, (plugin, push) => {
|
||||
@@ -229,6 +231,29 @@ export function findConflicts(plugins: Plugin[]): PluginConflict[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Declaring is optional, but the admin screens offer only declared names: a gate on one nobody
|
||||
// declares answers 403 until someone writes the Keto tuple by hand.
|
||||
function undeclaredGates(plugins: Plugin[]): PluginConflict[] {
|
||||
const declared = new Set(declaredPermissions(plugins).map((decl) => decl.name));
|
||||
const out: PluginConflict[] = [];
|
||||
for (const plugin of plugins) {
|
||||
const gated = new Set<string>();
|
||||
for (const route of plugin.routes ?? []) if (route.permission != null) gated.add(route.permission);
|
||||
collectNavPermissions(plugin.nav, (name) => gated.add(name));
|
||||
for (const name of [...gated].sort()) {
|
||||
if (!declared.has(name)) out.push({ kind: "permission", level: "warn", message: `plugin "${plugin.id}" gates on "${name}", which no installed plugin declares, so the admin screens can't grant it; add it to a plugin's \`permissions\``, plugins: [plugin.id] });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function collectNavPermissions(nodes: NavNode[] | undefined, push: (name: string) => void): void {
|
||||
for (const node of nodes ?? []) {
|
||||
if (node.permission != null) push(node.permission);
|
||||
collectNavPermissions(node.children, push);
|
||||
}
|
||||
}
|
||||
|
||||
// Map each emitted key → the plugin ids that emitted it (repeats kept, so within-plugin dups count).
|
||||
function collect(plugins: Plugin[], emit: (plugin: Plugin, push: (key: string) => void) => void): Map<string, string[]> {
|
||||
const owners = new Map<string, string[]>();
|
||||
|
||||
Reference in New Issue
Block a user