Skip to content

Commit e85be62

Browse files
committed
refactor: share plugin runtime scope test setup
1 parent 9cb052c commit e85be62

1 file changed

Lines changed: 151 additions & 125 deletions

File tree

src/gateway/server/plugins-http.runtime-scopes.test.ts

Lines changed: 151 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ import { makeMockHttpResponse } from "../test-http-response.js";
1717
import { createTestRegistry } from "./__tests__/test-utils.js";
1818
import { createGatewayPluginRequestHandler } from "./plugins-http.js";
1919

20+
const SECURE_HOOK_PATH = "/secure-hook";
21+
const SECURE_ADMIN_HOOK_PATH = "/secure-admin-hook";
22+
23+
type PluginHttpRoute = ReturnType<typeof createRoute>;
24+
type PluginRequestHandler = ReturnType<typeof createGatewayPluginRequestHandler>;
25+
type PluginRequestAuthContext = NonNullable<Parameters<PluginRequestHandler>[3]>;
26+
2027
function createRoute(params: {
2128
path: string;
2229
auth: "gateway" | "plugin";
@@ -71,6 +78,64 @@ function assertAdminHelperAllowed() {
7178
}
7279
}
7380

81+
function createPluginRequestHandler(params: {
82+
routes: PluginHttpRoute[];
83+
log?: SubsystemLogger;
84+
getRouteRegistry?: () => ReturnType<typeof createTestRegistry>;
85+
getGatewayRequestContext?: () => GatewayRequestContext;
86+
}) {
87+
return createGatewayPluginRequestHandler({
88+
registry: createTestRegistry({ httpRoutes: params.routes }),
89+
...(params.getRouteRegistry ? { getRouteRegistry: params.getRouteRegistry } : {}),
90+
log: params.log ?? createMockLogger(),
91+
...(params.getGatewayRequestContext
92+
? { getGatewayRequestContext: params.getGatewayRequestContext }
93+
: {}),
94+
});
95+
}
96+
97+
async function dispatchPluginRequest(
98+
handler: PluginRequestHandler,
99+
params: {
100+
path: string;
101+
authContext: PluginRequestAuthContext;
102+
},
103+
) {
104+
const response = makeMockHttpResponse();
105+
const handled = await handler(
106+
{ url: params.path } as IncomingMessage,
107+
response.res,
108+
undefined,
109+
params.authContext,
110+
);
111+
return { handled, ...response };
112+
}
113+
114+
async function dispatchTrustedGatewayRequest(handler: PluginRequestHandler, path: string) {
115+
return await dispatchPluginRequest(handler, {
116+
path,
117+
authContext: {
118+
gatewayAuthSatisfied: true,
119+
gatewayRequestAuth: { authMethod: "token", trustDeclaredOperatorScopes: false },
120+
gatewayRequestOperatorScopes: ["operator.write"],
121+
},
122+
});
123+
}
124+
125+
function expectMissingWriteScopeFailure(params: {
126+
res: ServerResponse;
127+
setHeader: ReturnType<typeof vi.fn>;
128+
end: ReturnType<typeof vi.fn>;
129+
log: SubsystemLogger;
130+
}) {
131+
expect(params.res.statusCode).toBe(500);
132+
expect(params.setHeader).toHaveBeenCalledWith("Content-Type", "text/plain; charset=utf-8");
133+
expect(params.end).toHaveBeenCalledWith("Internal Server Error");
134+
expect(params.log.warn).toHaveBeenCalledWith(
135+
"plugin http route failed (route): Error: missing scope: operator.write",
136+
);
137+
}
138+
74139
describe("plugin HTTP route runtime scopes", () => {
75140
afterEach(() => {
76141
releasePinnedPluginHttpRouteRegistry();
@@ -86,35 +151,30 @@ describe("plugin HTTP route runtime scopes", () => {
86151
gatewayRequestOperatorScopes?: readonly string[];
87152
}) {
88153
const log = createMockLogger();
89-
const handler = createGatewayPluginRequestHandler({
90-
registry: createTestRegistry({
91-
httpRoutes: [
92-
createRoute({
93-
path: params.path,
94-
auth: params.auth,
95-
gatewayRuntimeScopeSurface: params.gatewayRuntimeScopeSurface,
96-
handler: async () => {
97-
assertWriteHelperAllowed();
98-
return true;
99-
},
100-
}),
101-
],
102-
}),
154+
const handler = createPluginRequestHandler({
155+
routes: [
156+
createRoute({
157+
path: params.path,
158+
auth: params.auth,
159+
gatewayRuntimeScopeSurface: params.gatewayRuntimeScopeSurface,
160+
handler: async () => {
161+
assertWriteHelperAllowed();
162+
return true;
163+
},
164+
}),
165+
],
103166
log,
104167
});
105168

106-
const response = makeMockHttpResponse();
107-
const handled = await handler(
108-
{ url: params.path } as IncomingMessage,
109-
response.res,
110-
undefined,
111-
{
169+
const response = await dispatchPluginRequest(handler, {
170+
path: params.path,
171+
authContext: {
112172
gatewayAuthSatisfied: params.gatewayAuthSatisfied,
113173
gatewayRequestAuth: params.gatewayRequestAuth,
114174
gatewayRequestOperatorScopes: params.gatewayRequestOperatorScopes,
115175
},
116-
);
117-
return { handled, log, ...response };
176+
});
177+
return { log, ...response };
118178
}
119179

120180
it("keeps plugin-auth routes off write-capable runtime helpers", async () => {
@@ -125,12 +185,7 @@ describe("plugin HTTP route runtime scopes", () => {
125185
});
126186

127187
expect(handled).toBe(true);
128-
expect(res.statusCode).toBe(500);
129-
expect(setHeader).toHaveBeenCalledWith("Content-Type", "text/plain; charset=utf-8");
130-
expect(end).toHaveBeenCalledWith("Internal Server Error");
131-
expect(log.warn).toHaveBeenCalledWith(
132-
"plugin http route failed (route): Error: missing scope: operator.write",
133-
);
188+
expectMissingWriteScopeFailure({ res, setHeader, end, log });
134189
});
135190

136191
it("preserves write-capable runtime helpers on gateway-auth routes", async () => {
@@ -154,32 +209,31 @@ describe("plugin HTTP route runtime scopes", () => {
154209
gatewayMethodDispatchAllowed: boolean | undefined;
155210
}
156211
| undefined;
157-
const handler = createGatewayPluginRequestHandler({
158-
registry: createTestRegistry({
159-
httpRoutes: [
160-
createRoute({
161-
path: "/secure-hook",
162-
auth: "gateway",
163-
gatewayMethodDispatchAllowed: true,
164-
handler: async () => {
165-
const scope = getPluginRuntimeGatewayRequestScope();
166-
observed = {
167-
pluginId: scope?.pluginId,
168-
pluginSource: scope?.pluginSource,
169-
gatewayMethodDispatchAllowed: scope?.gatewayMethodDispatchAllowed,
170-
};
171-
return true;
172-
},
173-
}),
174-
],
175-
}),
176-
log: createMockLogger(),
212+
const handler = createPluginRequestHandler({
213+
routes: [
214+
createRoute({
215+
path: SECURE_HOOK_PATH,
216+
auth: "gateway",
217+
gatewayMethodDispatchAllowed: true,
218+
handler: async () => {
219+
const scope = getPluginRuntimeGatewayRequestScope();
220+
observed = {
221+
pluginId: scope?.pluginId,
222+
pluginSource: scope?.pluginSource,
223+
gatewayMethodDispatchAllowed: scope?.gatewayMethodDispatchAllowed,
224+
};
225+
return true;
226+
},
227+
}),
228+
],
177229
});
178230

179-
const { res } = makeMockHttpResponse();
180-
const handled = await handler({ url: "/secure-hook" } as IncomingMessage, res, undefined, {
181-
gatewayAuthSatisfied: true,
182-
gatewayRequestOperatorScopes: ["operator.write"],
231+
const { handled, res } = await dispatchPluginRequest(handler, {
232+
path: SECURE_HOOK_PATH,
233+
authContext: {
234+
gatewayAuthSatisfied: true,
235+
gatewayRequestOperatorScopes: ["operator.write"],
236+
},
183237
});
184238

185239
expect(handled).toBe(true);
@@ -198,7 +252,7 @@ describe("plugin HTTP route runtime scopes", () => {
198252
const serverARegistry = createTestRegistry({
199253
httpRoutes: [
200254
createRoute({
201-
path: "/secure-hook",
255+
path: SECURE_HOOK_PATH,
202256
auth: "gateway",
203257
handler: async () => {
204258
const context = getPluginRuntimeGatewayRequestScope()?.context;
@@ -211,7 +265,7 @@ describe("plugin HTTP route runtime scopes", () => {
211265
const serverBRegistry = createTestRegistry({
212266
httpRoutes: [
213267
createRoute({
214-
path: "/secure-hook",
268+
path: SECURE_HOOK_PATH,
215269
auth: "gateway",
216270
handler: async () => {
217271
const context = getPluginRuntimeGatewayRequestScope()?.context;
@@ -240,7 +294,7 @@ describe("plugin HTTP route runtime scopes", () => {
240294

241295
const responseA = makeMockHttpResponse();
242296
const handledA = await handlerA(
243-
{ url: "/secure-hook" } as IncomingMessage,
297+
{ url: SECURE_HOOK_PATH } as IncomingMessage,
244298
responseA.res,
245299
undefined,
246300
{
@@ -250,7 +304,7 @@ describe("plugin HTTP route runtime scopes", () => {
250304
);
251305
const responseB = makeMockHttpResponse();
252306
const handledB = await handlerB(
253-
{ url: "/secure-hook" } as IncomingMessage,
307+
{ url: SECURE_HOOK_PATH } as IncomingMessage,
254308
responseB.res,
255309
undefined,
256310
{
@@ -277,31 +331,30 @@ describe("plugin HTTP route runtime scopes", () => {
277331
record.requestedByClientId = "client-owner";
278332
let observedApprovalRuntime: boolean | undefined;
279333
let observedVisibility: boolean | undefined;
280-
const handler = createGatewayPluginRequestHandler({
281-
registry: createTestRegistry({
282-
httpRoutes: [
283-
createRoute({
284-
path: "/secure-hook",
285-
auth: "gateway",
286-
handler: async () => {
287-
const runtimeClient = getPluginRuntimeGatewayRequestScope()?.client;
288-
observedApprovalRuntime = runtimeClient?.internal?.approvalRuntime;
289-
observedVisibility = isApprovalRecordVisibleToClient({
290-
record,
291-
client: runtimeClient ?? null,
292-
});
293-
return true;
294-
},
295-
}),
296-
],
297-
}),
298-
log: createMockLogger(),
334+
const handler = createPluginRequestHandler({
335+
routes: [
336+
createRoute({
337+
path: SECURE_HOOK_PATH,
338+
auth: "gateway",
339+
handler: async () => {
340+
const runtimeClient = getPluginRuntimeGatewayRequestScope()?.client;
341+
observedApprovalRuntime = runtimeClient?.internal?.approvalRuntime;
342+
observedVisibility = isApprovalRecordVisibleToClient({
343+
record,
344+
client: runtimeClient ?? null,
345+
});
346+
return true;
347+
},
348+
}),
349+
],
299350
});
300351

301-
const { res } = makeMockHttpResponse();
302-
const handled = await handler({ url: "/secure-hook" } as IncomingMessage, res, undefined, {
303-
gatewayAuthSatisfied: true,
304-
gatewayRequestOperatorScopes: ["operator.approvals"],
352+
const { handled, res } = await dispatchPluginRequest(handler, {
353+
path: SECURE_HOOK_PATH,
354+
authContext: {
355+
gatewayAuthSatisfied: true,
356+
gatewayRequestOperatorScopes: ["operator.approvals"],
357+
},
305358
});
306359

307360
expect(handled).toBe(true);
@@ -333,49 +386,32 @@ describe("plugin HTTP route runtime scopes", () => {
333386
});
334387

335388
expect(handled).toBe(true);
336-
expect(res.statusCode).toBe(500);
337-
expect(setHeader).toHaveBeenCalledWith("Content-Type", "text/plain; charset=utf-8");
338-
expect(end).toHaveBeenCalledWith("Internal Server Error");
339-
expect(log.warn).toHaveBeenCalledWith(
340-
"plugin http route failed (route): Error: missing scope: operator.write",
341-
);
389+
expectMissingWriteScopeFailure({ res, setHeader, end, log });
342390
});
343391

344392
it("restores trusted-operator defaults for routes opting into trusted surface", async () => {
345393
let observedScopes: string[] | undefined;
346394
const log = createMockLogger();
347-
const handler = createGatewayPluginRequestHandler({
348-
registry: createTestRegistry({
349-
httpRoutes: [
350-
createRoute({
351-
path: "/secure-admin-hook",
352-
auth: "gateway",
353-
gatewayRuntimeScopeSurface: "trusted-operator",
354-
handler: async () => {
355-
observedScopes =
356-
getPluginRuntimeGatewayRequestScope()?.client?.connect?.scopes?.slice() ?? [];
357-
assertAdminHelperAllowed();
358-
return true;
359-
},
360-
}),
361-
],
362-
}),
395+
const handler = createPluginRequestHandler({
396+
routes: [
397+
createRoute({
398+
path: SECURE_ADMIN_HOOK_PATH,
399+
auth: "gateway",
400+
gatewayRuntimeScopeSurface: "trusted-operator",
401+
handler: async () => {
402+
observedScopes =
403+
getPluginRuntimeGatewayRequestScope()?.client?.connect?.scopes?.slice() ?? [];
404+
assertAdminHelperAllowed();
405+
return true;
406+
},
407+
}),
408+
],
363409
log,
364410
});
365411

366-
const response = makeMockHttpResponse();
367-
const handled = await handler(
368-
{ url: "/secure-admin-hook" } as IncomingMessage,
369-
response.res,
370-
undefined,
371-
{
372-
gatewayAuthSatisfied: true,
373-
gatewayRequestAuth: { authMethod: "token", trustDeclaredOperatorScopes: false },
374-
gatewayRequestOperatorScopes: ["operator.write"],
375-
},
376-
);
412+
const response = await dispatchTrustedGatewayRequest(handler, SECURE_ADMIN_HOOK_PATH);
377413

378-
expect(handled).toBe(true);
414+
expect(response.handled).toBe(true);
379415
expect(response.res.statusCode).toBe(200);
380416
expect(log.warn).not.toHaveBeenCalled();
381417
expect(observedScopes).toEqual(CLI_DEFAULT_OPERATOR_SCOPES);
@@ -420,19 +456,9 @@ describe("plugin HTTP route runtime scopes", () => {
420456
log,
421457
});
422458

423-
const response = makeMockHttpResponse();
424-
const handled = await handler(
425-
{ url: "/secure/admin-hook" } as IncomingMessage,
426-
response.res,
427-
undefined,
428-
{
429-
gatewayAuthSatisfied: true,
430-
gatewayRequestAuth: { authMethod: "token", trustDeclaredOperatorScopes: false },
431-
gatewayRequestOperatorScopes: ["operator.write"],
432-
},
433-
);
459+
const response = await dispatchTrustedGatewayRequest(handler, "/secure/admin-hook");
434460

435-
expect(handled).toBe(true);
461+
expect(response.handled).toBe(true);
436462
expect(response.res.statusCode).toBe(200);
437463
expect(log.warn).not.toHaveBeenCalled();
438464
expect(observed).toHaveLength(2);

0 commit comments

Comments
 (0)