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