Skip to content

Commit df5b60e

Browse files
pgbezerraclaude
andcommitted
test(auth): add comprehensive token refresh test coverage (#14618)
Add test cases for the getTokens() method in TokenOrchestrator to verify token refresh behavior when tokens expire. These tests prove that: - Expired access tokens trigger automatic refresh - Expired ID tokens trigger automatic refresh - forceRefresh option works correctly with valid tokens - signInDetails are preserved after token refresh - NotAuthorizedException returns null and clears tokens - Network errors are thrown (not swallowed) - clientMetadata is passed to the token refresher - New tokens are stored after successful refresh All 12 new tests pass, confirming the core token refresh logic works as expected. This suggests issues reported in #14618 may be related to specific user configurations rather than the refresh mechanism itself. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8f3044e commit df5b60e

File tree

1 file changed

+226
-0
lines changed

1 file changed

+226
-0
lines changed

packages/auth/__tests__/providers/cognito/tokenProvider/tokenOrchestrator.test.ts

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,44 @@ jest.mock('@aws-amplify/core', () => ({
1818
}));
1919
jest.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+
2159
describe('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

Comments
 (0)