zero-query 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +2 -0
- package/cli/args.js +33 -33
- package/cli/commands/build-api.js +443 -442
- package/cli/commands/build.js +254 -247
- package/cli/commands/bundle.js +1228 -1224
- package/cli/commands/create.js +137 -121
- package/cli/commands/dev/devtools/index.js +56 -56
- package/cli/commands/dev/devtools/js/components.js +49 -49
- package/cli/commands/dev/devtools/js/core.js +423 -423
- package/cli/commands/dev/devtools/js/elements.js +421 -421
- package/cli/commands/dev/devtools/js/network.js +166 -166
- package/cli/commands/dev/devtools/js/performance.js +73 -73
- package/cli/commands/dev/devtools/js/router.js +105 -105
- package/cli/commands/dev/devtools/js/source.js +132 -132
- package/cli/commands/dev/devtools/js/stats.js +35 -35
- package/cli/commands/dev/devtools/js/tabs.js +79 -79
- package/cli/commands/dev/devtools/panel.html +95 -95
- package/cli/commands/dev/devtools/styles.css +244 -244
- package/cli/commands/dev/index.js +107 -107
- package/cli/commands/dev/logger.js +75 -75
- package/cli/commands/dev/overlay.js +858 -858
- package/cli/commands/dev/server.js +220 -220
- package/cli/commands/dev/validator.js +94 -94
- package/cli/commands/dev/watcher.js +172 -172
- package/cli/help.js +114 -112
- package/cli/index.js +52 -52
- package/cli/scaffold/default/LICENSE +21 -21
- package/cli/scaffold/default/app/app.js +207 -207
- package/cli/scaffold/default/app/components/about.js +201 -201
- package/cli/scaffold/default/app/components/api-demo.js +143 -143
- package/cli/scaffold/default/app/components/contact-card.js +231 -231
- package/cli/scaffold/default/app/components/contacts/contacts.css +706 -706
- package/cli/scaffold/default/app/components/contacts/contacts.html +200 -200
- package/cli/scaffold/default/app/components/contacts/contacts.js +196 -196
- package/cli/scaffold/default/app/components/counter.js +127 -127
- package/cli/scaffold/default/app/components/home.js +249 -249
- package/cli/scaffold/default/app/components/not-found.js +16 -16
- package/cli/scaffold/default/app/components/playground/playground.css +115 -115
- package/cli/scaffold/default/app/components/playground/playground.html +161 -161
- package/cli/scaffold/default/app/components/playground/playground.js +116 -116
- package/cli/scaffold/default/app/components/todos.js +225 -225
- package/cli/scaffold/default/app/components/toolkit/toolkit.css +97 -97
- package/cli/scaffold/default/app/components/toolkit/toolkit.html +146 -146
- package/cli/scaffold/default/app/components/toolkit/toolkit.js +280 -280
- package/cli/scaffold/default/app/routes.js +15 -15
- package/cli/scaffold/default/app/store.js +101 -101
- package/cli/scaffold/default/global.css +552 -552
- package/cli/scaffold/default/index.html +99 -99
- package/cli/scaffold/minimal/app/app.js +85 -85
- package/cli/scaffold/minimal/app/components/about.js +68 -68
- package/cli/scaffold/minimal/app/components/counter.js +122 -122
- package/cli/scaffold/minimal/app/components/home.js +68 -68
- package/cli/scaffold/minimal/app/components/not-found.js +16 -16
- package/cli/scaffold/minimal/app/routes.js +9 -9
- package/cli/scaffold/minimal/app/store.js +36 -36
- package/cli/scaffold/minimal/global.css +300 -300
- package/cli/scaffold/minimal/index.html +44 -44
- package/cli/scaffold/ssr/app/app.js +41 -41
- package/cli/scaffold/ssr/app/components/about.js +55 -55
- package/cli/scaffold/ssr/app/components/blog/index.js +65 -65
- package/cli/scaffold/ssr/app/components/blog/post.js +86 -86
- package/cli/scaffold/ssr/app/components/home.js +37 -37
- package/cli/scaffold/ssr/app/components/not-found.js +15 -15
- package/cli/scaffold/ssr/app/routes.js +8 -8
- package/cli/scaffold/ssr/global.css +228 -228
- package/cli/scaffold/ssr/index.html +37 -37
- package/cli/scaffold/ssr/package.json +8 -8
- package/cli/scaffold/ssr/server/data/posts.js +144 -144
- package/cli/scaffold/ssr/server/index.js +213 -213
- package/cli/scaffold/webrtc/app/app.js +11 -0
- package/cli/scaffold/webrtc/app/components/video-room.js +295 -0
- package/cli/scaffold/webrtc/app/lib/room.js +252 -0
- package/cli/scaffold/webrtc/assets/.gitkeep +0 -0
- package/cli/scaffold/webrtc/global.css +250 -0
- package/cli/scaffold/webrtc/index.html +21 -0
- package/cli/utils.js +305 -287
- package/dist/API.md +661 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +10313 -6614
- package/dist/zquery.min.js +8 -631
- package/index.d.ts +570 -371
- package/index.js +311 -240
- package/package.json +76 -70
- package/src/component.js +1709 -1691
- package/src/core.js +921 -921
- package/src/diff.js +497 -497
- package/src/errors.js +209 -209
- package/src/expression.js +922 -922
- package/src/http.js +242 -242
- package/src/package.json +1 -1
- package/src/reactive.js +255 -255
- package/src/router.js +843 -843
- package/src/ssr.js +418 -418
- package/src/store.js +318 -318
- package/src/utils.js +515 -515
- package/src/webrtc/e2ee.js +351 -0
- package/src/webrtc/errors.js +116 -0
- package/src/webrtc/ice.js +301 -0
- package/src/webrtc/index.js +131 -0
- package/src/webrtc/joinToken.js +119 -0
- package/src/webrtc/observe.js +172 -0
- package/src/webrtc/peer.js +351 -0
- package/src/webrtc/reactive.js +268 -0
- package/src/webrtc/room.js +625 -0
- package/src/webrtc/sdp.js +302 -0
- package/src/webrtc/sfu/index.js +43 -0
- package/src/webrtc/sfu/livekit.js +131 -0
- package/src/webrtc/sfu/mediasoup.js +150 -0
- package/src/webrtc/signaling.js +373 -0
- package/src/webrtc/turn.js +237 -0
- package/tests/_helpers/webrtcFakes.js +289 -0
- package/tests/audit.test.js +4158 -4158
- package/tests/cli.test.js +1136 -1103
- package/tests/compare.test.js +497 -486
- package/tests/component.test.js +3969 -3938
- package/tests/core.test.js +1910 -1910
- package/tests/dev-server.test.js +489 -489
- package/tests/diff.test.js +1416 -1416
- package/tests/docs.test.js +1664 -1650
- package/tests/electron-features.test.js +864 -864
- package/tests/errors.test.js +619 -619
- package/tests/expression.test.js +1056 -1056
- package/tests/http.test.js +648 -648
- package/tests/reactive.test.js +819 -819
- package/tests/router.test.js +2327 -2327
- package/tests/ssr.test.js +870 -870
- package/tests/store.test.js +830 -830
- package/tests/test-minifier.js +153 -153
- package/tests/test-ssr.js +27 -27
- package/tests/utils.test.js +1377 -1377
- package/tests/webrtc/e2ee.test.js +283 -0
- package/tests/webrtc/ice.test.js +202 -0
- package/tests/webrtc/joinToken.test.js +89 -0
- package/tests/webrtc/observe.test.js +111 -0
- package/tests/webrtc/peer.test.js +373 -0
- package/tests/webrtc/reactive.test.js +235 -0
- package/tests/webrtc/room.test.js +406 -0
- package/tests/webrtc/sdp.test.js +151 -0
- package/tests/webrtc/sfu-livekit.test.js +119 -0
- package/tests/webrtc/sfu.test.js +160 -0
- package/tests/webrtc/signaling.test.js +251 -0
- package/tests/webrtc/turn.test.js +256 -0
- package/types/collection.d.ts +383 -383
- package/types/component.d.ts +186 -186
- package/types/errors.d.ts +135 -135
- package/types/http.d.ts +92 -92
- package/types/misc.d.ts +201 -201
- package/types/reactive.d.ts +98 -98
- package/types/router.d.ts +190 -190
- package/types/ssr.d.ts +102 -102
- package/types/store.d.ts +146 -146
- package/types/utils.d.ts +245 -245
- package/types/webrtc.d.ts +653 -0
package/tests/http.test.js
CHANGED
|
@@ -1,648 +1,648 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { http } from '../src/http.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// Mock fetch
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
let fetchSpy;
|
|
9
|
-
|
|
10
|
-
function mockFetch(response = {}, ok = true, status = 200) {
|
|
11
|
-
const body = typeof response === 'string' ? response : JSON.stringify(response);
|
|
12
|
-
const contentType = typeof response === 'string' ? 'text/plain' : 'application/json';
|
|
13
|
-
|
|
14
|
-
fetchSpy = vi.fn(() =>
|
|
15
|
-
Promise.resolve({
|
|
16
|
-
ok,
|
|
17
|
-
status,
|
|
18
|
-
statusText: ok ? 'OK' : 'Error',
|
|
19
|
-
headers: {
|
|
20
|
-
get: (h) => h.toLowerCase() === 'content-type' ? contentType : null,
|
|
21
|
-
entries: () => [[`content-type`, contentType]],
|
|
22
|
-
},
|
|
23
|
-
json: () => Promise.resolve(typeof response === 'object' ? response : JSON.parse(response)),
|
|
24
|
-
text: () => Promise.resolve(body),
|
|
25
|
-
blob: () => Promise.resolve(new Blob([body])),
|
|
26
|
-
})
|
|
27
|
-
);
|
|
28
|
-
globalThis.fetch = fetchSpy;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
beforeEach(() => {
|
|
32
|
-
http.configure({ baseURL: '', headers: { 'Content-Type': 'application/json' }, timeout: 30000 });
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
afterEach(() => {
|
|
36
|
-
vi.restoreAllMocks();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
// GET requests
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
|
|
44
|
-
describe('http.get', () => {
|
|
45
|
-
it('makes a GET request', async () => {
|
|
46
|
-
mockFetch({ users: [] });
|
|
47
|
-
const result = await http.get('https://api.test.com/users');
|
|
48
|
-
expect(fetchSpy).toHaveBeenCalledOnce();
|
|
49
|
-
expect(result.data).toEqual({ users: [] });
|
|
50
|
-
expect(result.ok).toBe(true);
|
|
51
|
-
expect(result.status).toBe(200);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('appends query params for GET', async () => {
|
|
55
|
-
mockFetch({});
|
|
56
|
-
await http.get('https://api.test.com/search', { q: 'hello', page: '1' });
|
|
57
|
-
const url = fetchSpy.mock.calls[0][0];
|
|
58
|
-
expect(url).toContain('q=hello');
|
|
59
|
-
expect(url).toContain('page=1');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('uses baseURL', async () => {
|
|
63
|
-
http.configure({ baseURL: 'https://api.test.com' });
|
|
64
|
-
mockFetch({});
|
|
65
|
-
await http.get('/users');
|
|
66
|
-
expect(fetchSpy.mock.calls[0][0]).toBe('https://api.test.com/users');
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
// POST requests
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
|
|
75
|
-
describe('http.post', () => {
|
|
76
|
-
it('sends JSON body', async () => {
|
|
77
|
-
mockFetch({ id: 1 });
|
|
78
|
-
const result = await http.post('https://api.test.com/users', { name: 'Tony' });
|
|
79
|
-
const opts = fetchSpy.mock.calls[0][1];
|
|
80
|
-
expect(opts.method).toBe('POST');
|
|
81
|
-
expect(opts.body).toBe(JSON.stringify({ name: 'Tony' }));
|
|
82
|
-
expect(result.data).toEqual({ id: 1 });
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('handles FormData body', async () => {
|
|
86
|
-
mockFetch({});
|
|
87
|
-
const form = new FormData();
|
|
88
|
-
form.append('file', 'data');
|
|
89
|
-
await http.post('https://api.test.com/upload', form);
|
|
90
|
-
const opts = fetchSpy.mock.calls[0][1];
|
|
91
|
-
expect(opts.body).toBe(form);
|
|
92
|
-
// Content-Type should be removed for FormData
|
|
93
|
-
expect(opts.headers['Content-Type']).toBeUndefined();
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
// Error handling
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
|
-
|
|
102
|
-
describe('http - error handling', () => {
|
|
103
|
-
it('throws on non-ok response', async () => {
|
|
104
|
-
mockFetch({ error: 'Not Found' }, false, 404);
|
|
105
|
-
await expect(http.get('https://api.test.com/missing')).rejects.toThrow('HTTP 404');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('error includes response data', async () => {
|
|
109
|
-
mockFetch({ message: 'Unauthorized' }, false, 401);
|
|
110
|
-
try {
|
|
111
|
-
await http.get('https://api.test.com/protected');
|
|
112
|
-
} catch (err) {
|
|
113
|
-
expect(err.response).toBeDefined();
|
|
114
|
-
expect(err.response.status).toBe(401);
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('throws on invalid URL', async () => {
|
|
119
|
-
await expect(http.get(null)).rejects.toThrow('requires a URL string');
|
|
120
|
-
await expect(http.get(undefined)).rejects.toThrow('requires a URL string');
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
// ---------------------------------------------------------------------------
|
|
126
|
-
// PUT / PATCH / DELETE
|
|
127
|
-
// ---------------------------------------------------------------------------
|
|
128
|
-
|
|
129
|
-
describe('http - other methods', () => {
|
|
130
|
-
it('PUT sends correct method', async () => {
|
|
131
|
-
mockFetch({});
|
|
132
|
-
await http.put('https://api.test.com/users/1', { name: 'Updated' });
|
|
133
|
-
expect(fetchSpy.mock.calls[0][1].method).toBe('PUT');
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('PATCH sends correct method', async () => {
|
|
137
|
-
mockFetch({});
|
|
138
|
-
await http.patch('https://api.test.com/users/1', { name: 'Patched' });
|
|
139
|
-
expect(fetchSpy.mock.calls[0][1].method).toBe('PATCH');
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('DELETE sends correct method', async () => {
|
|
143
|
-
mockFetch({});
|
|
144
|
-
await http.delete('https://api.test.com/users/1');
|
|
145
|
-
expect(fetchSpy.mock.calls[0][1].method).toBe('DELETE');
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
// ---------------------------------------------------------------------------
|
|
151
|
-
// configure
|
|
152
|
-
// ---------------------------------------------------------------------------
|
|
153
|
-
|
|
154
|
-
describe('http.configure', () => {
|
|
155
|
-
it('sets baseURL', async () => {
|
|
156
|
-
http.configure({ baseURL: 'https://example.com/api' });
|
|
157
|
-
mockFetch({});
|
|
158
|
-
await http.get('/data');
|
|
159
|
-
expect(fetchSpy.mock.calls[0][0]).toBe('https://example.com/api/data');
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('merges headers', async () => {
|
|
163
|
-
http.configure({ headers: { Authorization: 'Bearer token' } });
|
|
164
|
-
mockFetch({});
|
|
165
|
-
await http.get('https://api.test.com/me');
|
|
166
|
-
expect(fetchSpy.mock.calls[0][1].headers.Authorization).toBe('Bearer token');
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
// ---------------------------------------------------------------------------
|
|
172
|
-
// Text response
|
|
173
|
-
// ---------------------------------------------------------------------------
|
|
174
|
-
|
|
175
|
-
describe('http - text response', () => {
|
|
176
|
-
it('parses text response', async () => {
|
|
177
|
-
mockFetch('Hello World');
|
|
178
|
-
const result = await http.get('https://api.test.com/text');
|
|
179
|
-
expect(result.data).toBe('Hello World');
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// ---------------------------------------------------------------------------
|
|
185
|
-
// Interceptors
|
|
186
|
-
// ---------------------------------------------------------------------------
|
|
187
|
-
|
|
188
|
-
describe('http - interceptors', () => {
|
|
189
|
-
it('request interceptor via onRequest', async () => {
|
|
190
|
-
http.configure({ baseURL: '' });
|
|
191
|
-
http.onRequest((fetchOpts, url) => {
|
|
192
|
-
fetchOpts.headers['X-Custom'] = 'test';
|
|
193
|
-
});
|
|
194
|
-
mockFetch({});
|
|
195
|
-
await http.get('https://api.test.com/data');
|
|
196
|
-
const opts = fetchSpy.mock.calls[0][1];
|
|
197
|
-
expect(opts.headers['X-Custom']).toBe('test');
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('response interceptor via onResponse', async () => {
|
|
201
|
-
http.onResponse((result) => {
|
|
202
|
-
result.intercepted = true;
|
|
203
|
-
});
|
|
204
|
-
mockFetch({ x: 1 });
|
|
205
|
-
const result = await http.get('https://api.test.com/data');
|
|
206
|
-
expect(result.intercepted).toBe(true);
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
// ---------------------------------------------------------------------------
|
|
212
|
-
// Timeout / abort
|
|
213
|
-
// ---------------------------------------------------------------------------
|
|
214
|
-
|
|
215
|
-
describe('http - abort signal', () => {
|
|
216
|
-
it('passes signal through options', async () => {
|
|
217
|
-
const controller = new AbortController();
|
|
218
|
-
mockFetch({});
|
|
219
|
-
await http.get('https://api.test.com/data', null, { signal: controller.signal });
|
|
220
|
-
const opts = fetchSpy.mock.calls[0][1];
|
|
221
|
-
// Signal may be combined via AbortSignal.any - verify it responds to user abort
|
|
222
|
-
expect(opts.signal).toBeDefined();
|
|
223
|
-
expect(opts.signal.aborted).toBe(false);
|
|
224
|
-
controller.abort();
|
|
225
|
-
expect(opts.signal.aborted).toBe(true);
|
|
226
|
-
});
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
// ---------------------------------------------------------------------------
|
|
231
|
-
// Blob response
|
|
232
|
-
// ---------------------------------------------------------------------------
|
|
233
|
-
|
|
234
|
-
describe('http - blob response', () => {
|
|
235
|
-
it('can request blob responses', async () => {
|
|
236
|
-
mockFetch('binary data');
|
|
237
|
-
const result = await http.get('https://api.test.com/file', null, { responseType: 'blob' });
|
|
238
|
-
// Should have a data field (blob or fallback text)
|
|
239
|
-
expect(result.data).toBeDefined();
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// ---------------------------------------------------------------------------
|
|
245
|
-
// HEAD and OPTIONS methods
|
|
246
|
-
// ---------------------------------------------------------------------------
|
|
247
|
-
|
|
248
|
-
describe('http - raw fetch pass-through', () => {
|
|
249
|
-
it('raw() delegates to native fetch', async () => {
|
|
250
|
-
mockFetch({ ok: true });
|
|
251
|
-
await http.raw('https://api.test.com/ping', { method: 'HEAD' });
|
|
252
|
-
expect(fetchSpy).toHaveBeenCalledWith('https://api.test.com/ping', { method: 'HEAD' });
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
// ---------------------------------------------------------------------------
|
|
258
|
-
// Request with custom headers
|
|
259
|
-
// ---------------------------------------------------------------------------
|
|
260
|
-
|
|
261
|
-
describe('http - custom per-request headers', () => {
|
|
262
|
-
it('merges per-request headers', async () => {
|
|
263
|
-
mockFetch({});
|
|
264
|
-
await http.get('https://api.test.com/data', null, {
|
|
265
|
-
headers: { 'X-Request-Id': '123' },
|
|
266
|
-
});
|
|
267
|
-
const opts = fetchSpy.mock.calls[0][1];
|
|
268
|
-
expect(opts.headers['X-Request-Id']).toBe('123');
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
// ---------------------------------------------------------------------------
|
|
274
|
-
// Response metadata
|
|
275
|
-
// ---------------------------------------------------------------------------
|
|
276
|
-
|
|
277
|
-
describe('http - response metadata', () => {
|
|
278
|
-
it('includes status and ok in result', async () => {
|
|
279
|
-
mockFetch({ data: 'yes' }, true, 200);
|
|
280
|
-
const result = await http.get('https://api.test.com/data');
|
|
281
|
-
expect(result.ok).toBe(true);
|
|
282
|
-
expect(result.status).toBe(200);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it('includes statusText in error', async () => {
|
|
286
|
-
mockFetch({ error: 'bad' }, false, 500);
|
|
287
|
-
try {
|
|
288
|
-
await http.get('https://api.test.com/fail');
|
|
289
|
-
} catch (err) {
|
|
290
|
-
expect(err.message).toContain('500');
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
// ===========================================================================
|
|
297
|
-
// configure()
|
|
298
|
-
// ===========================================================================
|
|
299
|
-
|
|
300
|
-
describe('http.configure', () => {
|
|
301
|
-
it('sets baseURL', async () => {
|
|
302
|
-
http.configure({ baseURL: 'https://api.myapp.com' });
|
|
303
|
-
mockFetch({ ok: true });
|
|
304
|
-
await http.get('/users');
|
|
305
|
-
expect(fetchSpy.mock.calls[0][0]).toBe('https://api.myapp.com/users');
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
it('merges headers', async () => {
|
|
309
|
-
http.configure({ headers: { Authorization: 'Bearer token123' } });
|
|
310
|
-
mockFetch({ ok: true });
|
|
311
|
-
await http.get('https://api.test.com/me');
|
|
312
|
-
const opts = fetchSpy.mock.calls[0][1];
|
|
313
|
-
expect(opts.headers['Authorization']).toBe('Bearer token123');
|
|
314
|
-
expect(opts.headers['Content-Type']).toBe('application/json');
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
it('sets timeout', () => {
|
|
318
|
-
http.configure({ timeout: 5000 });
|
|
319
|
-
// Just verifying it doesn't throw
|
|
320
|
-
expect(true).toBe(true);
|
|
321
|
-
});
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
// ===========================================================================
|
|
326
|
-
// interceptors
|
|
327
|
-
// ===========================================================================
|
|
328
|
-
|
|
329
|
-
describe('http.onRequest - extra', () => {
|
|
330
|
-
it('interceptor modifying URL', async () => {
|
|
331
|
-
// Note: interceptors from earlier tests may still be registered; this one
|
|
332
|
-
// only adds a query param so it doesn't break others
|
|
333
|
-
http.onRequest((opts, url) => ({ url: url + '?token=abc', options: opts }));
|
|
334
|
-
mockFetch({ ok: true });
|
|
335
|
-
await http.get('https://api.test.com/data');
|
|
336
|
-
expect(fetchSpy.mock.calls[0][0]).toContain('?token=abc');
|
|
337
|
-
});
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
// Test interceptor blocking in isolation - it permanently modifies internal state,
|
|
341
|
-
// so we verify behavior without adding a blocking interceptor that breaks later tests
|
|
342
|
-
describe('http - interceptor blocking concept', () => {
|
|
343
|
-
it('onRequest returning false would throw blocked error', () => {
|
|
344
|
-
// Just verify the error message format expected by the source code
|
|
345
|
-
expect(() => { throw new Error('Request blocked by interceptor'); }).toThrow('blocked by interceptor');
|
|
346
|
-
});
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
describe('http.onResponse - extra', () => {
|
|
350
|
-
it('response interceptors are additive', async () => {
|
|
351
|
-
// onResponse was already called in earlier tests; verify the callback
|
|
352
|
-
const spy = vi.fn();
|
|
353
|
-
http.onResponse(spy);
|
|
354
|
-
mockFetch({ ok: true });
|
|
355
|
-
await http.get('https://api.test.com/data123');
|
|
356
|
-
expect(spy).toHaveBeenCalledTimes(1);
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
// ===========================================================================
|
|
362
|
-
// DELETE with body
|
|
363
|
-
// ===========================================================================
|
|
364
|
-
|
|
365
|
-
describe('http.delete', () => {
|
|
366
|
-
it('sends a DELETE request', async () => {
|
|
367
|
-
mockFetch({ deleted: true });
|
|
368
|
-
const result = await http.delete('https://api.test.com/item/1');
|
|
369
|
-
expect(fetchSpy.mock.calls[0][1].method).toBe('DELETE');
|
|
370
|
-
expect(result.data).toEqual({ deleted: true });
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
it('DELETE with body', async () => {
|
|
374
|
-
mockFetch({ deleted: true });
|
|
375
|
-
await http.delete('https://api.test.com/items', { ids: [1, 2] });
|
|
376
|
-
const opts = fetchSpy.mock.calls[0][1];
|
|
377
|
-
expect(opts.method).toBe('DELETE');
|
|
378
|
-
expect(JSON.parse(opts.body)).toEqual({ ids: [1, 2] });
|
|
379
|
-
});
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
// ===========================================================================
|
|
384
|
-
// GET with existing query string
|
|
385
|
-
// ===========================================================================
|
|
386
|
-
|
|
387
|
-
describe('http.get - query params', () => {
|
|
388
|
-
it('appends params with & when URL already has ?', async () => {
|
|
389
|
-
mockFetch({ ok: true });
|
|
390
|
-
await http.get('https://api.test.com/search?q=hello', { page: 2 });
|
|
391
|
-
expect(fetchSpy.mock.calls[0][0]).toContain('?q=hello&page=2');
|
|
392
|
-
});
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
// ===========================================================================
|
|
397
|
-
// string body
|
|
398
|
-
// ===========================================================================
|
|
399
|
-
|
|
400
|
-
describe('http.post - string body', () => {
|
|
401
|
-
it('sends string body as-is', async () => {
|
|
402
|
-
mockFetch({ ok: true });
|
|
403
|
-
await http.post('https://api.test.com/raw', 'plain text body');
|
|
404
|
-
const opts = fetchSpy.mock.calls[0][1];
|
|
405
|
-
expect(opts.body).toBe('plain text body');
|
|
406
|
-
});
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
// ===========================================================================
|
|
411
|
-
// URL validation
|
|
412
|
-
// ===========================================================================
|
|
413
|
-
|
|
414
|
-
describe('http - URL validation', () => {
|
|
415
|
-
it('throws on missing URL', async () => {
|
|
416
|
-
await expect(http.get(undefined)).rejects.toThrow('URL string');
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
it('throws on non-string URL', async () => {
|
|
420
|
-
await expect(http.get(123)).rejects.toThrow('URL string');
|
|
421
|
-
});
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
// ===========================================================================
|
|
426
|
-
// createAbort
|
|
427
|
-
// ===========================================================================
|
|
428
|
-
|
|
429
|
-
describe('http.createAbort', () => {
|
|
430
|
-
it('returns an AbortController', () => {
|
|
431
|
-
const controller = http.createAbort();
|
|
432
|
-
expect(controller).toBeInstanceOf(AbortController);
|
|
433
|
-
});
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
// ===========================================================================
|
|
438
|
-
// timeout / abort
|
|
439
|
-
// ===========================================================================
|
|
440
|
-
|
|
441
|
-
describe('http - AbortController integration', () => {
|
|
442
|
-
it('createAbort signal can be passed as option', async () => {
|
|
443
|
-
const controller = http.createAbort();
|
|
444
|
-
mockFetch({ ok: true });
|
|
445
|
-
await http.get('https://api.test.com/data', undefined, { signal: controller.signal });
|
|
446
|
-
expect(fetchSpy).toHaveBeenCalled();
|
|
447
|
-
});
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
// ===========================================================================
|
|
452
|
-
// HEAD requests
|
|
453
|
-
// ===========================================================================
|
|
454
|
-
|
|
455
|
-
describe('http.head', () => {
|
|
456
|
-
it('sends a HEAD request', async () => {
|
|
457
|
-
mockFetch({});
|
|
458
|
-
const result = await http.head('https://api.test.com/resource');
|
|
459
|
-
expect(fetchSpy.mock.calls[0][1].method).toBe('HEAD');
|
|
460
|
-
expect(result.ok).toBe(true);
|
|
461
|
-
expect(result.status).toBe(200);
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
it('does not send a body', async () => {
|
|
465
|
-
mockFetch({});
|
|
466
|
-
await http.head('https://api.test.com/resource');
|
|
467
|
-
const opts = fetchSpy.mock.calls[0][1];
|
|
468
|
-
expect(opts.body).toBeUndefined();
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
it('accepts per-request options', async () => {
|
|
472
|
-
mockFetch({});
|
|
473
|
-
await http.head('https://api.test.com/resource', {
|
|
474
|
-
headers: { 'X-Check': 'exists' },
|
|
475
|
-
});
|
|
476
|
-
const opts = fetchSpy.mock.calls[0][1];
|
|
477
|
-
expect(opts.headers['X-Check']).toBe('exists');
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
it('returns response headers', async () => {
|
|
481
|
-
mockFetch({});
|
|
482
|
-
const result = await http.head('https://api.test.com/resource');
|
|
483
|
-
expect(result.headers).toBeDefined();
|
|
484
|
-
expect(typeof result.headers).toBe('object');
|
|
485
|
-
});
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
// ===========================================================================
|
|
490
|
-
// Interceptor unsubscribe
|
|
491
|
-
// ===========================================================================
|
|
492
|
-
|
|
493
|
-
describe('http - interceptor unsubscribe', () => {
|
|
494
|
-
it('onRequest returns an unsubscribe function', async () => {
|
|
495
|
-
http.clearInterceptors();
|
|
496
|
-
const spy = vi.fn();
|
|
497
|
-
const unsub = http.onRequest(spy);
|
|
498
|
-
expect(typeof unsub).toBe('function');
|
|
499
|
-
|
|
500
|
-
mockFetch({});
|
|
501
|
-
await http.get('https://api.test.com/a');
|
|
502
|
-
expect(spy).toHaveBeenCalledTimes(1);
|
|
503
|
-
|
|
504
|
-
unsub();
|
|
505
|
-
await http.get('https://api.test.com/b');
|
|
506
|
-
expect(spy).toHaveBeenCalledTimes(1); // not called again
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
it('onResponse returns an unsubscribe function', async () => {
|
|
510
|
-
http.clearInterceptors();
|
|
511
|
-
const spy = vi.fn();
|
|
512
|
-
const unsub = http.onResponse(spy);
|
|
513
|
-
expect(typeof unsub).toBe('function');
|
|
514
|
-
|
|
515
|
-
mockFetch({});
|
|
516
|
-
await http.get('https://api.test.com/a');
|
|
517
|
-
expect(spy).toHaveBeenCalledTimes(1);
|
|
518
|
-
|
|
519
|
-
unsub();
|
|
520
|
-
await http.get('https://api.test.com/b');
|
|
521
|
-
expect(spy).toHaveBeenCalledTimes(1);
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
it('double-unsubscribe is safe', async () => {
|
|
525
|
-
http.clearInterceptors();
|
|
526
|
-
const spy = vi.fn();
|
|
527
|
-
const unsub = http.onRequest(spy);
|
|
528
|
-
unsub();
|
|
529
|
-
unsub(); // should not throw
|
|
530
|
-
mockFetch({});
|
|
531
|
-
await http.get('https://api.test.com/data');
|
|
532
|
-
expect(spy).not.toHaveBeenCalled();
|
|
533
|
-
});
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
// ===========================================================================
|
|
538
|
-
// clearInterceptors
|
|
539
|
-
// ===========================================================================
|
|
540
|
-
|
|
541
|
-
describe('http.clearInterceptors', () => {
|
|
542
|
-
it('clears all interceptors when called with no args', async () => {
|
|
543
|
-
http.clearInterceptors();
|
|
544
|
-
const reqSpy = vi.fn();
|
|
545
|
-
const resSpy = vi.fn();
|
|
546
|
-
http.onRequest(reqSpy);
|
|
547
|
-
http.onResponse(resSpy);
|
|
548
|
-
|
|
549
|
-
http.clearInterceptors();
|
|
550
|
-
mockFetch({});
|
|
551
|
-
await http.get('https://api.test.com/data');
|
|
552
|
-
expect(reqSpy).not.toHaveBeenCalled();
|
|
553
|
-
expect(resSpy).not.toHaveBeenCalled();
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
it('clears only request interceptors with "request"', async () => {
|
|
557
|
-
http.clearInterceptors();
|
|
558
|
-
const reqSpy = vi.fn();
|
|
559
|
-
const resSpy = vi.fn();
|
|
560
|
-
http.onRequest(reqSpy);
|
|
561
|
-
http.onResponse(resSpy);
|
|
562
|
-
|
|
563
|
-
http.clearInterceptors('request');
|
|
564
|
-
mockFetch({});
|
|
565
|
-
await http.get('https://api.test.com/data');
|
|
566
|
-
expect(reqSpy).not.toHaveBeenCalled();
|
|
567
|
-
expect(resSpy).toHaveBeenCalledTimes(1);
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
it('clears only response interceptors with "response"', async () => {
|
|
571
|
-
http.clearInterceptors();
|
|
572
|
-
const reqSpy = vi.fn();
|
|
573
|
-
const resSpy = vi.fn();
|
|
574
|
-
http.onRequest(reqSpy);
|
|
575
|
-
http.onResponse(resSpy);
|
|
576
|
-
|
|
577
|
-
http.clearInterceptors('response');
|
|
578
|
-
mockFetch({});
|
|
579
|
-
await http.get('https://api.test.com/data');
|
|
580
|
-
expect(reqSpy).toHaveBeenCalledTimes(1);
|
|
581
|
-
expect(resSpy).not.toHaveBeenCalled();
|
|
582
|
-
});
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
// ===========================================================================
|
|
587
|
-
// http.all - parallel requests
|
|
588
|
-
// ===========================================================================
|
|
589
|
-
|
|
590
|
-
describe('http.all', () => {
|
|
591
|
-
it('resolves all parallel requests', async () => {
|
|
592
|
-
mockFetch({ ok: true });
|
|
593
|
-
const results = await http.all([
|
|
594
|
-
http.get('https://api.test.com/a'),
|
|
595
|
-
http.get('https://api.test.com/b'),
|
|
596
|
-
http.get('https://api.test.com/c'),
|
|
597
|
-
]);
|
|
598
|
-
expect(results).toHaveLength(3);
|
|
599
|
-
expect(results.every(r => r.ok)).toBe(true);
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
it('rejects if any request fails', async () => {
|
|
603
|
-
http.clearInterceptors();
|
|
604
|
-
mockFetch({ error: 'fail' }, false, 500);
|
|
605
|
-
await expect(
|
|
606
|
-
http.all([
|
|
607
|
-
http.get('https://api.test.com/a'),
|
|
608
|
-
http.get('https://api.test.com/b'),
|
|
609
|
-
])
|
|
610
|
-
).rejects.toThrow();
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
it('handles empty array', async () => {
|
|
614
|
-
const results = await http.all([]);
|
|
615
|
-
expect(results).toEqual([]);
|
|
616
|
-
});
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
// ===========================================================================
|
|
621
|
-
// http.getConfig
|
|
622
|
-
// ===========================================================================
|
|
623
|
-
|
|
624
|
-
describe('http.getConfig', () => {
|
|
625
|
-
it('returns current config', () => {
|
|
626
|
-
http.configure({ baseURL: 'https://myapi.com', timeout: 5000 });
|
|
627
|
-
const config = http.getConfig();
|
|
628
|
-
expect(config.baseURL).toBe('https://myapi.com');
|
|
629
|
-
expect(config.timeout).toBe(5000);
|
|
630
|
-
expect(config.headers).toBeDefined();
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
it('returns a copy (not the internal reference)', () => {
|
|
634
|
-
const config = http.getConfig();
|
|
635
|
-
config.baseURL = 'https://mutated.com';
|
|
636
|
-
config.headers['X-Evil'] = 'injected';
|
|
637
|
-
const fresh = http.getConfig();
|
|
638
|
-
expect(fresh.baseURL).not.toBe('https://mutated.com');
|
|
639
|
-
expect(fresh.headers['X-Evil']).toBeUndefined();
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
it('reflects updates after configure', () => {
|
|
643
|
-
http.configure({ baseURL: '' });
|
|
644
|
-
expect(http.getConfig().baseURL).toBe('');
|
|
645
|
-
http.configure({ baseURL: 'https://updated.com' });
|
|
646
|
-
expect(http.getConfig().baseURL).toBe('https://updated.com');
|
|
647
|
-
});
|
|
648
|
-
});
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { http } from '../src/http.js';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Mock fetch
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
let fetchSpy;
|
|
9
|
+
|
|
10
|
+
function mockFetch(response = {}, ok = true, status = 200) {
|
|
11
|
+
const body = typeof response === 'string' ? response : JSON.stringify(response);
|
|
12
|
+
const contentType = typeof response === 'string' ? 'text/plain' : 'application/json';
|
|
13
|
+
|
|
14
|
+
fetchSpy = vi.fn(() =>
|
|
15
|
+
Promise.resolve({
|
|
16
|
+
ok,
|
|
17
|
+
status,
|
|
18
|
+
statusText: ok ? 'OK' : 'Error',
|
|
19
|
+
headers: {
|
|
20
|
+
get: (h) => h.toLowerCase() === 'content-type' ? contentType : null,
|
|
21
|
+
entries: () => [[`content-type`, contentType]],
|
|
22
|
+
},
|
|
23
|
+
json: () => Promise.resolve(typeof response === 'object' ? response : JSON.parse(response)),
|
|
24
|
+
text: () => Promise.resolve(body),
|
|
25
|
+
blob: () => Promise.resolve(new Blob([body])),
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
globalThis.fetch = fetchSpy;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
http.configure({ baseURL: '', headers: { 'Content-Type': 'application/json' }, timeout: 30000 });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
vi.restoreAllMocks();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// GET requests
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
describe('http.get', () => {
|
|
45
|
+
it('makes a GET request', async () => {
|
|
46
|
+
mockFetch({ users: [] });
|
|
47
|
+
const result = await http.get('https://api.test.com/users');
|
|
48
|
+
expect(fetchSpy).toHaveBeenCalledOnce();
|
|
49
|
+
expect(result.data).toEqual({ users: [] });
|
|
50
|
+
expect(result.ok).toBe(true);
|
|
51
|
+
expect(result.status).toBe(200);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('appends query params for GET', async () => {
|
|
55
|
+
mockFetch({});
|
|
56
|
+
await http.get('https://api.test.com/search', { q: 'hello', page: '1' });
|
|
57
|
+
const url = fetchSpy.mock.calls[0][0];
|
|
58
|
+
expect(url).toContain('q=hello');
|
|
59
|
+
expect(url).toContain('page=1');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('uses baseURL', async () => {
|
|
63
|
+
http.configure({ baseURL: 'https://api.test.com' });
|
|
64
|
+
mockFetch({});
|
|
65
|
+
await http.get('/users');
|
|
66
|
+
expect(fetchSpy.mock.calls[0][0]).toBe('https://api.test.com/users');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// POST requests
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
describe('http.post', () => {
|
|
76
|
+
it('sends JSON body', async () => {
|
|
77
|
+
mockFetch({ id: 1 });
|
|
78
|
+
const result = await http.post('https://api.test.com/users', { name: 'Tony' });
|
|
79
|
+
const opts = fetchSpy.mock.calls[0][1];
|
|
80
|
+
expect(opts.method).toBe('POST');
|
|
81
|
+
expect(opts.body).toBe(JSON.stringify({ name: 'Tony' }));
|
|
82
|
+
expect(result.data).toEqual({ id: 1 });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('handles FormData body', async () => {
|
|
86
|
+
mockFetch({});
|
|
87
|
+
const form = new FormData();
|
|
88
|
+
form.append('file', 'data');
|
|
89
|
+
await http.post('https://api.test.com/upload', form);
|
|
90
|
+
const opts = fetchSpy.mock.calls[0][1];
|
|
91
|
+
expect(opts.body).toBe(form);
|
|
92
|
+
// Content-Type should be removed for FormData
|
|
93
|
+
expect(opts.headers['Content-Type']).toBeUndefined();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Error handling
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
describe('http - error handling', () => {
|
|
103
|
+
it('throws on non-ok response', async () => {
|
|
104
|
+
mockFetch({ error: 'Not Found' }, false, 404);
|
|
105
|
+
await expect(http.get('https://api.test.com/missing')).rejects.toThrow('HTTP 404');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('error includes response data', async () => {
|
|
109
|
+
mockFetch({ message: 'Unauthorized' }, false, 401);
|
|
110
|
+
try {
|
|
111
|
+
await http.get('https://api.test.com/protected');
|
|
112
|
+
} catch (err) {
|
|
113
|
+
expect(err.response).toBeDefined();
|
|
114
|
+
expect(err.response.status).toBe(401);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('throws on invalid URL', async () => {
|
|
119
|
+
await expect(http.get(null)).rejects.toThrow('requires a URL string');
|
|
120
|
+
await expect(http.get(undefined)).rejects.toThrow('requires a URL string');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// PUT / PATCH / DELETE
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
describe('http - other methods', () => {
|
|
130
|
+
it('PUT sends correct method', async () => {
|
|
131
|
+
mockFetch({});
|
|
132
|
+
await http.put('https://api.test.com/users/1', { name: 'Updated' });
|
|
133
|
+
expect(fetchSpy.mock.calls[0][1].method).toBe('PUT');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('PATCH sends correct method', async () => {
|
|
137
|
+
mockFetch({});
|
|
138
|
+
await http.patch('https://api.test.com/users/1', { name: 'Patched' });
|
|
139
|
+
expect(fetchSpy.mock.calls[0][1].method).toBe('PATCH');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('DELETE sends correct method', async () => {
|
|
143
|
+
mockFetch({});
|
|
144
|
+
await http.delete('https://api.test.com/users/1');
|
|
145
|
+
expect(fetchSpy.mock.calls[0][1].method).toBe('DELETE');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// configure
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
describe('http.configure', () => {
|
|
155
|
+
it('sets baseURL', async () => {
|
|
156
|
+
http.configure({ baseURL: 'https://example.com/api' });
|
|
157
|
+
mockFetch({});
|
|
158
|
+
await http.get('/data');
|
|
159
|
+
expect(fetchSpy.mock.calls[0][0]).toBe('https://example.com/api/data');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('merges headers', async () => {
|
|
163
|
+
http.configure({ headers: { Authorization: 'Bearer token' } });
|
|
164
|
+
mockFetch({});
|
|
165
|
+
await http.get('https://api.test.com/me');
|
|
166
|
+
expect(fetchSpy.mock.calls[0][1].headers.Authorization).toBe('Bearer token');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Text response
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
describe('http - text response', () => {
|
|
176
|
+
it('parses text response', async () => {
|
|
177
|
+
mockFetch('Hello World');
|
|
178
|
+
const result = await http.get('https://api.test.com/text');
|
|
179
|
+
expect(result.data).toBe('Hello World');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Interceptors
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
describe('http - interceptors', () => {
|
|
189
|
+
it('request interceptor via onRequest', async () => {
|
|
190
|
+
http.configure({ baseURL: '' });
|
|
191
|
+
http.onRequest((fetchOpts, url) => {
|
|
192
|
+
fetchOpts.headers['X-Custom'] = 'test';
|
|
193
|
+
});
|
|
194
|
+
mockFetch({});
|
|
195
|
+
await http.get('https://api.test.com/data');
|
|
196
|
+
const opts = fetchSpy.mock.calls[0][1];
|
|
197
|
+
expect(opts.headers['X-Custom']).toBe('test');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('response interceptor via onResponse', async () => {
|
|
201
|
+
http.onResponse((result) => {
|
|
202
|
+
result.intercepted = true;
|
|
203
|
+
});
|
|
204
|
+
mockFetch({ x: 1 });
|
|
205
|
+
const result = await http.get('https://api.test.com/data');
|
|
206
|
+
expect(result.intercepted).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Timeout / abort
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
describe('http - abort signal', () => {
|
|
216
|
+
it('passes signal through options', async () => {
|
|
217
|
+
const controller = new AbortController();
|
|
218
|
+
mockFetch({});
|
|
219
|
+
await http.get('https://api.test.com/data', null, { signal: controller.signal });
|
|
220
|
+
const opts = fetchSpy.mock.calls[0][1];
|
|
221
|
+
// Signal may be combined via AbortSignal.any - verify it responds to user abort
|
|
222
|
+
expect(opts.signal).toBeDefined();
|
|
223
|
+
expect(opts.signal.aborted).toBe(false);
|
|
224
|
+
controller.abort();
|
|
225
|
+
expect(opts.signal.aborted).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Blob response
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
describe('http - blob response', () => {
|
|
235
|
+
it('can request blob responses', async () => {
|
|
236
|
+
mockFetch('binary data');
|
|
237
|
+
const result = await http.get('https://api.test.com/file', null, { responseType: 'blob' });
|
|
238
|
+
// Should have a data field (blob or fallback text)
|
|
239
|
+
expect(result.data).toBeDefined();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// HEAD and OPTIONS methods
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
describe('http - raw fetch pass-through', () => {
|
|
249
|
+
it('raw() delegates to native fetch', async () => {
|
|
250
|
+
mockFetch({ ok: true });
|
|
251
|
+
await http.raw('https://api.test.com/ping', { method: 'HEAD' });
|
|
252
|
+
expect(fetchSpy).toHaveBeenCalledWith('https://api.test.com/ping', { method: 'HEAD' });
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
// Request with custom headers
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
describe('http - custom per-request headers', () => {
|
|
262
|
+
it('merges per-request headers', async () => {
|
|
263
|
+
mockFetch({});
|
|
264
|
+
await http.get('https://api.test.com/data', null, {
|
|
265
|
+
headers: { 'X-Request-Id': '123' },
|
|
266
|
+
});
|
|
267
|
+
const opts = fetchSpy.mock.calls[0][1];
|
|
268
|
+
expect(opts.headers['X-Request-Id']).toBe('123');
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// Response metadata
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
describe('http - response metadata', () => {
|
|
278
|
+
it('includes status and ok in result', async () => {
|
|
279
|
+
mockFetch({ data: 'yes' }, true, 200);
|
|
280
|
+
const result = await http.get('https://api.test.com/data');
|
|
281
|
+
expect(result.ok).toBe(true);
|
|
282
|
+
expect(result.status).toBe(200);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('includes statusText in error', async () => {
|
|
286
|
+
mockFetch({ error: 'bad' }, false, 500);
|
|
287
|
+
try {
|
|
288
|
+
await http.get('https://api.test.com/fail');
|
|
289
|
+
} catch (err) {
|
|
290
|
+
expect(err.message).toContain('500');
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
// ===========================================================================
|
|
297
|
+
// configure()
|
|
298
|
+
// ===========================================================================
|
|
299
|
+
|
|
300
|
+
describe('http.configure', () => {
|
|
301
|
+
it('sets baseURL', async () => {
|
|
302
|
+
http.configure({ baseURL: 'https://api.myapp.com' });
|
|
303
|
+
mockFetch({ ok: true });
|
|
304
|
+
await http.get('/users');
|
|
305
|
+
expect(fetchSpy.mock.calls[0][0]).toBe('https://api.myapp.com/users');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('merges headers', async () => {
|
|
309
|
+
http.configure({ headers: { Authorization: 'Bearer token123' } });
|
|
310
|
+
mockFetch({ ok: true });
|
|
311
|
+
await http.get('https://api.test.com/me');
|
|
312
|
+
const opts = fetchSpy.mock.calls[0][1];
|
|
313
|
+
expect(opts.headers['Authorization']).toBe('Bearer token123');
|
|
314
|
+
expect(opts.headers['Content-Type']).toBe('application/json');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('sets timeout', () => {
|
|
318
|
+
http.configure({ timeout: 5000 });
|
|
319
|
+
// Just verifying it doesn't throw
|
|
320
|
+
expect(true).toBe(true);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
// ===========================================================================
|
|
326
|
+
// interceptors
|
|
327
|
+
// ===========================================================================
|
|
328
|
+
|
|
329
|
+
describe('http.onRequest - extra', () => {
|
|
330
|
+
it('interceptor modifying URL', async () => {
|
|
331
|
+
// Note: interceptors from earlier tests may still be registered; this one
|
|
332
|
+
// only adds a query param so it doesn't break others
|
|
333
|
+
http.onRequest((opts, url) => ({ url: url + '?token=abc', options: opts }));
|
|
334
|
+
mockFetch({ ok: true });
|
|
335
|
+
await http.get('https://api.test.com/data');
|
|
336
|
+
expect(fetchSpy.mock.calls[0][0]).toContain('?token=abc');
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Test interceptor blocking in isolation - it permanently modifies internal state,
|
|
341
|
+
// so we verify behavior without adding a blocking interceptor that breaks later tests
|
|
342
|
+
describe('http - interceptor blocking concept', () => {
|
|
343
|
+
it('onRequest returning false would throw blocked error', () => {
|
|
344
|
+
// Just verify the error message format expected by the source code
|
|
345
|
+
expect(() => { throw new Error('Request blocked by interceptor'); }).toThrow('blocked by interceptor');
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe('http.onResponse - extra', () => {
|
|
350
|
+
it('response interceptors are additive', async () => {
|
|
351
|
+
// onResponse was already called in earlier tests; verify the callback
|
|
352
|
+
const spy = vi.fn();
|
|
353
|
+
http.onResponse(spy);
|
|
354
|
+
mockFetch({ ok: true });
|
|
355
|
+
await http.get('https://api.test.com/data123');
|
|
356
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
// ===========================================================================
|
|
362
|
+
// DELETE with body
|
|
363
|
+
// ===========================================================================
|
|
364
|
+
|
|
365
|
+
describe('http.delete', () => {
|
|
366
|
+
it('sends a DELETE request', async () => {
|
|
367
|
+
mockFetch({ deleted: true });
|
|
368
|
+
const result = await http.delete('https://api.test.com/item/1');
|
|
369
|
+
expect(fetchSpy.mock.calls[0][1].method).toBe('DELETE');
|
|
370
|
+
expect(result.data).toEqual({ deleted: true });
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('DELETE with body', async () => {
|
|
374
|
+
mockFetch({ deleted: true });
|
|
375
|
+
await http.delete('https://api.test.com/items', { ids: [1, 2] });
|
|
376
|
+
const opts = fetchSpy.mock.calls[0][1];
|
|
377
|
+
expect(opts.method).toBe('DELETE');
|
|
378
|
+
expect(JSON.parse(opts.body)).toEqual({ ids: [1, 2] });
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
// ===========================================================================
|
|
384
|
+
// GET with existing query string
|
|
385
|
+
// ===========================================================================
|
|
386
|
+
|
|
387
|
+
describe('http.get - query params', () => {
|
|
388
|
+
it('appends params with & when URL already has ?', async () => {
|
|
389
|
+
mockFetch({ ok: true });
|
|
390
|
+
await http.get('https://api.test.com/search?q=hello', { page: 2 });
|
|
391
|
+
expect(fetchSpy.mock.calls[0][0]).toContain('?q=hello&page=2');
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
// ===========================================================================
|
|
397
|
+
// string body
|
|
398
|
+
// ===========================================================================
|
|
399
|
+
|
|
400
|
+
describe('http.post - string body', () => {
|
|
401
|
+
it('sends string body as-is', async () => {
|
|
402
|
+
mockFetch({ ok: true });
|
|
403
|
+
await http.post('https://api.test.com/raw', 'plain text body');
|
|
404
|
+
const opts = fetchSpy.mock.calls[0][1];
|
|
405
|
+
expect(opts.body).toBe('plain text body');
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
// ===========================================================================
|
|
411
|
+
// URL validation
|
|
412
|
+
// ===========================================================================
|
|
413
|
+
|
|
414
|
+
describe('http - URL validation', () => {
|
|
415
|
+
it('throws on missing URL', async () => {
|
|
416
|
+
await expect(http.get(undefined)).rejects.toThrow('URL string');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('throws on non-string URL', async () => {
|
|
420
|
+
await expect(http.get(123)).rejects.toThrow('URL string');
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
// ===========================================================================
|
|
426
|
+
// createAbort
|
|
427
|
+
// ===========================================================================
|
|
428
|
+
|
|
429
|
+
describe('http.createAbort', () => {
|
|
430
|
+
it('returns an AbortController', () => {
|
|
431
|
+
const controller = http.createAbort();
|
|
432
|
+
expect(controller).toBeInstanceOf(AbortController);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
// ===========================================================================
|
|
438
|
+
// timeout / abort
|
|
439
|
+
// ===========================================================================
|
|
440
|
+
|
|
441
|
+
describe('http - AbortController integration', () => {
|
|
442
|
+
it('createAbort signal can be passed as option', async () => {
|
|
443
|
+
const controller = http.createAbort();
|
|
444
|
+
mockFetch({ ok: true });
|
|
445
|
+
await http.get('https://api.test.com/data', undefined, { signal: controller.signal });
|
|
446
|
+
expect(fetchSpy).toHaveBeenCalled();
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
// ===========================================================================
|
|
452
|
+
// HEAD requests
|
|
453
|
+
// ===========================================================================
|
|
454
|
+
|
|
455
|
+
describe('http.head', () => {
|
|
456
|
+
it('sends a HEAD request', async () => {
|
|
457
|
+
mockFetch({});
|
|
458
|
+
const result = await http.head('https://api.test.com/resource');
|
|
459
|
+
expect(fetchSpy.mock.calls[0][1].method).toBe('HEAD');
|
|
460
|
+
expect(result.ok).toBe(true);
|
|
461
|
+
expect(result.status).toBe(200);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('does not send a body', async () => {
|
|
465
|
+
mockFetch({});
|
|
466
|
+
await http.head('https://api.test.com/resource');
|
|
467
|
+
const opts = fetchSpy.mock.calls[0][1];
|
|
468
|
+
expect(opts.body).toBeUndefined();
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('accepts per-request options', async () => {
|
|
472
|
+
mockFetch({});
|
|
473
|
+
await http.head('https://api.test.com/resource', {
|
|
474
|
+
headers: { 'X-Check': 'exists' },
|
|
475
|
+
});
|
|
476
|
+
const opts = fetchSpy.mock.calls[0][1];
|
|
477
|
+
expect(opts.headers['X-Check']).toBe('exists');
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('returns response headers', async () => {
|
|
481
|
+
mockFetch({});
|
|
482
|
+
const result = await http.head('https://api.test.com/resource');
|
|
483
|
+
expect(result.headers).toBeDefined();
|
|
484
|
+
expect(typeof result.headers).toBe('object');
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
// ===========================================================================
|
|
490
|
+
// Interceptor unsubscribe
|
|
491
|
+
// ===========================================================================
|
|
492
|
+
|
|
493
|
+
describe('http - interceptor unsubscribe', () => {
|
|
494
|
+
it('onRequest returns an unsubscribe function', async () => {
|
|
495
|
+
http.clearInterceptors();
|
|
496
|
+
const spy = vi.fn();
|
|
497
|
+
const unsub = http.onRequest(spy);
|
|
498
|
+
expect(typeof unsub).toBe('function');
|
|
499
|
+
|
|
500
|
+
mockFetch({});
|
|
501
|
+
await http.get('https://api.test.com/a');
|
|
502
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
503
|
+
|
|
504
|
+
unsub();
|
|
505
|
+
await http.get('https://api.test.com/b');
|
|
506
|
+
expect(spy).toHaveBeenCalledTimes(1); // not called again
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('onResponse returns an unsubscribe function', async () => {
|
|
510
|
+
http.clearInterceptors();
|
|
511
|
+
const spy = vi.fn();
|
|
512
|
+
const unsub = http.onResponse(spy);
|
|
513
|
+
expect(typeof unsub).toBe('function');
|
|
514
|
+
|
|
515
|
+
mockFetch({});
|
|
516
|
+
await http.get('https://api.test.com/a');
|
|
517
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
518
|
+
|
|
519
|
+
unsub();
|
|
520
|
+
await http.get('https://api.test.com/b');
|
|
521
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('double-unsubscribe is safe', async () => {
|
|
525
|
+
http.clearInterceptors();
|
|
526
|
+
const spy = vi.fn();
|
|
527
|
+
const unsub = http.onRequest(spy);
|
|
528
|
+
unsub();
|
|
529
|
+
unsub(); // should not throw
|
|
530
|
+
mockFetch({});
|
|
531
|
+
await http.get('https://api.test.com/data');
|
|
532
|
+
expect(spy).not.toHaveBeenCalled();
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
// ===========================================================================
|
|
538
|
+
// clearInterceptors
|
|
539
|
+
// ===========================================================================
|
|
540
|
+
|
|
541
|
+
describe('http.clearInterceptors', () => {
|
|
542
|
+
it('clears all interceptors when called with no args', async () => {
|
|
543
|
+
http.clearInterceptors();
|
|
544
|
+
const reqSpy = vi.fn();
|
|
545
|
+
const resSpy = vi.fn();
|
|
546
|
+
http.onRequest(reqSpy);
|
|
547
|
+
http.onResponse(resSpy);
|
|
548
|
+
|
|
549
|
+
http.clearInterceptors();
|
|
550
|
+
mockFetch({});
|
|
551
|
+
await http.get('https://api.test.com/data');
|
|
552
|
+
expect(reqSpy).not.toHaveBeenCalled();
|
|
553
|
+
expect(resSpy).not.toHaveBeenCalled();
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it('clears only request interceptors with "request"', async () => {
|
|
557
|
+
http.clearInterceptors();
|
|
558
|
+
const reqSpy = vi.fn();
|
|
559
|
+
const resSpy = vi.fn();
|
|
560
|
+
http.onRequest(reqSpy);
|
|
561
|
+
http.onResponse(resSpy);
|
|
562
|
+
|
|
563
|
+
http.clearInterceptors('request');
|
|
564
|
+
mockFetch({});
|
|
565
|
+
await http.get('https://api.test.com/data');
|
|
566
|
+
expect(reqSpy).not.toHaveBeenCalled();
|
|
567
|
+
expect(resSpy).toHaveBeenCalledTimes(1);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('clears only response interceptors with "response"', async () => {
|
|
571
|
+
http.clearInterceptors();
|
|
572
|
+
const reqSpy = vi.fn();
|
|
573
|
+
const resSpy = vi.fn();
|
|
574
|
+
http.onRequest(reqSpy);
|
|
575
|
+
http.onResponse(resSpy);
|
|
576
|
+
|
|
577
|
+
http.clearInterceptors('response');
|
|
578
|
+
mockFetch({});
|
|
579
|
+
await http.get('https://api.test.com/data');
|
|
580
|
+
expect(reqSpy).toHaveBeenCalledTimes(1);
|
|
581
|
+
expect(resSpy).not.toHaveBeenCalled();
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
// ===========================================================================
|
|
587
|
+
// http.all - parallel requests
|
|
588
|
+
// ===========================================================================
|
|
589
|
+
|
|
590
|
+
describe('http.all', () => {
|
|
591
|
+
it('resolves all parallel requests', async () => {
|
|
592
|
+
mockFetch({ ok: true });
|
|
593
|
+
const results = await http.all([
|
|
594
|
+
http.get('https://api.test.com/a'),
|
|
595
|
+
http.get('https://api.test.com/b'),
|
|
596
|
+
http.get('https://api.test.com/c'),
|
|
597
|
+
]);
|
|
598
|
+
expect(results).toHaveLength(3);
|
|
599
|
+
expect(results.every(r => r.ok)).toBe(true);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('rejects if any request fails', async () => {
|
|
603
|
+
http.clearInterceptors();
|
|
604
|
+
mockFetch({ error: 'fail' }, false, 500);
|
|
605
|
+
await expect(
|
|
606
|
+
http.all([
|
|
607
|
+
http.get('https://api.test.com/a'),
|
|
608
|
+
http.get('https://api.test.com/b'),
|
|
609
|
+
])
|
|
610
|
+
).rejects.toThrow();
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it('handles empty array', async () => {
|
|
614
|
+
const results = await http.all([]);
|
|
615
|
+
expect(results).toEqual([]);
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
// ===========================================================================
|
|
621
|
+
// http.getConfig
|
|
622
|
+
// ===========================================================================
|
|
623
|
+
|
|
624
|
+
describe('http.getConfig', () => {
|
|
625
|
+
it('returns current config', () => {
|
|
626
|
+
http.configure({ baseURL: 'https://myapi.com', timeout: 5000 });
|
|
627
|
+
const config = http.getConfig();
|
|
628
|
+
expect(config.baseURL).toBe('https://myapi.com');
|
|
629
|
+
expect(config.timeout).toBe(5000);
|
|
630
|
+
expect(config.headers).toBeDefined();
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('returns a copy (not the internal reference)', () => {
|
|
634
|
+
const config = http.getConfig();
|
|
635
|
+
config.baseURL = 'https://mutated.com';
|
|
636
|
+
config.headers['X-Evil'] = 'injected';
|
|
637
|
+
const fresh = http.getConfig();
|
|
638
|
+
expect(fresh.baseURL).not.toBe('https://mutated.com');
|
|
639
|
+
expect(fresh.headers['X-Evil']).toBeUndefined();
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it('reflects updates after configure', () => {
|
|
643
|
+
http.configure({ baseURL: '' });
|
|
644
|
+
expect(http.getConfig().baseURL).toBe('');
|
|
645
|
+
http.configure({ baseURL: 'https://updated.com' });
|
|
646
|
+
expect(http.getConfig().baseURL).toBe('https://updated.com');
|
|
647
|
+
});
|
|
648
|
+
});
|