@@ -17,6 +17,13 @@ import { makeMockHttpResponse } from "../test-http-response.js";
1717import { createTestRegistry } from "./__tests__/test-utils.js" ;
1818import { 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+
2027function 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+
74139describe ( "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