@@ -157,6 +157,36 @@ describe('refreshAccessToken', () => {
157157 . toThrow ( AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR ) ;
158158 } ) ;
159159
160+ it ( 'should throw AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR on HTTP 408 (Request Timeout)' , async ( ) => {
161+ const mockResponse = {
162+ ok : false ,
163+ status : 408 ,
164+ statusText : 'Request Timeout' ,
165+ } ;
166+ global . fetch = jest . fn ( ) . mockResolvedValue ( mockResponse ) ;
167+
168+ await expect ( refreshAccessToken ( 'valid-token' ) )
169+ . rejects
170+ . toThrow ( AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR ) ;
171+
172+ expect ( setSessionClearingState ) . not . toHaveBeenCalled ( ) ;
173+ } ) ;
174+
175+ it ( 'should throw AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR on HTTP 429 (Too Many Requests)' , async ( ) => {
176+ const mockResponse = {
177+ ok : false ,
178+ status : 429 ,
179+ statusText : 'Too Many Requests' ,
180+ } ;
181+ global . fetch = jest . fn ( ) . mockResolvedValue ( mockResponse ) ;
182+
183+ await expect ( refreshAccessToken ( 'valid-token' ) )
184+ . rejects
185+ . toThrow ( AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR ) ;
186+
187+ expect ( setSessionClearingState ) . not . toHaveBeenCalled ( ) ;
188+ } ) ;
189+
160190 it ( 'should throw AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR on network failure' , async ( ) => {
161191 global . fetch = jest . fn ( ) . mockRejectedValue ( new TypeError ( 'Failed to fetch' ) ) ;
162192
@@ -225,6 +255,32 @@ describe('refreshAccessToken', () => {
225255 . rejects
226256 . toThrow ( `${ AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR } : Failed to fetch` ) ;
227257 } ) ;
258+
259+ it ( 'should throw retryable error when response body is not valid JSON' , async ( ) => {
260+ const mockResponse = {
261+ ok : true ,
262+ status : 200 ,
263+ json : jest . fn ( ) . mockRejectedValue ( new SyntaxError ( 'Unexpected token < in JSON' ) ) ,
264+ } ;
265+ global . fetch = jest . fn ( ) . mockResolvedValue ( mockResponse ) ;
266+
267+ await expect ( refreshAccessToken ( 'valid-token' ) )
268+ . rejects
269+ . toThrow ( `${ AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR } : invalid JSON response from IDP` ) ;
270+
271+ expect ( setSessionClearingState ) . not . toHaveBeenCalled ( ) ;
272+ } ) ;
273+
274+ it ( 'should throw retryable error when fetch is aborted by timeout' , async ( ) => {
275+ const abortError = new DOMException ( 'The operation was aborted.' , 'AbortError' ) ;
276+ global . fetch = jest . fn ( ) . mockRejectedValue ( abortError ) ;
277+
278+ await expect ( refreshAccessToken ( 'valid-token' ) )
279+ . rejects
280+ . toThrow ( AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR ) ;
281+
282+ expect ( setSessionClearingState ) . not . toHaveBeenCalled ( ) ;
283+ } ) ;
228284} ) ;
229285
230286describe ( 'retryWithBackoff' , ( ) => {
@@ -241,7 +297,7 @@ describe('retryWithBackoff', () => {
241297
242298 it ( 'should retry network errors up to maxRetries then throw' , async ( ) => {
243299 jest . useRealTimers ( ) ;
244- const networkError = new Error ( ' network down' ) ;
300+ const networkError = new Error ( ` ${ AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR } : network down` ) ;
245301 const fn = jest . fn ( ) . mockRejectedValue ( networkError ) ;
246302
247303 await expect ( retryWithBackoff ( fn , 3 , 1 ) )
@@ -254,8 +310,8 @@ describe('retryWithBackoff', () => {
254310 it ( 'should succeed after transient failures' , async ( ) => {
255311 jest . useRealTimers ( ) ;
256312 const fn = jest . fn ( )
257- . mockRejectedValueOnce ( new Error ( ' transient' ) )
258- . mockRejectedValueOnce ( new Error ( ' transient' ) )
313+ . mockRejectedValueOnce ( new Error ( ` ${ AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR } : transient` ) )
314+ . mockRejectedValueOnce ( new Error ( ` ${ AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR } : transient` ) )
259315 . mockResolvedValue ( 'recovered' ) ;
260316
261317 const result = await retryWithBackoff ( fn , 5 , 1 ) ;
@@ -276,13 +332,25 @@ describe('retryWithBackoff', () => {
276332 expect ( fn ) . toHaveBeenCalledTimes ( 1 ) ;
277333 } ) ;
278334
335+ it ( 'should not retry unexpected errors (fail fast)' , async ( ) => {
336+ jest . useRealTimers ( ) ;
337+ const unexpectedError = new TypeError ( 'Cannot read properties of undefined' ) ;
338+ const fn = jest . fn ( ) . mockRejectedValue ( unexpectedError ) ;
339+
340+ await expect ( retryWithBackoff ( fn , 5 , 1 ) )
341+ . rejects
342+ . toThrow ( 'Cannot read properties of undefined' ) ;
343+
344+ expect ( fn ) . toHaveBeenCalledTimes ( 1 ) ;
345+ } ) ;
346+
279347 it ( 'should apply exponential backoff delays' , async ( ) => {
280348 jest . useRealTimers ( ) ;
281349 const setTimeoutSpy = jest . spyOn ( global , 'setTimeout' ) ;
282350
283351 const fn = jest . fn ( )
284- . mockRejectedValueOnce ( new Error ( ' fail 1' ) )
285- . mockRejectedValueOnce ( new Error ( ' fail 2' ) )
352+ . mockRejectedValueOnce ( new Error ( ` ${ AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR } : fail 1` ) )
353+ . mockRejectedValueOnce ( new Error ( ` ${ AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR } : fail 2` ) )
286354 . mockResolvedValue ( 'ok' ) ;
287355
288356 const baseDelay = 100 ;
0 commit comments