@@ -966,6 +966,83 @@ describe("GdbClient", () => {
966966 } ) ;
967967 } ) ;
968968
969+ describe ( "onBeforeRefresh" , ( ) => {
970+ it ( "uses token from config when another process already refreshed" , async ( ) => {
971+ const onRefresh = vi . fn ( ) ;
972+ const client = new GdbClient ( {
973+ baseUrl : "http://localhost:3000" ,
974+ token : "expired-token" ,
975+ refreshToken : "old-refresh" ,
976+ onTokenRefresh : onRefresh ,
977+ onBeforeRefresh : ( ) => ( {
978+ token : "fresh-token-from-config" ,
979+ refreshToken : "fresh-refresh-from-config" ,
980+ } ) ,
981+ } ) ;
982+
983+ let callCount = 0 ;
984+ vi . spyOn ( globalThis , "fetch" ) . mockImplementation ( async ( ) => {
985+ callCount ++ ;
986+ if ( callCount === 1 ) {
987+ return new Response ( JSON . stringify ( { error : "Unauthorized" } ) , {
988+ status : 401 ,
989+ headers : { "Content-Type" : "application/json" } ,
990+ } ) ;
991+ }
992+ return new Response ( JSON . stringify ( [ { id : "Room:001" } ] ) , {
993+ status : 200 ,
994+ headers : { "Content-Type" : "application/json" } ,
995+ } ) ;
996+ } ) ;
997+
998+ const result = await client . get ( "/entities" ) ;
999+ expect ( result . data ) . toEqual ( [ { id : "Room:001" } ] ) ;
1000+ // Should NOT have called /auth/refresh — used config token directly
1001+ expect ( onRefresh ) . not . toHaveBeenCalled ( ) ;
1002+ // Only 2 calls: original 401 + retry with fresh token
1003+ expect ( callCount ) . toBe ( 2 ) ;
1004+ } ) ;
1005+
1006+ it ( "uses latest refreshToken from config when token is the same" , async ( ) => {
1007+ const onRefresh = vi . fn ( ) ;
1008+ const client = new GdbClient ( {
1009+ baseUrl : "http://localhost:3000" ,
1010+ token : "expired-token" ,
1011+ refreshToken : "stale-refresh" ,
1012+ onTokenRefresh : onRefresh ,
1013+ onBeforeRefresh : ( ) => ( {
1014+ token : "expired-token" ,
1015+ refreshToken : "latest-refresh-from-config" ,
1016+ } ) ,
1017+ } ) ;
1018+
1019+ vi . spyOn ( globalThis , "fetch" ) . mockImplementation ( async ( url ) => {
1020+ const urlStr = typeof url === "string" ? url : url . toString ( ) ;
1021+ if ( urlStr . includes ( "/auth/refresh" ) ) {
1022+ return new Response (
1023+ JSON . stringify ( { accessToken : "new-token" , refreshToken : "new-refresh" } ) ,
1024+ { status : 200 , headers : { "Content-Type" : "application/json" } } ,
1025+ ) ;
1026+ }
1027+ return new Response ( JSON . stringify ( { error : "Unauthorized" } ) , {
1028+ status : 401 ,
1029+ headers : { "Content-Type" : "application/json" } ,
1030+ } ) ;
1031+ } ) ;
1032+
1033+ // Will fail because retry also returns 401, but we can verify the refresh used latest token
1034+ await expect ( client . get ( "/entities" ) ) . rejects . toThrow ( GdbClientError ) ;
1035+ expect ( onRefresh ) . toHaveBeenCalledWith ( "new-token" , "new-refresh" ) ;
1036+ // Verify /auth/refresh was called with the latest refreshToken
1037+ const refreshCall = vi . mocked ( fetch ) . mock . calls . find (
1038+ ( call ) => ( typeof call [ 0 ] === "string" ? call [ 0 ] : call [ 0 ] . toString ( ) ) . includes ( "/auth/refresh" ) ,
1039+ ) ;
1040+ expect ( refreshCall ) . toBeDefined ( ) ;
1041+ const body = JSON . parse ( refreshCall ! [ 1 ] ! . body as string ) ;
1042+ expect ( body . refreshToken ) . toBe ( "latest-refresh-from-config" ) ;
1043+ } ) ;
1044+ } ) ;
1045+
9691046 describe ( "concurrent refresh deduplication" , ( ) => {
9701047 it ( "reuses the same refresh promise for concurrent 401 retries" , async ( ) => {
9711048 const onRefresh = vi . fn ( ) ;
0 commit comments