@@ -220,19 +220,24 @@ describe('validate', () => {
220220 expect ( result . error ?. includes ( 'Invalid authorizer type' ) ) . toBeTruthy ( ) ;
221221 } ) ;
222222
223- // AC11: CUSTOM_JWT requires discoveryUrl and allowedClients (allowedAudience is optional)
223+ // AC11: CUSTOM_JWT requires discoveryUrl; at least one of allowedAudience/allowedClients/allowedScopes
224224 it ( 'returns error for CUSTOM_JWT missing required fields' , ( ) => {
225- const jwtFields : { field : keyof AddGatewayOptions ; error : string } [ ] = [
226- { field : 'discoveryUrl' , error : '--discovery-url is required for CUSTOM_JWT authorizer' } ,
227- { field : 'allowedClients' , error : '--allowed-clients is required for CUSTOM_JWT authorizer' } ,
228- ] ;
225+ // discoveryUrl is always required
226+ const result = validateAddGatewayOptions ( { ... validGatewayOptionsJwt , discoveryUrl : undefined } ) ;
227+ expect ( result . valid ) . toBe ( false ) ;
228+ expect ( result . error ) . toBe ( '--discovery-url is required for CUSTOM_JWT authorizer' ) ;
229229
230- for ( const { field, error } of jwtFields ) {
231- const opts = { ...validGatewayOptionsJwt , [ field ] : undefined } ;
232- const result = validateAddGatewayOptions ( opts ) ;
233- expect ( result . valid , `Should fail for missing ${ String ( field ) } ` ) . toBe ( false ) ;
234- expect ( result . error ) . toBe ( error ) ;
235- }
230+ // All three optional fields absent fails
231+ const noneResult = validateAddGatewayOptions ( {
232+ ...validGatewayOptionsJwt ,
233+ allowedAudience : undefined ,
234+ allowedClients : undefined ,
235+ allowedScopes : undefined ,
236+ } ) ;
237+ expect ( noneResult . valid ) . toBe ( false ) ;
238+ expect ( noneResult . error ) . toBe (
239+ 'At least one of --allowed-audience, --allowed-clients, --allowed-scopes, or --custom-claims must be provided for CUSTOM_JWT authorizer'
240+ ) ;
236241 } ) ;
237242
238243 // AC11b: allowedAudience is optional
@@ -255,11 +260,86 @@ describe('validate', () => {
255260 expect ( result . error ?. includes ( '.well-known/openid-configuration' ) ) . toBeTruthy ( ) ;
256261 } ) ;
257262
258- // AC13: Empty comma-separated clients rejected (audience can be empty)
259- it ( 'returns error for empty clients' , ( ) => {
260- const result = validateAddGatewayOptions ( { ...validGatewayOptionsJwt , allowedClients : ' , ' } ) ;
263+ // AC13: At least one of audience/clients/scopes must be non-empty
264+ it ( 'returns error when all of audience, clients, and scopes are empty' , ( ) => {
265+ const result = validateAddGatewayOptions ( {
266+ ...validGatewayOptionsJwt ,
267+ allowedAudience : ' ' ,
268+ allowedClients : undefined ,
269+ allowedScopes : undefined ,
270+ } ) ;
271+ expect ( result . valid ) . toBe ( false ) ;
272+ expect ( result . error ) . toBe (
273+ 'At least one of --allowed-audience, --allowed-clients, --allowed-scopes, or --custom-claims must be provided for CUSTOM_JWT authorizer'
274+ ) ;
275+ } ) ;
276+
277+ // AC-claims1: --custom-claims with valid JSON passes validation
278+ it ( 'accepts valid --custom-claims JSON' , ( ) => {
279+ const result = validateAddGatewayOptions ( {
280+ ...validGatewayOptionsJwt ,
281+ customClaims : JSON . stringify ( [
282+ {
283+ inboundTokenClaimName : 'dept' ,
284+ inboundTokenClaimValueType : 'STRING' ,
285+ authorizingClaimMatchValue : {
286+ claimMatchOperator : 'EQUALS' ,
287+ claimMatchValue : { matchValueString : 'engineering' } ,
288+ } ,
289+ } ,
290+ ] ) ,
291+ } ) ;
292+ expect ( result . valid ) . toBe ( true ) ;
293+ } ) ;
294+
295+ // AC-claims2: --custom-claims alone satisfies the "at least one constraint" check
296+ it ( 'allows CUSTOM_JWT with only --custom-claims (no audience/clients/scopes)' , ( ) => {
297+ const result = validateAddGatewayOptions ( {
298+ name : 'test-gw' ,
299+ authorizerType : 'CUSTOM_JWT' ,
300+ discoveryUrl : 'https://example.com/.well-known/openid-configuration' ,
301+ customClaims : JSON . stringify ( [
302+ {
303+ inboundTokenClaimName : 'role' ,
304+ inboundTokenClaimValueType : 'STRING_ARRAY' ,
305+ authorizingClaimMatchValue : {
306+ claimMatchOperator : 'CONTAINS_ANY' ,
307+ claimMatchValue : { matchValueStringList : [ 'admin' ] } ,
308+ } ,
309+ } ,
310+ ] ) ,
311+ } ) ;
312+ expect ( result . valid ) . toBe ( true ) ;
313+ } ) ;
314+
315+ // AC-claims3: --custom-claims with invalid JSON fails
316+ it ( 'returns error for --custom-claims with invalid JSON' , ( ) => {
317+ const result = validateAddGatewayOptions ( {
318+ ...validGatewayOptionsJwt ,
319+ customClaims : 'not json' ,
320+ } ) ;
321+ expect ( result . valid ) . toBe ( false ) ;
322+ expect ( result . error ) . toBe ( '--custom-claims must be valid JSON' ) ;
323+ } ) ;
324+
325+ // AC-claims4: --custom-claims with empty array fails
326+ it ( 'returns error for --custom-claims with empty array' , ( ) => {
327+ const result = validateAddGatewayOptions ( {
328+ ...validGatewayOptionsJwt ,
329+ customClaims : '[]' ,
330+ } ) ;
331+ expect ( result . valid ) . toBe ( false ) ;
332+ expect ( result . error ) . toBe ( '--custom-claims must be a non-empty JSON array' ) ;
333+ } ) ;
334+
335+ // AC-claims5: --custom-claims with invalid claim structure fails
336+ it ( 'returns error for --custom-claims with invalid claim structure' , ( ) => {
337+ const result = validateAddGatewayOptions ( {
338+ ...validGatewayOptionsJwt ,
339+ customClaims : JSON . stringify ( [ { badField : 'value' } ] ) ,
340+ } ) ;
261341 expect ( result . valid ) . toBe ( false ) ;
262- expect ( result . error ) . toBe ( 'At least one client value is required ') ;
342+ expect ( result . error ) . toContain ( 'Invalid custom claim at index 0 ') ;
263343 } ) ;
264344
265345 // AC14: Valid options pass
@@ -268,42 +348,42 @@ describe('validate', () => {
268348 expect ( validateAddGatewayOptions ( validGatewayOptionsJwt ) ) . toEqual ( { valid : true } ) ;
269349 } ) ;
270350
271- // AC15: agentClientId and agentClientSecret must be provided together
272- it ( 'returns error when agentClientId provided without agentClientSecret ' , ( ) => {
351+ // AC15: clientId and clientSecret must be provided together
352+ it ( 'returns error when clientId provided without clientSecret ' , ( ) => {
273353 const result = validateAddGatewayOptions ( {
274354 ...validGatewayOptionsJwt ,
275- agentClientId : 'my-client-id' ,
355+ clientId : 'my-client-id' ,
276356 } ) ;
277357 expect ( result . valid ) . toBe ( false ) ;
278- expect ( result . error ) . toBe ( 'Both --agent- client-id and --agent -client-secret must be provided together' ) ;
358+ expect ( result . error ) . toBe ( 'Both --client-id and --client-secret must be provided together' ) ;
279359 } ) ;
280360
281- it ( 'returns error when agentClientSecret provided without agentClientId ' , ( ) => {
361+ it ( 'returns error when clientSecret provided without clientId ' , ( ) => {
282362 const result = validateAddGatewayOptions ( {
283363 ...validGatewayOptionsJwt ,
284- agentClientSecret : 'my-secret' ,
364+ clientSecret : 'my-secret' ,
285365 } ) ;
286366 expect ( result . valid ) . toBe ( false ) ;
287- expect ( result . error ) . toBe ( 'Both --agent- client-id and --agent -client-secret must be provided together' ) ;
367+ expect ( result . error ) . toBe ( 'Both --client-id and --client-secret must be provided together' ) ;
288368 } ) ;
289369
290- // AC16: agent credentials only valid with CUSTOM_JWT
291- it ( 'returns error when agent credentials used with non-CUSTOM_JWT authorizer' , ( ) => {
370+ // AC16: OAuth client credentials only valid with CUSTOM_JWT
371+ it ( 'returns error when OAuth client credentials used with non-CUSTOM_JWT authorizer' , ( ) => {
292372 const result = validateAddGatewayOptions ( {
293373 ...validGatewayOptionsNone ,
294- agentClientId : 'my-client-id' ,
295- agentClientSecret : 'my-secret' ,
374+ clientId : 'my-client-id' ,
375+ clientSecret : 'my-secret' ,
296376 } ) ;
297377 expect ( result . valid ) . toBe ( false ) ;
298- expect ( result . error ) . toBe ( 'Agent OAuth credentials are only valid with CUSTOM_JWT authorizer' ) ;
378+ expect ( result . error ) . toBe ( 'OAuth client credentials are only valid with CUSTOM_JWT authorizer' ) ;
299379 } ) ;
300380
301- // AC17: valid CUSTOM_JWT with agent credentials passes
302- it ( 'passes for CUSTOM_JWT with agent credentials' , ( ) => {
381+ // AC17: valid CUSTOM_JWT with OAuth client credentials passes
382+ it ( 'passes for CUSTOM_JWT with OAuth client credentials' , ( ) => {
303383 const result = validateAddGatewayOptions ( {
304384 ...validGatewayOptionsJwt ,
305- agentClientId : 'my-client-id' ,
306- agentClientSecret : 'my-secret' ,
385+ clientId : 'my-client-id' ,
386+ clientSecret : 'my-secret' ,
307387 allowedScopes : 'scope1,scope2' ,
308388 } ) ;
309389 expect ( result . valid ) . toBe ( true ) ;
0 commit comments