-
Notifications
You must be signed in to change notification settings - Fork 239
Expand file tree
/
Copy pathNativeWebAuthProvider.spec.ts
More file actions
230 lines (183 loc) · 8.07 KB
/
Copy pathNativeWebAuthProvider.spec.ts
File metadata and controls
230 lines (183 loc) · 8.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
import { Linking, Platform } from 'react-native';
import { NativeWebAuthProvider } from '../NativeWebAuthProvider';
import { INativeBridge } from '../../bridge';
import { finalizeScope } from '../../../../core/utils';
// 1. Mock the dependencies
jest.mock('react-native', () => ({
Platform: { OS: 'ios' }, // Default to iOS for listener tests
Linking: {
addEventListener: jest.fn(),
},
}));
jest.mock('../../../../core/utils/scope');
const mockBridge: jest.Mocked<INativeBridge> = {
authorize: jest.fn(),
clearSession: jest.fn(),
cancelWebAuth: jest.fn(),
getBundleIdentifier: jest.fn().mockResolvedValue('com.my-app'),
resumeWebAuth: jest.fn(),
// Add stubs for other bridge methods
hasValidInstance: jest.fn(),
initialize: jest.fn(),
saveCredentials: jest.fn(),
getCredentials: jest.fn(),
hasValidCredentials: jest.fn(),
clearCredentials: jest.fn(),
};
const mockFinalizeScope = finalizeScope as jest.Mock;
const mockAddEventListener = Linking.addEventListener as jest.Mock;
describe('NativeWebAuthProvider', () => {
const domain = 'my-tenant.auth0.com';
let provider: NativeWebAuthProvider;
beforeEach(() => {
jest.clearAllMocks();
provider = new NativeWebAuthProvider(mockBridge, domain);
// Provide a default implementation for finalizeScope
mockFinalizeScope.mockImplementation((scope) =>
scope ? `openid ${scope}` : 'openid profile email'
);
// Mock the listener to return a mock subscription object
mockAddEventListener.mockReturnValue({ remove: jest.fn() });
});
describe('authorize', () => {
it('should call finalizeScope with the provided scope', async () => {
await provider.authorize({ scope: 'read:data' });
expect(mockFinalizeScope).toHaveBeenCalledWith('read:data');
});
it('should derive scheme and redirectUri if not provided', async () => {
await provider.authorize({});
const expectedScheme = 'com.my-app.auth0';
const expectedRedirectUri = `com.my-app.auth0://${domain}/ios/com.my-app/callback`;
expect(mockBridge.authorize).toHaveBeenCalledWith(
expect.objectContaining({
redirectUrl: expectedRedirectUri,
}),
expect.objectContaining({
customScheme: expectedScheme,
})
);
});
it('should use provided customScheme and redirectUrl', async () => {
const parameters = { redirectUrl: 'my-app://custom-callback' };
const options = { customScheme: 'my-app' };
await provider.authorize(parameters, options);
expect(mockBridge.authorize).toHaveBeenCalledWith(
expect.objectContaining({ redirectUrl: 'my-app://custom-callback' }),
expect.objectContaining({ customScheme: 'my-app' })
);
});
it('should use legacy scheme when useLegacyCallbackUrl is true', async () => {
const options = { useLegacyCallbackUrl: true };
await provider.authorize({}, options);
const expectedScheme = 'com.my-app'; // No '.auth0' suffix
const expectedRedirectUri = `com.my-app://${domain}/ios/com.my-app/callback`;
expect(mockBridge.authorize).toHaveBeenCalledWith(
expect.objectContaining({ redirectUrl: expectedRedirectUri }),
expect.objectContaining({ customScheme: expectedScheme })
);
});
describe('Linking listener (iOS)', () => {
beforeEach(() => {
Platform.OS = 'ios';
});
it('should add a Linking listener on iOS', async () => {
await provider.authorize({});
expect(Linking.addEventListener).toHaveBeenCalledWith(
'url',
expect.any(Function)
);
});
it('should remove the Linking listener on success', async () => {
const mockSubscription = { remove: jest.fn() };
mockAddEventListener.mockReturnValueOnce(mockSubscription);
mockBridge.authorize.mockResolvedValueOnce({} as any); // Simulate success
await provider.authorize({});
expect(mockSubscription.remove).toHaveBeenCalledTimes(1);
});
it('should remove the Linking listener on failure', async () => {
const mockSubscription = { remove: jest.fn() };
mockAddEventListener.mockReturnValueOnce(mockSubscription);
mockBridge.authorize.mockRejectedValueOnce(new Error('Auth failed'));
// The error is expected and should be caught by the caller.
await expect(provider.authorize({})).rejects.toThrow('Auth failed');
expect(mockSubscription.remove).toHaveBeenCalledTimes(1);
});
it('should call resumeWebAuth when the listener is triggered', async () => {
let listenerCallback: (event: { url: string }) => void = () => {};
mockAddEventListener.mockImplementation((_event, callback) => {
listenerCallback = callback;
return { remove: jest.fn() };
});
// We don't await this, as it would "hang" until the listener is called.
provider.authorize({});
// Simulate the deep link event with a URL matching the Auth0 domain.
const resumeUrl = `https://${domain}/ios/com.my-app/callback?code=123`;
await listenerCallback({ url: resumeUrl });
expect(mockBridge.resumeWebAuth).toHaveBeenCalledWith(resumeUrl);
});
it('should ignore URLs whose hostname does not match the Auth0 domain', async () => {
let listenerCallback: (event: { url: string }) => void = () => {};
const mockSubscription = { remove: jest.fn() };
mockAddEventListener.mockImplementation((_event, callback) => {
listenerCallback = callback;
return mockSubscription;
});
provider.authorize({});
// Simulate a universal link from a different domain.
await listenerCallback({
url: 'https://app.example.com/some-path',
});
expect(mockBridge.resumeWebAuth).not.toHaveBeenCalled();
// The subscription should NOT be removed, so it can still catch the real callback.
expect(mockSubscription.remove).not.toHaveBeenCalled();
});
it('should ignore URLs that cannot be parsed', async () => {
let listenerCallback: (event: { url: string }) => void = () => {};
const mockSubscription = { remove: jest.fn() };
mockAddEventListener.mockImplementation((_event, callback) => {
listenerCallback = callback;
return mockSubscription;
});
provider.authorize({});
// Simulate a malformed URL.
await listenerCallback({ url: 'not-a-valid-url' });
expect(mockBridge.resumeWebAuth).not.toHaveBeenCalled();
expect(mockSubscription.remove).not.toHaveBeenCalled();
});
});
it('should NOT add a Linking listener on Android', async () => {
Platform.OS = 'android';
await provider.authorize({});
expect(Linking.addEventListener).not.toHaveBeenCalled();
});
});
describe('clearSession', () => {
beforeEach(() => {
Platform.OS = 'ios'; // Ensure Platform.OS is set to iOS for clearSession tests
});
it('should derive scheme and returnToUrl if not provided', async () => {
await provider.clearSession({});
const expectedScheme = 'com.my-app.auth0';
const expectedReturnToUrl = `com.my-app.auth0://${domain}/ios/com.my-app/callback`;
expect(mockBridge.clearSession).toHaveBeenCalledWith(
expect.objectContaining({ returnToUrl: expectedReturnToUrl }),
expect.objectContaining({ customScheme: expectedScheme })
);
});
it('should use provided customScheme and returnToUrl', async () => {
const parameters = { returnToUrl: 'my-app://custom-logout' };
const options = { customScheme: 'my-app' };
await provider.clearSession(parameters, options);
expect(mockBridge.clearSession).toHaveBeenCalledWith(
expect.objectContaining({ returnToUrl: 'my-app://custom-logout' }),
expect.objectContaining({ customScheme: 'my-app' })
);
});
});
describe('cancelWebAuth', () => {
it('should call the bridge to cancel the web auth flow', async () => {
await provider.cancelWebAuth();
expect(mockBridge.cancelWebAuth).toHaveBeenCalledTimes(1);
});
});
});