Merge branch 'docs-header-fix' into 'main'
[ProtonMail-WebClient.git] / packages / shared / test / api / authHandlers.spec.js
blobfb52326b66cae3f892b3a7aacac7311a74d41a85
1 import { InactiveSessionError } from '../../lib/api/helpers/errors';
2 import withApiHandlers from '../../lib/api/helpers/withApiHandlers';
3 import { withUIDHeaders } from '../../lib/fetch/headers';
5 const getApiError = ({ message, response = { headers: { get: () => '' } }, data, status }) => {
6     const error = new Error(message);
7     error.status = status;
8     error.data = data;
9     error.response = response;
10     return error;
13 const getApiResult = (result) => {
14     return {
15         headers: {
16             get: () => '',
17         },
18         status: 200,
19         json: () => result,
20     };
23 describe('auth handlers', () => {
24     it('should unlock', async () => {
25         const call = jasmine
26             .createSpy('call')
27             .and.returnValues(Promise.reject(getApiError({ status: 403 })), Promise.resolve(getApiResult('123')));
28         const handleMissingScopes = jasmine.createSpy('unlock').and.callFake(({ options }) => {
29             return call(options);
30         });
31         const api = withApiHandlers({ call, onMissingScopes: handleMissingScopes });
32         const result = await api({}).then((r) => r.json());
33         expect(result).toBe('123');
34         expect(handleMissingScopes).toHaveBeenCalledTimes(1);
35         expect(call).toHaveBeenCalledTimes(2);
36     });
38     it('should unlock and be cancellable', async () => {
39         const unlockError = getApiError({ status: 403 });
40         const call = jasmine.createSpy('call').and.returnValues(Promise.reject(unlockError));
41         const handleUnlock = jasmine.createSpy('unlock').and.returnValues(Promise.reject(unlockError));
42         const handleError = jasmine.createSpy('error').and.callFake((e) => {
43             return e;
44         });
45         const api = withApiHandlers({ call, onMissingScopes: handleUnlock });
46         const error = await api({}).catch(handleError);
47         expect(error).toBe(unlockError);
48         expect(handleError).toHaveBeenCalledTimes(1);
49         expect(handleUnlock).toHaveBeenCalledTimes(1);
50         expect(call).toHaveBeenCalledTimes(1);
51     });
53     it('should retry 429 status', async () => {
54         const call = jasmine
55             .createSpy('call')
56             .and.returnValues(Promise.reject(getApiError({ status: 429 })), Promise.resolve(getApiResult('123')));
57         const handleError = jasmine.createSpy('error').and.callFake((e) => {
58             return e;
59         });
60         const api = withApiHandlers({ call });
61         const result = await api({})
62             .then((r) => r.json())
63             .catch(handleError);
64         expect(result).toBe('123');
65         expect(call).toHaveBeenCalledTimes(2);
66         expect(handleError).toHaveBeenCalledTimes(0);
67     });
69     it('should not retry 429 status if disabled', async () => {
70         const call = jasmine
71             .createSpy('call')
72             .and.returnValues(Promise.reject(getApiError({ status: 429 })), Promise.resolve(getApiResult('123')));
73         const handleError = jasmine.createSpy('error').and.callFake((e) => {
74             return e;
75         });
76         const api = withApiHandlers({ call });
77         const error = await api({ ignoreHandler: [429] }).catch(handleError);
78         expect(error.status).toBe(429);
79         expect(call).toHaveBeenCalledTimes(1);
80         expect(handleError).toHaveBeenCalledTimes(1);
81     });
83     it('should retry maximum 5 times', async () => {
84         const returns = [
85             () => Promise.reject(getApiError({ status: 429 })),
86             () => Promise.reject(getApiError({ status: 429 })),
87             () => Promise.reject(getApiError({ status: 429 })),
88             () => Promise.reject(getApiError({ status: 429 })),
89             () => Promise.reject(getApiError({ status: 429 })),
90         ];
91         let i = 0;
92         const call = jasmine.createSpy('call').and.callFake(() => returns[i++]());
93         const handleError = jasmine.createSpy('error').and.callFake((e) => {
94             return e;
95         });
96         const api = withApiHandlers({ call });
97         const error = await api({}).catch(handleError);
98         expect(error.status).toBe(429);
99         expect(call).toHaveBeenCalledTimes(5);
100         expect(handleError).toHaveBeenCalledTimes(1);
101     });
103     it('should not handle retry when its greater than 10', async () => {
104         const call = jasmine
105             .createSpy('call')
106             .and.returnValues(Promise.reject(getApiError({ status: 429, response: { headers: { get: () => '10' } } })));
107         const handleError = jasmine.createSpy('error').and.callFake((e) => {
108             return e;
109         });
110         const api = withApiHandlers({ call });
111         const error = await api({}).catch(handleError);
112         expect(error.status).toBe(429);
113         expect(call).toHaveBeenCalledTimes(1);
114         expect(handleError).toHaveBeenCalledTimes(1);
115     });
117     it('should not refresh if has no session', async () => {
118         const call = jasmine.createSpy('call').and.returnValues(Promise.reject(getApiError({ status: 401 })));
119         const handleError = jasmine.createSpy('error').and.callFake((e) => {
120             return e;
121         });
122         const api = withApiHandlers({ call });
123         const error = await api({}).catch(handleError);
124         expect(error.status).toBe(401);
125         expect(call).toHaveBeenCalledTimes(1);
126         expect(handleError).toHaveBeenCalledTimes(1);
127     });
129     it('should refresh once (if has session)', async () => {
130         let refreshed = false;
131         let refreshCalls = 0;
132         const call = jasmine.createSpy('call').and.callFake(async (args) => {
133             if (args.url === 'auth/refresh') {
134                 refreshed = true;
135                 refreshCalls++;
136                 return {
137                     headers: { get: () => '1' },
138                 };
139             }
140             if (!refreshed) {
141                 throw getApiError({ status: 401 });
142             }
143             return args;
144         });
145         const handleError = jasmine.createSpy('error').and.callFake((e) => {
146             return e;
147         });
148         const apiWithHandlers = withApiHandlers({ call });
149         apiWithHandlers.UID = '123';
150         const api = (a) => apiWithHandlers(a).catch(handleError);
151         const result = await Promise.all([api(123), api(231), api(321)]);
152         expect(result).toEqual([123, 231, 321]);
153         expect(call).toHaveBeenCalledTimes(7);
154         expect(handleError).toHaveBeenCalledTimes(0);
155         expect(refreshCalls).toBe(1);
156     });
158     it('should refresh once and fail all active calls (if has session)', async () => {
159         const call = jasmine.createSpy('call').and.callFake(async (args) => {
160             if (args.url === 'auth/refresh') {
161                 throw getApiError({ status: 422, data: args });
162             }
163             throw getApiError({ status: 401, data: args });
164         });
165         const handleError = jasmine.createSpy('error').and.callFake((e) => {
166             throw e;
167         });
168         const apiWithHandlers = withApiHandlers({ call });
169         apiWithHandlers.UID = '123';
170         const api = (a) => apiWithHandlers(a).catch(handleError);
171         const [p1, p2, p3] = [api(123), api(231), api(321)];
172         await expectAsync(p1).toBeRejectedWith(InactiveSessionError());
173         await expectAsync(p2).toBeRejectedWith(InactiveSessionError());
174         await expectAsync(p3).toBeRejectedWith(InactiveSessionError());
175         expect(call).toHaveBeenCalledTimes(4);
176         expect(handleError).toHaveBeenCalledTimes(3);
177     });
179     it('should refresh once and only logout if it is a 4xx error', async () => {
180         const returns = [
181             () => Promise.reject(getApiError({ status: 401 })),
182             () => Promise.reject(getApiError({ status: 500 })),
183             () => Promise.reject(getApiError({ status: 401 })),
184             () => Promise.reject(getApiError({ status: 422 })),
185         ];
186         let i = 0;
187         const call = jasmine.createSpy('call').and.callFake(() => returns[i++]());
189         const handleError = jasmine.createSpy('error').and.callFake((e) => {
190             return e;
191         });
192         const api = withApiHandlers({ call });
193         api.UID = '123';
195         const error = await api(123).catch(handleError);
196         expect(error.status).toBe(500);
197         expect(call).toHaveBeenCalledTimes(2);
198         expect(handleError).toHaveBeenCalledTimes(1);
200         const r2 = api(123);
201         await expectAsync(r2).toBeRejectedWith(InactiveSessionError());
202         expect(call).toHaveBeenCalledTimes(4);
203     });
205     it('should only error with InactiveSession if the initial UID is the same', async () => {
206         const returns = [
207             () => Promise.reject(getApiError({ status: 401 })),
208             () => Promise.reject(getApiError({ status: 400 })),
209             () => Promise.reject(getApiError({ status: 401 })),
210             () => Promise.reject(getApiError({ status: 400 })),
211         ];
212         let i = 0;
213         const call = jasmine.createSpy('call').and.callFake(() => returns[i++]());
214         const handleError = jasmine.createSpy('error').and.callFake((e) => {
215             return e;
216         });
217         const api = withApiHandlers({ call });
218         api.UID = '123';
220         const error = await api(withUIDHeaders('321', {})).catch(handleError);
221         expect(error.status).toBe(401);
222         expect(call).toHaveBeenCalledTimes(2);
223         expect(handleError).toHaveBeenCalledTimes(1);
225         const error2 = await api({}).catch(handleError);
226         expect(error2.name).toBe('InactiveSession');
227         expect(call).toHaveBeenCalledTimes(4);
228         expect(handleError).toHaveBeenCalledTimes(2);
229     });
231     it('should refresh once and handle 429 max attempts', async () => {
232         const returns = [
233             () => Promise.reject(getApiError({ status: 401 })),
234             () => Promise.reject(getApiError({ status: 429 })),
235             () => Promise.reject(getApiError({ status: 429 })),
236             () => Promise.reject(getApiError({ status: 429 })),
237             () => Promise.reject(getApiError({ status: 429 })),
238             () => Promise.reject(getApiError({ status: 429 })),
239             () => Promise.reject(getApiError({ status: 429 })),
240         ];
241         let i = 0;
242         const call = jasmine.createSpy('call').and.callFake(() => returns[i++]());
244         const api = withApiHandlers({ call });
245         api.UID = '126';
246         await expectAsync(api(123)).toBeRejectedWith(InactiveSessionError());
247         expect(call).toHaveBeenCalledTimes(6);
248     });
250     it('should refresh once and handle 429', async () => {
251         const returns = [
252             () => Promise.reject(getApiError({ status: 401 })), // need refresh
253             () => Promise.reject(getApiError({ status: 429 })), // retry
254             () => Promise.reject(getApiError({ status: 429 })), // retry
255             () => Promise.resolve(getApiResult('')), // refresh ok
256             () => Promise.resolve(getApiResult('123')), // actual result
257         ];
258         let i = 0;
259         const call = jasmine.createSpy('call').and.callFake(() => returns[i++]());
260         const api = withApiHandlers({ call });
261         api.UID = 'abc';
262         const result = await api(123).then((result) => result.json());
263         expect(call).toHaveBeenCalledTimes(5);
264         expect(result).toBe('123');
265     });
267     it('should fail all calls after it has logged out', async () => {
268         const call = jasmine.createSpy('call').and.callFake(async (args) => {
269             throw getApiError({ status: 401, data: args });
270         });
271         const api = withApiHandlers({ call });
272         api.UID = '128';
273         await expectAsync(api()).toBeRejectedWith(InactiveSessionError());
274         expect(call).toHaveBeenCalledTimes(2);
275         await expectAsync(api()).toBeRejectedWith(InactiveSessionError());
276         expect(call).toHaveBeenCalledTimes(2);
277     });