Update selected item color in Pass menu
[ProtonMail-WebClient.git] / packages / pass / lib / api / fetch-controller.spec.ts
blob6404b0aeedf4f45782efce263aaadf6f91970614
1 import { uniqueId } from '@proton/pass/utils/string/unique-id';
3 import { type FetchController, fetchControllerFactory, getRequestID, getUID } from './fetch-controller';
5 class MockFetchEvent {
6     public request: Request;
8     public respondWith: (res: Response | PromiseLike<Response>) => void;
10     constructor(_: string, init: { request: Request }) {
11         this.request = init.request;
12         this.respondWith = jest.fn(async (res: Response | PromiseLike<Response>) => {
13             try {
14                 await res;
15             } catch {}
16         });
17     }
20 const FetchEvent = MockFetchEvent as unknown as typeof globalThis.FetchEvent;
21 const asyncNextTick = () => new Promise(process.nextTick);
23 describe('fetch controller', () => {
24     describe('getUID', () => {
25         test('should return correct header value if present', () => {
26             const uid = uniqueId();
27             const request = new Request('https://pass.test/', { headers: { 'X-Pm-Uid': uid } });
28             const event = new FetchEvent('fetch', { request });
29             expect(getUID(event)).toEqual(uid);
30         });
32         test('should return null if header is not present', () => {
33             const request = new Request('https://pass.test/', {});
34             const event = new FetchEvent('fetch', { request });
35             expect(getUID(event)).toBeNull();
36         });
37     });
39     describe('getRequestID', () => {
40         test('should return correct header value if present', () => {
41             const requestId = uniqueId();
42             const request = new Request('https://pass.test/', { headers: { 'X-Pass-Worker-RequestID': requestId } });
43             const event = new FetchEvent('fetch', { request });
44             expect(getRequestID(event)).toEqual(requestId);
45         });
47         test('should return null if header is not present', () => {
48             const request = new Request('https://pass.test/', {});
49             const event = new FetchEvent('fetch', { request });
50             expect(getRequestID(event)).toBeNull();
51         });
52     });
54     describe('FetchController', () => {
55         let fetchController: FetchController;
56         beforeEach(() => (fetchController = fetchControllerFactory()));
58         describe('register', () => {
59             test('should setup abort controller and respond with handler response', async () => {
60                 const requestId = uniqueId();
61                 const headers = { 'X-Pm-Uid': uniqueId(), 'X-Pass-Worker-RequestID': requestId };
62                 const request = new Request('https://pass.test/', { headers });
63                 const response = new Response(uniqueId(), { status: 200 });
64                 const event = new FetchEvent('fetch', { request });
65                 const cloneSpy = jest.spyOn(response, 'clone');
67                 const handler = jest.fn().mockResolvedValue(response);
68                 fetchController.register(handler)(event);
70                 expect(fetchController._controllers.get(requestId)).toBeInstanceOf(AbortController);
72                 await asyncNextTick();
74                 expect(handler).toHaveBeenCalled();
75                 expect(cloneSpy).toHaveBeenCalled();
76                 expect(event.respondWith).toHaveBeenCalled();
78                 const eventResponse = await (event.respondWith as jest.Mock).mock?.lastCall[0];
79                 await expect(eventResponse).toMatchResponse(eventResponse);
80                 expect(fetchController._controllers.get(requestId)).toBeUndefined();
81             });
83             test('should fallback to requestUrl as identifier if no `X-Pass-Worker-RequestID` header', async () => {
84                 const headers = { 'X-Pm-Uid': uniqueId() };
85                 const request = new Request('https://pass.test/', { headers });
86                 const response = new Response(uniqueId(), { status: 200 });
87                 const event = new FetchEvent('fetch', { request });
88                 const cloneSpy = jest.spyOn(response, 'clone');
90                 const handler = jest.fn().mockResolvedValue(response);
92                 fetchController.register(handler)(event);
93                 expect(fetchController._controllers.get('https://pass.test/')).toBeInstanceOf(AbortController);
95                 await asyncNextTick();
97                 expect(handler).toHaveBeenCalled();
98                 expect(cloneSpy).toHaveBeenCalled();
99                 expect(event.respondWith).toHaveBeenCalled();
101                 const eventResponse = await (event.respondWith as jest.Mock<any>).mock?.lastCall[0];
102                 await expect(eventResponse).toMatchResponse(response);
103                 expect(fetchController._controllers.get('https://pass.test/')).toBeUndefined();
104             });
106             test('should allow unauthenticated handlers', async () => {
107                 const request = new Request('https://pass.test/', {});
108                 const response = new Response(uniqueId(), { status: 200 });
109                 const event = new FetchEvent('fetch', { request });
110                 const cloneSpy = jest.spyOn(response, 'clone');
112                 const handler = jest.fn().mockResolvedValue(response);
114                 fetchController.register(handler, { unauthenticated: true })(event);
115                 expect(fetchController._controllers.get('https://pass.test/')).toBeInstanceOf(AbortController);
117                 await asyncNextTick();
119                 expect(handler).toHaveBeenCalled();
120                 expect(cloneSpy).toHaveBeenCalled();
121                 expect(event.respondWith).toHaveBeenCalled();
123                 const eventResponse = await (event.respondWith as jest.Mock<any>).mock?.lastCall[0];
124                 await expect(eventResponse).toMatchResponse(response);
125                 expect(fetchController._controllers.get('https://pass.test/')).toBeUndefined();
126             });
128             test('should noop if handler does not return a response', () => {
129                 const handler = jest.fn();
131                 const requestId = uniqueId();
132                 const headers = { 'X-Pm-Uid': uniqueId(), 'X-Pass-Worker-RequestID': requestId };
133                 const request = new Request('https://pass.test/', { headers });
134                 const response = new Response(uniqueId(), { status: 200 });
135                 const event = new FetchEvent('fetch', { request });
136                 const cloneSpy = jest.spyOn(response, 'clone');
138                 fetchController.register(handler)(event);
140                 expect(handler).toHaveBeenCalled();
141                 expect(cloneSpy).not.toHaveBeenCalled();
142                 expect(event.respondWith).not.toHaveBeenCalled();
143                 expect(fetchController._controllers.get(requestId)).toBeUndefined();
144             });
146             test('should noop if no UID header', () => {
147                 const handler = jest.fn();
149                 const request = new Request('https://pass.test/', {});
150                 const response = new Response(uniqueId(), { status: 403 });
151                 const event = new FetchEvent('fetch', { request });
152                 const cloneSpy = jest.spyOn(response, 'clone');
154                 fetchController.register(handler)(event);
156                 expect(handler).not.toHaveBeenCalled();
157                 expect(cloneSpy).not.toHaveBeenCalled();
158                 expect(event.respondWith).not.toHaveBeenCalled();
159                 expect(fetchController._controllers.get('https://pass.test/')).toBeUndefined();
160             });
162             test('should clear abort controller if handler throws', async () => {
163                 const handler = jest.fn().mockRejectedValue('TestError');
165                 const headers = { 'X-Pm-Uid': uniqueId() };
166                 const request = new Request('https://pass.test/', { headers });
167                 const response = new Response(uniqueId(), { status: 522 });
168                 const event = new FetchEvent('fetch', { request });
169                 const cloneSpy = jest.spyOn(response, 'clone');
171                 fetchController.register(handler)(event);
173                 expect(fetchController._controllers.get('https://pass.test/')).toBeInstanceOf(AbortController);
174                 await asyncNextTick();
176                 expect(handler).toHaveBeenCalled();
177                 expect(cloneSpy).not.toHaveBeenCalled();
178                 expect(event.respondWith).toHaveBeenCalled();
179                 expect(fetchController._controllers.get('https://pass.test/')).toBeUndefined();
180             });
181         });
183         describe('fetch', () => {
184             beforeEach(() => {
185                 global.fetch = jest.fn().mockResolvedValue(new Response());
186             });
188             test('should forward abort signal', async () => {
189                 const request = new Request('https://pass.test/', { headers: {} });
190                 const abort = new AbortController();
191                 await fetchController.fetch(request as Request, abort.signal);
192                 const params = (global.fetch as jest.Mock<any>).mock.lastCall;
194                 expect(global.fetch).toHaveBeenCalledTimes(1);
195                 expect(params[1].signal).toEqual(abort.signal);
196             });
198             test('should fetch with modified request without `X-Pass-Worker-RequestID` header', async () => {
199                 const headers = { 'X-Pass-Worker-RequestID': uniqueId() };
200                 const request = new Request('https://pass.test/', { headers });
201                 const abort = new AbortController();
202                 await fetchController.fetch(request as Request, abort.signal);
203                 const params = (global.fetch as jest.Mock<any>).mock.lastCall;
205                 expect(global.fetch).toHaveBeenCalledTimes(1);
206                 expect(params[0].headers.get('X-Pass-Worker-RequestID')).toBeNull();
207                 expect(params[1].signal).toEqual(abort.signal);
208             });
209         });
211         describe('abort', () => {
212             test('should abort controller for requestId', () => {
213                 const abortCtrl = new AbortController();
214                 jest.spyOn(abortCtrl, 'abort');
215                 fetchController._controllers.set('test', abortCtrl);
216                 fetchController.abort('test');
218                 expect(abortCtrl.abort).toHaveBeenCalled();
219             });
220         });
221     });