Update selected item color in Pass menu
[ProtonMail-WebClient.git] / packages / pass / lib / api / refresh.spec.ts
blob7240aebecc393842739e7174319d6afe37a592fd
1 import { AuthMode } from '@proton/pass/types';
2 import { RETRY_ATTEMPTS_MAX } from '@proton/shared/lib/constants';
3 import { HTTP_ERROR_CODES } from '@proton/shared/lib/errors';
4 import * as time from '@proton/shared/lib/helpers/promise';
6 import { refreshHandlerFactory } from './refresh';
7 import { TEST_SERVER_TIME, mockAPIResponse } from './testing';
9 const { TOO_MANY_REQUESTS } = HTTP_ERROR_CODES;
11 describe('Refresh handlers', () => {
12     jest.useFakeTimers().setSystemTime(TEST_SERVER_TIME);
14     const call = jest.fn();
15     const onRefresh = jest.fn();
16     const getAuth = jest.fn();
17     const wait = jest.spyOn(time, 'wait').mockImplementation(() => Promise.resolve());
19     const getMockResponse = (date: Date = new Date()) =>
20         ({ headers: { get: () => date.toString() } }) as unknown as Response;
22     beforeEach(() => {
23         call.mockClear();
24         onRefresh.mockClear();
25         getAuth.mockClear();
26         wait.mockClear();
27     });
29     test('should throw InactiveSession error if no auth', async () => {
30         const refresh = refreshHandlerFactory({ call, getAuth, onRefresh });
31         await expect(refresh(getMockResponse(), {})).rejects.toThrow('Inactive session');
32     });
34     test('should call refresh in `AuthMode.TOKEN`', async () => {
35         getAuth.mockReturnValue({
36             type: AuthMode.TOKEN,
37             AccessToken: 'access-000',
38             UID: 'id-000',
39             RefreshToken: 'refresh-000',
40         });
42         call.mockResolvedValue(
43             mockAPIResponse({ UID: 'id-000', AccessToken: 'access-001', RefreshToken: 'refresh-001' })
44         );
45         const refresh = refreshHandlerFactory({ call, getAuth, onRefresh });
46         await refresh(getMockResponse(TEST_SERVER_TIME), {});
48         expect(call).toHaveBeenCalledTimes(1);
49         const [args] = call.mock.calls[0];
51         expect(args.data).toEqual({
52             GrantType: 'refresh_token',
53             RedirectURI: 'https://protonmail.com',
54             RefreshToken: 'refresh-000',
55             ResponseType: 'token',
56         });
58         expect(args.headers['x-pm-uid']).toEqual('id-000');
60         expect(onRefresh).toHaveBeenCalledTimes(1);
61         expect(onRefresh).toHaveBeenCalledWith({
62             UID: 'id-000',
63             AccessToken: 'access-001',
64             RefreshToken: 'refresh-001',
65             RefreshTime: +TEST_SERVER_TIME,
66         });
67     });
69     test('should call refresh in `AuthMode.COOKIE`', async () => {
70         getAuth.mockReturnValue({ type: AuthMode.COOKIE, UID: 'id-000' });
72         call.mockResolvedValue(mockAPIResponse({ UID: 'id-000' }));
73         const refresh = refreshHandlerFactory({ call, getAuth, onRefresh });
74         await refresh(getMockResponse(TEST_SERVER_TIME), {});
76         expect(call).toHaveBeenCalledTimes(1);
77         const [args] = call.mock.calls[0];
79         expect(args.data).toBeUndefined();
80         expect(args.headers['x-pm-uid']).toEqual('id-000');
82         expect(onRefresh).toHaveBeenCalledTimes(1);
83         expect(onRefresh).toHaveBeenCalledWith({
84             UID: 'id-000',
85             AccessToken: '',
86             RefreshToken: '',
87             RefreshTime: +TEST_SERVER_TIME,
88             cookies: true,
89         });
90     });
92     test('should use underlying api call `auth` option if provided', async () => {
93         getAuth.mockReturnValue({ type: AuthMode.COOKIE, UID: 'id-000' });
94         const auth = { type: AuthMode.COOKIE, UID: 'id-001' } as const;
96         call.mockResolvedValue(mockAPIResponse({ UID: 'id-001' }));
97         const refresh = refreshHandlerFactory({ call, getAuth, onRefresh });
98         await refresh(getMockResponse(TEST_SERVER_TIME), { auth });
100         expect(call).toHaveBeenCalledTimes(1);
101         const [args] = call.mock.calls[0];
103         expect(args.data).toBeUndefined();
104         expect(args.headers['x-pm-uid']).toEqual('id-001');
106         expect(onRefresh).toHaveBeenCalledTimes(1);
107         expect(onRefresh).toHaveBeenCalledWith({
108             UID: 'id-001',
109             AccessToken: '',
110             RefreshToken: '',
111             RefreshTime: +TEST_SERVER_TIME,
112             cookies: true,
113         });
114     });
116     test('should call refresh only once concurrently', async () => {
117         getAuth.mockReturnValue({
118             type: AuthMode.TOKEN,
119             AccessToken: 'access-000',
120             UID: 'id-000',
121             RefreshToken: 'refresh-000',
122         });
124         call.mockResolvedValue(mockAPIResponse({ RefreshToken: 'refresh-001' }));
125         const refresh = refreshHandlerFactory({ call, getAuth, onRefresh });
126         const res = getMockResponse(TEST_SERVER_TIME);
128         await Promise.all([refresh(res, {}), refresh(res, {}), refresh(res, {})]);
129         expect(call).toHaveBeenCalledTimes(1);
130         expect(onRefresh).toHaveBeenCalledTimes(1);
132         await refresh(res, {});
133         expect(call).toHaveBeenCalledTimes(2);
134         expect(onRefresh).toHaveBeenCalledTimes(2);
135     });
137     test('should not refresh if last refresh time is greater than request to refresh', async () => {
138         getAuth.mockReturnValue({
139             type: AuthMode.TOKEN,
140             AccessToken: 'access-000',
141             UID: 'id-000',
142             RefreshToken: 'refresh-000',
143             RefreshTime: +TEST_SERVER_TIME + 10,
144         });
146         call.mockResolvedValue(mockAPIResponse({ RefreshToken: 'refresh-001' }));
147         const refresh = refreshHandlerFactory({ call, getAuth, onRefresh });
148         await refresh(getMockResponse(TEST_SERVER_TIME), {});
150         expect(call).not.toHaveBeenCalled();
151         expect(onRefresh).not.toHaveBeenCalled();
152     });
154     test('should not retry if timeout error', async () => {
155         const timeoutError = new Error();
156         timeoutError.name = 'TimeoutError';
157         getAuth.mockReturnValue({
158             type: AuthMode.TOKEN,
159             AccessToken: 'access-000',
160             UID: 'id-000',
161             RefreshToken: 'refresh-000',
162         });
163         call.mockRejectedValueOnce(timeoutError);
165         const refresh = refreshHandlerFactory({ call, getAuth, onRefresh });
166         await expect(refresh(getMockResponse(), {})).rejects.toThrow();
168         expect(call).toHaveBeenCalledTimes(1);
169         expect(onRefresh).not.toHaveBeenCalled();
170     });
172     test('should not retry if offline error', async () => {
173         const offlineError = new Error();
174         offlineError.name = 'OfflineError';
175         getAuth.mockReturnValue({
176             type: AuthMode.TOKEN,
177             AccessToken: 'access-000',
178             UID: 'id-000',
179             RefreshToken: 'refresh-000',
180         });
181         call.mockRejectedValueOnce(offlineError);
183         const refresh = refreshHandlerFactory({ call, getAuth, onRefresh });
184         await expect(refresh(getMockResponse(), {})).rejects.toThrow();
186         expect(call).toHaveBeenCalledTimes(1);
187         expect(onRefresh).not.toHaveBeenCalled();
188     });
190     test('should retry if `retry-after` header present', async () => {
191         getAuth.mockReturnValue({
192             type: AuthMode.TOKEN,
193             AccessToken: 'access-000',
194             UID: 'id-000',
195             RefreshToken: 'refresh-000',
196         });
198         call.mockRejectedValueOnce(mockAPIResponse({}, TOO_MANY_REQUESTS, { 'retry-after': '10' }));
199         call.mockResolvedValueOnce(mockAPIResponse({ RefreshToken: 'refresh-001' }));
201         const refresh = refreshHandlerFactory({ call, getAuth, onRefresh });
202         await refresh(getMockResponse(), {});
204         expect(call).toHaveBeenCalledTimes(2);
205         expect(onRefresh).toHaveBeenCalledTimes(1);
206         expect(onRefresh).toHaveBeenCalledWith({ RefreshToken: 'refresh-001', RefreshTime: +TEST_SERVER_TIME });
207     });
209     test('should stop retrying after `maxAttempts` is reached', async () => {
210         getAuth.mockReturnValue({
211             type: AuthMode.TOKEN,
212             AccessToken: 'access-000',
213             UID: 'id-000',
214             RefreshToken: 'refresh-000',
215         });
217         call.mockRejectedValue(mockAPIResponse({}, TOO_MANY_REQUESTS, { 'retry-after': '10' }));
219         const refresh = refreshHandlerFactory({ call, getAuth, onRefresh });
220         await expect(refresh(getMockResponse(), {})).rejects.toBeTruthy();
221         expect(call).toHaveBeenCalledTimes(RETRY_ATTEMPTS_MAX);
222         expect(onRefresh).not.toHaveBeenCalled();
223     });