@@ -539,6 +539,94 @@ describe("Credentials", () => {
539539 expect ( scope . isDone ( ) ) . toBe ( true ) ;
540540 } ) ;
541541
542+ test ( "should send a single token request for concurrent access token reads" , async ( ) => {
543+ const apiTokenIssuer = "issuer.fga.example" ;
544+ const expectedBaseUrl = "https://issuer.fga.example" ;
545+ const expectedPath = `/${ DEFAULT_TOKEN_ENDPOINT_PATH } ` ;
546+
547+ const scope = nock ( expectedBaseUrl )
548+ . post ( expectedPath )
549+ . once ( )
550+ . delay ( 20 )
551+ . reply ( 200 , {
552+ access_token : "shared-token" ,
553+ expires_in : 300 ,
554+ } ) ;
555+
556+ const credentials = new Credentials (
557+ {
558+ method : CredentialsMethod . ClientCredentials ,
559+ config : {
560+ apiTokenIssuer,
561+ apiAudience : OPENFGA_API_AUDIENCE ,
562+ clientId : OPENFGA_CLIENT_ID ,
563+ clientSecret : OPENFGA_CLIENT_SECRET ,
564+ } ,
565+ } as AuthCredentialsConfig ,
566+ undefined ,
567+ mockTelemetryConfig ,
568+ ) ;
569+
570+ const headers = await Promise . all (
571+ Array . from ( { length : 5 } , ( ) => credentials . getAccessTokenHeader ( ) )
572+ ) ;
573+
574+ headers . forEach ( header => {
575+ expect ( header ?. value ) . toBe ( "Bearer shared-token" ) ;
576+ } ) ;
577+ expect ( scope . isDone ( ) ) . toBe ( true ) ;
578+ } ) ;
579+
580+ test ( "should clear shared refresh promise after failure and retry on the next call" , async ( ) => {
581+ const apiTokenIssuer = "issuer.fga.example" ;
582+ const expectedBaseUrl = "https://issuer.fga.example" ;
583+ const expectedPath = `/${ DEFAULT_TOKEN_ENDPOINT_PATH } ` ;
584+
585+ const scope = nock ( expectedBaseUrl )
586+ . post ( expectedPath )
587+ . once ( )
588+ . reply ( 404 , {
589+ code : "not_found" ,
590+ message : "token exchange failed" ,
591+ } )
592+ . post ( expectedPath )
593+ . once ( )
594+ . reply ( 200 , {
595+ access_token : "recovered-token" ,
596+ expires_in : 300 ,
597+ } ) ;
598+
599+ const credentials = new Credentials (
600+ {
601+ method : CredentialsMethod . ClientCredentials ,
602+ config : {
603+ apiTokenIssuer,
604+ apiAudience : OPENFGA_API_AUDIENCE ,
605+ clientId : OPENFGA_CLIENT_ID ,
606+ clientSecret : OPENFGA_CLIENT_SECRET ,
607+ } ,
608+ } as AuthCredentialsConfig ,
609+ undefined ,
610+ mockTelemetryConfig ,
611+ ) ;
612+
613+ const results = await Promise . allSettled (
614+ Array . from ( { length : 5 } , ( ) => credentials . getAccessTokenHeader ( ) )
615+ ) ;
616+ const rejected = results . filter ( ( result ) : result is PromiseRejectedResult => result . status === "rejected" ) ;
617+
618+ expect ( rejected ) . toHaveLength ( 5 ) ;
619+ expect ( rejected [ 0 ] . reason ) . toBe ( rejected [ 1 ] . reason ) ;
620+ expect ( rejected [ 1 ] . reason ) . toBe ( rejected [ 2 ] . reason ) ;
621+ expect ( rejected [ 2 ] . reason ) . toBe ( rejected [ 3 ] . reason ) ;
622+ expect ( rejected [ 3 ] . reason ) . toBe ( rejected [ 4 ] . reason ) ;
623+
624+ const header = await credentials . getAccessTokenHeader ( ) ;
625+
626+ expect ( header ?. value ) . toBe ( "Bearer recovered-token" ) ;
627+ expect ( scope . isDone ( ) ) . toBe ( true ) ;
628+ } ) ;
629+
542630 test ( "should refresh cached token when it is close to expiration" , async ( ) => {
543631 const apiTokenIssuer = "issuer.fga.example" ;
544632 const expectedBaseUrl = "https://issuer.fga.example" ;
0 commit comments