import { afterEach, describe, expect, it } from "vitest"; import { createEmptyPluginRegistry } from "../plugins/registry-empty.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import { authorizeOperatorScopesForMethod, isGatewayMethodClassified, resolveLeastPrivilegeOperatorScopesForMethod, } from "./method-scopes.js"; import { listGatewayMethods } from "./server-methods-list.js"; import { coreGatewayHandlers } from "./server-methods.js"; afterEach(() => { setActivePluginRegistry(createEmptyPluginRegistry()); }); describe("method scope resolution", () => { it.each([ ["sessions.resolve", ["operator.read"]], ["config.schema.lookup", ["operator.read"]], ["sessions.create", ["operator.write"]], ["sessions.send", ["operator.write"]], ["sessions.abort", ["operator.write"]], ["sessions.messages.subscribe", ["operator.read"]], ["sessions.messages.unsubscribe", ["operator.read"]], ["poll", ["operator.write"]], ["config.patch", ["operator.admin"]], ["wizard.start", ["operator.admin"]], ["update.run", ["operator.admin"]], ])("resolves least-privilege scopes for %s", (method, expected) => { expect(resolveLeastPrivilegeOperatorScopesForMethod(method)).toEqual(expected); }); it("leaves node-only pending drain outside operator scopes", () => { expect(resolveLeastPrivilegeOperatorScopesForMethod("node.pending.drain")).toEqual([]); }); it("returns empty scopes for unknown methods", () => { expect(resolveLeastPrivilegeOperatorScopesForMethod("totally.unknown.method")).toEqual([]); }); it("reads plugin-registered gateway method scopes from the active plugin registry", () => { const registry = createEmptyPluginRegistry(); registry.gatewayMethodScopes = { "browser.request": "operator.write", }; setActivePluginRegistry(registry); expect(resolveLeastPrivilegeOperatorScopesForMethod("browser.request")).toEqual([ "operator.write", ]); }); }); describe("operator scope authorization", () => { it.each([ ["health", ["operator.read"], { allowed: true }], ["health", ["operator.write"], { allowed: true }], ["config.schema.lookup", ["operator.read"], { allowed: true }], ["config.patch", ["operator.admin"], { allowed: true }], ])("authorizes %s for scopes %j", (method, scopes, expected) => { expect(authorizeOperatorScopesForMethod(method, scopes)).toEqual(expected); }); it("requires operator.write for write methods", () => { expect(authorizeOperatorScopesForMethod("send", ["operator.read"])).toEqual({ allowed: false, missingScope: "operator.write", }); }); it("requires approvals scope for approval methods", () => { expect(authorizeOperatorScopesForMethod("exec.approval.resolve", ["operator.write"])).toEqual({ allowed: false, missingScope: "operator.approvals", }); }); it("requires admin for unknown methods", () => { expect(authorizeOperatorScopesForMethod("unknown.method", ["operator.read"])).toEqual({ allowed: false, missingScope: "operator.admin", }); }); }); describe("core gateway method classification", () => { it("treats node-role methods as classified even without operator scopes", () => { expect(isGatewayMethodClassified("node.pending.drain")).toBe(true); expect(isGatewayMethodClassified("node.pending.pull")).toBe(true); }); it("classifies every exposed core gateway handler method", () => { const unclassified = Object.keys(coreGatewayHandlers).filter( (method) => !isGatewayMethodClassified(method), ); expect(unclassified).toEqual([]); }); it("classifies every listed gateway method name", () => { const unclassified = listGatewayMethods().filter( (method) => !isGatewayMethodClassified(method), ); expect(unclassified).toEqual([]); }); });