|
1 |
| -import { getSignInUrl, getSignUpUrl, signOut } from './auth.js'; |
| 1 | +import type { User } from '@workos-inc/node'; |
| 2 | +import { data, redirect } from 'react-router'; |
| 3 | +import { getSignInUrl, getSignUpUrl, signOut, switchToOrganization } from './auth.js'; |
2 | 4 | import * as authorizationUrl from './get-authorization-url.js';
|
3 | 5 | import * as session from './session.js';
|
| 6 | +import { assertIsResponse } from './test-utils/test-helpers.js'; |
4 | 7 |
|
5 | 8 | const terminateSession = jest.mocked(session.terminateSession);
|
| 9 | +const refreshSession = jest.mocked(session.refreshSession); |
6 | 10 |
|
7 | 11 | jest.mock('./session', () => ({
|
8 | 12 | terminateSession: jest.fn().mockResolvedValue(new Response()),
|
| 13 | + refreshSession: jest.fn(), |
9 | 14 | }));
|
10 | 15 |
|
| 16 | +// Mock redirect and data from react-router |
| 17 | +jest.mock('react-router', () => { |
| 18 | + const originalModule = jest.requireActual('react-router'); |
| 19 | + return { |
| 20 | + ...originalModule, |
| 21 | + redirect: jest.fn().mockImplementation((to, init) => { |
| 22 | + const response = new Response(null, { |
| 23 | + status: 302, |
| 24 | + headers: { Location: to, ...(init?.headers || {}) }, |
| 25 | + }); |
| 26 | + return response; |
| 27 | + }), |
| 28 | + data: jest.fn().mockImplementation((value, init) => ({ |
| 29 | + data: value, |
| 30 | + init, |
| 31 | + })), |
| 32 | + }; |
| 33 | +}); |
| 34 | + |
11 | 35 | describe('auth', () => {
|
12 | 36 | beforeEach(() => {
|
13 | 37 | jest.spyOn(authorizationUrl, 'getAuthorizationUrl');
|
| 38 | + jest.clearAllMocks(); |
14 | 39 | });
|
15 | 40 |
|
16 | 41 | describe('getSignInUrl', () => {
|
@@ -39,4 +64,216 @@ describe('auth', () => {
|
39 | 64 | expect(terminateSession).toHaveBeenCalledWith(request);
|
40 | 65 | });
|
41 | 66 | });
|
| 67 | + |
| 68 | + describe('switchToOrganization', () => { |
| 69 | + const request = new Request('https://example.com'); |
| 70 | + const organizationId = 'org_123456'; |
| 71 | + |
| 72 | + // Create a mock user that matches the User type |
| 73 | + const mockUser = { |
| 74 | + id: 'user-1', |
| 75 | + |
| 76 | + emailVerified: true, |
| 77 | + firstName: 'Test', |
| 78 | + lastName: 'User', |
| 79 | + profilePictureUrl: 'https://example.com/avatar.jpg', |
| 80 | + object: 'user', |
| 81 | + createdAt: '2021-01-01T00:00:00Z', |
| 82 | + updatedAt: '2021-01-01T00:00:00Z', |
| 83 | + lastSignInAt: '2021-01-01T00:00:00Z', |
| 84 | + externalId: null, |
| 85 | + } as User; |
| 86 | + |
| 87 | + // Mock the return type of refreshSession |
| 88 | + const mockAuthResponse = { |
| 89 | + user: mockUser, |
| 90 | + sessionId: 'session-123', |
| 91 | + accessToken: 'new-access-token', |
| 92 | + organizationId: 'org_123456' as string | undefined, |
| 93 | + role: 'admin' as string | undefined, |
| 94 | + permissions: ['read', 'write'] as string[] | undefined, |
| 95 | + entitlements: ['premium'] as string[] | undefined, |
| 96 | + impersonator: null, |
| 97 | + sealedSession: 'sealed-session-data', |
| 98 | + headers: { |
| 99 | + 'Set-Cookie': 'new-cookie-value', |
| 100 | + }, |
| 101 | + }; |
| 102 | + |
| 103 | + beforeEach(() => { |
| 104 | + refreshSession.mockResolvedValue(mockAuthResponse); |
| 105 | + }); |
| 106 | + |
| 107 | + it('should call refreshSession with the correct params', async () => { |
| 108 | + await switchToOrganization(request, organizationId); |
| 109 | + |
| 110 | + expect(refreshSession).toHaveBeenCalledWith(request, { organizationId }); |
| 111 | + }); |
| 112 | + |
| 113 | + it('should return data with success and auth when no returnTo is provided', async () => { |
| 114 | + const result = await switchToOrganization(request, organizationId); |
| 115 | + |
| 116 | + expect(data).toHaveBeenCalledWith( |
| 117 | + { success: true, auth: mockAuthResponse }, |
| 118 | + { |
| 119 | + headers: { |
| 120 | + 'Set-Cookie': 'new-cookie-value', |
| 121 | + }, |
| 122 | + }, |
| 123 | + ); |
| 124 | + expect(result).toEqual({ |
| 125 | + data: { success: true, auth: mockAuthResponse }, |
| 126 | + init: { |
| 127 | + headers: { |
| 128 | + 'Set-Cookie': 'new-cookie-value', |
| 129 | + }, |
| 130 | + }, |
| 131 | + }); |
| 132 | + }); |
| 133 | + |
| 134 | + it('should redirect to returnTo when provided', async () => { |
| 135 | + const returnTo = '/dashboard'; |
| 136 | + const result = await switchToOrganization(request, organizationId, { returnTo }); |
| 137 | + |
| 138 | + expect(redirect).toHaveBeenCalledWith(returnTo, { |
| 139 | + headers: { |
| 140 | + 'Set-Cookie': 'new-cookie-value', |
| 141 | + }, |
| 142 | + }); |
| 143 | + |
| 144 | + assertIsResponse(result); |
| 145 | + expect(result.status).toBe(302); |
| 146 | + expect(result.headers.get('Location')).toBe(returnTo); |
| 147 | + expect(result.headers.get('Set-Cookie')).toBe('new-cookie-value'); |
| 148 | + }); |
| 149 | + |
| 150 | + it('should handle case when refreshSession throws a redirect', async () => { |
| 151 | + const redirectResponse = new Response(null, { |
| 152 | + status: 302, |
| 153 | + headers: { Location: '/login' }, |
| 154 | + }); |
| 155 | + refreshSession.mockRejectedValueOnce(redirectResponse); |
| 156 | + |
| 157 | + try { |
| 158 | + await switchToOrganization(request, organizationId); |
| 159 | + fail('Expected redirect response to be thrown'); |
| 160 | + } catch (response) { |
| 161 | + assertIsResponse(response); |
| 162 | + expect(response.status).toBe(302); |
| 163 | + expect(response.headers.get('Location')).toBe('/login'); |
| 164 | + } |
| 165 | + }); |
| 166 | + |
| 167 | + it('should redirect to authorization URL for SSO_required errors', async () => { |
| 168 | + const authUrl = 'https://api.workos.com/sso/authorize'; |
| 169 | + const errorWithSSOCause = new Error('SSO Required', { |
| 170 | + cause: { error: 'sso_required' }, |
| 171 | + }); |
| 172 | + |
| 173 | + refreshSession.mockRejectedValueOnce(errorWithSSOCause); |
| 174 | + (authorizationUrl.getAuthorizationUrl as jest.Mock).mockResolvedValueOnce(authUrl); |
| 175 | + |
| 176 | + const result = await switchToOrganization(request, organizationId); |
| 177 | + |
| 178 | + expect(authorizationUrl.getAuthorizationUrl).toHaveBeenCalled(); |
| 179 | + expect(redirect).toHaveBeenCalledWith(authUrl); |
| 180 | + |
| 181 | + assertIsResponse(result); |
| 182 | + expect(result.status).toBe(302); |
| 183 | + expect(result.headers.get('Location')).toBe(authUrl); |
| 184 | + }); |
| 185 | + |
| 186 | + it('should handle mfa_enrollment errors', async () => { |
| 187 | + const authUrl = 'https://api.workos.com/sso/authorize'; |
| 188 | + const errorWithMFACause = new Error('MFA Enrollment Required', { |
| 189 | + cause: { error: 'mfa_enrollment' }, |
| 190 | + }); |
| 191 | + |
| 192 | + refreshSession.mockRejectedValueOnce(errorWithMFACause); |
| 193 | + (authorizationUrl.getAuthorizationUrl as jest.Mock).mockResolvedValueOnce(authUrl); |
| 194 | + |
| 195 | + const result = await switchToOrganization(request, organizationId); |
| 196 | + |
| 197 | + expect(authorizationUrl.getAuthorizationUrl).toHaveBeenCalled(); |
| 198 | + expect(redirect).toHaveBeenCalledWith(authUrl); |
| 199 | + |
| 200 | + assertIsResponse(result); |
| 201 | + expect(result.status).toBe(302); |
| 202 | + expect(result.headers.get('Location')).toBe(authUrl); |
| 203 | + }); |
| 204 | + |
| 205 | + it('should return error data for Error instances', async () => { |
| 206 | + const error = new Error('Invalid organization'); |
| 207 | + refreshSession.mockRejectedValueOnce(error); |
| 208 | + |
| 209 | + const result = await switchToOrganization(request, organizationId); |
| 210 | + |
| 211 | + expect(data).toHaveBeenCalledWith( |
| 212 | + { |
| 213 | + success: false, |
| 214 | + error: 'Invalid organization', |
| 215 | + }, |
| 216 | + { status: 400 }, |
| 217 | + ); |
| 218 | + expect(result).toEqual({ |
| 219 | + data: { |
| 220 | + success: false, |
| 221 | + error: 'Invalid organization', |
| 222 | + }, |
| 223 | + init: { status: 400 }, |
| 224 | + }); |
| 225 | + }); |
| 226 | + |
| 227 | + it('should return error data for non-Error objects', async () => { |
| 228 | + const error = 'String error message'; |
| 229 | + refreshSession.mockRejectedValueOnce(error); |
| 230 | + |
| 231 | + await switchToOrganization(request, organizationId); |
| 232 | + |
| 233 | + expect(data).toHaveBeenCalledWith( |
| 234 | + { |
| 235 | + success: false, |
| 236 | + error: 'String error message', |
| 237 | + }, |
| 238 | + { status: 400 }, |
| 239 | + ); |
| 240 | + }); |
| 241 | + |
| 242 | + it('should handle when Set-Cookie header is missing', async () => { |
| 243 | + // Create a mock without the Set-Cookie header |
| 244 | + const mockResponseWithoutCookie = { |
| 245 | + ...mockAuthResponse, |
| 246 | + headers: {}, |
| 247 | + }; |
| 248 | + refreshSession.mockResolvedValueOnce(mockResponseWithoutCookie); |
| 249 | + |
| 250 | + await switchToOrganization(request, organizationId); |
| 251 | + |
| 252 | + expect(data).toHaveBeenCalledWith( |
| 253 | + { success: true, auth: mockResponseWithoutCookie }, |
| 254 | + { |
| 255 | + headers: { |
| 256 | + 'Set-Cookie': '', |
| 257 | + }, |
| 258 | + }, |
| 259 | + ); |
| 260 | + }); |
| 261 | + |
| 262 | + it('should handle when returnTo is provided but Set-Cookie header is missing', async () => { |
| 263 | + // Create a mock without the Set-Cookie header |
| 264 | + const mockResponseWithoutCookie = { |
| 265 | + ...mockAuthResponse, |
| 266 | + headers: {}, |
| 267 | + }; |
| 268 | + refreshSession.mockResolvedValueOnce(mockResponseWithoutCookie); |
| 269 | + |
| 270 | + await switchToOrganization(request, organizationId, { returnTo: '/dashboard' }); |
| 271 | + |
| 272 | + expect(redirect).toHaveBeenCalledWith('/dashboard', { |
| 273 | + headers: { |
| 274 | + 'Set-Cookie': '', |
| 275 | + }, |
| 276 | + }); |
| 277 | + }); |
| 278 | + }); |
42 | 279 | });
|
0 commit comments