@@ -38,12 +38,14 @@ import * as reCAPTCHA from './recaptcha';
3838import * as client from './client' ;
3939import * as storage from './storage' ;
4040import * as util from './util' ;
41+ import { logger } from './logger' ;
4142import { getState , clearState , setState , getDebugState } from './state' ;
4243import { AppCheckTokenListener } from './public-types' ;
43- import { Deferred } from '@firebase/util' ;
44+ import { Deferred , FirebaseError } from '@firebase/util' ;
4445import { ReCaptchaEnterpriseProvider , ReCaptchaV3Provider } from './providers' ;
4546import { AppCheckService } from './factory' ;
4647import { ListenerType } from './types' ;
48+ import { AppCheckError } from './errors' ;
4749
4850const fakeRecaptchaToken = 'fake-recaptcha-token' ;
4951const fakeRecaptchaAppCheckToken = {
@@ -385,6 +387,62 @@ describe('internal api', () => {
385387 ) ;
386388 expect ( token ) . to . deep . equal ( { token : fakeRecaptchaAppCheckToken . token } ) ;
387389 } ) ;
390+
391+ it ( 'throttles for a period less than 1d on 503' , async ( ) => {
392+ // More detailed check of exponential backoff in providers.test.ts
393+ const appCheck = initializeAppCheck ( app , {
394+ provider : new ReCaptchaV3Provider ( FAKE_SITE_KEY )
395+ } ) ;
396+ const warnStub = stub ( logger , 'warn' ) ;
397+ stub ( client , 'exchangeToken' ) . returns (
398+ Promise . reject (
399+ new FirebaseError (
400+ AppCheckError . FETCH_STATUS_ERROR ,
401+ 'test error msg' ,
402+ { httpStatus : 503 }
403+ )
404+ )
405+ ) ;
406+
407+ const token = await getToken ( appCheck as AppCheckService ) ;
408+
409+ // ReCaptchaV3Provider's _throttleData is private so checking
410+ // the resulting error message to be sure it has roughly the
411+ // correct throttle time. This also tests the time formatter.
412+ // Check both the error itself and that it makes it through to
413+ // console.warn
414+ expect ( token . error ?. message ) . to . include ( '503' ) ;
415+ expect ( token . error ?. message ) . to . include ( '00m' ) ;
416+ expect ( token . error ?. message ) . to . not . include ( '1d' ) ;
417+ expect ( warnStub . args [ 0 ] [ 0 ] ) . to . include ( '503' ) ;
418+ } ) ;
419+
420+ it ( 'throttles 1d on 403' , async ( ) => {
421+ const appCheck = initializeAppCheck ( app , {
422+ provider : new ReCaptchaV3Provider ( FAKE_SITE_KEY )
423+ } ) ;
424+ const warnStub = stub ( logger , 'warn' ) ;
425+ stub ( client , 'exchangeToken' ) . returns (
426+ Promise . reject (
427+ new FirebaseError (
428+ AppCheckError . FETCH_STATUS_ERROR ,
429+ 'test error msg' ,
430+ { httpStatus : 403 }
431+ )
432+ )
433+ ) ;
434+
435+ const token = await getToken ( appCheck as AppCheckService ) ;
436+
437+ // ReCaptchaV3Provider's _throttleData is private so checking
438+ // the resulting error message to be sure it has roughly the
439+ // correct throttle time. This also tests the time formatter.
440+ // Check both the error itself and that it makes it through to
441+ // console.warn
442+ expect ( token . error ?. message ) . to . include ( '403' ) ;
443+ expect ( token . error ?. message ) . to . include ( '1d' ) ;
444+ expect ( warnStub . args [ 0 ] [ 0 ] ) . to . include ( '403' ) ;
445+ } ) ;
388446 } ) ;
389447
390448 describe ( 'addTokenListener' , ( ) => {
@@ -404,7 +462,7 @@ describe('internal api', () => {
404462 expect ( getState ( app ) . tokenObservers [ 0 ] . next ) . to . equal ( listener ) ;
405463 } ) ;
406464
407- it ( 'starts proactively refreshing token after adding the first listener' , ( ) => {
465+ it ( 'starts proactively refreshing token after adding the first listener' , async ( ) => {
408466 const listener = ( ) : void => { } ;
409467 setState ( app , {
410468 ...getState ( app ) ,
@@ -420,6 +478,12 @@ describe('internal api', () => {
420478 listener
421479 ) ;
422480
481+ expect ( getState ( app ) . tokenRefresher ?. isRunning ( ) ) . to . be . undefined ;
482+
483+ // addTokenListener() waits for the result of cachedTokenPromise
484+ // before starting the refresher
485+ await getState ( app ) . cachedTokenPromise ;
486+
423487 expect ( getState ( app ) . tokenRefresher ?. isRunning ( ) ) . to . be . true ;
424488 } ) ;
425489
@@ -430,6 +494,7 @@ describe('internal api', () => {
430494
431495 setState ( app , {
432496 ...getState ( app ) ,
497+ cachedTokenPromise : Promise . resolve ( undefined ) ,
433498 token : {
434499 token : `fake-memory-app-check-token` ,
435500 expireTimeMillis : Date . now ( ) + 60000 ,
@@ -493,7 +558,7 @@ describe('internal api', () => {
493558 expect ( getState ( app ) . tokenObservers . length ) . to . equal ( 0 ) ;
494559 } ) ;
495560
496- it ( 'should stop proactively refreshing token after deleting the last listener' , ( ) => {
561+ it ( 'should stop proactively refreshing token after deleting the last listener' , async ( ) => {
497562 const listener = ( ) : void => { } ;
498563 setState ( app , { ...getState ( app ) , isTokenAutoRefreshEnabled : true } ) ;
499564 setState ( app , {
@@ -506,6 +571,11 @@ describe('internal api', () => {
506571 ListenerType . INTERNAL ,
507572 listener
508573 ) ;
574+
575+ // addTokenListener() waits for the result of cachedTokenPromise
576+ // before starting the refresher
577+ await getState ( app ) . cachedTokenPromise ;
578+
509579 expect ( getState ( app ) . tokenObservers . length ) . to . equal ( 1 ) ;
510580 expect ( getState ( app ) . tokenRefresher ?. isRunning ( ) ) . to . be . true ;
511581
0 commit comments