@@ -442,11 +442,16 @@ describe('OAuth access-token refresh headroom', () => {
442442 describe . each ( readers ) ( '$name' , ( { read, throwsOnFailure } ) => {
443443 it . each ( [
444444 { providerId : 'google-drive' , remainingMs : 120_000 , refresh : 'wait' } ,
445- { providerId : 'google-drive' , remainingMs : 240_000 , refresh : 'ahead' } ,
446- { providerId : 'google-drive' , remainingMs : 300_001 , refresh : 'none' } ,
445+ { providerId : 'google-drive' , remainingMs : 240_000 , refresh : 'wait' } ,
446+ { providerId : 'google-drive' , remainingMs : 300_000 , refresh : 'wait' } ,
447+ { providerId : 'google-drive' , remainingMs : 300_001 , refresh : 'ahead' } ,
448+ /** Inside the refresh-ahead window for every account, whatever its jitter. */
449+ { providerId : 'google-drive' , remainingMs : 540_000 , refresh : 'ahead' } ,
450+ { providerId : 'google-drive' , remainingMs : 600_001 , refresh : 'none' } ,
447451 { providerId : 'google-drive' , remainingMs : null , refresh : 'none' } ,
448452 { providerId : 'confluence' , remainingMs : 300_000 , refresh : 'wait' } ,
449453 { providerId : 'confluence' , remainingMs : 300_001 , refresh : 'none' } ,
454+ { providerId : 'confluence' , remainingMs : 540_000 , refresh : 'none' } ,
450455 ] ) (
451456 'handles $providerId with $remainingMs milliseconds remaining' ,
452457 async ( { providerId, remainingMs, refresh } ) => {
@@ -463,7 +468,7 @@ describe('OAuth access-token refresh headroom', () => {
463468 it ( 'uses a still-valid Google token while its refresh is still with the provider' , async ( ) => {
464469 const provider = createDeferred < unknown > ( )
465470 mocks . refreshOAuthToken . mockReturnValue ( provider . promise )
466- await expect ( read ( createOAuthAccount ( 240_000 ) ) ) . resolves . toBe ( 'original-access-token' )
471+ await expect ( read ( createOAuthAccount ( 360_000 ) ) ) . resolves . toBe ( 'original-access-token' )
467472 await vi . waitFor ( ( ) => expect ( mocks . refreshOAuthToken ) . toHaveBeenCalledOnce ( ) )
468473 expect ( dbChainMockFns . set ) . not . toHaveBeenCalled ( )
469474 provider . resolve ( {
@@ -495,6 +500,16 @@ describe('OAuth access-token refresh headroom', () => {
495500 providerId : 'google-drive' ,
496501 remainingMs : 60_000 ,
497502 } ,
503+ {
504+ label : 'a Google token with less than five minutes left' ,
505+ providerId : 'google-drive' ,
506+ remainingMs : 240_000 ,
507+ } ,
508+ {
509+ label : 'a Google token with exactly five minutes left' ,
510+ providerId : 'google-drive' ,
511+ remainingMs : 300_000 ,
512+ } ,
498513 ] ) ( 'waits for the provider with $label' , async ( { providerId, remainingMs } ) => {
499514 const provider = createDeferred < unknown > ( )
500515 mocks . refreshOAuthToken . mockReturnValue ( provider . promise )
@@ -632,6 +647,50 @@ describe('OAuth access-token refresh headroom', () => {
632647 expect ( mocks . refreshOAuthToken ) . toHaveBeenCalledTimes ( 1 )
633648 } )
634649
650+ it ( 'shares one background refresh across every caller served inside the refresh-ahead window' , async ( ) => {
651+ const { coalesceLocally } = await vi . importActual <
652+ typeof import ( '@/lib/concurrency/singleflight' )
653+ > ( '@/lib/concurrency/singleflight' )
654+ mocks . coalesceLocally . mockImplementation ( coalesceLocally )
655+ const provider = createDeferred < unknown > ( )
656+ mocks . refreshOAuthToken . mockReturnValue ( provider . promise )
657+ const row = createOAuthAccount ( 360_000 )
658+ await expect ( refreshTokenIfNeeded ( 'test' , row , RAW_ACCOUNT_ID ) ) . resolves . toEqual ( {
659+ accessToken : 'original-access-token' ,
660+ refreshed : false ,
661+ } )
662+ await vi . waitFor ( ( ) => expect ( mocks . refreshOAuthToken ) . toHaveBeenCalledOnce ( ) )
663+ await expect ( refreshTokenIfNeeded ( 'test' , row , RAW_ACCOUNT_ID ) ) . resolves . toEqual ( {
664+ accessToken : 'original-access-token' ,
665+ refreshed : false ,
666+ } )
667+ await flushMicrotasks ( 20 )
668+ expect ( mocks . refreshOAuthToken ) . toHaveBeenCalledOnce ( )
669+ const refreshAheadLogs = serviceLogger . info . mock . calls . filter ( ( [ message ] ) =>
670+ String ( message ) . includes ( 'Refreshing ahead of expiry' )
671+ )
672+ expect ( refreshAheadLogs ) . toHaveLength ( 1 )
673+ provider . resolve ( {
674+ ok : true ,
675+ accessToken : 'refreshed-access-token' ,
676+ refreshToken : 'rotated-refresh-token' ,
677+ expiresIn : 3600 ,
678+ } )
679+ await vi . waitFor ( ( ) => expect ( dbChainMockFns . set ) . toHaveBeenCalled ( ) )
680+ expect ( mocks . refreshOAuthToken ) . toHaveBeenCalledOnce ( )
681+ } )
682+
683+ it ( 'uses a token another writer stored after the caller read the account instead of refreshing again' , async ( ) => {
684+ queueCredentialAccount ( createOAuthAccount ( ) )
685+ queueTableRows ( account , [
686+ { ...createOAuthAccount ( 3_600_000 ) , accessToken : 'stored-meanwhile-token' , updatedAt : now } ,
687+ ] )
688+ await expect (
689+ resolveCredentialTokenBundle ( RAW_CREDENTIAL_ID , RAW_USER_ID , 'test' )
690+ ) . resolves . toEqual ( { accessToken : 'stored-meanwhile-token' } )
691+ expect ( mocks . refreshOAuthToken ) . not . toHaveBeenCalled ( )
692+ } )
693+
635694 it . each ( [
636695 { remainingMs : 300_000 , refresh : true } ,
637696 { remainingMs : 300_001 , refresh : false } ,
@@ -679,6 +738,9 @@ describe('OAuth access-token refresh headroom', () => {
679738 await expect (
680739 resolveCredentialTokenBundle ( RAW_CREDENTIAL_ID , RAW_USER_ID , 'test' )
681740 ) . resolves . toEqual ( { accessToken : 'original-access-token' } )
741+ await vi . waitFor ( ( ) => expect ( mocks . refreshOAuthToken ) . toHaveBeenCalledOnce ( ) )
742+ /** Let the first background refresh settle so the second entry point starts its own. */
743+ await flushMicrotasks ( 20 )
682744 await expect ( refreshTokenIfNeeded ( 'test' , row , RAW_ACCOUNT_ID ) ) . resolves . toEqual ( {
683745 accessToken : 'original-access-token' ,
684746 refreshed : false ,
0 commit comments