@@ -218,40 +218,29 @@ describe('validate', () => {
218218 expect ( result . error ?. includes ( 'Invalid authorizer type' ) ) . toBeTruthy ( ) ;
219219 } ) ;
220220
221- // AC11: CUSTOM_JWT requires discoveryUrl
222- it ( 'returns error for CUSTOM_JWT missing discoveryUrl ' , ( ) => {
223- const opts = { ... validGatewayOptionsJwt , discoveryUrl : undefined } ;
224- const result = validateAddGatewayOptions ( opts ) ;
221+ // AC11: CUSTOM_JWT requires discoveryUrl; at least one of allowedAudience/allowedClients/allowedScopes
222+ it ( 'returns error for CUSTOM_JWT missing required fields ' , ( ) => {
223+ // discoveryUrl is always required
224+ const result = validateAddGatewayOptions ( { ... validGatewayOptionsJwt , discoveryUrl : undefined } ) ;
225225 expect ( result . valid ) . toBe ( false ) ;
226226 expect ( result . error ) . toBe ( '--discovery-url is required for CUSTOM_JWT authorizer' ) ;
227- } ) ;
228227
229- // AC11b: at least one of audience/clients/scopes required
230- it ( 'returns error when all of audience, clients, and scopes are missing' , ( ) => {
231- const opts = {
228+ // All three optional fields absent fails
229+ const noneResult = validateAddGatewayOptions ( {
232230 ...validGatewayOptionsJwt ,
233231 allowedAudience : undefined ,
234232 allowedClients : undefined ,
235233 allowedScopes : undefined ,
236- } ;
237- const result = validateAddGatewayOptions ( opts ) ;
238- expect ( result . valid ) . toBe ( false ) ;
239- expect ( result . error ) . toContain ( 'At least one of' ) ;
240- } ) ;
241-
242- it ( 'allows CUSTOM_JWT with only allowedScopes' , ( ) => {
243- const opts = {
244- ...validGatewayOptionsJwt ,
245- allowedAudience : undefined ,
246- allowedClients : undefined ,
247- allowedScopes : 'scope1' ,
248- } ;
249- const result = validateAddGatewayOptions ( opts ) ;
250- expect ( result . valid ) . toBe ( true ) ;
234+ } ) ;
235+ expect ( noneResult . valid ) . toBe ( false ) ;
236+ expect ( noneResult . error ) . toBe (
237+ 'At least one of --allowed-audience, --allowed-clients, --allowed-scopes, or --custom-claims must be provided for CUSTOM_JWT authorizer'
238+ ) ;
251239 } ) ;
252240
253- it ( 'allows CUSTOM_JWT with only allowedAudience' , ( ) => {
254- const opts = { ...validGatewayOptionsJwt , allowedClients : undefined , allowedScopes : undefined } ;
241+ // AC11b: allowedAudience is optional
242+ it ( 'allows CUSTOM_JWT without allowedAudience' , ( ) => {
243+ const opts = { ...validGatewayOptionsJwt , allowedAudience : undefined } ;
255244 const result = validateAddGatewayOptions ( opts ) ;
256245 expect ( result . valid ) . toBe ( true ) ;
257246 } ) ;
@@ -269,21 +258,88 @@ describe('validate', () => {
269258 expect ( result . error ?. includes ( '.well-known/openid-configuration' ) ) . toBeTruthy ( ) ;
270259 } ) ;
271260
272- it ( 'returns error for HTTP discoveryUrl (HTTPS required)' , ( ) => {
261+ // AC13: At least one of audience/clients/scopes must be non-empty
262+ it ( 'returns error when all of audience, clients, and scopes are empty' , ( ) => {
273263 const result = validateAddGatewayOptions ( {
274264 ...validGatewayOptionsJwt ,
275- discoveryUrl : 'http://example.com/.well-known/openid-configuration' ,
265+ allowedAudience : ' ' ,
266+ allowedClients : undefined ,
267+ allowedScopes : undefined ,
276268 } ) ;
277269 expect ( result . valid ) . toBe ( false ) ;
278- expect ( result . error ) . toBe ( 'Discovery URL must use HTTPS' ) ;
270+ expect ( result . error ) . toBe (
271+ 'At least one of --allowed-audience, --allowed-clients, --allowed-scopes, or --custom-claims must be provided for CUSTOM_JWT authorizer'
272+ ) ;
279273 } ) ;
280274
281- it ( 'allows CUSTOM_JWT with only allowedClients' , ( ) => {
282- const opts = { ...validGatewayOptionsJwt , allowedAudience : undefined , allowedScopes : undefined } ;
283- const result = validateAddGatewayOptions ( opts ) ;
275+ // AC-claims1: --custom-claims with valid JSON passes validation
276+ it ( 'accepts valid --custom-claims JSON' , ( ) => {
277+ const result = validateAddGatewayOptions ( {
278+ ...validGatewayOptionsJwt ,
279+ customClaims : JSON . stringify ( [
280+ {
281+ inboundTokenClaimName : 'dept' ,
282+ inboundTokenClaimValueType : 'STRING' ,
283+ authorizingClaimMatchValue : {
284+ claimMatchOperator : 'EQUALS' ,
285+ claimMatchValue : { matchValueString : 'engineering' } ,
286+ } ,
287+ } ,
288+ ] ) ,
289+ } ) ;
284290 expect ( result . valid ) . toBe ( true ) ;
285291 } ) ;
286292
293+ // AC-claims2: --custom-claims alone satisfies the "at least one constraint" check
294+ it ( 'allows CUSTOM_JWT with only --custom-claims (no audience/clients/scopes)' , ( ) => {
295+ const result = validateAddGatewayOptions ( {
296+ name : 'test-gw' ,
297+ authorizerType : 'CUSTOM_JWT' ,
298+ discoveryUrl : 'https://example.com/.well-known/openid-configuration' ,
299+ customClaims : JSON . stringify ( [
300+ {
301+ inboundTokenClaimName : 'role' ,
302+ inboundTokenClaimValueType : 'STRING_ARRAY' ,
303+ authorizingClaimMatchValue : {
304+ claimMatchOperator : 'CONTAINS_ANY' ,
305+ claimMatchValue : { matchValueStringList : [ 'admin' ] } ,
306+ } ,
307+ } ,
308+ ] ) ,
309+ } ) ;
310+ expect ( result . valid ) . toBe ( true ) ;
311+ } ) ;
312+
313+ // AC-claims3: --custom-claims with invalid JSON fails
314+ it ( 'returns error for --custom-claims with invalid JSON' , ( ) => {
315+ const result = validateAddGatewayOptions ( {
316+ ...validGatewayOptionsJwt ,
317+ customClaims : 'not json' ,
318+ } ) ;
319+ expect ( result . valid ) . toBe ( false ) ;
320+ expect ( result . error ) . toBe ( '--custom-claims must be valid JSON' ) ;
321+ } ) ;
322+
323+ // AC-claims4: --custom-claims with empty array fails
324+ it ( 'returns error for --custom-claims with empty array' , ( ) => {
325+ const result = validateAddGatewayOptions ( {
326+ ...validGatewayOptionsJwt ,
327+ customClaims : '[]' ,
328+ } ) ;
329+ expect ( result . valid ) . toBe ( false ) ;
330+ expect ( result . error ) . toBe ( '--custom-claims must be a non-empty JSON array' ) ;
331+ } ) ;
332+
333+ // AC-claims5: --custom-claims with invalid claim structure fails
334+ it ( 'returns error for --custom-claims with invalid claim structure' , ( ) => {
335+ const result = validateAddGatewayOptions ( {
336+ ...validGatewayOptionsJwt ,
337+ customClaims : JSON . stringify ( [ { badField : 'value' } ] ) ,
338+ } ) ;
339+ expect ( result . valid ) . toBe ( false ) ;
340+ expect ( result . error ) . toContain ( 'Invalid custom claim at index 0' ) ;
341+ } ) ;
342+
287343 // AC14: Valid options pass
288344 it ( 'passes for valid options' , ( ) => {
289345 expect ( validateAddGatewayOptions ( validGatewayOptionsNone ) ) . toEqual ( { valid : true } ) ;
@@ -309,8 +365,8 @@ describe('validate', () => {
309365 expect ( result . error ) . toBe ( 'Both --client-id and --client-secret must be provided together' ) ;
310366 } ) ;
311367
312- // AC16: OAuth credentials only valid with CUSTOM_JWT
313- it ( 'returns error when OAuth credentials used with non-CUSTOM_JWT authorizer' , ( ) => {
368+ // AC16: OAuth client credentials only valid with CUSTOM_JWT
369+ it ( 'returns error when OAuth client credentials used with non-CUSTOM_JWT authorizer' , ( ) => {
314370 const result = validateAddGatewayOptions ( {
315371 ...validGatewayOptionsNone ,
316372 clientId : 'my-client-id' ,
@@ -320,8 +376,8 @@ describe('validate', () => {
320376 expect ( result . error ) . toBe ( 'OAuth client credentials are only valid with CUSTOM_JWT authorizer' ) ;
321377 } ) ;
322378
323- // AC17: valid CUSTOM_JWT with OAuth credentials passes
324- it ( 'passes for CUSTOM_JWT with OAuth credentials' , ( ) => {
379+ // AC17: valid CUSTOM_JWT with OAuth client credentials passes
380+ it ( 'passes for CUSTOM_JWT with OAuth client credentials' , ( ) => {
325381 const result = validateAddGatewayOptions ( {
326382 ...validGatewayOptionsJwt ,
327383 clientId : 'my-client-id' ,
0 commit comments