@workos-inc/authkit-nextjs 2.6.0 → 2.7.0

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.
Files changed (46) hide show
  1. package/README.md +124 -29
  2. package/dist/esm/components/tokenStore.js +110 -11
  3. package/dist/esm/components/tokenStore.js.map +1 -1
  4. package/dist/esm/components/useAccessToken.js +6 -1
  5. package/dist/esm/components/useAccessToken.js.map +1 -1
  6. package/dist/esm/cookie.js +51 -0
  7. package/dist/esm/cookie.js.map +1 -1
  8. package/dist/esm/middleware.js +2 -2
  9. package/dist/esm/middleware.js.map +1 -1
  10. package/dist/esm/session.js +35 -2
  11. package/dist/esm/session.js.map +1 -1
  12. package/dist/esm/test-helpers.js +57 -0
  13. package/dist/esm/test-helpers.js.map +1 -0
  14. package/dist/esm/types/components/tokenStore.d.ts +7 -2
  15. package/dist/esm/types/cookie.d.ts +1 -0
  16. package/dist/esm/types/interfaces.d.ts +2 -0
  17. package/dist/esm/types/middleware.d.ts +1 -1
  18. package/dist/esm/types/session.d.ts +1 -1
  19. package/dist/esm/types/test-helpers.d.ts +3 -0
  20. package/dist/esm/types/workos.d.ts +1 -1
  21. package/dist/esm/workos.js +1 -1
  22. package/package.json +4 -3
  23. package/src/actions.spec.ts +100 -0
  24. package/src/auth.spec.ts +347 -0
  25. package/src/authkit-callback-route.spec.ts +258 -0
  26. package/src/components/authkit-provider.spec.tsx +471 -0
  27. package/src/components/button.spec.tsx +46 -0
  28. package/src/components/impersonation.spec.tsx +134 -0
  29. package/src/components/min-max-button.spec.tsx +60 -0
  30. package/src/components/tokenStore.spec.ts +816 -0
  31. package/src/components/tokenStore.ts +147 -12
  32. package/src/components/useAccessToken.spec.tsx +731 -0
  33. package/src/components/useAccessToken.ts +6 -1
  34. package/src/components/useTokenClaims.spec.tsx +194 -0
  35. package/src/cookie.spec.ts +276 -0
  36. package/src/cookie.ts +56 -0
  37. package/src/get-authorization-url.spec.ts +60 -0
  38. package/src/interfaces.ts +2 -0
  39. package/src/jwt.spec.ts +159 -0
  40. package/src/middleware.ts +2 -1
  41. package/src/session.spec.ts +1152 -0
  42. package/src/session.ts +41 -1
  43. package/src/test-helpers.ts +70 -0
  44. package/src/utils.spec.ts +142 -0
  45. package/src/workos.spec.ts +67 -0
  46. package/src/workos.ts +1 -1
