blob: e686535d94cfb4cfec517a5ff68371813a6f90f0 [file] [log] [blame]
Adrià Vilanova Martínez9c418ab2024-12-05 15:34:40 +01001/**
2 * @jest-environment ./src/xhrInterceptor/fetchProxy/__environments__/fetchEnvironment.ts
3 */
4
5import { beforeEach, describe, expect, it, jest } from '@jest/globals';
6import FetchProxy from './FetchProxy';
7import MessageIdTracker from '../MessageIdTracker';
8import { ResponseModifierPort } from '../ResponseModifier.port';
9import {
10 InterceptorHandlerMock,
11 matchInterceptorsMock,
12 triggerEventMock,
13} from '../interceptors/__mocks__/InterceptorHandler.mock';
14import {
15 Interceptor,
16 InterceptorFilter,
17} from '../interceptors/InterceptorHandler.port';
18
19jest.mock('../interceptors/InterceptorHandler.adapter');
20
21const interceptMock = jest.fn<ResponseModifierPort['intercept']>();
22class MockResponseModifier implements ResponseModifierPort {
23 async intercept(
24 ...args: Parameters<ResponseModifierPort['intercept']>
25 ): ReturnType<ResponseModifierPort['intercept']> {
26 return interceptMock(...args);
27 }
28}
29
30const fetchMock = jest.fn<typeof window.fetch>();
31
32const consoleErrorMock = jest.spyOn(global.console, 'error');
33
34const dummyResponse = new Response('{}', { status: 200 });
35beforeEach(() => {
36 jest.resetAllMocks();
37
38 window.fetch = fetchMock;
39
40 // Sensible defaults which can be overriden in each test.
41 fetchMock.mockResolvedValue(dummyResponse);
42 interceptMock.mockResolvedValue({ wasModified: false });
43 matchInterceptorsMock.mockReturnValue([]);
44 consoleErrorMock.mockImplementation(() => {});
45});
46
47describe('FetchProxy', () => {
48 describe('when calling fetch after enabling interception', () => {
49 const dummyUrl: string = 'https://dummy.avm99963.com/';
50 const dummyInit: RequestInit = {
51 body: '{"1":"request"}',
52 };
53 const dummyInitProtoJs = {
54 body: '["dummy"]',
55 headers: {
56 'Content-Type': 'application/json+protobuf',
57 },
58 };
59
60 let fetchProxy: FetchProxy;
61 beforeEach(() => {
62 fetchProxy = new FetchProxy(
63 new MockResponseModifier(),
64 new InterceptorHandlerMock(),
65 new MessageIdTracker(),
66 );
67 fetchProxy.enableInterception();
68 });
69
70 it('should call originalFetch with the original arguments', async () => {
71 await window.fetch(dummyUrl, dummyInit);
72
73 expect(fetchMock).toBeCalledTimes(1);
74 expect(fetchMock).toBeCalledWith(dummyUrl, dummyInit);
75 });
76
77 it('should remove the X-Client header before passing the request to fetch', async () => {
78 await window.fetch(dummyUrl, {
79 ...dummyInit,
80 headers: {
81 'X-Client': 'twpt',
82 },
83 });
84
85 expect(fetchMock).toBeCalledTimes(1);
86 expect(fetchMock.mock.calls[0][1].headers).toBeDefined();
87 expect(fetchMock.mock.calls[0][1].headers).not.toHaveProperty('X-Client');
88 });
89
90 describe.each(['request', 'response'])(
91 'regarding %s interceptors',
92 (interceptorFilter: InterceptorFilter) => {
93 describe('when no interceptors match', () => {
94 it(`should not send a ${interceptorFilter} interceptor event`, async () => {
95 matchInterceptorsMock.mockReturnValue([]);
96
97 await window.fetch(dummyUrl, dummyInit);
98
99 expect(triggerEventMock).toHaveBeenCalledTimes(0);
100 });
101 });
102
103 describe('when an interceptor matches', () => {
104 const dummyInterceptor: Interceptor = {
105 eventName: 'dummy_event',
106 urlRegex: /.*/,
107 intercepts: interceptorFilter,
108 };
109 const dummyResponse = { 42: 'response' };
110 beforeEach(() => {
111 matchInterceptorsMock.mockImplementation(
112 (filter: InterceptorFilter) =>
113 filter === interceptorFilter ? [dummyInterceptor] : [],
114 );
115 fetchMock.mockResolvedValue(
116 new Response(JSON.stringify(dummyResponse)),
117 );
118 });
119
120 it(`should send a ${interceptorFilter} interceptor event`, async () => {
121 await window.fetch(dummyUrl, dummyInit);
122
123 expect(triggerEventMock).toHaveBeenCalledTimes(1);
124 const expectedBody =
125 interceptorFilter === 'request'
126 ? { 1: 'request' }
127 : dummyResponse;
128 expect(triggerEventMock).toHaveBeenCalledWith(
129 dummyInterceptor.eventName,
130 expectedBody,
131 expect.anything(),
132 );
133 });
134
135 it(`should send a ${interceptorFilter} interceptor event with normalized protobuf when the request is application/json+protobuf`, async () => {
136 fetchMock.mockResolvedValue(new Response('["dummy"]'));
137
138 await window.fetch(dummyUrl, dummyInitProtoJs);
139
140 expect(triggerEventMock).toHaveBeenCalledTimes(1);
141 const eventBody = triggerEventMock.mock.calls[0][1];
142 expect(eventBody[1]).toBe('dummy');
143 });
144
145 it(`should not reject when triggering the event fails`, async () => {
146 triggerEventMock.mockImplementation(() => {
147 throw new Error('dummy error');
148 });
149
150 expect(await window.fetch(dummyUrl, dummyInit)).resolves;
151 });
152
153 // TODO: add test to ensure something is logged when the previous condition happens
154 });
155 },
156 );
157
158 describe('regarding response modifiers', () => {
159 const dummyModifiedResponse = { 99: 'modified' };
160 beforeEach(() => {
161 interceptMock.mockResolvedValue({
162 wasModified: true,
163 modifiedResponse: dummyModifiedResponse,
164 });
165 });
166
167 it('should pass the intercepted response to ResponseModifier', async () => {
168 const dummyResponse = { 1: 'request' };
169 fetchMock.mockResolvedValue(
170 new Response(JSON.stringify(dummyResponse)),
171 );
172
173 await window.fetch(dummyUrl, dummyInit);
174
175 expect(interceptMock).toHaveBeenCalledTimes(1);
176 expect(interceptMock).toHaveBeenCalledWith({
177 url: dummyUrl,
178 originalResponse: dummyResponse,
179 });
180 });
181
182 it('should return the modified response when ResponseModifier modifies it', async () => {
183 const response = await window.fetch(dummyUrl, dummyInit);
184 const result = await response.json();
185
186 expect(result).toEqual(dummyModifiedResponse);
187 });
188
189 it('should not reject when ResponseModifier throws an error', async () => {
190 interceptMock.mockImplementation(() => {
191 throw new Error('dummy error');
192 });
193
194 expect(await window.fetch(dummyUrl, dummyInit)).resolves;
195 });
196 });
197
198 it('should not reject when a body is not passed', async () => {
199 matchInterceptorsMock.mockImplementation((filter: InterceptorFilter) => [
200 {
201 eventName: 'dummy_event',
202 urlRegex: /.*/,
203 intercepts: filter,
204 },
205 ]);
206
207 expect(await window.fetch(dummyUrl)).resolves;
208 });
209 });
210});