@workos-inc/authkit-nextjs 3.0.0-beta.1 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +305 -102
- package/dist/esm/actions.js +35 -5
- package/dist/esm/actions.js.map +1 -1
- package/dist/esm/auth.js +71 -21
- package/dist/esm/auth.js.map +1 -1
- package/dist/esm/authkit-callback-route.js +90 -92
- package/dist/esm/authkit-callback-route.js.map +1 -1
- package/dist/esm/components/authkit-provider.js +36 -15
- package/dist/esm/components/authkit-provider.js.map +1 -1
- package/dist/esm/components/impersonation.js +17 -15
- package/dist/esm/components/impersonation.js.map +1 -1
- package/dist/esm/components/min-max-button.js +1 -1
- package/dist/esm/components/min-max-button.js.map +1 -1
- package/dist/esm/components/tokenStore.js +28 -19
- package/dist/esm/components/tokenStore.js.map +1 -1
- package/dist/esm/components/useAccessToken.js +1 -1
- package/dist/esm/components/useAccessToken.js.map +1 -1
- package/dist/esm/components/useTokenClaims.js +1 -1
- package/dist/esm/components/useTokenClaims.js.map +1 -1
- package/dist/esm/cookie.js +20 -5
- package/dist/esm/cookie.js.map +1 -1
- package/dist/esm/env-variables.js +6 -6
- package/dist/esm/env-variables.js.map +1 -1
- package/dist/esm/errors.js +36 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/get-authorization-url.js +51 -12
- package/dist/esm/get-authorization-url.js.map +1 -1
- package/dist/esm/index.js +5 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/interfaces.js +7 -1
- package/dist/esm/interfaces.js.map +1 -1
- package/dist/esm/middleware-helpers.js +102 -0
- package/dist/esm/middleware-helpers.js.map +1 -0
- package/dist/esm/middleware.js +3 -1
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/pkce.js +52 -0
- package/dist/esm/pkce.js.map +1 -0
- package/dist/esm/session.js +82 -35
- package/dist/esm/session.js.map +1 -1
- package/dist/esm/test-helpers.js +1 -1
- package/dist/esm/test-helpers.js.map +1 -1
- package/dist/esm/types/actions.d.ts +34 -5
- package/dist/esm/types/auth.d.ts +7 -15
- package/dist/esm/types/components/authkit-provider.d.ts +6 -2
- package/dist/esm/types/components/impersonation.d.ts +2 -1
- package/dist/esm/types/cookie.d.ts +9 -0
- package/dist/esm/types/env-variables.d.ts +2 -1
- package/dist/esm/types/errors.d.ts +15 -0
- package/dist/esm/types/get-authorization-url.d.ts +2 -2
- package/dist/esm/types/index.d.ts +5 -2
- package/dist/esm/types/interfaces.d.ts +12 -0
- package/dist/esm/types/jwt.d.ts +9 -9
- package/dist/esm/types/middleware-helpers.d.ts +27 -0
- package/dist/esm/types/middleware.d.ts +3 -1
- package/dist/esm/types/pkce.d.ts +17 -0
- package/dist/esm/types/session.d.ts +1 -1
- package/dist/esm/types/utils.d.ts +5 -0
- package/dist/esm/types/validate-api-key.d.ts +1 -0
- package/dist/esm/types/workos.d.ts +1 -1
- package/dist/esm/utils.js +10 -2
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/validate-api-key.js +16 -0
- package/dist/esm/validate-api-key.js.map +1 -0
- package/dist/esm/workos.js +1 -1
- package/package.json +33 -34
- package/src/actions.spec.ts +91 -18
- package/src/actions.ts +44 -6
- package/src/auth.spec.ts +79 -29
- package/src/auth.ts +74 -42
- package/src/authkit-callback-route.spec.ts +372 -58
- package/src/authkit-callback-route.ts +121 -103
- package/src/components/authkit-provider.spec.tsx +264 -70
- package/src/components/authkit-provider.tsx +40 -15
- package/src/components/button.spec.tsx +4 -6
- package/src/components/impersonation.spec.tsx +152 -35
- package/src/components/impersonation.tsx +37 -30
- package/src/components/min-max-button.spec.tsx +2 -1
- package/src/components/tokenStore.spec.ts +59 -44
- package/src/components/tokenStore.ts +11 -3
- package/src/components/useAccessToken.spec.tsx +82 -83
- package/src/components/useTokenClaims.spec.tsx +23 -22
- package/src/cookie.spec.ts +63 -9
- package/src/cookie.ts +35 -0
- package/src/env-variables.ts +2 -0
- package/src/errors.spec.ts +108 -0
- package/src/errors.ts +46 -0
- package/src/get-authorization-url.spec.ts +170 -15
- package/src/get-authorization-url.ts +69 -23
- package/src/index.ts +20 -2
- package/src/interfaces.ts +15 -0
- package/src/jwt.ts +9 -9
- package/src/middleware-helpers.spec.ts +238 -0
- package/src/middleware-helpers.ts +134 -0
- package/src/middleware.spec.ts +25 -0
- package/src/middleware.ts +4 -1
- package/src/pkce.spec.ts +146 -0
- package/src/pkce.ts +59 -0
- package/src/session.spec.ts +87 -89
- package/src/session.ts +104 -27
- package/src/test-helpers.ts +1 -1
- package/src/utils.spec.ts +14 -31
- package/src/utils.ts +9 -0
- package/src/validate-api-key.spec.ts +111 -0
- package/src/validate-api-key.ts +19 -0
- package/src/workos.spec.ts +2 -2
- package/src/workos.ts +1 -1
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { getAccessTokenAction, refreshAccessTokenAction } from '../actions.js';
|
|
2
|
+
import type { RefreshAccessTokenActionResult } from '../actions.js';
|
|
2
3
|
import { decodeJwt } from '../jwt.js';
|
|
3
4
|
|
|
5
|
+
function unwrapRefreshResult(result: RefreshAccessTokenActionResult): string | undefined {
|
|
6
|
+
if (result.error) {
|
|
7
|
+
throw new Error(result.error);
|
|
8
|
+
}
|
|
9
|
+
return result.accessToken;
|
|
10
|
+
}
|
|
11
|
+
|
|
4
12
|
interface TokenState {
|
|
5
13
|
token: string | undefined;
|
|
6
14
|
loading: boolean;
|
|
@@ -323,7 +331,7 @@ export class TokenStore {
|
|
|
323
331
|
|
|
324
332
|
if (!silent) {
|
|
325
333
|
// Manual refresh - always force refresh
|
|
326
|
-
token = await refreshAccessTokenAction();
|
|
334
|
+
token = unwrapRefreshResult(await refreshAccessTokenAction());
|
|
327
335
|
} else {
|
|
328
336
|
// Silent refresh - only fetch from server if we don't have a local token
|
|
329
337
|
if (!previousToken) {
|
|
@@ -342,14 +350,14 @@ export class TokenStore {
|
|
|
342
350
|
|
|
343
351
|
// If the token from server is expiring, refresh it
|
|
344
352
|
if (!token || (tokenData && tokenData.isExpiring)) {
|
|
345
|
-
const refreshedToken = await refreshAccessTokenAction();
|
|
353
|
+
const refreshedToken = unwrapRefreshResult(await refreshAccessTokenAction());
|
|
346
354
|
if (refreshedToken) {
|
|
347
355
|
token = refreshedToken;
|
|
348
356
|
}
|
|
349
357
|
}
|
|
350
358
|
} else {
|
|
351
359
|
// We have a local token that needs refreshing (already checked by getAccessTokenSilently)
|
|
352
|
-
token = await refreshAccessTokenAction();
|
|
360
|
+
token = unwrapRefreshResult(await refreshAccessTokenAction());
|
|
353
361
|
}
|
|
354
362
|
}
|
|
355
363
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Mock } from 'vitest';
|
|
1
2
|
import '@testing-library/jest-dom';
|
|
2
3
|
import { act, render, waitFor } from '@testing-library/react';
|
|
3
4
|
import React from 'react';
|
|
@@ -6,41 +7,37 @@ import { useAuth } from './authkit-provider.js';
|
|
|
6
7
|
import { useAccessToken } from './useAccessToken.js';
|
|
7
8
|
import { tokenStore } from './tokenStore.js';
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
getAccessTokenAction:
|
|
11
|
-
refreshAccessTokenAction:
|
|
10
|
+
vi.mock('../actions.js', () => ({
|
|
11
|
+
getAccessTokenAction: vi.fn(),
|
|
12
|
+
refreshAccessTokenAction: vi.fn(),
|
|
12
13
|
}));
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
const originalModule =
|
|
15
|
+
vi.mock('./authkit-provider.js', async () => {
|
|
16
|
+
const originalModule = await vi.importActual<typeof import('./authkit-provider.js')>('./authkit-provider.js');
|
|
16
17
|
return {
|
|
17
18
|
...originalModule,
|
|
18
|
-
useAuth:
|
|
19
|
+
useAuth: vi.fn(),
|
|
19
20
|
};
|
|
20
21
|
});
|
|
21
22
|
|
|
22
23
|
describe('useAccessToken', () => {
|
|
23
24
|
beforeEach(() => {
|
|
24
25
|
tokenStore.reset();
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
vi.resetAllMocks();
|
|
27
|
+
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
(getAccessTokenAction as jest.Mock).mockReset();
|
|
30
|
-
(refreshAccessTokenAction as jest.Mock).mockReset();
|
|
31
|
-
|
|
32
|
-
(useAuth as jest.Mock).mockImplementation(() => ({
|
|
29
|
+
(useAuth as Mock).mockImplementation(() => ({
|
|
33
30
|
user: { id: 'user_123' },
|
|
34
31
|
sessionId: 'session_123',
|
|
35
|
-
refreshAuth:
|
|
32
|
+
refreshAuth: vi.fn().mockResolvedValue({}),
|
|
36
33
|
}));
|
|
37
34
|
});
|
|
38
35
|
|
|
39
36
|
afterEach(() => {
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
vi.clearAllTimers();
|
|
38
|
+
vi.useRealTimers();
|
|
42
39
|
tokenStore.reset();
|
|
43
|
-
|
|
40
|
+
vi.clearAllMocks();
|
|
44
41
|
});
|
|
45
42
|
|
|
46
43
|
const TestComponent = () => {
|
|
@@ -60,7 +57,7 @@ describe('useAccessToken', () => {
|
|
|
60
57
|
it('should fetch an access token on mount and show loading state initially', async () => {
|
|
61
58
|
const mockToken =
|
|
62
59
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwic2lkIjoic2Vzc2lvbl8xMjMiLCJleHAiOjk5OTk5OTk5OTl9.mock-signature';
|
|
63
|
-
(getAccessTokenAction as
|
|
60
|
+
(getAccessTokenAction as Mock).mockResolvedValueOnce(mockToken);
|
|
64
61
|
|
|
65
62
|
const { getByTestId } = render(<TestComponent />);
|
|
66
63
|
|
|
@@ -92,8 +89,8 @@ describe('useAccessToken', () => {
|
|
|
92
89
|
const refreshedToken =
|
|
93
90
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwic2lkIjoic2Vzc2lvbl8xMjMiLCJleHAiOjk5OTk5OTk5OTl9.mock-signature';
|
|
94
91
|
|
|
95
|
-
(getAccessTokenAction as
|
|
96
|
-
(refreshAccessTokenAction as
|
|
92
|
+
(getAccessTokenAction as Mock).mockResolvedValueOnce(expiringToken);
|
|
93
|
+
(refreshAccessTokenAction as Mock).mockResolvedValueOnce({ accessToken: refreshedToken });
|
|
97
94
|
|
|
98
95
|
const { getByTestId } = render(<TestComponent />);
|
|
99
96
|
|
|
@@ -115,8 +112,8 @@ describe('useAccessToken', () => {
|
|
|
115
112
|
const refreshedToken =
|
|
116
113
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJyZWZyZXNoZWQiLCJzaWQiOiJzZXNzaW9uXzEyMyIsImV4cCI6OTk5OTk5OTk5OX0.mock-signature-2';
|
|
117
114
|
|
|
118
|
-
(getAccessTokenAction as
|
|
119
|
-
(refreshAccessTokenAction as
|
|
115
|
+
(getAccessTokenAction as Mock).mockResolvedValueOnce(initialToken);
|
|
116
|
+
(refreshAccessTokenAction as Mock).mockResolvedValueOnce({ accessToken: refreshedToken });
|
|
120
117
|
|
|
121
118
|
const { getByTestId } = render(<TestComponent />);
|
|
122
119
|
|
|
@@ -141,10 +138,10 @@ describe('useAccessToken', () => {
|
|
|
141
138
|
});
|
|
142
139
|
|
|
143
140
|
it('should handle the not loggged in state', async () => {
|
|
144
|
-
(useAuth as
|
|
141
|
+
(useAuth as Mock).mockImplementation(() => ({
|
|
145
142
|
user: undefined,
|
|
146
143
|
sessionId: undefined,
|
|
147
|
-
refreshAuth:
|
|
144
|
+
refreshAuth: vi.fn().mockResolvedValue({}),
|
|
148
145
|
}));
|
|
149
146
|
|
|
150
147
|
const { getByTestId } = render(<TestComponent />);
|
|
@@ -157,7 +154,7 @@ describe('useAccessToken', () => {
|
|
|
157
154
|
|
|
158
155
|
it('should handle errors during token fetch', async () => {
|
|
159
156
|
const error = new Error('Failed to fetch token');
|
|
160
|
-
(getAccessTokenAction as
|
|
157
|
+
(getAccessTokenAction as Mock).mockRejectedValueOnce(error);
|
|
161
158
|
|
|
162
159
|
const { getByTestId } = render(<TestComponent />);
|
|
163
160
|
|
|
@@ -174,10 +171,12 @@ describe('useAccessToken', () => {
|
|
|
174
171
|
it('should handle errors during manual refresh', async () => {
|
|
175
172
|
const initialToken =
|
|
176
173
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwic2lkIjoic2Vzc2lvbl8xMjMiLCJleHAiOjk5OTk5OTk5OTl9.mock-signature';
|
|
177
|
-
const error = new Error('Failed to refresh token');
|
|
178
174
|
|
|
179
|
-
(getAccessTokenAction as
|
|
180
|
-
(refreshAccessTokenAction as
|
|
175
|
+
(getAccessTokenAction as Mock).mockResolvedValueOnce(initialToken);
|
|
176
|
+
(refreshAccessTokenAction as Mock).mockResolvedValueOnce({
|
|
177
|
+
accessToken: undefined,
|
|
178
|
+
error: 'Failed to refresh token',
|
|
179
|
+
});
|
|
181
180
|
|
|
182
181
|
const { getByTestId } = render(<TestComponent />);
|
|
183
182
|
|
|
@@ -200,13 +199,13 @@ describe('useAccessToken', () => {
|
|
|
200
199
|
it('should reset token state when user is undefined', async () => {
|
|
201
200
|
const mockToken =
|
|
202
201
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwic2lkIjoic2Vzc2lvbl8xMjMiLCJleHAiOjk5OTk5OTk5OTl9.mock-signature';
|
|
203
|
-
(getAccessTokenAction as
|
|
202
|
+
(getAccessTokenAction as Mock).mockResolvedValueOnce(mockToken);
|
|
204
203
|
|
|
205
204
|
// First render with user
|
|
206
|
-
(useAuth as
|
|
205
|
+
(useAuth as Mock).mockImplementation(() => ({
|
|
207
206
|
user: { id: 'user_123' },
|
|
208
207
|
sessionId: 'session_123',
|
|
209
|
-
refreshAuth:
|
|
208
|
+
refreshAuth: vi.fn().mockResolvedValue({}),
|
|
210
209
|
}));
|
|
211
210
|
|
|
212
211
|
const { getByTestId, rerender } = render(<TestComponent />);
|
|
@@ -215,10 +214,10 @@ describe('useAccessToken', () => {
|
|
|
215
214
|
expect(getByTestId('token')).toHaveTextContent(mockToken);
|
|
216
215
|
});
|
|
217
216
|
|
|
218
|
-
(useAuth as
|
|
217
|
+
(useAuth as Mock).mockImplementation(() => ({
|
|
219
218
|
user: undefined,
|
|
220
219
|
sessionId: undefined,
|
|
221
|
-
refreshAuth:
|
|
220
|
+
refreshAuth: vi.fn().mockResolvedValue({}),
|
|
222
221
|
}));
|
|
223
222
|
|
|
224
223
|
rerender(<TestComponent />);
|
|
@@ -230,7 +229,7 @@ describe('useAccessToken', () => {
|
|
|
230
229
|
|
|
231
230
|
it('should handle invalid tokens gracefully', async () => {
|
|
232
231
|
const invalidToken = 'invalid-token';
|
|
233
|
-
(getAccessTokenAction as
|
|
232
|
+
(getAccessTokenAction as Mock).mockResolvedValueOnce(invalidToken);
|
|
234
233
|
|
|
235
234
|
const { getByTestId } = render(<TestComponent />);
|
|
236
235
|
|
|
@@ -246,7 +245,7 @@ describe('useAccessToken', () => {
|
|
|
246
245
|
const mockToken =
|
|
247
246
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwic2lkIjoic2Vzc2lvbl8xMjMiLCJleHAiOjk5OTk5OTk5OTl9.mock-signature';
|
|
248
247
|
|
|
249
|
-
(getAccessTokenAction as
|
|
248
|
+
(getAccessTokenAction as Mock).mockRejectedValueOnce(error).mockResolvedValueOnce(mockToken);
|
|
250
249
|
|
|
251
250
|
const { getByTestId } = render(<TestComponent />);
|
|
252
251
|
|
|
@@ -257,7 +256,7 @@ describe('useAccessToken', () => {
|
|
|
257
256
|
});
|
|
258
257
|
|
|
259
258
|
act(() => {
|
|
260
|
-
|
|
259
|
+
vi.advanceTimersByTime(5 * 60 * 1000); // RETRY_DELAY
|
|
261
260
|
});
|
|
262
261
|
|
|
263
262
|
// Loading should remain false during retry
|
|
@@ -281,10 +280,12 @@ describe('useAccessToken', () => {
|
|
|
281
280
|
iat: currentTimeInSeconds - 35,
|
|
282
281
|
};
|
|
283
282
|
const expiringToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.${btoa(JSON.stringify(payload))}.mock-signature`;
|
|
284
|
-
const error = new Error('Failed to refresh token');
|
|
285
283
|
|
|
286
|
-
(getAccessTokenAction as
|
|
287
|
-
(refreshAccessTokenAction as
|
|
284
|
+
(getAccessTokenAction as Mock).mockResolvedValueOnce(expiringToken);
|
|
285
|
+
(refreshAccessTokenAction as Mock).mockResolvedValueOnce({
|
|
286
|
+
accessToken: undefined,
|
|
287
|
+
error: 'Failed to refresh token',
|
|
288
|
+
});
|
|
288
289
|
|
|
289
290
|
const { getByTestId } = render(<TestComponent />);
|
|
290
291
|
|
|
@@ -301,7 +302,7 @@ describe('useAccessToken', () => {
|
|
|
301
302
|
|
|
302
303
|
it('should handle token with an invalid payload format', async () => {
|
|
303
304
|
const badPayloadToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.invalidpayload.mock-signature';
|
|
304
|
-
(getAccessTokenAction as
|
|
305
|
+
(getAccessTokenAction as Mock).mockResolvedValueOnce(badPayloadToken);
|
|
305
306
|
|
|
306
307
|
const { getByTestId } = render(<TestComponent />);
|
|
307
308
|
|
|
@@ -313,7 +314,7 @@ describe('useAccessToken', () => {
|
|
|
313
314
|
});
|
|
314
315
|
|
|
315
316
|
it('should immediately try to update token when token is undefined', async () => {
|
|
316
|
-
(getAccessTokenAction as
|
|
317
|
+
(getAccessTokenAction as Mock).mockResolvedValueOnce(undefined).mockResolvedValueOnce(undefined);
|
|
317
318
|
|
|
318
319
|
const { getByTestId } = render(<TestComponent />);
|
|
319
320
|
|
|
@@ -327,17 +328,17 @@ describe('useAccessToken', () => {
|
|
|
327
328
|
|
|
328
329
|
it('should react to sessionId changes', async () => {
|
|
329
330
|
// Clear any previous mocks to ensure clean state
|
|
330
|
-
|
|
331
|
+
vi.clearAllMocks();
|
|
331
332
|
|
|
332
333
|
const token1 = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMSIsImV4cCI6OTk5OTk5OTk5OX0.mock-signature-1';
|
|
333
334
|
const token2 = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMSIsImV4cCI6OTk5OTk5OTk5OX0.mock-signature-2';
|
|
334
335
|
|
|
335
|
-
(getAccessTokenAction as
|
|
336
|
+
(getAccessTokenAction as Mock).mockResolvedValueOnce(token1).mockResolvedValueOnce(token2);
|
|
336
337
|
|
|
337
|
-
(useAuth as
|
|
338
|
+
(useAuth as Mock).mockImplementation(() => ({
|
|
338
339
|
user: { id: 'user1' },
|
|
339
340
|
sessionId: 'session1',
|
|
340
|
-
refreshAuth:
|
|
341
|
+
refreshAuth: vi.fn().mockResolvedValue({}),
|
|
341
342
|
}));
|
|
342
343
|
|
|
343
344
|
const { rerender } = render(<TestComponent />);
|
|
@@ -346,10 +347,10 @@ describe('useAccessToken', () => {
|
|
|
346
347
|
expect(getAccessTokenAction).toHaveBeenCalledTimes(1);
|
|
347
348
|
});
|
|
348
349
|
|
|
349
|
-
(useAuth as
|
|
350
|
+
(useAuth as Mock).mockImplementation(() => ({
|
|
350
351
|
user: { id: 'user1' }, // Same user ID
|
|
351
352
|
sessionId: 'session2',
|
|
352
|
-
refreshAuth:
|
|
353
|
+
refreshAuth: vi.fn().mockResolvedValue({}),
|
|
353
354
|
}));
|
|
354
355
|
|
|
355
356
|
rerender(<TestComponent />);
|
|
@@ -360,8 +361,8 @@ describe('useAccessToken', () => {
|
|
|
360
361
|
});
|
|
361
362
|
|
|
362
363
|
it('should prevent concurrent token fetches via updateToken', async () => {
|
|
363
|
-
|
|
364
|
-
(getAccessTokenAction as
|
|
364
|
+
vi.clearAllMocks();
|
|
365
|
+
(getAccessTokenAction as Mock).mockReset();
|
|
365
366
|
|
|
366
367
|
const mockToken =
|
|
367
368
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwic2lkIjoic2Vzc2lvbl8xMjMiLCJleHAiOjk5OTk5OTk5OTl9.mock-signature';
|
|
@@ -374,7 +375,7 @@ describe('useAccessToken', () => {
|
|
|
374
375
|
}, 0);
|
|
375
376
|
});
|
|
376
377
|
|
|
377
|
-
(getAccessTokenAction as
|
|
378
|
+
(getAccessTokenAction as Mock).mockImplementation(() => {
|
|
378
379
|
fetchCalls++;
|
|
379
380
|
return tokenPromise;
|
|
380
381
|
});
|
|
@@ -397,7 +398,7 @@ describe('useAccessToken', () => {
|
|
|
397
398
|
});
|
|
398
399
|
|
|
399
400
|
it('should prevent concurrent manual refresh operations', async () => {
|
|
400
|
-
|
|
401
|
+
vi.clearAllMocks();
|
|
401
402
|
|
|
402
403
|
let refreshCalls = 0;
|
|
403
404
|
|
|
@@ -412,12 +413,12 @@ describe('useAccessToken', () => {
|
|
|
412
413
|
setTimeout(() => resolve(refreshedToken), 10);
|
|
413
414
|
});
|
|
414
415
|
|
|
415
|
-
(refreshAccessTokenAction as
|
|
416
|
+
(refreshAccessTokenAction as Mock).mockImplementation(() => {
|
|
416
417
|
refreshCalls++;
|
|
417
|
-
return refreshPromise;
|
|
418
|
+
return refreshPromise.then((t) => ({ accessToken: t }));
|
|
418
419
|
});
|
|
419
420
|
|
|
420
|
-
(getAccessTokenAction as
|
|
421
|
+
(getAccessTokenAction as Mock).mockImplementation(() => {
|
|
421
422
|
return Promise.resolve(mockToken);
|
|
422
423
|
});
|
|
423
424
|
|
|
@@ -446,7 +447,7 @@ describe('useAccessToken', () => {
|
|
|
446
447
|
|
|
447
448
|
it('should handle non-Error objects thrown during token fetch', async () => {
|
|
448
449
|
// Simulate a string error being thrown
|
|
449
|
-
(getAccessTokenAction as
|
|
450
|
+
(getAccessTokenAction as Mock).mockImplementation(() => {
|
|
450
451
|
throw 'String error message';
|
|
451
452
|
});
|
|
452
453
|
|
|
@@ -461,13 +462,13 @@ describe('useAccessToken', () => {
|
|
|
461
462
|
|
|
462
463
|
it('should show loading state immediately on first render when user exists but no token', () => {
|
|
463
464
|
// Mock user with no token initially
|
|
464
|
-
(useAuth as
|
|
465
|
+
(useAuth as Mock).mockImplementation(() => ({
|
|
465
466
|
user: { id: 'user_123' },
|
|
466
467
|
sessionId: 'session_123',
|
|
467
|
-
refreshAuth:
|
|
468
|
+
refreshAuth: vi.fn().mockResolvedValue({}),
|
|
468
469
|
}));
|
|
469
470
|
|
|
470
|
-
(getAccessTokenAction as
|
|
471
|
+
(getAccessTokenAction as Mock).mockImplementation(
|
|
471
472
|
() => new Promise((resolve) => setTimeout(() => resolve('token'), 100)),
|
|
472
473
|
);
|
|
473
474
|
|
|
@@ -482,12 +483,12 @@ describe('useAccessToken', () => {
|
|
|
482
483
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJleGlzdGluZyIsInNpZCI6InNlc3Npb24xMjMiLCJleHAiOjk5OTk5OTk5OTl9.existing';
|
|
483
484
|
|
|
484
485
|
await act(async () => {
|
|
485
|
-
(getAccessTokenAction as
|
|
486
|
+
(getAccessTokenAction as Mock).mockResolvedValueOnce(existingToken);
|
|
486
487
|
await tokenStore.getAccessTokenSilently();
|
|
487
488
|
});
|
|
488
489
|
|
|
489
490
|
// Reset the mock to track new calls
|
|
490
|
-
(getAccessTokenAction as
|
|
491
|
+
(getAccessTokenAction as Mock).mockClear();
|
|
491
492
|
|
|
492
493
|
const { getByTestId } = render(<TestComponent />);
|
|
493
494
|
|
|
@@ -511,8 +512,8 @@ describe('useAccessToken', () => {
|
|
|
511
512
|
resolveRefreshPromise = resolve;
|
|
512
513
|
});
|
|
513
514
|
|
|
514
|
-
(refreshAccessTokenAction as
|
|
515
|
-
(getAccessTokenAction as
|
|
515
|
+
(refreshAccessTokenAction as Mock).mockReturnValue(refreshPromise.then((t) => ({ accessToken: t })));
|
|
516
|
+
(getAccessTokenAction as Mock).mockResolvedValue(initialToken);
|
|
516
517
|
|
|
517
518
|
const { getByTestId } = render(<TestComponent />);
|
|
518
519
|
|
|
@@ -541,7 +542,7 @@ describe('useAccessToken', () => {
|
|
|
541
542
|
it('should clear refresh timeout on unmount', async () => {
|
|
542
543
|
const mockToken =
|
|
543
544
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwic2lkIjoic2Vzc2lvbl8xMjMiLCJleHAiOjk5OTk5OTk5OTl9.mock-signature';
|
|
544
|
-
(getAccessTokenAction as
|
|
545
|
+
(getAccessTokenAction as Mock).mockResolvedValueOnce(mockToken);
|
|
545
546
|
|
|
546
547
|
const { getByTestId, unmount } = render(<TestComponent />);
|
|
547
548
|
|
|
@@ -555,7 +556,7 @@ describe('useAccessToken', () => {
|
|
|
555
556
|
it('should handle edge cases when token data is null', async () => {
|
|
556
557
|
// Create a token that resembles a JWT but with a null payload
|
|
557
558
|
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.bnVsbA==.mock-signature'; // "null" in base64
|
|
558
|
-
(getAccessTokenAction as
|
|
559
|
+
(getAccessTokenAction as Mock).mockResolvedValueOnce(token);
|
|
559
560
|
|
|
560
561
|
const { getByTestId } = render(<TestComponent />);
|
|
561
562
|
|
|
@@ -570,7 +571,7 @@ describe('useAccessToken', () => {
|
|
|
570
571
|
it('should handle errors with string messages instead of Error objects', async () => {
|
|
571
572
|
const error = 'String error message';
|
|
572
573
|
const errorObj = new Error(error);
|
|
573
|
-
(getAccessTokenAction as
|
|
574
|
+
(getAccessTokenAction as Mock).mockRejectedValueOnce(errorObj);
|
|
574
575
|
|
|
575
576
|
const { getByTestId } = render(<TestComponent />);
|
|
576
577
|
|
|
@@ -585,11 +586,9 @@ describe('useAccessToken', () => {
|
|
|
585
586
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwic2lkIjoic2Vzc2lvbl8xMjMiLCJleHAiOjk5OTk5OTk5OTl9.mock-signature';
|
|
586
587
|
const stringError = 'String error directly'; // Not wrapped in Error object
|
|
587
588
|
|
|
588
|
-
(getAccessTokenAction as
|
|
589
|
-
// Mock refreshAccessTokenAction to
|
|
590
|
-
(refreshAccessTokenAction as
|
|
591
|
-
return Promise.reject(stringError); // Directly reject with string
|
|
592
|
-
});
|
|
589
|
+
(getAccessTokenAction as Mock).mockResolvedValueOnce(initialToken);
|
|
590
|
+
// Mock refreshAccessTokenAction to return an error result (as server actions do)
|
|
591
|
+
(refreshAccessTokenAction as Mock).mockResolvedValueOnce({ accessToken: undefined, error: stringError });
|
|
593
592
|
|
|
594
593
|
const { getByTestId } = render(<TestComponent />);
|
|
595
594
|
|
|
@@ -613,12 +612,12 @@ describe('useAccessToken', () => {
|
|
|
613
612
|
const token =
|
|
614
613
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwic2lkIjoic2Vzc2lvbl8xMjMiLCJleHAiOjk5OTk5OTk5OTl9.mock-signature';
|
|
615
614
|
|
|
616
|
-
(getAccessTokenAction as
|
|
615
|
+
(getAccessTokenAction as Mock).mockResolvedValue(token);
|
|
617
616
|
|
|
618
|
-
(useAuth as
|
|
617
|
+
(useAuth as Mock).mockImplementation(() => ({
|
|
619
618
|
user: { id: 'user_123' },
|
|
620
619
|
sessionId: 'session_123',
|
|
621
|
-
refreshAuth:
|
|
620
|
+
refreshAuth: vi.fn().mockResolvedValue({}),
|
|
622
621
|
}));
|
|
623
622
|
|
|
624
623
|
const { getByTestId, rerender } = render(<TestComponent />);
|
|
@@ -628,10 +627,10 @@ describe('useAccessToken', () => {
|
|
|
628
627
|
expect(getAccessTokenAction).toHaveBeenCalledTimes(1);
|
|
629
628
|
});
|
|
630
629
|
|
|
631
|
-
(useAuth as
|
|
630
|
+
(useAuth as Mock).mockImplementation(() => ({
|
|
632
631
|
user: { id: 'user_456' }, // Different user
|
|
633
632
|
sessionId: 'session_123', // Same session
|
|
634
|
-
refreshAuth:
|
|
633
|
+
refreshAuth: vi.fn().mockResolvedValue({}),
|
|
635
634
|
}));
|
|
636
635
|
|
|
637
636
|
rerender(<TestComponent />);
|
|
@@ -642,10 +641,10 @@ describe('useAccessToken', () => {
|
|
|
642
641
|
});
|
|
643
642
|
|
|
644
643
|
it('should handle getAccessToken when user is not authenticated', async () => {
|
|
645
|
-
(useAuth as
|
|
644
|
+
(useAuth as Mock).mockImplementation(() => ({
|
|
646
645
|
user: null,
|
|
647
646
|
sessionId: undefined,
|
|
648
|
-
refreshAuth:
|
|
647
|
+
refreshAuth: vi.fn().mockResolvedValue({}),
|
|
649
648
|
}));
|
|
650
649
|
|
|
651
650
|
const TestComponentWithGetAccessToken = () => {
|
|
@@ -670,11 +669,11 @@ describe('useAccessToken', () => {
|
|
|
670
669
|
const mockToken =
|
|
671
670
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwic2lkIjoic2Vzc2lvbl8xMjMiLCJleHAiOjk5OTk5OTk5OTl9.mock-signature';
|
|
672
671
|
|
|
673
|
-
(getAccessTokenAction as
|
|
674
|
-
(useAuth as
|
|
672
|
+
(getAccessTokenAction as Mock).mockResolvedValue(mockToken);
|
|
673
|
+
(useAuth as Mock).mockImplementation(() => ({
|
|
675
674
|
user: { id: 'user_123' },
|
|
676
675
|
sessionId: 'session_123',
|
|
677
|
-
refreshAuth:
|
|
676
|
+
refreshAuth: vi.fn().mockResolvedValue({}),
|
|
678
677
|
}));
|
|
679
678
|
|
|
680
679
|
const TestComponentWithGetAccessToken = () => {
|
|
@@ -696,7 +695,7 @@ describe('useAccessToken', () => {
|
|
|
696
695
|
|
|
697
696
|
// Advance timers to trigger getAccessToken call
|
|
698
697
|
act(() => {
|
|
699
|
-
|
|
698
|
+
vi.advanceTimersByTime(100);
|
|
700
699
|
});
|
|
701
700
|
|
|
702
701
|
await waitFor(() => {
|
|
@@ -705,10 +704,10 @@ describe('useAccessToken', () => {
|
|
|
705
704
|
});
|
|
706
705
|
|
|
707
706
|
it('should handle manual refresh when user is not authenticated', async () => {
|
|
708
|
-
(useAuth as
|
|
707
|
+
(useAuth as Mock).mockImplementation(() => ({
|
|
709
708
|
user: null,
|
|
710
709
|
sessionId: undefined,
|
|
711
|
-
refreshAuth:
|
|
710
|
+
refreshAuth: vi.fn().mockResolvedValue({}),
|
|
712
711
|
}));
|
|
713
712
|
|
|
714
713
|
const TestComponentWithRefresh = () => {
|
|
@@ -1,27 +1,28 @@
|
|
|
1
|
+
import type { Mock } from 'vitest';
|
|
1
2
|
import '@testing-library/jest-dom';
|
|
2
3
|
import { render, waitFor } from '@testing-library/react';
|
|
3
4
|
import React from 'react';
|
|
4
5
|
import { useAuth } from './authkit-provider.js';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
getAccessTokenAction:
|
|
8
|
-
refreshAccessTokenAction:
|
|
7
|
+
vi.mock('../actions.js', () => ({
|
|
8
|
+
getAccessTokenAction: vi.fn(),
|
|
9
|
+
refreshAccessTokenAction: vi.fn(),
|
|
9
10
|
}));
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
const originalModule =
|
|
12
|
+
vi.mock('./authkit-provider.js', async () => {
|
|
13
|
+
const originalModule = await vi.importActual<typeof import('./authkit-provider.js')>('./authkit-provider.js');
|
|
13
14
|
return {
|
|
14
15
|
...originalModule,
|
|
15
|
-
useAuth:
|
|
16
|
+
useAuth: vi.fn(),
|
|
16
17
|
};
|
|
17
18
|
});
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
useAccessToken:
|
|
20
|
+
vi.mock('./useAccessToken.js', () => ({
|
|
21
|
+
useAccessToken: vi.fn(() => ({ accessToken: undefined })),
|
|
21
22
|
}));
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
decodeJwt:
|
|
24
|
+
vi.mock('jose', () => ({
|
|
25
|
+
decodeJwt: vi.fn((token: string) => {
|
|
25
26
|
if (token === 'malformed-token' || token === 'throw-error-token') {
|
|
26
27
|
throw new Error('Invalid JWT');
|
|
27
28
|
}
|
|
@@ -42,21 +43,21 @@ import { useTokenClaims } from './useTokenClaims.js';
|
|
|
42
43
|
|
|
43
44
|
describe('useTokenClaims', () => {
|
|
44
45
|
beforeEach(() => {
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
vi.clearAllMocks();
|
|
47
|
+
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
47
48
|
|
|
48
|
-
(useAuth as
|
|
49
|
+
(useAuth as Mock).mockImplementation(() => ({
|
|
49
50
|
user: { id: 'user_123' },
|
|
50
51
|
sessionId: 'session_123',
|
|
51
|
-
refreshAuth:
|
|
52
|
+
refreshAuth: vi.fn().mockResolvedValue({}),
|
|
52
53
|
}));
|
|
53
54
|
|
|
54
55
|
// Reset useAccessToken mock to default
|
|
55
|
-
(useAccessToken as
|
|
56
|
+
(useAccessToken as Mock).mockReturnValue({ accessToken: undefined });
|
|
56
57
|
});
|
|
57
58
|
|
|
58
59
|
afterEach(() => {
|
|
59
|
-
|
|
60
|
+
vi.useRealTimers();
|
|
60
61
|
});
|
|
61
62
|
|
|
62
63
|
const TokenClaimsTestComponent = () => {
|
|
@@ -69,7 +70,7 @@ describe('useTokenClaims', () => {
|
|
|
69
70
|
};
|
|
70
71
|
|
|
71
72
|
it('should return empty object when no access token is available', async () => {
|
|
72
|
-
(useAccessToken as
|
|
73
|
+
(useAccessToken as Mock).mockReturnValue({ accessToken: undefined });
|
|
73
74
|
|
|
74
75
|
const { getByTestId } = render(<TokenClaimsTestComponent />);
|
|
75
76
|
|
|
@@ -101,7 +102,7 @@ describe('useTokenClaims', () => {
|
|
|
101
102
|
};
|
|
102
103
|
const token = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.${btoa(JSON.stringify(payload))}.mock-signature`;
|
|
103
104
|
|
|
104
|
-
(useAccessToken as
|
|
105
|
+
(useAccessToken as Mock).mockReturnValue({ accessToken: token });
|
|
105
106
|
|
|
106
107
|
const { getByTestId } = render(<TokenClaimsTestComponent />);
|
|
107
108
|
|
|
@@ -129,7 +130,7 @@ describe('useTokenClaims', () => {
|
|
|
129
130
|
};
|
|
130
131
|
const token = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.${btoa(JSON.stringify(payload))}.mock-signature`;
|
|
131
132
|
|
|
132
|
-
(useAccessToken as
|
|
133
|
+
(useAccessToken as Mock).mockReturnValue({ accessToken: token });
|
|
133
134
|
|
|
134
135
|
const { getByTestId } = render(<TokenClaimsTestComponent />);
|
|
135
136
|
|
|
@@ -147,7 +148,7 @@ describe('useTokenClaims', () => {
|
|
|
147
148
|
};
|
|
148
149
|
const token = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.${btoa(JSON.stringify(payload))}.mock-signature`;
|
|
149
150
|
|
|
150
|
-
(useAccessToken as
|
|
151
|
+
(useAccessToken as Mock).mockReturnValue({ accessToken: token });
|
|
151
152
|
|
|
152
153
|
const { getByTestId } = render(<TokenClaimsTestComponent />);
|
|
153
154
|
|
|
@@ -175,7 +176,7 @@ describe('useTokenClaims', () => {
|
|
|
175
176
|
};
|
|
176
177
|
const token = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.${btoa(JSON.stringify(payload))}.mock-signature`;
|
|
177
178
|
|
|
178
|
-
(useAccessToken as
|
|
179
|
+
(useAccessToken as Mock).mockReturnValue({ accessToken: token });
|
|
179
180
|
|
|
180
181
|
const { getByTestId } = render(<TokenClaimsTestComponent />);
|
|
181
182
|
|
|
@@ -185,7 +186,7 @@ describe('useTokenClaims', () => {
|
|
|
185
186
|
});
|
|
186
187
|
|
|
187
188
|
it('should return empty object when decodeJwt throws an error', async () => {
|
|
188
|
-
(useAccessToken as
|
|
189
|
+
(useAccessToken as Mock).mockReturnValue({ accessToken: 'malformed-token' });
|
|
189
190
|
|
|
190
191
|
const { getByTestId } = render(<TokenClaimsTestComponent />);
|
|
191
192
|
|