@@ -368,13 +368,22 @@ describe("FetchHttpApi", () => {
368
368
expect ( emitter . emit ) . not . toHaveBeenCalledWith ( HttpApiEvent . SessionLoggedOut , unknownTokenErr ) ;
369
369
} ) ;
370
370
371
- it ( "should only try to refresh the token once" , async ( ) => {
371
+ it ( "should not try to refresh the token if it has plenty of time left before expiry" , async ( ) => {
372
+ // We can't specify an expiry for the initial token, so this should:
373
+ // * Try once, fail
374
+ // * Attempt a refresh, get a token that's not expired
375
+ // * Try again, still fail
376
+ // * Not refresh the token because it's not expired
377
+ // ...which is TWO attempts and ONE refresh (which doesn't really
378
+ // count because it's only to get a token with an expiry)
372
379
const newAccessToken = "new-access-token" ;
373
380
const newRefreshToken = "new-refresh-token" ;
374
- const tokenRefreshFunction = jest . fn ( ) . mockResolvedValue ( {
381
+ const tokenRefreshFunction = jest . fn ( ) . mockReturnValue ( {
375
382
accessToken : newAccessToken ,
376
383
refreshToken : newRefreshToken ,
377
- expiry : new Date ( Date . now ( ) + 1000 ) ,
384
+ // This needs to be sufficiently high that it's over the threshold for
385
+ // 'plenty of time' (which is a minute in practice).
386
+ expiry : new Date ( Date . now ( ) + 5 * 60 * 1000 ) ,
378
387
} ) ;
379
388
380
389
// fetch doesn't like our new or old tokens
@@ -394,7 +403,7 @@ describe("FetchHttpApi", () => {
394
403
unknownTokenErr ,
395
404
) ;
396
405
397
- // tried to refresh the token once
406
+ // tried to refresh the token once (to get the one with an expiry)
398
407
expect ( tokenRefreshFunction ) . toHaveBeenCalledWith ( refreshToken ) ;
399
408
expect ( tokenRefreshFunction ) . toHaveBeenCalledTimes ( 1 ) ;
400
409
@@ -405,6 +414,54 @@ describe("FetchHttpApi", () => {
405
414
// logged out after refreshed access token is rejected
406
415
expect ( emitter . emit ) . toHaveBeenCalledWith ( HttpApiEvent . SessionLoggedOut , unknownTokenErr ) ;
407
416
} ) ;
417
+
418
+ it ( "should try to refresh the token if it will expire soon" , async ( ) => {
419
+ const newAccessToken = "new-access-token" ;
420
+ const newRefreshToken = "new-refresh-token" ;
421
+
422
+ // first refresh is to get a token with an expiry at all, because we
423
+ // can't specify an expiry on the token we inject
424
+ const tokenRefreshFunction = jest . fn ( ) . mockResolvedValueOnce ( {
425
+ accessToken : newAccessToken ,
426
+ refreshToken : newRefreshToken ,
427
+ expiry : new Date ( Date . now ( ) + 1000 ) ,
428
+ } ) ;
429
+
430
+ // next refresh is to return a token that will expire 'soon'
431
+ tokenRefreshFunction . mockResolvedValueOnce ( {
432
+ accessToken : newAccessToken ,
433
+ refreshToken : newRefreshToken ,
434
+ expiry : new Date ( Date . now ( ) + 1000 ) ,
435
+ } ) ;
436
+
437
+ // ...and finally we return a token that has adequate time left
438
+ // so that it will cease retrying and fail the request.
439
+ tokenRefreshFunction . mockResolvedValueOnce ( {
440
+ accessToken : newAccessToken ,
441
+ refreshToken : newRefreshToken ,
442
+ expiry : new Date ( Date . now ( ) + 5 * 60 * 1000 ) ,
443
+ } ) ;
444
+
445
+ const fetchFn = jest . fn ( ) . mockResolvedValue ( unknownTokenResponse ) ;
446
+
447
+ const emitter = new TypedEventEmitter < HttpApiEvent , HttpApiEventHandlerMap > ( ) ;
448
+ jest . spyOn ( emitter , "emit" ) ;
449
+ const api = new FetchHttpApi ( emitter , {
450
+ baseUrl,
451
+ prefix,
452
+ fetchFn,
453
+ tokenRefreshFunction,
454
+ accessToken,
455
+ refreshToken,
456
+ } ) ;
457
+ await expect ( api . authedRequest ( Method . Post , "/account/password" ) ) . rejects . toThrow (
458
+ unknownTokenErr ,
459
+ ) ;
460
+
461
+ // We should have seen the 3 token refreshes, as above.
462
+ expect ( tokenRefreshFunction ) . toHaveBeenCalledWith ( refreshToken ) ;
463
+ expect ( tokenRefreshFunction ) . toHaveBeenCalledTimes ( 3 ) ;
464
+ } ) ;
408
465
} ) ;
409
466
} ) ;
410
467
} ) ;
0 commit comments