Skip to content

Commit cd4dce4

Browse files
author
Ahmed Hamouda
committed
feat(rtn-web-browser): add unit tests
1 parent 652416c commit cd4dce4

File tree

6 files changed

+456
-1
lines changed

6 files changed

+456
-1
lines changed
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { AppState, Linking, Platform } from 'react-native';
5+
6+
import { openAuthSessionAsync } from '../../src/apis/openAuthSessionAsync';
7+
import { nativeModule } from '../../src/nativeModule';
8+
import {
9+
mockDeepLinkUrl,
10+
mockRedirectUrls,
11+
mockReturnUrl,
12+
mockUrl,
13+
} from '../testUtils/data';
14+
15+
// Mock React Native modules
16+
jest.mock('react-native', () => ({
17+
Platform: { OS: 'ios' },
18+
AppState: {
19+
currentState: 'active',
20+
addEventListener: jest.fn(),
21+
},
22+
Linking: {
23+
addEventListener: jest.fn(),
24+
},
25+
}));
26+
27+
// Mock EmitterSubscription type
28+
const mockEmitterSubscription = {
29+
remove: jest.fn(),
30+
emitter: {},
31+
listener: jest.fn(),
32+
context: {},
33+
eventType: 'test',
34+
key: 'test-key',
35+
subscriber: {},
36+
} as any;
37+
38+
// Mock native module
39+
jest.mock('../../src/nativeModule', () => ({
40+
nativeModule: {
41+
openAuthSessionAsync: jest.fn(),
42+
},
43+
}));
44+
45+
describe('openAuthSessionAsync', () => {
46+
const mockNativeModule = nativeModule.openAuthSessionAsync as jest.Mock;
47+
const mockPlatform = Platform as jest.Mocked<typeof Platform>;
48+
const mockAppState = AppState as jest.Mocked<typeof AppState>;
49+
const mockLinking = Linking as jest.Mocked<typeof Linking>;
50+
51+
beforeEach(() => {
52+
jest.clearAllMocks();
53+
});
54+
55+
describe('iOS platform', () => {
56+
beforeEach(() => {
57+
mockPlatform.OS = 'ios';
58+
});
59+
60+
it('calls iOS native module with correct parameters', async () => {
61+
mockNativeModule.mockResolvedValue(mockReturnUrl);
62+
63+
const result = await openAuthSessionAsync(mockUrl, mockRedirectUrls);
64+
65+
expect(mockNativeModule).toHaveBeenCalledWith(
66+
'https://example.com/auth',
67+
mockDeepLinkUrl,
68+
false,
69+
);
70+
expect(result).toBe(mockReturnUrl);
71+
});
72+
73+
it('enforces HTTPS URLs', async () => {
74+
const httpUrl = 'http://example.com/auth';
75+
mockNativeModule.mockResolvedValue(mockReturnUrl);
76+
77+
await openAuthSessionAsync(httpUrl, mockRedirectUrls);
78+
79+
expect(mockNativeModule).toHaveBeenCalledWith(
80+
'https://example.com/auth',
81+
mockDeepLinkUrl,
82+
false,
83+
);
84+
});
85+
86+
it('passes prefersEphemeralSession parameter', async () => {
87+
mockNativeModule.mockResolvedValue(mockReturnUrl);
88+
89+
await openAuthSessionAsync(mockUrl, mockRedirectUrls, true);
90+
91+
expect(mockNativeModule).toHaveBeenCalledWith(
92+
'https://example.com/auth',
93+
mockDeepLinkUrl,
94+
true,
95+
);
96+
});
97+
98+
it('finds first non-web redirect URL', async () => {
99+
const redirectUrls = [
100+
'https://web.com',
101+
'myapp://callback',
102+
'anotherapp://test',
103+
];
104+
mockNativeModule.mockResolvedValue(mockReturnUrl);
105+
106+
await openAuthSessionAsync(mockUrl, redirectUrls);
107+
108+
expect(mockNativeModule).toHaveBeenCalledWith(
109+
'https://example.com/auth',
110+
'myapp://callback',
111+
false,
112+
);
113+
});
114+
115+
it('handles undefined redirect URL when no deep links found', async () => {
116+
const webOnlyUrls = ['https://web.com', 'http://another.com'];
117+
mockNativeModule.mockResolvedValue(mockReturnUrl);
118+
119+
await openAuthSessionAsync(mockUrl, webOnlyUrls);
120+
121+
expect(mockNativeModule).toHaveBeenCalledWith(
122+
'https://example.com/auth',
123+
undefined,
124+
false,
125+
);
126+
});
127+
});
128+
129+
describe('Android platform', () => {
130+
beforeEach(() => {
131+
mockPlatform.OS = 'android';
132+
mockAppState.currentState = 'active';
133+
});
134+
135+
it('sets up listeners and calls native module', async () => {
136+
const mockAppStateListener = { ...mockEmitterSubscription };
137+
const mockLinkingListener = { ...mockEmitterSubscription };
138+
139+
mockAppState.addEventListener.mockReturnValue(mockAppStateListener);
140+
mockLinking.addEventListener.mockReturnValue(mockLinkingListener);
141+
mockNativeModule.mockResolvedValue(undefined);
142+
143+
// Simulate app state change from background to active
144+
mockAppState.currentState = 'background';
145+
mockAppState.addEventListener.mockImplementation((event, callback) => {
146+
// Immediately trigger the callback to resolve the promise
147+
setTimeout(() => {
148+
callback('active');
149+
}, 0);
150+
151+
return mockAppStateListener;
152+
});
153+
154+
const result = await openAuthSessionAsync(mockUrl, mockRedirectUrls);
155+
156+
expect(mockNativeModule).toHaveBeenCalledWith('https://example.com/auth');
157+
expect(mockAppState.addEventListener).toHaveBeenCalledWith(
158+
'change',
159+
expect.any(Function),
160+
);
161+
expect(mockLinking.addEventListener).toHaveBeenCalledWith(
162+
'url',
163+
expect.any(Function),
164+
);
165+
expect(result).toBeNull();
166+
}, 10000);
167+
168+
it('resolves with redirect URL when matching URL received', async () => {
169+
const mockAppStateListener = { ...mockEmitterSubscription };
170+
const mockLinkingListener = { ...mockEmitterSubscription };
171+
172+
mockAppState.addEventListener.mockReturnValue(mockAppStateListener);
173+
mockLinking.addEventListener.mockImplementation((event, callback) => {
174+
// Immediately trigger the callback to resolve the promise
175+
setTimeout(() => {
176+
callback({ url: mockReturnUrl });
177+
}, 0);
178+
179+
return mockLinkingListener;
180+
});
181+
mockNativeModule.mockResolvedValue(undefined);
182+
183+
const result = await openAuthSessionAsync(mockUrl, mockRedirectUrls);
184+
185+
expect(result).toBe(mockReturnUrl);
186+
}, 10000);
187+
188+
it('ignores non-matching redirect URLs', async () => {
189+
const mockAppStateListener = { ...mockEmitterSubscription };
190+
const mockLinkingListener = { ...mockEmitterSubscription };
191+
192+
mockAppState.currentState = 'background';
193+
let appStateCallback: any;
194+
195+
mockAppState.addEventListener.mockImplementation((event, callback) => {
196+
appStateCallback = callback;
197+
198+
return mockAppStateListener;
199+
});
200+
201+
mockLinking.addEventListener.mockImplementation((event, callback) => {
202+
// First call with non-matching URL (should be ignored)
203+
setTimeout(() => {
204+
callback({ url: 'other://app' });
205+
}, 0);
206+
// Then trigger app state change to resolve
207+
setTimeout(() => {
208+
appStateCallback('active');
209+
}, 10);
210+
211+
return mockLinkingListener;
212+
});
213+
214+
mockNativeModule.mockResolvedValue(undefined);
215+
216+
const result = await openAuthSessionAsync(mockUrl, mockRedirectUrls);
217+
218+
expect(result).toBeNull();
219+
}, 10000);
220+
221+
it('cleans up listeners after completion', async () => {
222+
const mockAppStateListener = { ...mockEmitterSubscription };
223+
const mockLinkingListener = { ...mockEmitterSubscription };
224+
225+
mockAppState.currentState = 'background';
226+
mockAppState.addEventListener.mockReturnValue(mockAppStateListener);
227+
mockLinking.addEventListener.mockReturnValue(mockLinkingListener);
228+
mockNativeModule.mockResolvedValue(undefined);
229+
230+
mockAppState.addEventListener.mockImplementation((event, callback) => {
231+
setTimeout(() => {
232+
callback('active');
233+
}, 0);
234+
235+
return mockAppStateListener;
236+
});
237+
238+
await openAuthSessionAsync(mockUrl, mockRedirectUrls);
239+
240+
expect(mockAppStateListener.remove).toHaveBeenCalled();
241+
expect(mockLinkingListener.remove).toHaveBeenCalled();
242+
}, 10000);
243+
244+
it('handles app state transition from background to active', async () => {
245+
const mockAppStateListener = { ...mockEmitterSubscription };
246+
const mockLinkingListener = { ...mockEmitterSubscription };
247+
248+
// Set initial state to background to test the transition
249+
mockAppState.currentState = 'background';
250+
mockAppState.addEventListener.mockReturnValue(mockAppStateListener);
251+
mockLinking.addEventListener.mockReturnValue(mockLinkingListener);
252+
mockNativeModule.mockResolvedValue(undefined);
253+
254+
mockAppState.addEventListener.mockImplementation((event, callback) => {
255+
// Simulate transition from background to active
256+
setTimeout(() => {
257+
callback('active');
258+
}, 0);
259+
260+
return mockAppStateListener;
261+
});
262+
263+
const result = await openAuthSessionAsync(mockUrl, mockRedirectUrls);
264+
265+
expect(result).toBeNull();
266+
}, 10000);
267+
268+
it('handles app state change when already active', async () => {
269+
const mockAppStateListener = { ...mockEmitterSubscription };
270+
const mockLinkingListener = { ...mockEmitterSubscription };
271+
272+
// Set initial state to active
273+
mockAppState.currentState = 'active';
274+
mockAppState.addEventListener.mockReturnValue(mockAppStateListener);
275+
mockLinking.addEventListener.mockReturnValue(mockLinkingListener);
276+
mockNativeModule.mockResolvedValue(undefined);
277+
278+
mockAppState.addEventListener.mockImplementation((event, callback) => {
279+
// Simulate state change from active to background then back to active
280+
setTimeout(() => {
281+
callback('background'); // This should not trigger resolve
282+
setTimeout(() => {
283+
callback('active');
284+
}, 0); // This should trigger resolve
285+
}, 0);
286+
287+
return mockAppStateListener;
288+
});
289+
290+
const result = await openAuthSessionAsync(mockUrl, mockRedirectUrls);
291+
292+
expect(result).toBeNull();
293+
}, 10000);
294+
});
295+
296+
describe('unsupported platform', () => {
297+
it('returns undefined for unsupported platforms', async () => {
298+
mockPlatform.OS = 'web' as any;
299+
300+
const result = await openAuthSessionAsync(mockUrl, mockRedirectUrls);
301+
302+
expect(result).toBeUndefined();
303+
expect(mockNativeModule).not.toHaveBeenCalled();
304+
});
305+
});
306+
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { PACKAGE_NAME } from '../src/constants';
5+
6+
jest.mock('react-native', () => ({
7+
Platform: {
8+
select: jest.fn(),
9+
},
10+
}));
11+
12+
// const mockPlatformSelect = require('react-native').Platform.select;
13+
14+
describe('constants', () => {
15+
beforeEach(() => {
16+
jest.clearAllMocks();
17+
});
18+
19+
it('exports correct package name', () => {
20+
expect(PACKAGE_NAME).toBe('@aws-amplify/rtn-web-browser');
21+
});
22+
23+
it('generates iOS-specific linking error', () => {
24+
// Re-mock after resetModules
25+
jest.resetModules();
26+
jest.doMock('react-native', () => ({
27+
Platform: {
28+
select: jest.fn().mockReturnValue("- You have run 'pod install'\n"),
29+
},
30+
}));
31+
32+
const { LINKING_ERROR: freshLinkingError } = require('../src/constants');
33+
34+
expect(freshLinkingError).toContain('pod install');
35+
expect(freshLinkingError).toContain('@aws-amplify/rtn-web-browser');
36+
expect(freshLinkingError).toContain('rebuilt the app');
37+
expect(freshLinkingError).toContain('not using Expo Go');
38+
});
39+
40+
it('generates generic linking error for other platforms', () => {
41+
// Re-mock after resetModules
42+
jest.resetModules();
43+
jest.doMock('react-native', () => ({
44+
Platform: {
45+
select: jest.fn().mockReturnValue(''),
46+
},
47+
}));
48+
49+
const { LINKING_ERROR: freshLinkingError } = require('../src/constants');
50+
51+
expect(freshLinkingError).toContain('rebuilt the app');
52+
expect(freshLinkingError).toContain('@aws-amplify/rtn-web-browser');
53+
expect(freshLinkingError).not.toContain('pod install');
54+
});
55+
});

0 commit comments

Comments
 (0)