Files
Termix/src/backend/tests/utils/permission-catalog.test.ts
T

179 lines
5.4 KiB
TypeScript

import { describe, expect, it, afterEach } from "vitest";
import {
PERMISSION_CATALOG,
getPermissionCatalog,
isValidPermission,
loadKnownPermissions,
markPluginPermissionsDisabled,
registerPluginPermissions,
rememberPermissionGroup,
resetPermissionCatalog,
SYSTEM_ROLE_DEFAULTS,
} from "../../utils/permission-catalog.js";
const GROUP = "testplugin";
const PERMISSIONS = ["testplugin.view", "testplugin.manage"];
function register() {
registerPluginPermissions({
group: GROUP,
pluginId: GROUP,
label: "Test Plugin",
icon: "Sparkles",
permissions: PERMISSIONS,
items: PERMISSIONS.map((permission) => ({
permission,
titleKey: "permissions.x.title",
descriptionKey: "permissions.x.description",
})),
});
}
describe("permission catalog", () => {
afterEach(() => {
resetPermissionCatalog();
});
it("accepts every cataloged permission and group wildcard", () => {
for (const entry of PERMISSION_CATALOG) {
expect(isValidPermission(`${entry.group}.*`)).toBe(true);
for (const permission of entry.permissions) {
expect(isValidPermission(permission)).toBe(true);
}
}
});
it("accepts the global wildcard", () => {
expect(isValidPermission("*")).toBe(true);
});
it("rejects unknown permissions and malformed wildcards", () => {
expect(isValidPermission("hosts.hack")).toBe(false);
expect(isValidPermission("unknown.*")).toBe(false);
expect(isValidPermission("")).toBe(false);
expect(isValidPermission("hosts")).toBe(false);
});
// Both moved out to their plugins in A5; the ids did not change, so no role
// had to be migrated.
it("no longer carries the ai or automations groups", () => {
const groups = PERMISSION_CATALOG.map((entry) => entry.group);
expect(groups).not.toContain("ai");
expect(groups).not.toContain("automations");
});
it("no longer seeds ai or automations onto the user role", () => {
expect(SYSTEM_ROLE_DEFAULTS.user.permissions).not.toContain("ai.*");
expect(SYSTEM_ROLE_DEFAULTS.user.permissions).not.toContain(
"automations.*",
);
expect(SYSTEM_ROLE_DEFAULTS.user.permissions).toContain("hosts.*");
});
it("gives every core group a label key for the role editor", () => {
for (const entry of PERMISSION_CATALOG) {
expect(entry.labelKey).toBeTruthy();
}
});
});
describe("plugin permission lifecycle", () => {
afterEach(() => {
resetPermissionCatalog();
});
it("rejects a plugin's permissions before it registers", () => {
expect(isValidPermission("testplugin.view")).toBe(false);
expect(isValidPermission("testplugin.*")).toBe(false);
});
it("accepts them once registered", () => {
register();
expect(isValidPermission("testplugin.view")).toBe(true);
expect(isValidPermission("testplugin.manage")).toBe(true);
expect(isValidPermission("testplugin.*")).toBe(true);
});
it("carries the plugin id, label and icon into the catalog", () => {
register();
const entry = getPermissionCatalog().find((row) => row.group === GROUP);
expect(entry?.pluginId).toBe(GROUP);
expect(entry?.label).toBe("Test Plugin");
expect(entry?.icon).toBe("Sparkles");
expect(entry?.enabled).toBe(true);
expect(entry?.items).toHaveLength(2);
});
// The bug A5 fixes: unregistering made PUT /rbac/roles/:id reject every role
// that still held one of these, so an admin could not save an unrelated
// change while the plugin was off.
it("keeps a disabled plugin's permissions valid", () => {
register();
markPluginPermissionsDisabled(GROUP);
expect(isValidPermission("testplugin.view")).toBe(true);
expect(isValidPermission("testplugin.*")).toBe(true);
});
it("marks a disabled plugin's group so the editor can grey it", () => {
register();
markPluginPermissionsDisabled(GROUP);
const entry = getPermissionCatalog().find((row) => row.group === GROUP);
expect(entry?.enabled).toBe(false);
});
it("does not mutate the static PERMISSION_CATALOG array", () => {
const originalLength = PERMISSION_CATALOG.length;
register();
expect(PERMISSION_CATALOG.length).toBe(originalLength);
expect(PERMISSION_CATALOG.some((entry) => entry.group === GROUP)).toBe(
false,
);
});
it("lists a plugin group exactly once when re-registered", () => {
register();
register();
const matches = getPermissionCatalog().filter(
(entry) => entry.group === GROUP,
);
expect(matches).toHaveLength(1);
});
});
describe("known permissions", () => {
afterEach(() => {
resetPermissionCatalog();
});
// The upgrade path: a permission a role already holds stays grantable even
// if no plugin ever registers it again.
it("accepts a permission loaded from the record with no plugin present", () => {
expect(isValidPermission("gone.use")).toBe(false);
loadKnownPermissions(["gone.use"]);
expect(isValidPermission("gone.use")).toBe(true);
expect(isValidPermission("gone.*")).toBe(true);
});
it("accepts a group recorded from a wildcard nothing else named", () => {
rememberPermissionGroup("legacy");
expect(isValidPermission("legacy.*")).toBe(true);
// Still rejects a concrete permission nobody ever registered.
expect(isValidPermission("legacy.invent")).toBe(false);
});
it("still rejects a permission nobody has ever registered", () => {
loadKnownPermissions(["gone.use"]);
expect(isValidPermission("madeup.use")).toBe(false);
});
});