@@ -18,6 +18,44 @@ jest.mock('@aws-amplify/core', () => ({
1818} ) ) ;
1919jest . mock ( '../../../../src/providers/cognito/utils/oauth/oAuthStore' ) ;
2020
21+ // Helper function for creating test tokens with configurable expiration
22+ function createTokens ( {
23+ accessTokenExpired = false ,
24+ idTokenExpired = false ,
25+ overrides = { } ,
26+ } : {
27+ accessTokenExpired ?: boolean ;
28+ idTokenExpired ?: boolean ;
29+ overrides ?: Partial < CognitoAuthTokens > ;
30+ } = { } ) : CognitoAuthTokens {
31+ const now = Math . floor ( Date . now ( ) / 1000 ) ;
32+ const pastExp = now - 3600 ; // 1 hour ago
33+ const futureExp = now + 3600 ; // 1 hour from now
34+
35+ return {
36+ accessToken : {
37+ payload : {
38+ exp : accessTokenExpired ? pastExp : futureExp ,
39+ iat : accessTokenExpired ? pastExp - 3600 : now ,
40+ } ,
41+ toString : ( ) =>
42+ accessTokenExpired ? 'mock-expired-access-token' : 'mock-access-token' ,
43+ } ,
44+ idToken : {
45+ payload : {
46+ exp : idTokenExpired ? pastExp : futureExp ,
47+ iat : idTokenExpired ? pastExp - 3600 : now ,
48+ } ,
49+ toString : ( ) =>
50+ idTokenExpired ? 'mock-expired-id-token' : 'mock-id-token' ,
51+ } ,
52+ refreshToken : 'mock-refresh-token' ,
53+ clockDrift : 0 ,
54+ username : 'testuser' ,
55+ ...overrides ,
56+ } ;
57+ }
58+
2159describe ( 'tokenOrchestrator' , ( ) => {
2260 const mockTokenRefresher = jest . fn ( ) ;
2361 const mockTokenStore = {
@@ -225,4 +263,192 @@ describe('tokenOrchestrator', () => {
225263 expect ( clearTokensSpy ) . not . toHaveBeenCalled ( ) ;
226264 } ) ;
227265 } ) ;
266+
267+ describe ( 'getTokens method' , ( ) => {
268+ const mockAuthConfig = {
269+ Cognito : {
270+ userPoolId : 'us-east-1_testpool' ,
271+ userPoolClientId : 'testclientid' ,
272+ } ,
273+ } ;
274+
275+ beforeEach ( ( ) => {
276+ tokenOrchestrator . setAuthConfig ( mockAuthConfig ) ;
277+ jest . clearAllMocks ( ) ;
278+ ( oAuthStore . loadOAuthInFlight as jest . Mock ) . mockResolvedValue ( false ) ;
279+ } ) ;
280+
281+ it ( 'should return null when no tokens are stored' , async ( ) => {
282+ mockTokenStore . loadTokens . mockResolvedValue ( null ) ;
283+
284+ const result = await tokenOrchestrator . getTokens ( ) ;
285+
286+ expect ( result ) . toBeNull ( ) ;
287+ expect ( mockTokenRefresher ) . not . toHaveBeenCalled ( ) ;
288+ } ) ;
289+
290+ it ( 'should return tokens without refresh when tokens are valid' , async ( ) => {
291+ const validTokens = createTokens ( ) ;
292+ mockTokenStore . loadTokens . mockResolvedValue ( validTokens ) ;
293+ mockTokenStore . getLastAuthUser . mockResolvedValue ( 'testuser' ) ;
294+
295+ const result = await tokenOrchestrator . getTokens ( ) ;
296+
297+ expect ( mockTokenRefresher ) . not . toHaveBeenCalled ( ) ;
298+ expect ( result ?. accessToken ) . toBeDefined ( ) ;
299+ expect ( result ?. idToken ) . toBeDefined ( ) ;
300+ } ) ;
301+
302+ it . each ( [
303+ [
304+ 'access token is expired' ,
305+ { accessTokenExpired : true , idTokenExpired : false } ,
306+ ] ,
307+ [
308+ 'ID token is expired' ,
309+ { accessTokenExpired : false , idTokenExpired : true } ,
310+ ] ,
311+ [
312+ 'both tokens are expired' ,
313+ { accessTokenExpired : true , idTokenExpired : true } ,
314+ ] ,
315+ ] ) ( 'should trigger refresh when %s' , async ( _scenario , tokenConfig ) => {
316+ const expiredTokens = createTokens ( tokenConfig ) ;
317+ const newTokens = createTokens ( ) ;
318+ mockTokenStore . loadTokens . mockResolvedValue ( expiredTokens ) ;
319+ mockTokenStore . getLastAuthUser . mockResolvedValue ( 'testuser' ) ;
320+ mockTokenRefresher . mockResolvedValue ( newTokens ) ;
321+
322+ const result = await tokenOrchestrator . getTokens ( ) ;
323+
324+ expect ( mockTokenRefresher ) . toHaveBeenCalledWith (
325+ expect . objectContaining ( {
326+ tokens : expiredTokens ,
327+ username : 'testuser' ,
328+ } ) ,
329+ ) ;
330+ expect ( result ?. accessToken ) . toEqual ( newTokens . accessToken ) ;
331+ } ) ;
332+
333+ it ( 'should trigger refresh when forceRefresh is true even with valid tokens' , async ( ) => {
334+ const validTokens = createTokens ( ) ;
335+ const newTokens = createTokens ( ) ;
336+ mockTokenStore . loadTokens . mockResolvedValue ( validTokens ) ;
337+ mockTokenStore . getLastAuthUser . mockResolvedValue ( 'testuser' ) ;
338+ mockTokenRefresher . mockResolvedValue ( newTokens ) ;
339+
340+ const result = await tokenOrchestrator . getTokens ( { forceRefresh : true } ) ;
341+
342+ expect ( mockTokenRefresher ) . toHaveBeenCalledWith (
343+ expect . objectContaining ( {
344+ tokens : validTokens ,
345+ username : 'testuser' ,
346+ } ) ,
347+ ) ;
348+ expect ( result ?. accessToken ) . toEqual ( newTokens . accessToken ) ;
349+ } ) ;
350+
351+ it ( 'should preserve signInDetails after token refresh' , async ( ) => {
352+ const expiredTokens = createTokens ( {
353+ accessTokenExpired : true ,
354+ overrides : {
355+ signInDetails : {
356+ authFlowType : 'USER_SRP_AUTH' ,
357+ loginId : 'testuser' ,
358+ } ,
359+ } ,
360+ } ) ;
361+ const newTokens = createTokens ( ) ;
362+
363+ mockTokenStore . loadTokens . mockResolvedValue ( expiredTokens ) ;
364+ mockTokenStore . getLastAuthUser . mockResolvedValue ( 'testuser' ) ;
365+ mockTokenRefresher . mockResolvedValue ( newTokens ) ;
366+
367+ const result = await tokenOrchestrator . getTokens ( ) ;
368+
369+ expect ( result ?. signInDetails ?. authFlowType ) . toBe ( 'USER_SRP_AUTH' ) ;
370+ expect ( result ?. signInDetails ?. loginId ) . toBe ( 'testuser' ) ;
371+ } ) ;
372+
373+ it ( 'should return null when refresh fails with NotAuthorizedException' , async ( ) => {
374+ const expiredTokens = createTokens ( { accessTokenExpired : true } ) ;
375+ mockTokenStore . loadTokens . mockResolvedValue ( expiredTokens ) ;
376+ mockTokenStore . getLastAuthUser . mockResolvedValue ( 'testuser' ) ;
377+ mockTokenRefresher . mockRejectedValue (
378+ new AmplifyError ( {
379+ name : 'NotAuthorizedException' ,
380+ message : 'Refresh token has expired' ,
381+ } ) ,
382+ ) ;
383+
384+ const result = await tokenOrchestrator . getTokens ( ) ;
385+
386+ expect ( result ) . toBeNull ( ) ;
387+ expect ( mockTokenStore . clearTokens ) . toHaveBeenCalled ( ) ;
388+ } ) ;
389+
390+ it ( 'should throw error when refresh fails with network error' , async ( ) => {
391+ const expiredTokens = createTokens ( { accessTokenExpired : true } ) ;
392+ mockTokenStore . loadTokens . mockResolvedValue ( expiredTokens ) ;
393+ mockTokenStore . getLastAuthUser . mockResolvedValue ( 'testuser' ) ;
394+ mockTokenRefresher . mockRejectedValue (
395+ new AmplifyError ( {
396+ name : AmplifyErrorCode . NetworkError ,
397+ message : 'Network Error' ,
398+ } ) ,
399+ ) ;
400+
401+ await expect ( tokenOrchestrator . getTokens ( ) ) . rejects . toThrow (
402+ 'Network Error' ,
403+ ) ;
404+ expect ( mockTokenStore . clearTokens ) . not . toHaveBeenCalled ( ) ;
405+ } ) ;
406+
407+ it ( 'should not refresh tokens when idToken is missing but accessToken is valid' , async ( ) => {
408+ const tokensWithoutIdToken = createTokens ( ) ;
409+ delete ( tokensWithoutIdToken as any ) . idToken ;
410+ mockTokenStore . loadTokens . mockResolvedValue ( tokensWithoutIdToken ) ;
411+ mockTokenStore . getLastAuthUser . mockResolvedValue ( 'testuser' ) ;
412+
413+ const result = await tokenOrchestrator . getTokens ( ) ;
414+
415+ expect ( mockTokenRefresher ) . not . toHaveBeenCalled ( ) ;
416+ expect ( result ?. accessToken ) . toBeDefined ( ) ;
417+ expect ( result ?. idToken ) . toBeUndefined ( ) ;
418+ } ) ;
419+
420+ it ( 'should pass clientMetadata to token refresher' , async ( ) => {
421+ const expiredTokens = createTokens ( { accessTokenExpired : true } ) ;
422+ const newTokens = createTokens ( ) ;
423+ const clientMetadata = { customKey : 'customValue' } ;
424+ mockTokenStore . loadTokens . mockResolvedValue ( expiredTokens ) ;
425+ mockTokenStore . getLastAuthUser . mockResolvedValue ( 'testuser' ) ;
426+ mockTokenRefresher . mockResolvedValue ( newTokens ) ;
427+
428+ await tokenOrchestrator . getTokens ( { clientMetadata } ) ;
429+
430+ expect ( mockTokenRefresher ) . toHaveBeenCalledWith (
431+ expect . objectContaining ( {
432+ clientMetadata,
433+ } ) ,
434+ ) ;
435+ } ) ;
436+
437+ it ( 'should store new tokens after successful refresh' , async ( ) => {
438+ const expiredTokens = createTokens ( { accessTokenExpired : true } ) ;
439+ const newTokens = createTokens ( ) ;
440+ mockTokenStore . loadTokens . mockResolvedValue ( expiredTokens ) ;
441+ mockTokenStore . getLastAuthUser . mockResolvedValue ( 'testuser' ) ;
442+ mockTokenRefresher . mockResolvedValue ( newTokens ) ;
443+
444+ await tokenOrchestrator . getTokens ( ) ;
445+
446+ expect ( mockTokenStore . storeTokens ) . toHaveBeenCalledWith (
447+ expect . objectContaining ( {
448+ accessToken : newTokens . accessToken ,
449+ idToken : newTokens . idToken ,
450+ } ) ,
451+ ) ;
452+ } ) ;
453+ } ) ;
228454} ) ;
0 commit comments