@@ -2492,6 +2492,99 @@ describe('OAuth Authorization', () => {
24922492 expect ( body . get ( 'refresh_token' ) ) . toBe ( 'refresh123' ) ;
24932493 } ) ;
24942494
2495+ it ( 'deduplicates concurrent refreshes for the same provider and resource' , async ( ) => {
2496+ let releaseTokensReaders : ( ( ) => void ) | undefined ;
2497+ const tokensReady = new Promise < void > ( resolve => {
2498+ releaseTokensReaders = resolve ;
2499+ } ) ;
2500+ let tokensReaderCount = 0 ;
2501+ let resolveRefreshResponse : ( ( value : { ok : true ; status : 200 ; json : ( ) => Promise < OAuthTokens > } ) => void ) | undefined ;
2502+ const refreshResponse = new Promise < { ok : true ; status : 200 ; json : ( ) => Promise < OAuthTokens > } > ( resolve => {
2503+ resolveRefreshResponse = resolve ;
2504+ } ) ;
2505+ let refreshRequestCount = 0 ;
2506+
2507+ mockFetch . mockImplementation ( url => {
2508+ const urlString = url . toString ( ) ;
2509+
2510+ if ( urlString . includes ( '/.well-known/oauth-protected-resource' ) ) {
2511+ return Promise . resolve ( {
2512+ ok : true ,
2513+ status : 200 ,
2514+ json : async ( ) => ( {
2515+ resource : 'https://api.example.com/mcp-server' ,
2516+ authorization_servers : [ 'https://auth.example.com' ]
2517+ } )
2518+ } ) ;
2519+ }
2520+
2521+ if ( urlString . includes ( '/.well-known/oauth-authorization-server' ) ) {
2522+ return Promise . resolve ( {
2523+ ok : true ,
2524+ status : 200 ,
2525+ json : async ( ) => ( {
2526+ issuer : 'https://auth.example.com' ,
2527+ authorization_endpoint : 'https://auth.example.com/authorize' ,
2528+ token_endpoint : 'https://auth.example.com/token' ,
2529+ response_types_supported : [ 'code' ] ,
2530+ code_challenge_methods_supported : [ 'S256' ]
2531+ } )
2532+ } ) ;
2533+ }
2534+
2535+ if ( urlString . includes ( '/token' ) ) {
2536+ refreshRequestCount ++ ;
2537+ if ( refreshRequestCount > 1 ) {
2538+ throw new Error ( 'duplicate refresh request' ) ;
2539+ }
2540+ return refreshResponse ;
2541+ }
2542+
2543+ return Promise . resolve ( { ok : false , status : 404 } ) ;
2544+ } ) ;
2545+
2546+ ( mockProvider . clientInformation as Mock ) . mockResolvedValue ( {
2547+ client_id : 'test-client' ,
2548+ client_secret : 'test-secret'
2549+ } ) ;
2550+ ( mockProvider . tokens as Mock ) . mockImplementation ( async ( ) => {
2551+ tokensReaderCount ++ ;
2552+ if ( tokensReaderCount === 2 ) {
2553+ releaseTokensReaders ?.( ) ;
2554+ }
2555+ await tokensReady ;
2556+ return {
2557+ access_token : 'old-access' ,
2558+ refresh_token : 'refresh123'
2559+ } ;
2560+ } ) ;
2561+ ( mockProvider . saveTokens as Mock ) . mockResolvedValue ( undefined ) ;
2562+
2563+ const authResults = Promise . all ( [
2564+ auth ( mockProvider , { serverUrl : 'https://api.example.com/mcp-server' } ) ,
2565+ auth ( mockProvider , { serverUrl : 'https://api.example.com/mcp-server' } )
2566+ ] ) ;
2567+
2568+ await vi . waitFor ( ( ) => {
2569+ expect ( refreshRequestCount ) . toBe ( 1 ) ;
2570+ } ) ;
2571+
2572+ resolveRefreshResponse ?.( {
2573+ ok : true ,
2574+ status : 200 ,
2575+ json : async ( ) => ( {
2576+ access_token : 'new-access123' ,
2577+ refresh_token : 'new-refresh456' ,
2578+ token_type : 'Bearer' ,
2579+ expires_in : 3600
2580+ } )
2581+ } ) ;
2582+
2583+ await expect ( authResults ) . resolves . toEqual ( [ 'AUTHORIZED' , 'AUTHORIZED' ] ) ;
2584+ expect ( refreshRequestCount ) . toBe ( 1 ) ;
2585+ expect ( mockProvider . saveTokens ) . toHaveBeenCalledTimes ( 1 ) ;
2586+ } ) ;
2587+
24952588 it ( 'skips default PRM resource validation when custom validateResourceURL is provided' , async ( ) => {
24962589 const mockValidateResourceURL = vi . fn ( ) . mockResolvedValue ( undefined ) ;
24972590 const providerWithCustomValidation = {
0 commit comments