@@ -0,0 +1,347 @@
1
+ import { describe, it, expect, beforeEach, jest } from '@jest/globals';
2
+
3
+ import { getSignInUrl, getSignUpUrl, signOut, switchToOrganization } from './auth.js';
4
+ import * as session from './session.js';
5
+ import * as cache from 'next/cache';
6
+ import * as workosModule from './workos.js';
7
+
8
+ // These are mocked in jest.setup.ts
9
+ import { cookies, headers } from 'next/headers';
10
+ import { redirect } from 'next/navigation';
11
+ import { generateSession, generateTestToken } from './test-helpers.js';
12
+ import { sealData } from 'iron-session';
13
+ import { getWorkOS } from './workos.js';
14
+
15
+ const workos = getWorkOS();
16
+
17
+ jest.mock('next/cache', () => {
18
+ const actual = jest.requireActual<typeof cache>('next/cache');
19
+ return {
20
+ ...actual,
21
+ revalidateTag: jest.fn(),
22
+ revalidatePath: jest.fn(),
23
+ };
24
+ });
25
+
26
+ // Create a fake WorkOS instance that will be used only in the "on error" tests
27
+ const fakeWorkosInstance = {
28
+ userManagement: {
29
+ authenticateWithRefreshToken: jest.fn(),
30
+ getAuthorizationUrl: jest.fn(),
31
+ getJwksUrl: jest.fn(() => 'https://api.workos.com/sso/jwks/client_1234567890'),
32
+ getLogoutUrl: jest.fn(),
33
+ },
34
+ };
35
+
36
+ const revalidatePath = jest.mocked(cache.revalidatePath);
37
+ const revalidateTag = jest.mocked(cache.revalidateTag);
38
+ // We'll only use these in the "on error" tests
39
+ const authenticateWithRefreshToken = fakeWorkosInstance.userManagement.authenticateWithRefreshToken;
40
+ const getAuthorizationUrl = fakeWorkosInstance.userManagement.getAuthorizationUrl;
41
+
42
+ jest.mock('../src/session', () => {
43
+ const actual = jest.requireActual<typeof session>('../src/session');
44
+
45
+ return {
46
+ ...actual,
47
+ refreshSession: jest.fn(actual.refreshSession),
48
+ };
49
+ });
50
+
51
+ describe('auth.ts', () => {
52
+ beforeEach(async () => {
53
+ // Clear all mocks between tests
54
+ jest.clearAllMocks();
55
+
56
+ // Reset the cookie store
57
+ const nextCookies = await cookies();
58
+ // @ts-expect-error - _reset is part of the mock
59
+ nextCookies._reset();
60
+
61
+ const nextHeaders = await headers();
62
+ // @ts-expect-error - _reset is part of the mock
63
+ nextHeaders._reset();
64
+ });
65
+
66
+ describe('getSignInUrl', () => {
67
+ it('should return a valid URL', async () => {
68
+ const url = await getSignInUrl();
69
+ expect(url).toBeDefined();
70
+ expect(() => new URL(url)).not.toThrow();
71
+ });
72
+
73
+ it('should use the organizationId if provided', async () => {
74
+ const url = await getSignInUrl({ organizationId: 'org_123' });
75
+ expect(url).toContain('organization_id=org_123');
76
+ expect(url).toBeDefined();
77
+ expect(() => new URL(url)).not.toThrow();
78
+ });
79
+ });
80
+
81
+ it('should not include prompt when not specified for getSignInUrl', async () => {
82
+ const url = await getSignInUrl();
83
+ expect(url).not.toContain('prompt=');
84
+ });
85
+
86
+ it('should include prompt=consent when explicitly specified for getSignInUrl', async () => {
87
+ const url = await getSignInUrl({ prompt: 'consent' });
88
+ expect(url).toContain('prompt=consent');
89
+ });
90
+
91
+ describe('getSignUpUrl', () => {
92
+ it('should return a valid URL', async () => {
93
+ const url = await getSignUpUrl();
94
+ expect(url).toBeDefined();
95
+ expect(() => new URL(url)).not.toThrow();
96
+ });
97
+ it('should not include prompt when not specified for getSignUpUrl', async () => {
98
+ const url = await getSignUpUrl();
99
+ expect(url).not.toContain('prompt=');
100
+ });
101
+
102
+ it('should include prompt=consent when explicitly specified for getSignUpUrl', async () => {
103
+ const url = await getSignUpUrl({ prompt: 'consent' });
104
+ expect(url).toContain('prompt=consent');
105
+ });
106
+ });
107
+
108
+ describe('switchToOrganization', () => {
109
+ it('should refresh the session with the new organizationId', async () => {
110
+ const nextHeaders = await headers();
111
+ nextHeaders.set('x-url', 'http://localhost/test');
112
+ await switchToOrganization('org_123');
113
+ expect(revalidatePath).toHaveBeenCalledWith('http://localhost/test');
114
+ });
115
+
116
+ it('should revalidate the path and refresh the session with the new organizationId', async () => {
117
+ const nextHeaders = await headers();
118
+ nextHeaders.set('x-url', 'http://localhost/test');
119
+ await switchToOrganization('org_123', { returnTo: '/test' });
120
+ expect(session.refreshSession).toHaveBeenCalledTimes(1);
121
+ expect(session.refreshSession).toHaveBeenCalledWith({ organizationId: 'org_123', ensureSignedIn: true });
122
+ expect(revalidatePath).toHaveBeenCalledWith('/test');
123
+ });
124
+
125
+ it('should revalidate the provided tags and refresh the session with the new organizationId', async () => {
126
+ const nextHeaders = await headers();
127
+ nextHeaders.set('x-url', 'http://localhost/test');
128
+ await switchToOrganization('org_123', { revalidationStrategy: 'tag', revalidationTags: ['tag1', 'tag2'] });
129
+ expect(revalidateTag).toHaveBeenCalledTimes(2);
130
+ });
131
+
132
+ describe('on error', () => {
133
+ beforeEach(async () => {
134
+ const nextHeaders = await headers();
135
+ nextHeaders.set('x-url', 'http://localhost/test');
136
+ await generateSession();
137
+
138
+ // Create a WorkOS-like object that matches what our tests need
139
+ const mockWorkOS = {
140
+ userManagement: fakeWorkosInstance.userManagement,
141
+ // Add minimal properties to satisfy TypeScript
142
+ createHttpClient: jest.fn(),
143
+ createWebhookClient: jest.fn(),
144
+ createActionsClient: jest.fn(),
145
+ createIronSessionProvider: jest.fn(),
146
+ apiKey: 'test',
147
+ clientId: 'test',
148
+ host: 'test',
149
+ port: 443,
150
+ protocol: 'https',
151
+ headers: {},
152
+ version: '0.0.0',
153
+ };
154
+
155
+ // Apply the mock for these tests only
156
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
157
+ jest.spyOn(workosModule, 'getWorkOS').mockImplementation(() => mockWorkOS as any);
158
+ });
159
+
160
+ afterEach(() => {
161
+ // Restore all mocks after each test
162
+ jest.restoreAllMocks();
163
+ });
164
+
165
+ it('should redirect to sign in when error is "sso_required"', async () => {
166
+ authenticateWithRefreshToken.mockImplementation(() => {
167
+ return Promise.reject({
168
+ status: 500,
169
+ requestID: 'sso_required',
170
+ error: 'sso_required',
171
+ errorDescription: 'User must authenticate using one of the matching connections.',
172
+ });
173
+ });
174
+
175
+ await switchToOrganization('org_123');
176
+ expect(getAuthorizationUrl).toHaveBeenCalledWith(expect.objectContaining({ organizationId: 'org_123' }));
177
+ expect(redirect).toHaveBeenCalledTimes(1);
178
+ });
179
+
180
+ it('should redirect to sign in when error is "mfa_enrollment"', async () => {
181
+ authenticateWithRefreshToken.mockImplementation(() => {
182
+ return Promise.reject({
183
+ status: 500,
184
+ requestID: 'mfa_enrollment',
185
+ error: 'mfa_enrollment',
186
+ errorDescription: 'User must authenticate using one of the matching connections.',
187
+ });
188
+ });
189
+
190
+ await switchToOrganization('org_123');
191
+ expect(getAuthorizationUrl).toHaveBeenCalledWith(expect.objectContaining({ organizationId: 'org_123' }));
192
+ expect(redirect).toHaveBeenCalledTimes(1);
193
+ });
194
+
195
+ it('should redirect to the authkit_redirect_url when provided', async () => {
196
+ authenticateWithRefreshToken.mockImplementation(() => {
197
+ return Promise.reject({
198
+ rawData: {
199
+ authkit_redirect_url: 'http://localhost/test',
200
+ },
201
+ });
202
+ });
203
+ await switchToOrganization('org_123');
204
+ expect(redirect).toHaveBeenCalledWith('http://localhost/test');
205
+ });
206
+
207
+ it('throws other errors', async () => {
208
+ authenticateWithRefreshToken.mockImplementation(() => {
209
+ return Promise.reject(new Error('Fail'));
210
+ });
211
+ await expect(switchToOrganization('org_123')).rejects.toThrow('Fail');
212
+ });
213
+ });
214
+ });
215
+
216
+ describe('signOut', () => {
217
+ it('should delete the cookie and redirect', async () => {
218
+ const nextCookies = await cookies();
219
+ const nextHeaders = await headers();
220
+
221
+ nextHeaders.set('x-workos-middleware', 'true');
222
+ nextCookies.set('wos-session', 'foo');
223
+
224
+ await signOut();
225
+
226
+ const sessionCookie = nextCookies.get('wos-session');
227
+
228
+ expect(sessionCookie).toBeUndefined();
229
+ expect(redirect).toHaveBeenCalledTimes(1);
230
+ expect(redirect).toHaveBeenCalledWith('/');
231
+ });
232
+
233
+ it('should delete the cookie with a specific domain', async () => {
234
+ const nextCookies = await cookies();
235
+ const nextHeaders = await headers();
236
+
237
+ nextHeaders.set('x-workos-middleware', 'true');
238
+ nextCookies.set('wos-session', 'foo', { domain: 'example.com' });
239
+
240
+ await signOut();
241
+
242
+ const sessionCookie = nextCookies.get('wos-session');
243
+ expect(sessionCookie).toBeUndefined();
244
+ });
245
+
246
+ describe('when given a `returnTo` parameter', () => {
247
+ it('passes the `returnTo` through to the `getLogoutUrl` call', async () => {
248
+ jest
249
+ .spyOn(workos.userManagement, 'getLogoutUrl')
250
+ .mockReturnValue('https://user-management-logout.com/signed-out');
251
+ const mockSession = {
252
+ accessToken: await generateTestToken(),
253
+ sessionId: 'session_123',
254
+ } as const;
255
+
256
+ const nextHeaders = await headers();
257
+ nextHeaders.set(
258
+ 'x-workos-session',
259
+ await sealData(mockSession, { password: process.env.WORKOS_COOKIE_PASSWORD as string }),
260
+ );
261
+
262
+ nextHeaders.set('x-workos-middleware', 'true');
263
+
264
+ await signOut({ returnTo: 'https://example.com/signed-out' });
265
+
266
+ expect(redirect).toHaveBeenCalledTimes(1);
267
+ expect(redirect).toHaveBeenCalledWith('https://user-management-logout.com/signed-out');
268
+ expect(workos.userManagement.getLogoutUrl).toHaveBeenCalledWith(
269
+ expect.objectContaining({
270
+ returnTo: 'https://example.com/signed-out',
271
+ }),
272
+ );
273
+ });
274
+
275
+ describe('when there is no session', () => {
276
+ it('returns to the `returnTo`', async () => {
277
+ const nextHeaders = await headers();
278
+
279
+ nextHeaders.set('x-workos-middleware', 'true');
280
+
281
+ await signOut({ returnTo: 'https://example.com/signed-out' });
282
+
283
+ expect(redirect).toHaveBeenCalledTimes(1);
284
+ expect(redirect).toHaveBeenCalledWith('https://example.com/signed-out');
285
+ });
286
+ });
287
+ });
288
+
289
+ describe('when called outside of middleware', () => {
290
+ it('should fall back to reading session from cookie and redirect to logout URL', async () => {
291
+ const nextCookies = await cookies();
292
+
293
+ // Don't set x-workos-middleware header to simulate being outside middleware
294
+ // This will cause withAuth to throw
295
+
296
+ // Set up a session cookie with a valid access token
297
+ const mockSession = {
298
+ accessToken: await generateTestToken(),
299
+ refreshToken: 'refresh_token',
300
+ user: { id: 'user_123' },
301
+ };
302
+
303
+ const encryptedSession = await sealData(mockSession, {
304
+ password: process.env.WORKOS_COOKIE_PASSWORD as string,
305
+ });
306
+
307
+ nextCookies.set('wos-session', encryptedSession);
308
+
309
+ jest
310
+ .spyOn(workos.userManagement, 'getLogoutUrl')
311
+ .mockReturnValue('https://api.workos.com/user_management/sessions/logout?session_id=session_123');
312
+
313
+ await signOut();
314
+
315
+ // Cookie should be deleted
316
+ const sessionCookie = nextCookies.get('wos-session');
317
+ expect(sessionCookie).toBeUndefined();
318
+
319
+ // Should redirect to WorkOS logout URL with session ID
320
+ expect(redirect).toHaveBeenCalledTimes(1);
321
+ expect(redirect).toHaveBeenCalledWith(
322
+ 'https://api.workos.com/user_management/sessions/logout?session_id=session_123',
323
+ );
324
+ expect(workos.userManagement.getLogoutUrl).toHaveBeenCalledWith(
325
+ expect.objectContaining({
326
+ sessionId: expect.stringMatching(/^session_/),
327
+ }),
328
+ );
329
+ });
330
+
331
+ it('should throw the original error when no session cookie exists outside middleware', async () => {
332
+ const nextCookies = await cookies();
333
+
334
+ // Don't set x-workos-middleware header to simulate being outside middleware
335
+ // Set a cookie to verify it gets deleted
336
+ nextCookies.set('wos-session', 'dummy-value');
337
+
338
+ // Should throw the error from withAuth since we can't recover
339
+ await expect(signOut()).rejects.toThrow(/You are calling 'withAuth'/);
340
+
341
+ // Cookie should still be deleted even though it throws
342
+ const sessionCookie = nextCookies.get('wos-session');
343
+ expect(sessionCookie).toBeUndefined();
344
+ });
345
+ });
346
+ });
347
+ });
@@ -0,0 +1,258 @@
1
+ import { getWorkOS } from './workos.js';
2
+ import { handleAuth } from './authkit-callback-route.js';
3
+ import { NextRequest, NextResponse } from 'next/server';
4
+
5
+ // Mocked in jest.setup.ts
6
+ import { cookies, headers } from 'next/headers';
7
+
8
+ // Mock dependencies
9
+ const fakeWorkosInstance = {
10
+ userManagement: {
11
+ authenticateWithCode: jest.fn(),
12
+ getJwksUrl: jest.fn(() => 'https://api.workos.com/sso/jwks/client_1234567890'),
13
+ },
14
+ };
15
+
16
+ jest.mock('../src/workos', () => ({
17
+ getWorkOS: jest.fn(() => fakeWorkosInstance),
18
+ }));
19
+
20
+ describe('authkit-callback-route', () => {
21
+ const workos = getWorkOS();
22
+ const mockAuthResponse = {
23
+ accessToken: 'access123',
24
+ refreshToken: 'refresh123',
25
+ user: {
26
+ id: 'user_123',
27
+ email: 'test@example.com',
28
+ emailVerified: true,
29
+ profilePictureUrl: 'https://example.com/photo.jpg',
30
+ firstName: 'Test',
31
+ lastName: 'User',
32
+ object: 'user' as const,
33
+ createdAt: '2024-01-01T00:00:00Z',
34
+ updatedAt: '2024-01-01T00:00:00Z',
35
+ lastSignInAt: '2024-01-01T00:00:00Z',
36
+ externalId: null,
37
+ metadata: {},
38
+ },
39
+ oauthTokens: {
40
+ accessToken: 'access123',
41
+ refreshToken: 'refresh123',
42
+ expiresAt: 1719811200,
43
+ scopes: ['foo', 'bar'],
44
+ },
45
+ };
46
+
47
+ describe('handleAuth', () => {
48
+ let request: NextRequest;
49
+
50
+ beforeAll(() => {
51
+ // Silence console.error during tests
52
+ jest.spyOn(console, 'error').mockImplementation(() => {});
53
+ });
54
+
55
+ beforeEach(async () => {
56
+ // Reset all mocks
57
+ jest.clearAllMocks();
58
+
59
+ // Create a new request with searchParams
60
+ request = new NextRequest(new URL('http://example.com/callback'));
61
+
62
+ // Reset the cookie store
63
+ const nextCookies = await cookies();
64
+ // @ts-expect-error - _reset is part of the mock
65
+ nextCookies._reset();
66
+
67
+ const nextHeaders = await headers();
68
+ // @ts-expect-error - _reset is part of the mock
69
+ nextHeaders._reset();
70
+ });
71
+
72
+ it('should handle successful authentication', async () => {
73
+ jest.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
74
+
75
+ // Set up request with code
76
+ request.nextUrl.searchParams.set('code', 'test-code');
77
+
78
+ const handler = handleAuth();
79
+ const response = await handler(request);
80
+
81
+ expect(workos.userManagement.authenticateWithCode).toHaveBeenCalledWith({
82
+ clientId: process.env.WORKOS_CLIENT_ID,
83
+ code: 'test-code',
84
+ });
85
+ expect(response).toBeInstanceOf(NextResponse);
86
+ });
87
+
88
+ it('should handle authentication failure', async () => {
89
+ // Mock authentication failure
90
+ (workos.userManagement.authenticateWithCode as jest.Mock).mockRejectedValue(new Error('Auth failed'));
91
+
92
+ request.nextUrl.searchParams.set('code', 'invalid-code');
93
+
94
+ const handler = handleAuth();
95
+ const response = await handler(request);
96
+
97
+ expect(response.status).toBe(500);
98
+ const data = await response.json();
99
+ expect(data.error.message).toBe('Something went wrong');
100
+ });
101
+
102
+ it('should handle authentication failure if a non-Error object is thrown', async () => {
103
+ // Mock authentication failure
104
+ jest.mocked(workos.userManagement.authenticateWithCode).mockRejectedValue('Auth failed');
105
+
106
+ request.nextUrl.searchParams.set('code', 'invalid-code');
107
+
108
+ const handler = handleAuth();
109
+ const response = await handler(request);
110
+
111
+ expect(response.status).toBe(500);
112
+ const data = await response.json();
113
+ expect(data.error.message).toBe('Something went wrong');
114
+ });
115
+
116
+ it('should handle authentication failure with custom onError handler', async () => {
117
+ // Mock authentication failure
118
+ jest.mocked(workos.userManagement.authenticateWithCode).mockRejectedValue('Auth failed');
119
+ request.nextUrl.searchParams.set('code', 'invalid-code');
120
+
121
+ const handler = handleAuth({
122
+ onError: () => {
123
+ return new Response(JSON.stringify({ error: { message: 'Custom error' } }), {
124
+ status: 500,
125
+ headers: { 'Content-Type': 'application/json' },
126
+ });
127
+ },
128
+ });
129
+ const response = await handler(request);
130
+
131
+ expect(response.status).toBe(500);
132
+ const data = await response.json();
133
+ expect(data.error.message).toBe('Custom error');
134
+ });
135
+
136
+ it('should handle missing code parameter', async () => {
137
+ const handler = handleAuth();
138
+ const response = await handler(request);
139
+
140
+ expect(response.status).toBe(500);
141
+ const data = await response.json();
142
+ expect(data.error.message).toBe('Something went wrong');
143
+ });
144
+
145
+ it('should respect custom returnPathname', async () => {
146
+ jest.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
147
+
148
+ request.nextUrl.searchParams.set('code', 'test-code');
149
+
150
+ const handler = handleAuth({ returnPathname: '/dashboard' });
151
+ const response = await handler(request);
152
+
153
+ expect(response.headers.get('Location')).toContain('/dashboard');
154
+ });
155
+
156
+ it('should handle state parameter with returnPathname', async () => {
157
+ jest.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
158
+
159
+ const state = btoa(JSON.stringify({ returnPathname: '/custom-path' }));
160
+ request.nextUrl.searchParams.set('code', 'test-code');
161
+ request.nextUrl.searchParams.set('state', state);
162
+
163
+ const handler = handleAuth();
164
+ const response = await handler(request);
165
+
166
+ expect(response.headers.get('Location')).toContain('/custom-path');
167
+ });
168
+
169
+ it('should extract custom search params from returnPathname', async () => {
170
+ jest.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
171
+
172
+ const state = btoa(JSON.stringify({ returnPathname: '/custom-path?foo=bar&baz=qux' }));
173
+ request.nextUrl.searchParams.set('code', 'test-code');
174
+ request.nextUrl.searchParams.set('state', state);
175
+
176
+ const handler = handleAuth();
177
+ const response = await handler(request);
178
+
179
+ expect(response.headers.get('Location')).toContain('/custom-path?foo=bar&baz=qux');
180
+ });
181
+
182
+ it('should use Response if NextResponse.redirect is not available', async () => {
183
+ const originalRedirect = NextResponse.redirect;
184
+ (NextResponse as Partial<typeof NextResponse>).redirect = undefined;
185
+
186
+ jest.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
187
+
188
+ // Set up request with code
189
+ request.nextUrl.searchParams.set('code', 'test-code');
190
+
191
+ const handler = handleAuth();
192
+ const response = await handler(request);
193
+
194
+ expect(response).toBeInstanceOf(Response);
195
+
196
+ // Restore the original redirect method
197
+ (NextResponse as Partial<typeof NextResponse>).redirect = originalRedirect;
198
+ });
199
+
200
+ it('should use Response if NextResponse.json is not available', async () => {
201
+ const originalJson = NextResponse.json;
202
+ (NextResponse as Partial<typeof NextResponse>).json = undefined;
203
+
204
+ const handler = handleAuth();
205
+ const response = await handler(request);
206
+
207
+ expect(response).toBeInstanceOf(Response);
208
+
209
+ // Restore the original json method
210
+ (NextResponse as Partial<typeof NextResponse>).json = originalJson;
211
+ });
212
+
213
+ it('should throw an error if baseURL is provided but invalid', async () => {
214
+ expect(() => handleAuth({ baseURL: 'invalid-url' })).toThrow('Invalid baseURL: invalid-url');
215
+ });
216
+
217
+ it('should use baseURL if provided', async () => {
218
+ jest.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
219
+
220
+ // Set up request with code
221
+ request.nextUrl.searchParams.set('code', 'test-code');
222
+
223
+ const handler = handleAuth({ baseURL: 'https://base.com' });
224
+ const response = await handler(request);
225
+
226
+ expect(response.headers.get('Location')).toContain('https://base.com');
227
+ });
228
+
229
+ it('should throw an error if response is missing tokens', async () => {
230
+ const mockAuthResponse = {
231
+ user: { id: 'user_123' },
232
+ };
233
+
234
+ (workos.userManagement.authenticateWithCode as jest.Mock).mockResolvedValue(mockAuthResponse);
235
+
236
+ // Set up request with code
237
+ request.nextUrl.searchParams.set('code', 'test-code');
238
+
239
+ const handler = handleAuth();
240
+ const response = await handler(request);
241
+
242
+ expect(response.status).toBe(500);
243
+ });
244
+
245
+ it('should call onSuccess if provided', async () => {
246
+ jest.mocked(workos.userManagement.authenticateWithCode).mockResolvedValue(mockAuthResponse);
247
+
248
+ // Set up request with code
249
+ request.nextUrl.searchParams.set('code', 'test-code');
250
+
251
+ const onSuccess = jest.fn();
252
+ const handler = handleAuth({ onSuccess: onSuccess });
253
+ await handler(request);
254
+
255
+ expect(onSuccess).toHaveBeenCalledWith(mockAuthResponse);
256
+ });
257
+ });
258
+ });