@@ -368,6 +368,103 @@ describe('internal api', () => {
368368 } ) ;
369369 } ) ;
370370
371+ it ( 'no dangling exchangeToken promise internal' , async ( ) => {
372+ const appCheck = initializeAppCheck ( app , {
373+ provider : new ReCaptchaV3Provider ( FAKE_SITE_KEY )
374+ } ) ;
375+
376+ setState ( app , {
377+ ...getState ( app ) ,
378+ token : fakeRecaptchaAppCheckToken ,
379+ cachedTokenPromise : undefined
380+ } ) ;
381+
382+ stub ( reCAPTCHA , 'getToken' ) . returns ( Promise . resolve ( fakeRecaptchaToken ) ) ;
383+ stub ( client , 'exchangeToken' ) . returns (
384+ Promise . resolve ( {
385+ token : 'new-recaptcha-app-check-token' ,
386+ expireTimeMillis : Date . now ( ) + 60000 ,
387+ issuedAtTimeMillis : 0
388+ } )
389+ ) ;
390+
391+ const getTokenPromise = getToken ( appCheck as AppCheckService , true ) ;
392+
393+ expect ( getState ( app ) . exchangeTokenFetcher . promise ) . to . be . instanceOf (
394+ Promise
395+ ) ;
396+
397+ const state = {
398+ ...getState ( app )
399+ } ;
400+
401+ await getTokenPromise ;
402+
403+ setState ( app , state ) ;
404+
405+ expect ( getState ( app ) . exchangeTokenFetcher . promise ) . to . be . equal ( undefined ) ;
406+ } ) ;
407+
408+ it ( 'no dangling exchangeToken promise' , async ( ) => {
409+ const clock = useFakeTimers ( ) ;
410+
411+ const appCheck = initializeAppCheck ( app , {
412+ provider : new ReCaptchaV3Provider ( FAKE_SITE_KEY )
413+ } ) ;
414+
415+ const soonExpiredToken = {
416+ token : `recaptcha-app-check-token-old` ,
417+ expireTimeMillis : Date . now ( ) + 1000 ,
418+ issuedAtTimeMillis : 0
419+ } ;
420+
421+ setState ( app , {
422+ ...getState ( app ) ,
423+ token : soonExpiredToken ,
424+ cachedTokenPromise : undefined
425+ } ) ;
426+
427+ stub ( reCAPTCHA , 'getToken' ) . returns ( Promise . resolve ( fakeRecaptchaToken ) ) ;
428+ let count = 0 ;
429+ stub ( client , 'exchangeToken' ) . callsFake (
430+ ( ) =>
431+ new Promise ( res =>
432+ setTimeout (
433+ ( ) =>
434+ res ( {
435+ token : `recaptcha-app-check-token-new-${ count ++ } ` ,
436+ expireTimeMillis : Date . now ( ) + 60000 ,
437+ issuedAtTimeMillis : 0
438+ } ) ,
439+ 3000
440+ )
441+ )
442+ ) ;
443+
444+ // start fetch token
445+ void getToken ( appCheck as AppCheckService , true ) ;
446+
447+ clock . tick ( 2000 ) ;
448+
449+ // save expired `token-old` with copied state and wait fetch token
450+ void getToken ( appCheck as AppCheckService ) ;
451+
452+ // wait fetch token with copied state
453+ void getToken ( appCheck as AppCheckService ) ;
454+
455+ // stored copied state with `token-new-0`
456+ await clock . runAllAsync ( ) ;
457+
458+ // fetch token with copied state
459+ const newToken = getToken ( appCheck as AppCheckService , true ) ;
460+
461+ await clock . runAllAsync ( ) ;
462+
463+ expect ( await newToken ) . to . deep . equal ( {
464+ token : 'recaptcha-app-check-token-new-1'
465+ } ) ;
466+ } ) ;
467+
371468 it ( 'ignores in-memory token if it is invalid and continues to exchange request' , async ( ) => {
372469 const appCheck = initializeAppCheck ( app , {
373470 provider : new ReCaptchaV3Provider ( FAKE_SITE_KEY )
0 commit comments