mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-04 20:52:53 +00:00
refactor: share plugin runtime scope test setup
This commit is contained in:
@@ -17,6 +17,13 @@ import { makeMockHttpResponse } from "../test-http-response.js";
|
||||
import { createTestRegistry } from "./__tests__/test-utils.js";
|
||||
import { createGatewayPluginRequestHandler } from "./plugins-http.js";
|
||||
|
||||
const SECURE_HOOK_PATH = "/secure-hook";
|
||||
const SECURE_ADMIN_HOOK_PATH = "/secure-admin-hook";
|
||||
|
||||
type PluginHttpRoute = ReturnType<typeof createRoute>;
|
||||
type PluginRequestHandler = ReturnType<typeof createGatewayPluginRequestHandler>;
|
||||
type PluginRequestAuthContext = NonNullable<Parameters<PluginRequestHandler>[3]>;
|
||||
|
||||
function createRoute(params: {
|
||||
path: string;
|
||||
auth: "gateway" | "plugin";
|
||||
@@ -71,6 +78,64 @@ function assertAdminHelperAllowed() {
|
||||
}
|
||||
}
|
||||
|
||||
function createPluginRequestHandler(params: {
|
||||
routes: PluginHttpRoute[];
|
||||
log?: SubsystemLogger;
|
||||
getRouteRegistry?: () => ReturnType<typeof createTestRegistry>;
|
||||
getGatewayRequestContext?: () => GatewayRequestContext;
|
||||
}) {
|
||||
return createGatewayPluginRequestHandler({
|
||||
registry: createTestRegistry({ httpRoutes: params.routes }),
|
||||
...(params.getRouteRegistry ? { getRouteRegistry: params.getRouteRegistry } : {}),
|
||||
log: params.log ?? createMockLogger(),
|
||||
...(params.getGatewayRequestContext
|
||||
? { getGatewayRequestContext: params.getGatewayRequestContext }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
async function dispatchPluginRequest(
|
||||
handler: PluginRequestHandler,
|
||||
params: {
|
||||
path: string;
|
||||
authContext: PluginRequestAuthContext;
|
||||
},
|
||||
) {
|
||||
const response = makeMockHttpResponse();
|
||||
const handled = await handler(
|
||||
{ url: params.path } as IncomingMessage,
|
||||
response.res,
|
||||
undefined,
|
||||
params.authContext,
|
||||
);
|
||||
return { handled, ...response };
|
||||
}
|
||||
|
||||
async function dispatchTrustedGatewayRequest(handler: PluginRequestHandler, path: string) {
|
||||
return await dispatchPluginRequest(handler, {
|
||||
path,
|
||||
authContext: {
|
||||
gatewayAuthSatisfied: true,
|
||||
gatewayRequestAuth: { authMethod: "token", trustDeclaredOperatorScopes: false },
|
||||
gatewayRequestOperatorScopes: ["operator.write"],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function expectMissingWriteScopeFailure(params: {
|
||||
res: ServerResponse;
|
||||
setHeader: ReturnType<typeof vi.fn>;
|
||||
end: ReturnType<typeof vi.fn>;
|
||||
log: SubsystemLogger;
|
||||
}) {
|
||||
expect(params.res.statusCode).toBe(500);
|
||||
expect(params.setHeader).toHaveBeenCalledWith("Content-Type", "text/plain; charset=utf-8");
|
||||
expect(params.end).toHaveBeenCalledWith("Internal Server Error");
|
||||
expect(params.log.warn).toHaveBeenCalledWith(
|
||||
"plugin http route failed (route): Error: missing scope: operator.write",
|
||||
);
|
||||
}
|
||||
|
||||
describe("plugin HTTP route runtime scopes", () => {
|
||||
afterEach(() => {
|
||||
releasePinnedPluginHttpRouteRegistry();
|
||||
@@ -86,35 +151,30 @@ describe("plugin HTTP route runtime scopes", () => {
|
||||
gatewayRequestOperatorScopes?: readonly string[];
|
||||
}) {
|
||||
const log = createMockLogger();
|
||||
const handler = createGatewayPluginRequestHandler({
|
||||
registry: createTestRegistry({
|
||||
httpRoutes: [
|
||||
createRoute({
|
||||
path: params.path,
|
||||
auth: params.auth,
|
||||
gatewayRuntimeScopeSurface: params.gatewayRuntimeScopeSurface,
|
||||
handler: async () => {
|
||||
assertWriteHelperAllowed();
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
const handler = createPluginRequestHandler({
|
||||
routes: [
|
||||
createRoute({
|
||||
path: params.path,
|
||||
auth: params.auth,
|
||||
gatewayRuntimeScopeSurface: params.gatewayRuntimeScopeSurface,
|
||||
handler: async () => {
|
||||
assertWriteHelperAllowed();
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
],
|
||||
log,
|
||||
});
|
||||
|
||||
const response = makeMockHttpResponse();
|
||||
const handled = await handler(
|
||||
{ url: params.path } as IncomingMessage,
|
||||
response.res,
|
||||
undefined,
|
||||
{
|
||||
const response = await dispatchPluginRequest(handler, {
|
||||
path: params.path,
|
||||
authContext: {
|
||||
gatewayAuthSatisfied: params.gatewayAuthSatisfied,
|
||||
gatewayRequestAuth: params.gatewayRequestAuth,
|
||||
gatewayRequestOperatorScopes: params.gatewayRequestOperatorScopes,
|
||||
},
|
||||
);
|
||||
return { handled, log, ...response };
|
||||
});
|
||||
return { log, ...response };
|
||||
}
|
||||
|
||||
it("keeps plugin-auth routes off write-capable runtime helpers", async () => {
|
||||
@@ -125,12 +185,7 @@ describe("plugin HTTP route runtime scopes", () => {
|
||||
});
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(res.statusCode).toBe(500);
|
||||
expect(setHeader).toHaveBeenCalledWith("Content-Type", "text/plain; charset=utf-8");
|
||||
expect(end).toHaveBeenCalledWith("Internal Server Error");
|
||||
expect(log.warn).toHaveBeenCalledWith(
|
||||
"plugin http route failed (route): Error: missing scope: operator.write",
|
||||
);
|
||||
expectMissingWriteScopeFailure({ res, setHeader, end, log });
|
||||
});
|
||||
|
||||
it("preserves write-capable runtime helpers on gateway-auth routes", async () => {
|
||||
@@ -154,32 +209,31 @@ describe("plugin HTTP route runtime scopes", () => {
|
||||
gatewayMethodDispatchAllowed: boolean | undefined;
|
||||
}
|
||||
| undefined;
|
||||
const handler = createGatewayPluginRequestHandler({
|
||||
registry: createTestRegistry({
|
||||
httpRoutes: [
|
||||
createRoute({
|
||||
path: "/secure-hook",
|
||||
auth: "gateway",
|
||||
gatewayMethodDispatchAllowed: true,
|
||||
handler: async () => {
|
||||
const scope = getPluginRuntimeGatewayRequestScope();
|
||||
observed = {
|
||||
pluginId: scope?.pluginId,
|
||||
pluginSource: scope?.pluginSource,
|
||||
gatewayMethodDispatchAllowed: scope?.gatewayMethodDispatchAllowed,
|
||||
};
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
log: createMockLogger(),
|
||||
const handler = createPluginRequestHandler({
|
||||
routes: [
|
||||
createRoute({
|
||||
path: SECURE_HOOK_PATH,
|
||||
auth: "gateway",
|
||||
gatewayMethodDispatchAllowed: true,
|
||||
handler: async () => {
|
||||
const scope = getPluginRuntimeGatewayRequestScope();
|
||||
observed = {
|
||||
pluginId: scope?.pluginId,
|
||||
pluginSource: scope?.pluginSource,
|
||||
gatewayMethodDispatchAllowed: scope?.gatewayMethodDispatchAllowed,
|
||||
};
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const { res } = makeMockHttpResponse();
|
||||
const handled = await handler({ url: "/secure-hook" } as IncomingMessage, res, undefined, {
|
||||
gatewayAuthSatisfied: true,
|
||||
gatewayRequestOperatorScopes: ["operator.write"],
|
||||
const { handled, res } = await dispatchPluginRequest(handler, {
|
||||
path: SECURE_HOOK_PATH,
|
||||
authContext: {
|
||||
gatewayAuthSatisfied: true,
|
||||
gatewayRequestOperatorScopes: ["operator.write"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(handled).toBe(true);
|
||||
@@ -198,7 +252,7 @@ describe("plugin HTTP route runtime scopes", () => {
|
||||
const serverARegistry = createTestRegistry({
|
||||
httpRoutes: [
|
||||
createRoute({
|
||||
path: "/secure-hook",
|
||||
path: SECURE_HOOK_PATH,
|
||||
auth: "gateway",
|
||||
handler: async () => {
|
||||
const context = getPluginRuntimeGatewayRequestScope()?.context;
|
||||
@@ -211,7 +265,7 @@ describe("plugin HTTP route runtime scopes", () => {
|
||||
const serverBRegistry = createTestRegistry({
|
||||
httpRoutes: [
|
||||
createRoute({
|
||||
path: "/secure-hook",
|
||||
path: SECURE_HOOK_PATH,
|
||||
auth: "gateway",
|
||||
handler: async () => {
|
||||
const context = getPluginRuntimeGatewayRequestScope()?.context;
|
||||
@@ -240,7 +294,7 @@ describe("plugin HTTP route runtime scopes", () => {
|
||||
|
||||
const responseA = makeMockHttpResponse();
|
||||
const handledA = await handlerA(
|
||||
{ url: "/secure-hook" } as IncomingMessage,
|
||||
{ url: SECURE_HOOK_PATH } as IncomingMessage,
|
||||
responseA.res,
|
||||
undefined,
|
||||
{
|
||||
@@ -250,7 +304,7 @@ describe("plugin HTTP route runtime scopes", () => {
|
||||
);
|
||||
const responseB = makeMockHttpResponse();
|
||||
const handledB = await handlerB(
|
||||
{ url: "/secure-hook" } as IncomingMessage,
|
||||
{ url: SECURE_HOOK_PATH } as IncomingMessage,
|
||||
responseB.res,
|
||||
undefined,
|
||||
{
|
||||
@@ -277,31 +331,30 @@ describe("plugin HTTP route runtime scopes", () => {
|
||||
record.requestedByClientId = "client-owner";
|
||||
let observedApprovalRuntime: boolean | undefined;
|
||||
let observedVisibility: boolean | undefined;
|
||||
const handler = createGatewayPluginRequestHandler({
|
||||
registry: createTestRegistry({
|
||||
httpRoutes: [
|
||||
createRoute({
|
||||
path: "/secure-hook",
|
||||
auth: "gateway",
|
||||
handler: async () => {
|
||||
const runtimeClient = getPluginRuntimeGatewayRequestScope()?.client;
|
||||
observedApprovalRuntime = runtimeClient?.internal?.approvalRuntime;
|
||||
observedVisibility = isApprovalRecordVisibleToClient({
|
||||
record,
|
||||
client: runtimeClient ?? null,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
log: createMockLogger(),
|
||||
const handler = createPluginRequestHandler({
|
||||
routes: [
|
||||
createRoute({
|
||||
path: SECURE_HOOK_PATH,
|
||||
auth: "gateway",
|
||||
handler: async () => {
|
||||
const runtimeClient = getPluginRuntimeGatewayRequestScope()?.client;
|
||||
observedApprovalRuntime = runtimeClient?.internal?.approvalRuntime;
|
||||
observedVisibility = isApprovalRecordVisibleToClient({
|
||||
record,
|
||||
client: runtimeClient ?? null,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const { res } = makeMockHttpResponse();
|
||||
const handled = await handler({ url: "/secure-hook" } as IncomingMessage, res, undefined, {
|
||||
gatewayAuthSatisfied: true,
|
||||
gatewayRequestOperatorScopes: ["operator.approvals"],
|
||||
const { handled, res } = await dispatchPluginRequest(handler, {
|
||||
path: SECURE_HOOK_PATH,
|
||||
authContext: {
|
||||
gatewayAuthSatisfied: true,
|
||||
gatewayRequestOperatorScopes: ["operator.approvals"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(handled).toBe(true);
|
||||
@@ -333,49 +386,32 @@ describe("plugin HTTP route runtime scopes", () => {
|
||||
});
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(res.statusCode).toBe(500);
|
||||
expect(setHeader).toHaveBeenCalledWith("Content-Type", "text/plain; charset=utf-8");
|
||||
expect(end).toHaveBeenCalledWith("Internal Server Error");
|
||||
expect(log.warn).toHaveBeenCalledWith(
|
||||
"plugin http route failed (route): Error: missing scope: operator.write",
|
||||
);
|
||||
expectMissingWriteScopeFailure({ res, setHeader, end, log });
|
||||
});
|
||||
|
||||
it("restores trusted-operator defaults for routes opting into trusted surface", async () => {
|
||||
let observedScopes: string[] | undefined;
|
||||
const log = createMockLogger();
|
||||
const handler = createGatewayPluginRequestHandler({
|
||||
registry: createTestRegistry({
|
||||
httpRoutes: [
|
||||
createRoute({
|
||||
path: "/secure-admin-hook",
|
||||
auth: "gateway",
|
||||
gatewayRuntimeScopeSurface: "trusted-operator",
|
||||
handler: async () => {
|
||||
observedScopes =
|
||||
getPluginRuntimeGatewayRequestScope()?.client?.connect?.scopes?.slice() ?? [];
|
||||
assertAdminHelperAllowed();
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
const handler = createPluginRequestHandler({
|
||||
routes: [
|
||||
createRoute({
|
||||
path: SECURE_ADMIN_HOOK_PATH,
|
||||
auth: "gateway",
|
||||
gatewayRuntimeScopeSurface: "trusted-operator",
|
||||
handler: async () => {
|
||||
observedScopes =
|
||||
getPluginRuntimeGatewayRequestScope()?.client?.connect?.scopes?.slice() ?? [];
|
||||
assertAdminHelperAllowed();
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
],
|
||||
log,
|
||||
});
|
||||
|
||||
const response = makeMockHttpResponse();
|
||||
const handled = await handler(
|
||||
{ url: "/secure-admin-hook" } as IncomingMessage,
|
||||
response.res,
|
||||
undefined,
|
||||
{
|
||||
gatewayAuthSatisfied: true,
|
||||
gatewayRequestAuth: { authMethod: "token", trustDeclaredOperatorScopes: false },
|
||||
gatewayRequestOperatorScopes: ["operator.write"],
|
||||
},
|
||||
);
|
||||
const response = await dispatchTrustedGatewayRequest(handler, SECURE_ADMIN_HOOK_PATH);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(response.handled).toBe(true);
|
||||
expect(response.res.statusCode).toBe(200);
|
||||
expect(log.warn).not.toHaveBeenCalled();
|
||||
expect(observedScopes).toEqual(CLI_DEFAULT_OPERATOR_SCOPES);
|
||||
@@ -420,19 +456,9 @@ describe("plugin HTTP route runtime scopes", () => {
|
||||
log,
|
||||
});
|
||||
|
||||
const response = makeMockHttpResponse();
|
||||
const handled = await handler(
|
||||
{ url: "/secure/admin-hook" } as IncomingMessage,
|
||||
response.res,
|
||||
undefined,
|
||||
{
|
||||
gatewayAuthSatisfied: true,
|
||||
gatewayRequestAuth: { authMethod: "token", trustDeclaredOperatorScopes: false },
|
||||
gatewayRequestOperatorScopes: ["operator.write"],
|
||||
},
|
||||
);
|
||||
const response = await dispatchTrustedGatewayRequest(handler, "/secure/admin-hook");
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(response.handled).toBe(true);
|
||||
expect(response.res.statusCode).toBe(200);
|
||||
expect(log.warn).not.toHaveBeenCalled();
|
||||
expect(observed).toHaveLength(2);
|
||||
|
||||
Reference in New Issue
Block a user