@@ -220,40 +220,29 @@ describe('validate', () => {
220220 expect ( result . error ?. includes ( 'Invalid authorizer type' ) ) . toBeTruthy ( ) ;
221221 } ) ;
222222
223- // AC11: CUSTOM_JWT requires discoveryUrl
224- it ( 'returns error for CUSTOM_JWT missing discoveryUrl ' , ( ) => {
225- const opts = { ... validGatewayOptionsJwt , discoveryUrl : undefined } ;
226- const result = validateAddGatewayOptions ( opts ) ;
223+ // AC11: CUSTOM_JWT requires discoveryUrl; at least one of allowedAudience/allowedClients/allowedScopes
224+ it ( 'returns error for CUSTOM_JWT missing required fields ' , ( ) => {
225+ // discoveryUrl is always required
226+ const result = validateAddGatewayOptions ( { ... validGatewayOptionsJwt , discoveryUrl : undefined } ) ;
227227 expect ( result . valid ) . toBe ( false ) ;
228228 expect ( result . error ) . toBe ( '--discovery-url is required for CUSTOM_JWT authorizer' ) ;
229- } ) ;
230229
231- // AC11b: at least one of audience/clients/scopes required
232- it ( 'returns error when all of audience, clients, and scopes are missing' , ( ) => {
233- const opts = {
230+ // All three optional fields absent fails
231+ const noneResult = validateAddGatewayOptions ( {
234232 ...validGatewayOptionsJwt ,
235233 allowedAudience : undefined ,
236234 allowedClients : undefined ,
237235 allowedScopes : undefined ,
238- } ;
239- const result = validateAddGatewayOptions ( opts ) ;
240- expect ( result . valid ) . toBe ( false ) ;
241- expect ( result . error ) . toContain ( 'At least one of' ) ;
242- } ) ;
243-
244- it ( 'allows CUSTOM_JWT with only allowedScopes' , ( ) => {
245- const opts = {
246- ...validGatewayOptionsJwt ,
247- allowedAudience : undefined ,
248- allowedClients : undefined ,
249- allowedScopes : 'scope1' ,
250- } ;
251- const result = validateAddGatewayOptions ( opts ) ;
252- expect ( result . valid ) . toBe ( true ) ;
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+ ) ;
253241 } ) ;
254242
255- it ( 'allows CUSTOM_JWT with only allowedAudience' , ( ) => {
256- const opts = { ...validGatewayOptionsJwt , allowedClients : undefined , allowedScopes : undefined } ;
243+ // AC11b: allowedAudience is optional
244+ it ( 'allows CUSTOM_JWT without allowedAudience' , ( ) => {
245+ const opts = { ...validGatewayOptionsJwt , allowedAudience : undefined } ;
257246 const result = validateAddGatewayOptions ( opts ) ;
258247 expect ( result . valid ) . toBe ( true ) ;
259248 } ) ;
@@ -271,21 +260,88 @@ describe('validate', () => {
271260 expect ( result . error ?. includes ( '.well-known/openid-configuration' ) ) . toBeTruthy ( ) ;
272261 } ) ;
273262
274- it ( 'returns error for HTTP discoveryUrl (HTTPS required)' , ( ) => {
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' , ( ) => {
275265 const result = validateAddGatewayOptions ( {
276266 ...validGatewayOptionsJwt ,
277- discoveryUrl : 'http://example.com/.well-known/openid-configuration' ,
267+ allowedAudience : ' ' ,
268+ allowedClients : undefined ,
269+ allowedScopes : undefined ,
278270 } ) ;
279271 expect ( result . valid ) . toBe ( false ) ;
280- expect ( result . error ) . toBe ( 'Discovery URL must use HTTPS' ) ;
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+ ) ;
281275 } ) ;
282276
283- it ( 'allows CUSTOM_JWT with only allowedClients' , ( ) => {
284- const opts = { ...validGatewayOptionsJwt , allowedAudience : undefined , allowedScopes : undefined } ;
285- const result = validateAddGatewayOptions ( opts ) ;
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+ } ) ;
286292 expect ( result . valid ) . toBe ( true ) ;
287293 } ) ;
288294
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+ } ) ;
341+ expect ( result . valid ) . toBe ( false ) ;
342+ expect ( result . error ) . toContain ( 'Invalid custom claim at index 0' ) ;
343+ } ) ;
344+
289345 // AC14: Valid options pass
290346 it ( 'passes for valid options' , ( ) => {
291347 expect ( validateAddGatewayOptions ( validGatewayOptionsNone ) ) . toEqual ( { valid : true } ) ;
@@ -311,8 +367,8 @@ describe('validate', () => {
311367 expect ( result . error ) . toBe ( 'Both --client-id and --client-secret must be provided together' ) ;
312368 } ) ;
313369
314- // AC16: OAuth credentials only valid with CUSTOM_JWT
315- it ( 'returns error when OAuth 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' , ( ) => {
316372 const result = validateAddGatewayOptions ( {
317373 ...validGatewayOptionsNone ,
318374 clientId : 'my-client-id' ,
@@ -322,8 +378,8 @@ describe('validate', () => {
322378 expect ( result . error ) . toBe ( 'OAuth client credentials are only valid with CUSTOM_JWT authorizer' ) ;
323379 } ) ;
324380
325- // AC17: valid CUSTOM_JWT with OAuth credentials passes
326- it ( 'passes for CUSTOM_JWT with OAuth credentials' , ( ) => {
381+ // AC17: valid CUSTOM_JWT with OAuth client credentials passes
382+ it ( 'passes for CUSTOM_JWT with OAuth client credentials' , ( ) => {
327383 const result = validateAddGatewayOptions ( {
328384 ...validGatewayOptionsJwt ,
329385 clientId : 'my-client-id' ,
0 commit comments