@furystack/rest-service 10.0.28 → 10.1.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.
Files changed (74) hide show
  1. package/README.md +249 -12
  2. package/esm/api-manager.d.ts +1 -3
  3. package/esm/api-manager.d.ts.map +1 -1
  4. package/esm/api-manager.js +4 -6
  5. package/esm/api-manager.js.map +1 -1
  6. package/esm/header-processor.d.ts +39 -0
  7. package/esm/header-processor.d.ts.map +1 -0
  8. package/esm/header-processor.js +113 -0
  9. package/esm/header-processor.js.map +1 -0
  10. package/esm/header-processor.spec.d.ts +2 -0
  11. package/esm/header-processor.spec.d.ts.map +1 -0
  12. package/esm/header-processor.spec.js +420 -0
  13. package/esm/header-processor.spec.js.map +1 -0
  14. package/esm/helpers.d.ts +69 -1
  15. package/esm/helpers.d.ts.map +1 -1
  16. package/esm/helpers.js +70 -1
  17. package/esm/helpers.js.map +1 -1
  18. package/esm/helpers.spec.js +21 -5
  19. package/esm/helpers.spec.js.map +1 -1
  20. package/esm/http-proxy-handler.d.ts +53 -0
  21. package/esm/http-proxy-handler.d.ts.map +1 -0
  22. package/esm/http-proxy-handler.js +179 -0
  23. package/esm/http-proxy-handler.js.map +1 -0
  24. package/esm/http-user-context.d.ts +4 -4
  25. package/esm/http-user-context.d.ts.map +1 -1
  26. package/esm/http-user-context.js +4 -4
  27. package/esm/http-user-context.js.map +1 -1
  28. package/esm/index.d.ts +1 -0
  29. package/esm/index.d.ts.map +1 -1
  30. package/esm/index.js +1 -0
  31. package/esm/index.js.map +1 -1
  32. package/esm/path-processor.d.ts +33 -0
  33. package/esm/path-processor.d.ts.map +1 -0
  34. package/esm/path-processor.js +58 -0
  35. package/esm/path-processor.js.map +1 -0
  36. package/esm/path-processor.spec.d.ts +2 -0
  37. package/esm/path-processor.spec.d.ts.map +1 -0
  38. package/esm/path-processor.spec.js +256 -0
  39. package/esm/path-processor.spec.js.map +1 -0
  40. package/esm/proxy-manager.d.ts +52 -0
  41. package/esm/proxy-manager.d.ts.map +1 -0
  42. package/esm/proxy-manager.js +84 -0
  43. package/esm/proxy-manager.js.map +1 -0
  44. package/esm/proxy-manager.spec.d.ts +2 -0
  45. package/esm/proxy-manager.spec.d.ts.map +1 -0
  46. package/esm/proxy-manager.spec.js +1781 -0
  47. package/esm/proxy-manager.spec.js.map +1 -0
  48. package/esm/server-manager.d.ts +7 -0
  49. package/esm/server-manager.d.ts.map +1 -1
  50. package/esm/server-manager.js +12 -0
  51. package/esm/server-manager.js.map +1 -1
  52. package/esm/static-server-manager.d.ts.map +1 -1
  53. package/esm/static-server-manager.js +5 -7
  54. package/esm/static-server-manager.js.map +1 -1
  55. package/esm/websocket-proxy-handler.d.ts +44 -0
  56. package/esm/websocket-proxy-handler.d.ts.map +1 -0
  57. package/esm/websocket-proxy-handler.js +157 -0
  58. package/esm/websocket-proxy-handler.js.map +1 -0
  59. package/package.json +11 -9
  60. package/src/api-manager.ts +5 -15
  61. package/src/header-processor.spec.ts +514 -0
  62. package/src/header-processor.ts +140 -0
  63. package/src/helpers.spec.ts +23 -5
  64. package/src/helpers.ts +72 -1
  65. package/src/http-proxy-handler.ts +215 -0
  66. package/src/http-user-context.ts +6 -6
  67. package/src/index.ts +1 -0
  68. package/src/path-processor.spec.ts +318 -0
  69. package/src/path-processor.ts +69 -0
  70. package/src/proxy-manager.spec.ts +2094 -0
  71. package/src/proxy-manager.ts +101 -0
  72. package/src/server-manager.ts +19 -0
  73. package/src/static-server-manager.ts +5 -7
  74. package/src/websocket-proxy-handler.ts +204 -0
@@ -0,0 +1,1781 @@
1
+ import { getPort } from '@furystack/core/port-generator';
2
+ import { Injector } from '@furystack/inject';
3
+ import { usingAsync } from '@furystack/utils';
4
+ import { mkdirSync, writeFileSync } from 'fs';
5
+ import { createServer as createHttpServer } from 'http';
6
+ import { tmpdir } from 'os';
7
+ import { join } from 'path';
8
+ import { beforeAll, describe, expect, it } from 'vitest';
9
+ import { WebSocket, WebSocketServer } from 'ws';
10
+ import { ProxyManager } from './proxy-manager.js';
11
+ import { StaticServerManager } from './static-server-manager.js';
12
+ describe('ProxyManager', () => {
13
+ // Create a temporary directory for test files
14
+ const testDir = join(tmpdir(), 'furystack-proxy-test');
15
+ beforeAll(() => {
16
+ try {
17
+ mkdirSync(testDir, { recursive: true });
18
+ }
19
+ catch (error) {
20
+ // Directory might already exist
21
+ }
22
+ });
23
+ // Helper function to convert RawData to string
24
+ const rawDataToString = (data) => Buffer.isBuffer(data)
25
+ ? data.toString('utf8')
26
+ : Array.isArray(data)
27
+ ? Buffer.concat(data).toString('utf8')
28
+ : Buffer.from(data).toString('utf8');
29
+ // Helper function to create a simple echo server
30
+ const createEchoServer = (port, handler) => {
31
+ const server = createHttpServer(handler ||
32
+ ((req, res) => {
33
+ res.writeHead(200, { 'Content-Type': 'application/json' });
34
+ res.end(JSON.stringify({ url: req.url, method: req.method }));
35
+ }));
36
+ return new Promise((resolve) => {
37
+ server.listen(port, () => resolve(server));
38
+ });
39
+ };
40
+ // Helper function to create a WebSocket test server
41
+ const createWsTestServer = (port, onConnection) => {
42
+ const server = createHttpServer();
43
+ const wss = new WebSocketServer({ server });
44
+ wss.on('connection', onConnection);
45
+ return new Promise((resolve) => {
46
+ server.listen(port, () => resolve({ server, wss }));
47
+ });
48
+ };
49
+ describe('Basic proxy functionality', () => {
50
+ it('Should proxy requests from source URL to target URL', async () => {
51
+ await usingAsync(new Injector(), async (injector) => {
52
+ const proxyManager = injector.getInstance(ProxyManager);
53
+ const staticServerManager = injector.getInstance(StaticServerManager);
54
+ const proxyPort = getPort();
55
+ const targetPort = getPort();
56
+ // Create a test file for the target server
57
+ const testFile = join(testDir, 'test.json');
58
+ writeFileSync(testFile, JSON.stringify({
59
+ message: 'Hello from target server',
60
+ url: '/test',
61
+ timestamp: Date.now(),
62
+ }));
63
+ // Set up target server (static file server) - serve files from testDir
64
+ await staticServerManager.addStaticSite({
65
+ baseUrl: '/',
66
+ path: testDir,
67
+ port: targetPort,
68
+ });
69
+ // Set up proxy server
70
+ await proxyManager.addProxy({
71
+ sourceBaseUrl: '/old',
72
+ targetBaseUrl: `http://localhost:${targetPort}`,
73
+ pathRewrite: (path) => path.replace('/path', ''),
74
+ sourcePort: proxyPort,
75
+ });
76
+ const result = await fetch(`http://127.0.0.1:${proxyPort}/old/path/test.json`);
77
+ expect(result.status).toBe(200);
78
+ expect(result.url).toBe(`http://127.0.0.1:${proxyPort}/old/path/test.json`);
79
+ const responseData = (await result.json());
80
+ expect(responseData.message).toBe('Hello from target server');
81
+ expect(responseData.url).toBe('/test');
82
+ });
83
+ });
84
+ it('Should preserve query strings when proxying', async () => {
85
+ await usingAsync(new Injector(), async (injector) => {
86
+ const proxyManager = injector.getInstance(ProxyManager);
87
+ const targetPort = getPort();
88
+ const proxyPort = getPort();
89
+ // Create a simple echo server that returns query parameters
90
+ const targetServer = createHttpServer((req, res) => {
91
+ res.writeHead(200, { 'Content-Type': 'application/json' });
92
+ res.end(JSON.stringify({ url: req.url, query: req.url?.split('?')[1] || '' }));
93
+ });
94
+ await new Promise((resolve) => {
95
+ targetServer.listen(targetPort, () => resolve());
96
+ });
97
+ try {
98
+ await proxyManager.addProxy({
99
+ sourceBaseUrl: '/api',
100
+ targetBaseUrl: `http://localhost:${targetPort}`,
101
+ sourcePort: proxyPort,
102
+ });
103
+ const result = await fetch(`http://127.0.0.1:${proxyPort}/api/test?foo=bar&baz=qux`);
104
+ expect(result.status).toBe(200);
105
+ const data = (await result.json());
106
+ expect(data.query).toBe('foo=bar&baz=qux');
107
+ }
108
+ finally {
109
+ targetServer.close();
110
+ }
111
+ });
112
+ });
113
+ it('Should handle header transformation', async () => {
114
+ await usingAsync(new Injector(), async (injector) => {
115
+ const proxyManager = injector.getInstance(ProxyManager);
116
+ const staticServerManager = injector.getInstance(StaticServerManager);
117
+ const proxyPort = getPort();
118
+ const targetPort = getPort();
119
+ // Create a test file that returns headers
120
+ const headersFile = join(testDir, 'headers.json');
121
+ writeFileSync(headersFile, JSON.stringify({
122
+ message: 'Headers test',
123
+ receivedHeaders: '{{HEADERS}}', // Placeholder for dynamic content
124
+ }));
125
+ // Set up target server
126
+ await staticServerManager.addStaticSite({
127
+ baseUrl: '/',
128
+ path: testDir,
129
+ port: targetPort,
130
+ });
131
+ // Set up proxy server with header transformation
132
+ await proxyManager.addProxy({
133
+ sourceBaseUrl: '/old',
134
+ targetBaseUrl: `http://localhost:${targetPort}`,
135
+ pathRewrite: (path) => path.replace('/path', ''),
136
+ sourcePort: proxyPort,
137
+ headers: () => ({
138
+ 'X-Custom-Header': 'custom-value',
139
+ Authorization: 'Bearer new-token',
140
+ }),
141
+ });
142
+ const result = await fetch(`http://127.0.0.1:${proxyPort}/old/path/headers.json`, {
143
+ headers: {
144
+ Authorization: 'Bearer old-token',
145
+ 'User-Agent': 'test-agent',
146
+ },
147
+ });
148
+ expect(result.status).toBe(200);
149
+ const responseData = (await result.json());
150
+ expect(responseData.message).toBe('Headers test');
151
+ });
152
+ });
153
+ it('Should handle cookie transformation', async () => {
154
+ await usingAsync(new Injector(), async (injector) => {
155
+ const proxyManager = injector.getInstance(ProxyManager);
156
+ const staticServerManager = injector.getInstance(StaticServerManager);
157
+ const proxyPort = getPort();
158
+ const targetPort = getPort();
159
+ // Create a test file for cookies
160
+ const cookiesFile = join(testDir, 'cookies.json');
161
+ writeFileSync(cookiesFile, JSON.stringify({
162
+ message: 'Cookies test',
163
+ receivedCookies: '{{COOKIES}}', // Placeholder for dynamic content
164
+ }));
165
+ // Set up target server
166
+ await staticServerManager.addStaticSite({
167
+ baseUrl: '/',
168
+ path: testDir,
169
+ port: targetPort,
170
+ });
171
+ // Set up proxy server with cookie transformation
172
+ await proxyManager.addProxy({
173
+ sourceBaseUrl: '/old',
174
+ targetBaseUrl: `http://localhost:${targetPort}`,
175
+ pathRewrite: (path) => path.replace('/path', ''),
176
+ sourcePort: proxyPort,
177
+ cookies: (originalCookies) => [...originalCookies, 'newCookie=newValue', 'sessionId=updated-session'],
178
+ });
179
+ const result = await fetch(`http://127.0.0.1:${proxyPort}/old/path/cookies.json`, {
180
+ headers: {
181
+ Cookie: 'oldCookie=oldValue; sessionId=old-session',
182
+ },
183
+ });
184
+ expect(result.status).toBe(200);
185
+ const responseData = (await result.json());
186
+ expect(responseData.message).toBe('Cookies test');
187
+ });
188
+ });
189
+ it('Should handle POST requests with JSON body', async () => {
190
+ await usingAsync(new Injector(), async (injector) => {
191
+ const proxyManager = injector.getInstance(ProxyManager);
192
+ const targetPort = getPort();
193
+ const proxyPort = getPort();
194
+ // Create an echo server that returns the request body
195
+ const targetServer = createHttpServer((req, res) => {
196
+ const chunks = [];
197
+ req.on('data', (chunk) => {
198
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
199
+ });
200
+ req.on('end', () => {
201
+ const body = Buffer.concat(chunks).toString('utf8');
202
+ res.writeHead(200, { 'Content-Type': 'application/json' });
203
+ res.end(JSON.stringify({ received: JSON.parse(body), method: req.method }));
204
+ });
205
+ });
206
+ await new Promise((resolve) => {
207
+ targetServer.listen(targetPort, () => resolve());
208
+ });
209
+ try {
210
+ await proxyManager.addProxy({
211
+ sourceBaseUrl: '/api',
212
+ targetBaseUrl: `http://localhost:${targetPort}`,
213
+ sourcePort: proxyPort,
214
+ });
215
+ const testData = { name: 'test', value: 123, nested: { key: 'value' } };
216
+ const result = await fetch(`http://127.0.0.1:${proxyPort}/api/data`, {
217
+ method: 'POST',
218
+ headers: { 'Content-Type': 'application/json' },
219
+ body: JSON.stringify(testData),
220
+ });
221
+ expect(result.status).toBe(200);
222
+ const data = (await result.json());
223
+ expect(data.received).toEqual(testData);
224
+ expect(data.method).toBe('POST');
225
+ }
226
+ finally {
227
+ targetServer.close();
228
+ }
229
+ });
230
+ });
231
+ it('Should handle response cookies transformation', async () => {
232
+ await usingAsync(new Injector(), async (injector) => {
233
+ const proxyManager = injector.getInstance(ProxyManager);
234
+ const targetPort = getPort();
235
+ const proxyPort = getPort();
236
+ // Create a server that sets cookies
237
+ const targetServer = createHttpServer((_req, res) => {
238
+ res.writeHead(200, {
239
+ 'Content-Type': 'application/json',
240
+ 'Set-Cookie': ['sessionId=abc123; HttpOnly', 'theme=dark; Path=/'],
241
+ });
242
+ res.end(JSON.stringify({ message: 'Cookies set' }));
243
+ });
244
+ await new Promise((resolve) => {
245
+ targetServer.listen(targetPort, () => resolve());
246
+ });
247
+ try {
248
+ await proxyManager.addProxy({
249
+ sourceBaseUrl: '/api',
250
+ targetBaseUrl: `http://localhost:${targetPort}`,
251
+ sourcePort: proxyPort,
252
+ responseCookies: (cookies) => {
253
+ // Transform the cookies
254
+ return cookies.map((cookie) => cookie.replace('sessionId=abc123', 'sessionId=xyz789'));
255
+ },
256
+ });
257
+ const result = await fetch(`http://127.0.0.1:${proxyPort}/api/test`);
258
+ expect(result.status).toBe(200);
259
+ const setCookieHeader = result.headers.get('set-cookie');
260
+ expect(setCookieHeader).toContain('sessionId=xyz789');
261
+ expect(setCookieHeader).not.toContain('sessionId=abc123');
262
+ }
263
+ finally {
264
+ targetServer.close();
265
+ }
266
+ });
267
+ });
268
+ it('Should not match requests outside source base URL', async () => {
269
+ await usingAsync(new Injector(), async (injector) => {
270
+ const proxyManager = injector.getInstance(ProxyManager);
271
+ const staticServerManager = injector.getInstance(StaticServerManager);
272
+ const proxyPort = getPort();
273
+ const targetPort = getPort();
274
+ // Create a test file
275
+ const testFile = join(testDir, 'test.json');
276
+ writeFileSync(testFile, JSON.stringify({ message: 'test' }));
277
+ // Set up target server
278
+ await staticServerManager.addStaticSite({
279
+ baseUrl: '/',
280
+ path: testDir,
281
+ port: targetPort,
282
+ });
283
+ // Set up proxy server
284
+ await proxyManager.addProxy({
285
+ sourceBaseUrl: '/old',
286
+ targetBaseUrl: `http://localhost:${targetPort}`,
287
+ pathRewrite: (path) => path.replace('/path', ''),
288
+ sourcePort: proxyPort,
289
+ });
290
+ try {
291
+ const result = await fetch(`http://127.0.0.1:${proxyPort}/different/path`);
292
+ // Should not proxy, request should be handled by other handlers or return 404
293
+ expect(result.status).not.toBe(200);
294
+ }
295
+ catch (error) {
296
+ // Connection error is expected when no handler matches
297
+ expect(error).toBeDefined();
298
+ }
299
+ });
300
+ });
301
+ it('Should handle exact URL matches', async () => {
302
+ await usingAsync(new Injector(), async (injector) => {
303
+ const proxyManager = injector.getInstance(ProxyManager);
304
+ const staticServerManager = injector.getInstance(StaticServerManager);
305
+ const proxyPort = getPort();
306
+ const targetPort = getPort();
307
+ // Create a test file
308
+ const exactFile = join(testDir, 'exact.json');
309
+ writeFileSync(exactFile, JSON.stringify({
310
+ message: 'Exact match test',
311
+ url: '/exact',
312
+ }));
313
+ // Set up target server
314
+ await staticServerManager.addStaticSite({
315
+ baseUrl: '/',
316
+ path: testDir,
317
+ port: targetPort,
318
+ });
319
+ // Set up proxy server
320
+ await proxyManager.addProxy({
321
+ sourceBaseUrl: '/exact',
322
+ targetBaseUrl: `http://localhost:${targetPort}`,
323
+ pathRewrite: (path) => path, // Keep the path as-is
324
+ sourcePort: proxyPort,
325
+ });
326
+ const result = await fetch(`http://127.0.0.1:${proxyPort}/exact/exact.json`);
327
+ expect(result.status).toBe(200);
328
+ const responseData = (await result.json());
329
+ expect(responseData.message).toBe('Exact match test');
330
+ expect(responseData.url).toBe('/exact');
331
+ });
332
+ });
333
+ it('Should handle server errors gracefully', async () => {
334
+ await usingAsync(new Injector(), async (injector) => {
335
+ const proxyManager = injector.getInstance(ProxyManager);
336
+ const proxyPort = getPort();
337
+ const unavailablePort = getPort(); // Get a valid port number that doesn't have a server
338
+ // Set up proxy to non-existent target
339
+ await proxyManager.addProxy({
340
+ sourceBaseUrl: '/api',
341
+ targetBaseUrl: `http://localhost:${unavailablePort}`,
342
+ sourcePort: proxyPort,
343
+ });
344
+ const result = await fetch(`http://127.0.0.1:${proxyPort}/api/test`);
345
+ expect(result.status).toBe(502); // Bad Gateway
346
+ expect(await result.text()).toBe('Bad Gateway');
347
+ });
348
+ });
349
+ it('Should emit onProxyFailed event on error', async () => {
350
+ await usingAsync(new Injector(), async (injector) => {
351
+ const proxyManager = injector.getInstance(ProxyManager);
352
+ const proxyPort = getPort();
353
+ const unavailablePort = getPort();
354
+ let failedEvent;
355
+ proxyManager.subscribe('onProxyFailed', (event) => {
356
+ failedEvent = event;
357
+ });
358
+ // Set up proxy to non-existent target
359
+ await proxyManager.addProxy({
360
+ sourceBaseUrl: '/api',
361
+ targetBaseUrl: `http://localhost:${unavailablePort}`,
362
+ sourcePort: proxyPort,
363
+ });
364
+ await fetch(`http://127.0.0.1:${proxyPort}/api/test`);
365
+ expect(failedEvent).toBeDefined();
366
+ expect(failedEvent?.from).toBe('/api');
367
+ expect(failedEvent?.to).toContain(`http://localhost:${unavailablePort}`);
368
+ });
369
+ });
370
+ it('Should validate targetBaseUrl on addProxy', async () => {
371
+ await usingAsync(new Injector(), async (injector) => {
372
+ const proxyManager = injector.getInstance(ProxyManager);
373
+ await expect(proxyManager.addProxy({
374
+ sourceBaseUrl: '/api',
375
+ targetBaseUrl: 'not-a-valid-url',
376
+ sourcePort: getPort(),
377
+ })).rejects.toThrow('Invalid targetBaseUrl');
378
+ });
379
+ });
380
+ it('Should filter hop-by-hop headers', async () => {
381
+ await usingAsync(new Injector(), async (injector) => {
382
+ const proxyManager = injector.getInstance(ProxyManager);
383
+ const targetPort = getPort();
384
+ const proxyPort = getPort();
385
+ let receivedHeaders = {};
386
+ // Create a server that captures headers
387
+ const targetServer = createHttpServer((req, res) => {
388
+ receivedHeaders = { ...req.headers };
389
+ res.writeHead(200, { 'Content-Type': 'application/json' });
390
+ res.end(JSON.stringify({ message: 'ok' }));
391
+ });
392
+ await new Promise((resolve) => {
393
+ targetServer.listen(targetPort, () => resolve());
394
+ });
395
+ try {
396
+ await proxyManager.addProxy({
397
+ sourceBaseUrl: '/api',
398
+ targetBaseUrl: `http://localhost:${targetPort}`,
399
+ sourcePort: proxyPort,
400
+ headers: (original) => {
401
+ // Verify that Connection header was filtered from original headers
402
+ expect(original.connection).toBeUndefined();
403
+ return original;
404
+ },
405
+ });
406
+ await fetch(`http://127.0.0.1:${proxyPort}/api/test`, {
407
+ headers: {
408
+ Connection: 'keep-alive',
409
+ 'X-Custom-Header': 'should-be-forwarded',
410
+ 'X-Hop-Header': 'test-value',
411
+ },
412
+ });
413
+ // Note: fetch() may add its own Connection header when making the request
414
+ // The important thing is that custom headers are forwarded
415
+ expect(receivedHeaders['x-custom-header']).toBe('should-be-forwarded');
416
+ expect(receivedHeaders['x-hop-header']).toBe('test-value');
417
+ }
418
+ finally {
419
+ targetServer.close();
420
+ }
421
+ });
422
+ });
423
+ it('Should preserve multiple Set-Cookie headers from target', async () => {
424
+ await usingAsync(new Injector(), async (injector) => {
425
+ const proxyManager = injector.getInstance(ProxyManager);
426
+ const targetPort = getPort();
427
+ const proxyPort = getPort();
428
+ const targetServer = createHttpServer((_req, res) => {
429
+ res.writeHead(200, {
430
+ 'Content-Type': 'application/json',
431
+ 'Set-Cookie': ['cookieA=a; Path=/; HttpOnly', 'cookieB=b; Path=/', 'cookieC=c; Path=/'],
432
+ });
433
+ res.end(JSON.stringify({ ok: true }));
434
+ });
435
+ await new Promise((resolve) => {
436
+ targetServer.listen(targetPort, () => resolve());
437
+ });
438
+ try {
439
+ await proxyManager.addProxy({
440
+ sourceBaseUrl: '/api',
441
+ targetBaseUrl: `http://localhost:${targetPort}`,
442
+ sourcePort: proxyPort,
443
+ });
444
+ const result = await fetch(`http://127.0.0.1:${proxyPort}/api/test`);
445
+ expect(result.status).toBe(200);
446
+ const anyHeaders = result.headers;
447
+ const cookies = anyHeaders.getSetCookie?.();
448
+ if (cookies) {
449
+ expect(cookies.length).toBeGreaterThanOrEqual(3);
450
+ expect(cookies.join('\n')).toContain('cookieA=a');
451
+ expect(cookies.join('\n')).toContain('cookieB=b');
452
+ expect(cookies.join('\n')).toContain('cookieC=c');
453
+ }
454
+ else {
455
+ // Fallback: combined header still should contain at least one cookie
456
+ const setCookieHeader = result.headers.get('set-cookie');
457
+ expect(setCookieHeader).toBeTruthy();
458
+ expect(setCookieHeader).toContain('cookieA=a');
459
+ }
460
+ }
461
+ finally {
462
+ targetServer.close();
463
+ }
464
+ });
465
+ });
466
+ it('Should add X-Forwarded headers to target request', async () => {
467
+ await usingAsync(new Injector(), async (injector) => {
468
+ const proxyManager = injector.getInstance(ProxyManager);
469
+ const targetPort = getPort();
470
+ const proxyPort = getPort();
471
+ let received = {};
472
+ const targetServer = createHttpServer((req, res) => {
473
+ received = { ...req.headers };
474
+ res.writeHead(200, { 'Content-Type': 'application/json' });
475
+ res.end(JSON.stringify({ ok: true }));
476
+ });
477
+ await new Promise((resolve) => {
478
+ targetServer.listen(targetPort, () => resolve());
479
+ });
480
+ try {
481
+ await proxyManager.addProxy({
482
+ sourceBaseUrl: '/api',
483
+ targetBaseUrl: `http://localhost:${targetPort}`,
484
+ sourcePort: proxyPort,
485
+ });
486
+ await fetch(`http://127.0.0.1:${proxyPort}/api/test`);
487
+ expect(received['x-forwarded-for']).toBeTruthy();
488
+ expect(received['x-forwarded-host']).toBeTruthy();
489
+ expect(received['x-forwarded-proto']).toBeTruthy();
490
+ }
491
+ finally {
492
+ targetServer.close();
493
+ }
494
+ });
495
+ });
496
+ it('Should abort upstream when client disconnects', async () => {
497
+ await usingAsync(new Injector(), async (injector) => {
498
+ const proxyManager = injector.getInstance(ProxyManager);
499
+ const targetPort = getPort();
500
+ const proxyPort = getPort();
501
+ let chunksWritten = 0;
502
+ let closedEarly = false;
503
+ const totalChunks = 100;
504
+ const targetServer = createHttpServer((_req, res) => {
505
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
506
+ const interval = setInterval(() => {
507
+ chunksWritten++;
508
+ res.write('chunk\n');
509
+ if (chunksWritten >= totalChunks) {
510
+ clearInterval(interval);
511
+ res.end('done');
512
+ }
513
+ }, 5);
514
+ res.on('close', () => {
515
+ closedEarly = chunksWritten < totalChunks;
516
+ clearInterval(interval);
517
+ });
518
+ });
519
+ await new Promise((resolve) => {
520
+ targetServer.listen(targetPort, () => resolve());
521
+ });
522
+ try {
523
+ await proxyManager.addProxy({
524
+ sourceBaseUrl: '/api',
525
+ targetBaseUrl: `http://localhost:${targetPort}`,
526
+ sourcePort: proxyPort,
527
+ });
528
+ const controller = new AbortController();
529
+ const fetchPromise = fetch(`http://127.0.0.1:${proxyPort}/api/stream`, { signal: controller.signal }).then(async (response) => {
530
+ // Start reading the body stream
531
+ const reader = response.body?.getReader();
532
+ if (!reader)
533
+ throw new Error('No body');
534
+ // Read one chunk
535
+ await reader.read();
536
+ // Then abort
537
+ controller.abort();
538
+ // Try to read more (should fail)
539
+ return reader.read();
540
+ });
541
+ await expect(fetchPromise).rejects.toBeDefined();
542
+ // Give the server a moment to receive the abort
543
+ await new Promise((r) => setTimeout(r, 100));
544
+ expect(closedEarly).toBe(true);
545
+ }
546
+ finally {
547
+ targetServer.close();
548
+ }
549
+ });
550
+ });
551
+ it('Should handle request timeout', async () => {
552
+ await usingAsync(new Injector(), async (injector) => {
553
+ const proxyManager = injector.getInstance(ProxyManager);
554
+ const targetPort = getPort();
555
+ const proxyPort = getPort();
556
+ let failedEvent;
557
+ proxyManager.subscribe('onProxyFailed', (event) => {
558
+ failedEvent = event;
559
+ });
560
+ // Create a server that delays response longer than timeout
561
+ const targetServer = createHttpServer((_req, res) => {
562
+ // Delay for 2 seconds (longer than our 100ms timeout)
563
+ setTimeout(() => {
564
+ res.writeHead(200, { 'Content-Type': 'application/json' });
565
+ res.end(JSON.stringify({ message: 'delayed response' }));
566
+ }, 2000);
567
+ });
568
+ await new Promise((resolve) => {
569
+ targetServer.listen(targetPort, () => resolve());
570
+ });
571
+ try {
572
+ await proxyManager.addProxy({
573
+ sourceBaseUrl: '/api',
574
+ targetBaseUrl: `http://localhost:${targetPort}`,
575
+ sourcePort: proxyPort,
576
+ timeout: 100, // Very short timeout to trigger quickly
577
+ });
578
+ const result = await fetch(`http://127.0.0.1:${proxyPort}/api/test`);
579
+ // Should return 502 Bad Gateway due to timeout
580
+ expect(result.status).toBe(502);
581
+ expect(await result.text()).toBe('Bad Gateway');
582
+ // Should emit onProxyFailed event
583
+ expect(failedEvent).toBeDefined();
584
+ expect(failedEvent?.from).toBe('/api');
585
+ expect(failedEvent?.to).toContain(`http://localhost:${targetPort}`);
586
+ }
587
+ finally {
588
+ targetServer.close();
589
+ }
590
+ });
591
+ });
592
+ it('Should complete successfully when response is faster than timeout', async () => {
593
+ await usingAsync(new Injector(), async (injector) => {
594
+ const proxyManager = injector.getInstance(ProxyManager);
595
+ const targetPort = getPort();
596
+ const proxyPort = getPort();
597
+ // Create a fast-responding server
598
+ const targetServer = createHttpServer((_req, res) => {
599
+ res.writeHead(200, { 'Content-Type': 'application/json' });
600
+ res.end(JSON.stringify({ message: 'fast response' }));
601
+ });
602
+ await new Promise((resolve) => {
603
+ targetServer.listen(targetPort, () => resolve());
604
+ });
605
+ try {
606
+ await proxyManager.addProxy({
607
+ sourceBaseUrl: '/api',
608
+ targetBaseUrl: `http://localhost:${targetPort}`,
609
+ sourcePort: proxyPort,
610
+ timeout: 5000, // Generous timeout
611
+ });
612
+ const result = await fetch(`http://127.0.0.1:${proxyPort}/api/test`);
613
+ // Should succeed
614
+ expect(result.status).toBe(200);
615
+ const data = (await result.json());
616
+ expect(data.message).toBe('fast response');
617
+ }
618
+ finally {
619
+ targetServer.close();
620
+ }
621
+ });
622
+ });
623
+ it('Should use default timeout when not specified', async () => {
624
+ await usingAsync(new Injector(), async (injector) => {
625
+ const proxyManager = injector.getInstance(ProxyManager);
626
+ const targetPort = getPort();
627
+ const proxyPort = getPort();
628
+ // Create a server with moderate delay (500ms)
629
+ const targetServer = createHttpServer((_req, res) => {
630
+ setTimeout(() => {
631
+ res.writeHead(200, { 'Content-Type': 'application/json' });
632
+ res.end(JSON.stringify({ message: 'response after delay' }));
633
+ }, 500);
634
+ });
635
+ await new Promise((resolve) => {
636
+ targetServer.listen(targetPort, () => resolve());
637
+ });
638
+ try {
639
+ // No timeout specified - should use default 30000ms
640
+ await proxyManager.addProxy({
641
+ sourceBaseUrl: '/api',
642
+ targetBaseUrl: `http://localhost:${targetPort}`,
643
+ sourcePort: proxyPort,
644
+ // timeout not specified - uses default
645
+ });
646
+ const result = await fetch(`http://127.0.0.1:${proxyPort}/api/test`);
647
+ // Should succeed with default timeout
648
+ expect(result.status).toBe(200);
649
+ const data = (await result.json());
650
+ expect(data.message).toBe('response after delay');
651
+ }
652
+ finally {
653
+ targetServer.close();
654
+ }
655
+ });
656
+ });
657
+ });
658
+ describe('Edge cases and error handling', () => {
659
+ it('Should validate invalid protocol (non-HTTP/HTTPS)', async () => {
660
+ await usingAsync(new Injector(), async (injector) => {
661
+ const proxyManager = injector.getInstance(ProxyManager);
662
+ const proxyPort = getPort();
663
+ await expect(proxyManager.addProxy({
664
+ sourceBaseUrl: '/api',
665
+ targetBaseUrl: 'ftp://example.com',
666
+ sourcePort: proxyPort,
667
+ })).rejects.toThrow('Invalid targetBaseUrl protocol');
668
+ });
669
+ });
670
+ it('Should handle invalid target URL created by pathRewrite', async () => {
671
+ await usingAsync(new Injector(), async (injector) => {
672
+ const proxyManager = injector.getInstance(ProxyManager);
673
+ const targetPort = getPort();
674
+ const proxyPort = getPort();
675
+ let failedEvent;
676
+ proxyManager.subscribe('onProxyFailed', (event) => {
677
+ failedEvent = event;
678
+ });
679
+ // Create a simple server
680
+ const targetServer = createHttpServer((_req, res) => {
681
+ res.writeHead(200, { 'Content-Type': 'application/json' });
682
+ res.end(JSON.stringify({ message: 'ok' }));
683
+ });
684
+ await new Promise((resolve) => {
685
+ targetServer.listen(targetPort, () => resolve());
686
+ });
687
+ try {
688
+ await proxyManager.addProxy({
689
+ sourceBaseUrl: '/api',
690
+ targetBaseUrl: `http://localhost:${targetPort}`,
691
+ sourcePort: proxyPort,
692
+ // PathHelper.joinUrl now properly handles edge cases, making previously
693
+ // "invalid" URLs valid. The test now verifies that the proxy still works
694
+ // even with unusual pathRewrite results.
695
+ pathRewrite: () => '://unusual-but-valid',
696
+ });
697
+ const result = await fetch(`http://127.0.0.1:${proxyPort}/api/test`);
698
+ // With PathHelper.joinUrl, the URL is now valid and routes to the target server
699
+ // The target server returns 200, demonstrating robust URL handling
700
+ expect(result.status).toBe(200);
701
+ const body = await result.json();
702
+ expect(body).toStrictEqual({ message: 'ok' });
703
+ // No error event should be emitted since the request succeeded
704
+ expect(failedEvent).toBeUndefined();
705
+ }
706
+ finally {
707
+ targetServer.close();
708
+ }
709
+ });
710
+ });
711
+ it('Should handle number-type header values', async () => {
712
+ await usingAsync(new Injector(), async (injector) => {
713
+ const proxyManager = injector.getInstance(ProxyManager);
714
+ const targetPort = getPort();
715
+ const proxyPort = getPort();
716
+ let receivedHeaders = {};
717
+ let transformedHeaders;
718
+ const targetServer = createHttpServer((req, res) => {
719
+ receivedHeaders = { ...req.headers };
720
+ res.writeHead(200, { 'Content-Type': 'application/json' });
721
+ res.end(JSON.stringify({ ok: true }));
722
+ });
723
+ await new Promise((resolve) => {
724
+ targetServer.listen(targetPort, () => resolve());
725
+ });
726
+ try {
727
+ await proxyManager.addProxy({
728
+ sourceBaseUrl: '/api',
729
+ targetBaseUrl: `http://localhost:${targetPort}`,
730
+ sourcePort: proxyPort,
731
+ headers: (original) => {
732
+ transformedHeaders = {
733
+ ...original,
734
+ 'X-Custom-Number': 12345,
735
+ 'X-Custom-String': 'string-value',
736
+ };
737
+ return transformedHeaders;
738
+ },
739
+ });
740
+ await fetch(`http://127.0.0.1:${proxyPort}/api/test`);
741
+ // Verify headers transformer was called and returns correct types
742
+ expect(transformedHeaders).toBeDefined();
743
+ expect(transformedHeaders?.['X-Custom-Number']).toBe(12345);
744
+ expect(transformedHeaders?.['X-Custom-String']).toBe('string-value');
745
+ // Verify string header was forwarded
746
+ expect(receivedHeaders['x-custom-string']).toBe('string-value');
747
+ // Number headers get converted to strings by the proxy logic
748
+ expect(receivedHeaders['x-custom-number']).toBe('12345');
749
+ }
750
+ finally {
751
+ targetServer.close();
752
+ }
753
+ });
754
+ });
755
+ it('Should handle array-type header values', async () => {
756
+ await usingAsync(new Injector(), async (injector) => {
757
+ const proxyManager = injector.getInstance(ProxyManager);
758
+ const targetPort = getPort();
759
+ const proxyPort = getPort();
760
+ let receivedHeaders = {};
761
+ const targetServer = createHttpServer((req, res) => {
762
+ receivedHeaders = { ...req.headers };
763
+ res.writeHead(200, { 'Content-Type': 'application/json' });
764
+ res.end(JSON.stringify({ ok: true }));
765
+ });
766
+ await new Promise((resolve) => {
767
+ targetServer.listen(targetPort, () => resolve());
768
+ });
769
+ try {
770
+ await proxyManager.addProxy({
771
+ sourceBaseUrl: '/api',
772
+ targetBaseUrl: `http://localhost:${targetPort}`,
773
+ sourcePort: proxyPort,
774
+ headers: () => ({
775
+ 'X-Array-Header': ['value1', 'value2', 'value3'],
776
+ }),
777
+ });
778
+ await fetch(`http://127.0.0.1:${proxyPort}/api/test`);
779
+ expect(receivedHeaders['x-array-header']).toBe('value1, value2, value3');
780
+ }
781
+ finally {
782
+ targetServer.close();
783
+ }
784
+ });
785
+ });
786
+ it('Should handle undefined header values', async () => {
787
+ await usingAsync(new Injector(), async (injector) => {
788
+ const proxyManager = injector.getInstance(ProxyManager);
789
+ const targetPort = getPort();
790
+ const proxyPort = getPort();
791
+ let receivedHeaders = {};
792
+ const targetServer = createHttpServer((req, res) => {
793
+ receivedHeaders = { ...req.headers };
794
+ res.writeHead(200, { 'Content-Type': 'application/json' });
795
+ res.end(JSON.stringify({ ok: true }));
796
+ });
797
+ await new Promise((resolve) => {
798
+ targetServer.listen(targetPort, () => resolve());
799
+ });
800
+ try {
801
+ await proxyManager.addProxy({
802
+ sourceBaseUrl: '/api',
803
+ targetBaseUrl: `http://localhost:${targetPort}`,
804
+ sourcePort: proxyPort,
805
+ headers: () => ({
806
+ 'X-Defined-Header': 'defined',
807
+ 'X-Undefined-Header': undefined,
808
+ }),
809
+ });
810
+ await fetch(`http://127.0.0.1:${proxyPort}/api/test`);
811
+ expect(receivedHeaders['x-defined-header']).toBe('defined');
812
+ expect(receivedHeaders['x-undefined-header']).toBeUndefined();
813
+ }
814
+ finally {
815
+ targetServer.close();
816
+ }
817
+ });
818
+ });
819
+ });
820
+ describe('WebSocket proxy functionality', () => {
821
+ it('Should proxy WebSocket connections when enabled', async () => {
822
+ await usingAsync(new Injector(), async (injector) => {
823
+ const proxyManager = injector.getInstance(ProxyManager);
824
+ const targetPort = getPort();
825
+ const proxyPort = getPort();
826
+ // Create target WebSocket server
827
+ const { server: targetServer, wss: targetWss } = await createWsTestServer(targetPort, (ws) => {
828
+ ws.on('message', (data) => {
829
+ ws.send(`echo: ${rawDataToString(data)}`);
830
+ });
831
+ });
832
+ try {
833
+ // Set up proxy with WebSocket support
834
+ await proxyManager.addProxy({
835
+ sourceBaseUrl: '/ws',
836
+ targetBaseUrl: `http://localhost:${targetPort}`,
837
+ sourcePort: proxyPort,
838
+ enableWebsockets: true,
839
+ });
840
+ // Connect WebSocket client through proxy
841
+ const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`);
842
+ await new Promise((resolve, reject) => {
843
+ clientWs.on('open', () => {
844
+ clientWs.send('hello');
845
+ });
846
+ clientWs.on('message', (data) => {
847
+ expect(rawDataToString(data)).toBe('echo: hello');
848
+ clientWs.close();
849
+ resolve();
850
+ });
851
+ clientWs.on('error', (err) => {
852
+ clientWs.close();
853
+ reject(err);
854
+ });
855
+ });
856
+ }
857
+ finally {
858
+ targetWss.close();
859
+ targetServer.close();
860
+ }
861
+ });
862
+ });
863
+ it('Should handle bidirectional WebSocket communication', async () => {
864
+ await usingAsync(new Injector(), async (injector) => {
865
+ const proxyManager = injector.getInstance(ProxyManager);
866
+ const targetPort = getPort();
867
+ const proxyPort = getPort();
868
+ // Create target WebSocket server
869
+ const targetServer = createHttpServer();
870
+ const targetWss = new WebSocketServer({ server: targetServer });
871
+ targetWss.on('connection', (ws) => {
872
+ // Send messages to client
873
+ ws.send('server-message-1');
874
+ ws.send('server-message-2');
875
+ ws.on('message', (data) => {
876
+ const message = Buffer.isBuffer(data)
877
+ ? data.toString('utf8')
878
+ : Array.isArray(data)
879
+ ? Buffer.concat(data).toString('utf8')
880
+ : Buffer.from(data).toString('utf8');
881
+ ws.send(`received: ${message}`);
882
+ });
883
+ });
884
+ await new Promise((resolve) => {
885
+ targetServer.listen(targetPort, () => resolve());
886
+ });
887
+ try {
888
+ await proxyManager.addProxy({
889
+ sourceBaseUrl: '/ws',
890
+ targetBaseUrl: `http://localhost:${targetPort}`,
891
+ sourcePort: proxyPort,
892
+ enableWebsockets: true,
893
+ });
894
+ const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`);
895
+ const receivedMessages = [];
896
+ await new Promise((resolve, reject) => {
897
+ clientWs.on('open', () => {
898
+ clientWs.send('client-message-1');
899
+ clientWs.send('client-message-2');
900
+ });
901
+ clientWs.on('message', (data) => {
902
+ const message = Buffer.isBuffer(data)
903
+ ? data.toString('utf8')
904
+ : Array.isArray(data)
905
+ ? Buffer.concat(data).toString('utf8')
906
+ : Buffer.from(data).toString('utf8');
907
+ receivedMessages.push(message);
908
+ if (receivedMessages.length >= 4) {
909
+ expect(receivedMessages).toContain('server-message-1');
910
+ expect(receivedMessages).toContain('server-message-2');
911
+ expect(receivedMessages).toContain('received: client-message-1');
912
+ expect(receivedMessages).toContain('received: client-message-2');
913
+ clientWs.close();
914
+ resolve();
915
+ }
916
+ });
917
+ clientWs.on('error', (err) => {
918
+ clientWs.close();
919
+ reject(err);
920
+ });
921
+ });
922
+ }
923
+ finally {
924
+ targetWss.close();
925
+ targetServer.close();
926
+ }
927
+ });
928
+ });
929
+ it('Should not proxy WebSocket when disabled', async () => {
930
+ await usingAsync(new Injector(), async (injector) => {
931
+ const proxyManager = injector.getInstance(ProxyManager);
932
+ const targetPort = getPort();
933
+ const proxyPort = getPort();
934
+ // Create target WebSocket server
935
+ const targetServer = createHttpServer();
936
+ const targetWss = new WebSocketServer({ server: targetServer });
937
+ await new Promise((resolve) => {
938
+ targetServer.listen(targetPort, () => resolve());
939
+ });
940
+ try {
941
+ // Set up proxy WITHOUT WebSocket support
942
+ await proxyManager.addProxy({
943
+ sourceBaseUrl: '/ws',
944
+ targetBaseUrl: `http://localhost:${targetPort}`,
945
+ sourcePort: proxyPort,
946
+ enableWebsockets: false,
947
+ });
948
+ // Try to connect WebSocket client through proxy
949
+ const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`);
950
+ await new Promise((resolve) => {
951
+ clientWs.on('error', (error) => {
952
+ // Should fail because WebSocket is not enabled
953
+ expect(error).toBeDefined();
954
+ clientWs.close();
955
+ resolve();
956
+ });
957
+ clientWs.on('open', () => {
958
+ // Should not open successfully
959
+ clientWs.close();
960
+ resolve();
961
+ });
962
+ });
963
+ }
964
+ finally {
965
+ targetWss.close();
966
+ targetServer.close();
967
+ }
968
+ });
969
+ });
970
+ it('Should preserve WebSocket subprotocols', async () => {
971
+ await usingAsync(new Injector(), async (injector) => {
972
+ const proxyManager = injector.getInstance(ProxyManager);
973
+ const targetPort = getPort();
974
+ const proxyPort = getPort();
975
+ let receivedProtocol;
976
+ // Create target WebSocket server
977
+ const targetServer = createHttpServer();
978
+ const targetWss = new WebSocketServer({ server: targetServer });
979
+ targetWss.on('connection', (ws, req) => {
980
+ receivedProtocol = req.headers['sec-websocket-protocol'];
981
+ ws.send('connected');
982
+ });
983
+ await new Promise((resolve) => {
984
+ targetServer.listen(targetPort, () => resolve());
985
+ });
986
+ try {
987
+ await proxyManager.addProxy({
988
+ sourceBaseUrl: '/ws',
989
+ targetBaseUrl: `http://localhost:${targetPort}`,
990
+ sourcePort: proxyPort,
991
+ enableWebsockets: true,
992
+ });
993
+ const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`, ['v1.chat', 'v2.chat']);
994
+ await new Promise((resolve, reject) => {
995
+ clientWs.on('open', () => {
996
+ // Wait for server message
997
+ });
998
+ clientWs.on('message', (data) => {
999
+ const message = Buffer.isBuffer(data)
1000
+ ? data.toString('utf8')
1001
+ : Array.isArray(data)
1002
+ ? Buffer.concat(data).toString('utf8')
1003
+ : Buffer.from(data).toString('utf8');
1004
+ expect(message).toBe('connected');
1005
+ expect(receivedProtocol).toContain('v1.chat');
1006
+ clientWs.close();
1007
+ resolve();
1008
+ });
1009
+ clientWs.on('error', (err) => {
1010
+ clientWs.close();
1011
+ reject(err);
1012
+ });
1013
+ });
1014
+ }
1015
+ finally {
1016
+ targetWss.close();
1017
+ targetServer.close();
1018
+ }
1019
+ });
1020
+ });
1021
+ it('Should handle WebSocket path rewriting', async () => {
1022
+ await usingAsync(new Injector(), async (injector) => {
1023
+ const proxyManager = injector.getInstance(ProxyManager);
1024
+ const targetPort = getPort();
1025
+ const proxyPort = getPort();
1026
+ let receivedPath;
1027
+ // Create target WebSocket server
1028
+ const targetServer = createHttpServer();
1029
+ const targetWss = new WebSocketServer({ server: targetServer });
1030
+ targetWss.on('connection', (ws, req) => {
1031
+ receivedPath = req.url;
1032
+ ws.send('connected');
1033
+ });
1034
+ await new Promise((resolve) => {
1035
+ targetServer.listen(targetPort, () => resolve());
1036
+ });
1037
+ try {
1038
+ await proxyManager.addProxy({
1039
+ sourceBaseUrl: '/old/ws',
1040
+ targetBaseUrl: `http://localhost:${targetPort}`,
1041
+ sourcePort: proxyPort,
1042
+ enableWebsockets: true,
1043
+ pathRewrite: (path) => path.replace('/chat', '/api/chat'),
1044
+ });
1045
+ const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/old/ws/chat`);
1046
+ await new Promise((resolve, reject) => {
1047
+ clientWs.on('message', (data) => {
1048
+ const message = Buffer.isBuffer(data)
1049
+ ? data.toString('utf8')
1050
+ : Array.isArray(data)
1051
+ ? Buffer.concat(data).toString('utf8')
1052
+ : Buffer.from(data).toString('utf8');
1053
+ expect(message).toBe('connected');
1054
+ expect(receivedPath).toBe('/api/chat');
1055
+ clientWs.close();
1056
+ resolve();
1057
+ });
1058
+ clientWs.on('error', (err) => {
1059
+ clientWs.close();
1060
+ reject(err);
1061
+ });
1062
+ });
1063
+ }
1064
+ finally {
1065
+ targetWss.close();
1066
+ targetServer.close();
1067
+ }
1068
+ });
1069
+ });
1070
+ it('Should handle WebSocket connection closure from client', async () => {
1071
+ await usingAsync(new Injector(), async (injector) => {
1072
+ const proxyManager = injector.getInstance(ProxyManager);
1073
+ const targetPort = getPort();
1074
+ const proxyPort = getPort();
1075
+ let serverClosed = false;
1076
+ // Create target WebSocket server
1077
+ const targetServer = createHttpServer();
1078
+ const targetWss = new WebSocketServer({ server: targetServer });
1079
+ targetWss.on('connection', (ws) => {
1080
+ ws.on('close', () => {
1081
+ serverClosed = true;
1082
+ });
1083
+ });
1084
+ await new Promise((resolve) => {
1085
+ targetServer.listen(targetPort, () => resolve());
1086
+ });
1087
+ try {
1088
+ await proxyManager.addProxy({
1089
+ sourceBaseUrl: '/ws',
1090
+ targetBaseUrl: `http://localhost:${targetPort}`,
1091
+ sourcePort: proxyPort,
1092
+ enableWebsockets: true,
1093
+ });
1094
+ const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`);
1095
+ await new Promise((resolve, reject) => {
1096
+ clientWs.on('open', () => {
1097
+ clientWs.close();
1098
+ });
1099
+ clientWs.on('close', () => {
1100
+ // Give server time to process close event
1101
+ setTimeout(() => {
1102
+ expect(serverClosed).toBe(true);
1103
+ resolve();
1104
+ }, 100);
1105
+ });
1106
+ clientWs.on('error', (err) => {
1107
+ clientWs.close();
1108
+ reject(err);
1109
+ });
1110
+ });
1111
+ }
1112
+ finally {
1113
+ targetWss.close();
1114
+ targetServer.close();
1115
+ }
1116
+ });
1117
+ });
1118
+ it('Should handle WebSocket connection closure from server', async () => {
1119
+ await usingAsync(new Injector(), async (injector) => {
1120
+ const proxyManager = injector.getInstance(ProxyManager);
1121
+ const targetPort = getPort();
1122
+ const proxyPort = getPort();
1123
+ // Create target WebSocket server
1124
+ const targetServer = createHttpServer();
1125
+ const targetWss = new WebSocketServer({ server: targetServer });
1126
+ targetWss.on('connection', (ws) => {
1127
+ // Close connection immediately after opening
1128
+ ws.send('goodbye');
1129
+ setTimeout(() => ws.close(), 50);
1130
+ });
1131
+ await new Promise((resolve) => {
1132
+ targetServer.listen(targetPort, () => resolve());
1133
+ });
1134
+ try {
1135
+ await proxyManager.addProxy({
1136
+ sourceBaseUrl: '/ws',
1137
+ targetBaseUrl: `http://localhost:${targetPort}`,
1138
+ sourcePort: proxyPort,
1139
+ enableWebsockets: true,
1140
+ });
1141
+ const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`);
1142
+ await new Promise((resolve, reject) => {
1143
+ let messageReceived = false;
1144
+ clientWs.on('message', (data) => {
1145
+ const message = Buffer.isBuffer(data)
1146
+ ? data.toString('utf8')
1147
+ : Array.isArray(data)
1148
+ ? Buffer.concat(data).toString('utf8')
1149
+ : Buffer.from(data).toString('utf8');
1150
+ expect(message).toBe('goodbye');
1151
+ messageReceived = true;
1152
+ });
1153
+ clientWs.on('close', () => {
1154
+ expect(messageReceived).toBe(true);
1155
+ resolve();
1156
+ });
1157
+ clientWs.on('error', (err) => {
1158
+ clientWs.close();
1159
+ reject(err);
1160
+ });
1161
+ });
1162
+ }
1163
+ finally {
1164
+ targetWss.close();
1165
+ targetServer.close();
1166
+ }
1167
+ });
1168
+ });
1169
+ it('Should emit onWebSocketProxyFailed event on error', async () => {
1170
+ await usingAsync(new Injector(), async (injector) => {
1171
+ const proxyManager = injector.getInstance(ProxyManager);
1172
+ const proxyPort = getPort();
1173
+ const unavailablePort = getPort();
1174
+ let failedEvent;
1175
+ proxyManager.subscribe('onWebSocketProxyFailed', (event) => {
1176
+ failedEvent = event;
1177
+ });
1178
+ await proxyManager.addProxy({
1179
+ sourceBaseUrl: '/ws',
1180
+ targetBaseUrl: `http://localhost:${unavailablePort}`,
1181
+ sourcePort: proxyPort,
1182
+ enableWebsockets: true,
1183
+ });
1184
+ const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`);
1185
+ await new Promise((resolve) => {
1186
+ clientWs.on('error', () => {
1187
+ // Expected error
1188
+ });
1189
+ clientWs.on('close', () => {
1190
+ // Give time for event to be emitted
1191
+ setTimeout(() => {
1192
+ expect(failedEvent).toBeDefined();
1193
+ expect(failedEvent?.from).toBe('/ws');
1194
+ expect(failedEvent?.to).toContain(`http://localhost:${unavailablePort}`);
1195
+ resolve();
1196
+ }, 100);
1197
+ });
1198
+ });
1199
+ });
1200
+ });
1201
+ it('Should handle large WebSocket messages', async () => {
1202
+ await usingAsync(new Injector(), async (injector) => {
1203
+ const proxyManager = injector.getInstance(ProxyManager);
1204
+ const targetPort = getPort();
1205
+ const proxyPort = getPort();
1206
+ // Create target WebSocket server
1207
+ const targetServer = createHttpServer();
1208
+ const targetWss = new WebSocketServer({ server: targetServer });
1209
+ targetWss.on('connection', (ws) => {
1210
+ ws.on('message', (data) => {
1211
+ // Echo back the large message
1212
+ ws.send(data);
1213
+ });
1214
+ });
1215
+ await new Promise((resolve) => {
1216
+ targetServer.listen(targetPort, () => resolve());
1217
+ });
1218
+ try {
1219
+ await proxyManager.addProxy({
1220
+ sourceBaseUrl: '/ws',
1221
+ targetBaseUrl: `http://localhost:${targetPort}`,
1222
+ sourcePort: proxyPort,
1223
+ enableWebsockets: true,
1224
+ });
1225
+ const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`);
1226
+ // Create a large message (1MB)
1227
+ const largeMessage = 'x'.repeat(1024 * 1024);
1228
+ await new Promise((resolve, reject) => {
1229
+ clientWs.on('open', () => {
1230
+ clientWs.send(largeMessage);
1231
+ });
1232
+ clientWs.on('message', (data) => {
1233
+ const dataStr = Buffer.isBuffer(data)
1234
+ ? data.toString('utf8')
1235
+ : Array.isArray(data)
1236
+ ? Buffer.concat(data).toString('utf8')
1237
+ : Buffer.from(data).toString('utf8');
1238
+ expect(dataStr.length).toBe(largeMessage.length);
1239
+ expect(dataStr).toBe(largeMessage);
1240
+ clientWs.close();
1241
+ resolve();
1242
+ });
1243
+ clientWs.on('error', (err) => {
1244
+ clientWs.close();
1245
+ reject(err);
1246
+ });
1247
+ });
1248
+ }
1249
+ finally {
1250
+ targetWss.close();
1251
+ targetServer.close();
1252
+ }
1253
+ });
1254
+ });
1255
+ it('Should handle binary WebSocket messages', async () => {
1256
+ await usingAsync(new Injector(), async (injector) => {
1257
+ const proxyManager = injector.getInstance(ProxyManager);
1258
+ const targetPort = getPort();
1259
+ const proxyPort = getPort();
1260
+ // Create target WebSocket server
1261
+ const targetServer = createHttpServer();
1262
+ const targetWss = new WebSocketServer({ server: targetServer });
1263
+ targetWss.on('connection', (ws) => {
1264
+ ws.on('message', (data) => {
1265
+ // Echo back the binary data
1266
+ ws.send(data);
1267
+ });
1268
+ });
1269
+ await new Promise((resolve) => {
1270
+ targetServer.listen(targetPort, () => resolve());
1271
+ });
1272
+ try {
1273
+ await proxyManager.addProxy({
1274
+ sourceBaseUrl: '/ws',
1275
+ targetBaseUrl: `http://localhost:${targetPort}`,
1276
+ sourcePort: proxyPort,
1277
+ enableWebsockets: true,
1278
+ });
1279
+ const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`);
1280
+ // Create binary data
1281
+ const binaryData = Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05, 0xff, 0xfe, 0xfd]);
1282
+ await new Promise((resolve, reject) => {
1283
+ clientWs.on('open', () => {
1284
+ clientWs.send(binaryData);
1285
+ });
1286
+ clientWs.on('message', (data) => {
1287
+ expect(Buffer.isBuffer(data)).toBe(true);
1288
+ expect(Buffer.compare(data, binaryData)).toBe(0);
1289
+ clientWs.close();
1290
+ resolve();
1291
+ });
1292
+ clientWs.on('error', (err) => {
1293
+ clientWs.close();
1294
+ reject(err);
1295
+ });
1296
+ });
1297
+ }
1298
+ finally {
1299
+ targetWss.close();
1300
+ targetServer.close();
1301
+ }
1302
+ });
1303
+ });
1304
+ it('Should apply header transformations to WebSocket upgrade', async () => {
1305
+ await usingAsync(new Injector(), async (injector) => {
1306
+ const proxyManager = injector.getInstance(ProxyManager);
1307
+ const targetPort = getPort();
1308
+ const proxyPort = getPort();
1309
+ let receivedHeaders = {};
1310
+ // Create target WebSocket server
1311
+ const targetServer = createHttpServer();
1312
+ const targetWss = new WebSocketServer({ server: targetServer });
1313
+ targetWss.on('connection', (ws, req) => {
1314
+ receivedHeaders = { ...req.headers };
1315
+ ws.send('connected');
1316
+ });
1317
+ await new Promise((resolve) => {
1318
+ targetServer.listen(targetPort, () => resolve());
1319
+ });
1320
+ try {
1321
+ await proxyManager.addProxy({
1322
+ sourceBaseUrl: '/ws',
1323
+ targetBaseUrl: `http://localhost:${targetPort}`,
1324
+ sourcePort: proxyPort,
1325
+ enableWebsockets: true,
1326
+ headers: () => ({
1327
+ 'X-Custom-Header': 'custom-value',
1328
+ 'X-Auth-Token': 'bearer-token',
1329
+ }),
1330
+ });
1331
+ const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`);
1332
+ await new Promise((resolve, reject) => {
1333
+ clientWs.on('message', (data) => {
1334
+ const message = Buffer.isBuffer(data)
1335
+ ? data.toString('utf8')
1336
+ : Array.isArray(data)
1337
+ ? Buffer.concat(data).toString('utf8')
1338
+ : Buffer.from(data).toString('utf8');
1339
+ expect(message).toBe('connected');
1340
+ expect(receivedHeaders['x-custom-header']).toBe('custom-value');
1341
+ expect(receivedHeaders['x-auth-token']).toBe('bearer-token');
1342
+ clientWs.close();
1343
+ resolve();
1344
+ });
1345
+ clientWs.on('error', (err) => {
1346
+ clientWs.close();
1347
+ reject(err);
1348
+ });
1349
+ });
1350
+ }
1351
+ finally {
1352
+ targetWss.close();
1353
+ targetServer.close();
1354
+ }
1355
+ });
1356
+ });
1357
+ it('Should handle WebSocket upgrade timeout', async () => {
1358
+ await usingAsync(new Injector(), async (injector) => {
1359
+ const proxyManager = injector.getInstance(ProxyManager);
1360
+ const targetPort = getPort();
1361
+ const proxyPort = getPort();
1362
+ // Create a server that delays WebSocket upgrade longer than timeout
1363
+ const targetServer = createHttpServer();
1364
+ targetServer.on('upgrade', () => {
1365
+ // Don't respond to upgrade - let it timeout
1366
+ });
1367
+ await new Promise((resolve) => {
1368
+ targetServer.listen(targetPort, () => resolve());
1369
+ });
1370
+ try {
1371
+ await proxyManager.addProxy({
1372
+ sourceBaseUrl: '/ws',
1373
+ targetBaseUrl: `http://localhost:${targetPort}`,
1374
+ sourcePort: proxyPort,
1375
+ enableWebsockets: true,
1376
+ timeout: 200, // Short timeout
1377
+ });
1378
+ const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`);
1379
+ await new Promise((resolve, reject) => {
1380
+ const timeoutId = setTimeout(() => {
1381
+ // If we haven't resolved by now, the test failed
1382
+ reject(new Error('Test timeout - WebSocket did not fail as expected'));
1383
+ }, 2000);
1384
+ clientWs.on('error', () => {
1385
+ // Expected error
1386
+ });
1387
+ clientWs.on('close', () => {
1388
+ // Give time for event to be emitted
1389
+ setTimeout(() => {
1390
+ clearTimeout(timeoutId);
1391
+ // The timeout test might not always trigger the event reliably
1392
+ // Just verify the connection was closed
1393
+ resolve();
1394
+ }, 300);
1395
+ });
1396
+ });
1397
+ }
1398
+ finally {
1399
+ targetServer.close();
1400
+ }
1401
+ });
1402
+ }, 10000);
1403
+ it('Should handle unusual path in WebSocket upgrade with PathHelper', async () => {
1404
+ await usingAsync(new Injector(), async (injector) => {
1405
+ const proxyManager = injector.getInstance(ProxyManager);
1406
+ const targetPort = getPort();
1407
+ const proxyPort = getPort();
1408
+ let failedEvent;
1409
+ proxyManager.subscribe('onWebSocketProxyFailed', (event) => {
1410
+ failedEvent = event;
1411
+ });
1412
+ // Create target server that will receive the WebSocket connection
1413
+ const targetServer = createHttpServer();
1414
+ const targetWss = new WebSocketServer({ server: targetServer });
1415
+ let connectionReceived = false;
1416
+ targetWss.on('connection', () => {
1417
+ connectionReceived = true;
1418
+ });
1419
+ await new Promise((resolve) => {
1420
+ targetServer.listen(targetPort, () => resolve());
1421
+ });
1422
+ try {
1423
+ await proxyManager.addProxy({
1424
+ sourceBaseUrl: '/ws',
1425
+ targetBaseUrl: `http://localhost:${targetPort}`,
1426
+ sourcePort: proxyPort,
1427
+ enableWebsockets: true,
1428
+ // PathHelper.joinUrl now properly handles edge cases, making previously
1429
+ // "invalid" URLs valid. The WebSocket connection succeeds.
1430
+ pathRewrite: () => '://unusual-but-valid',
1431
+ });
1432
+ const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws/test`);
1433
+ await new Promise((resolve) => {
1434
+ clientWs.on('open', () => {
1435
+ // Connection successful thanks to robust URL handling
1436
+ clientWs.close();
1437
+ });
1438
+ clientWs.on('close', () => {
1439
+ // Give time for event to be processed
1440
+ setTimeout(() => {
1441
+ // No error event should be emitted since the connection succeeded
1442
+ expect(failedEvent).toBeUndefined();
1443
+ expect(connectionReceived).toBe(true);
1444
+ resolve();
1445
+ }, 100);
1446
+ });
1447
+ });
1448
+ }
1449
+ finally {
1450
+ targetWss.close();
1451
+ targetServer.close();
1452
+ }
1453
+ });
1454
+ });
1455
+ it('Should handle WebSocket header value types (number, array, undefined)', async () => {
1456
+ await usingAsync(new Injector(), async (injector) => {
1457
+ const proxyManager = injector.getInstance(ProxyManager);
1458
+ const targetPort = getPort();
1459
+ const proxyPort = getPort();
1460
+ let receivedHeaders = {};
1461
+ // Create target WebSocket server
1462
+ const { server: targetServer, wss: targetWss } = await createWsTestServer(targetPort, (ws, req) => {
1463
+ receivedHeaders = { ...req.headers };
1464
+ ws.send('connected');
1465
+ });
1466
+ try {
1467
+ await proxyManager.addProxy({
1468
+ sourceBaseUrl: '/ws',
1469
+ targetBaseUrl: `http://localhost:${targetPort}`,
1470
+ sourcePort: proxyPort,
1471
+ enableWebsockets: true,
1472
+ headers: () => ({
1473
+ 'X-Number-Header': 42,
1474
+ 'X-Array-Header': ['val1', 'val2'],
1475
+ 'X-Undefined-Header': undefined,
1476
+ 'X-String-Header': 'string-value',
1477
+ }),
1478
+ });
1479
+ const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`);
1480
+ await new Promise((resolve, reject) => {
1481
+ clientWs.on('message', (data) => {
1482
+ expect(rawDataToString(data)).toBe('connected');
1483
+ expect(receivedHeaders['x-number-header']).toBe('42');
1484
+ expect(receivedHeaders['x-array-header']).toBe('val1, val2');
1485
+ expect(receivedHeaders['x-string-header']).toBe('string-value');
1486
+ expect(receivedHeaders['x-undefined-header']).toBeUndefined();
1487
+ clientWs.close();
1488
+ resolve();
1489
+ });
1490
+ clientWs.on('error', (err) => {
1491
+ clientWs.close();
1492
+ reject(err);
1493
+ });
1494
+ });
1495
+ }
1496
+ finally {
1497
+ targetWss.close();
1498
+ targetServer.close();
1499
+ }
1500
+ });
1501
+ });
1502
+ it('Should handle WebSocket connections with query parameters', async () => {
1503
+ await usingAsync(new Injector(), async (injector) => {
1504
+ const proxyManager = injector.getInstance(ProxyManager);
1505
+ const targetPort = getPort();
1506
+ const proxyPort = getPort();
1507
+ let receivedUrl;
1508
+ const { server: targetServer, wss: targetWss } = await createWsTestServer(targetPort, (ws, req) => {
1509
+ receivedUrl = req.url;
1510
+ ws.send('connected');
1511
+ });
1512
+ try {
1513
+ await proxyManager.addProxy({
1514
+ sourceBaseUrl: '/ws',
1515
+ targetBaseUrl: `http://localhost:${targetPort}`,
1516
+ sourcePort: proxyPort,
1517
+ enableWebsockets: true,
1518
+ });
1519
+ const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws/channel?token=abc123&user=test`);
1520
+ await new Promise((resolve, reject) => {
1521
+ clientWs.on('message', (data) => {
1522
+ expect(rawDataToString(data)).toBe('connected');
1523
+ expect(receivedUrl).toBe('/channel?token=abc123&user=test');
1524
+ clientWs.close();
1525
+ resolve();
1526
+ });
1527
+ clientWs.on('error', (err) => {
1528
+ clientWs.close();
1529
+ reject(err);
1530
+ });
1531
+ });
1532
+ }
1533
+ finally {
1534
+ targetWss.close();
1535
+ targetServer.close();
1536
+ }
1537
+ });
1538
+ });
1539
+ it('Should handle WebSocket close with code and reason', async () => {
1540
+ await usingAsync(new Injector(), async (injector) => {
1541
+ const proxyManager = injector.getInstance(ProxyManager);
1542
+ const targetPort = getPort();
1543
+ const proxyPort = getPort();
1544
+ const { server: targetServer, wss: targetWss } = await createWsTestServer(targetPort, (ws) => {
1545
+ ws.close(1008, 'Policy Violation');
1546
+ });
1547
+ try {
1548
+ await proxyManager.addProxy({
1549
+ sourceBaseUrl: '/ws',
1550
+ targetBaseUrl: `http://localhost:${targetPort}`,
1551
+ sourcePort: proxyPort,
1552
+ enableWebsockets: true,
1553
+ });
1554
+ const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`);
1555
+ await new Promise((resolve) => {
1556
+ clientWs.on('close', (code, reason) => {
1557
+ expect(code).toBe(1008);
1558
+ expect(reason.toString()).toBe('Policy Violation');
1559
+ resolve();
1560
+ });
1561
+ });
1562
+ }
1563
+ finally {
1564
+ targetWss.close();
1565
+ targetServer.close();
1566
+ }
1567
+ });
1568
+ });
1569
+ });
1570
+ describe('HTTP Methods and Status Codes', () => {
1571
+ it('Should handle PUT requests with body', async () => {
1572
+ await usingAsync(new Injector(), async (injector) => {
1573
+ const proxyManager = injector.getInstance(ProxyManager);
1574
+ const targetPort = getPort();
1575
+ const proxyPort = getPort();
1576
+ const targetServer = await createEchoServer(targetPort, (req, res) => {
1577
+ const chunks = [];
1578
+ req.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
1579
+ req.on('end', () => {
1580
+ const body = Buffer.concat(chunks).toString('utf8');
1581
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1582
+ res.end(JSON.stringify({ method: req.method, body: JSON.parse(body) }));
1583
+ });
1584
+ });
1585
+ try {
1586
+ await proxyManager.addProxy({
1587
+ sourceBaseUrl: '/api',
1588
+ targetBaseUrl: `http://localhost:${targetPort}`,
1589
+ sourcePort: proxyPort,
1590
+ });
1591
+ const testData = { id: 1, name: 'updated' };
1592
+ const result = await fetch(`http://127.0.0.1:${proxyPort}/api/resource/1`, {
1593
+ method: 'PUT',
1594
+ headers: { 'Content-Type': 'application/json' },
1595
+ body: JSON.stringify(testData),
1596
+ });
1597
+ expect(result.status).toBe(200);
1598
+ const data = (await result.json());
1599
+ expect(data.method).toBe('PUT');
1600
+ expect(data.body).toEqual(testData);
1601
+ }
1602
+ finally {
1603
+ targetServer.close();
1604
+ }
1605
+ });
1606
+ });
1607
+ it('Should handle PATCH requests with body', async () => {
1608
+ await usingAsync(new Injector(), async (injector) => {
1609
+ const proxyManager = injector.getInstance(ProxyManager);
1610
+ const targetPort = getPort();
1611
+ const proxyPort = getPort();
1612
+ const targetServer = await createEchoServer(targetPort, (req, res) => {
1613
+ const chunks = [];
1614
+ req.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
1615
+ req.on('end', () => {
1616
+ const body = Buffer.concat(chunks).toString('utf8');
1617
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1618
+ res.end(JSON.stringify({ method: req.method, body: JSON.parse(body) }));
1619
+ });
1620
+ });
1621
+ try {
1622
+ await proxyManager.addProxy({
1623
+ sourceBaseUrl: '/api',
1624
+ targetBaseUrl: `http://localhost:${targetPort}`,
1625
+ sourcePort: proxyPort,
1626
+ });
1627
+ const testData = { name: 'patched' };
1628
+ const result = await fetch(`http://127.0.0.1:${proxyPort}/api/resource/1`, {
1629
+ method: 'PATCH',
1630
+ headers: { 'Content-Type': 'application/json' },
1631
+ body: JSON.stringify(testData),
1632
+ });
1633
+ expect(result.status).toBe(200);
1634
+ const data = (await result.json());
1635
+ expect(data.method).toBe('PATCH');
1636
+ expect(data.body).toEqual(testData);
1637
+ }
1638
+ finally {
1639
+ targetServer.close();
1640
+ }
1641
+ });
1642
+ });
1643
+ it('Should handle DELETE requests', async () => {
1644
+ await usingAsync(new Injector(), async (injector) => {
1645
+ const proxyManager = injector.getInstance(ProxyManager);
1646
+ const targetPort = getPort();
1647
+ const proxyPort = getPort();
1648
+ const targetServer = await createEchoServer(targetPort, (_req, res) => {
1649
+ res.writeHead(204, { 'Content-Type': 'application/json' });
1650
+ res.end();
1651
+ });
1652
+ try {
1653
+ await proxyManager.addProxy({
1654
+ sourceBaseUrl: '/api',
1655
+ targetBaseUrl: `http://localhost:${targetPort}`,
1656
+ sourcePort: proxyPort,
1657
+ });
1658
+ const result = await fetch(`http://127.0.0.1:${proxyPort}/api/resource/1`, {
1659
+ method: 'DELETE',
1660
+ });
1661
+ expect(result.status).toBe(204);
1662
+ }
1663
+ finally {
1664
+ targetServer.close();
1665
+ }
1666
+ });
1667
+ });
1668
+ it('Should handle OPTIONS requests (CORS preflight)', async () => {
1669
+ await usingAsync(new Injector(), async (injector) => {
1670
+ const proxyManager = injector.getInstance(ProxyManager);
1671
+ const targetPort = getPort();
1672
+ const proxyPort = getPort();
1673
+ const targetServer = await createEchoServer(targetPort, (_req, res) => {
1674
+ res.writeHead(200, {
1675
+ 'Access-Control-Allow-Origin': '*',
1676
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
1677
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
1678
+ });
1679
+ res.end();
1680
+ });
1681
+ try {
1682
+ await proxyManager.addProxy({
1683
+ sourceBaseUrl: '/api',
1684
+ targetBaseUrl: `http://localhost:${targetPort}`,
1685
+ sourcePort: proxyPort,
1686
+ });
1687
+ const result = await fetch(`http://127.0.0.1:${proxyPort}/api/resource`, {
1688
+ method: 'OPTIONS',
1689
+ });
1690
+ expect(result.status).toBe(200);
1691
+ expect(result.headers.get('access-control-allow-origin')).toBe('*');
1692
+ expect(result.headers.get('access-control-allow-methods')).toContain('DELETE');
1693
+ }
1694
+ finally {
1695
+ targetServer.close();
1696
+ }
1697
+ });
1698
+ });
1699
+ it('Should handle HEAD requests', async () => {
1700
+ await usingAsync(new Injector(), async (injector) => {
1701
+ const proxyManager = injector.getInstance(ProxyManager);
1702
+ const targetPort = getPort();
1703
+ const proxyPort = getPort();
1704
+ const targetServer = await createEchoServer(targetPort, (_req, res) => {
1705
+ res.writeHead(200, {
1706
+ 'Content-Type': 'application/json',
1707
+ 'Content-Length': '100',
1708
+ });
1709
+ res.end();
1710
+ });
1711
+ try {
1712
+ await proxyManager.addProxy({
1713
+ sourceBaseUrl: '/api',
1714
+ targetBaseUrl: `http://localhost:${targetPort}`,
1715
+ sourcePort: proxyPort,
1716
+ });
1717
+ const result = await fetch(`http://127.0.0.1:${proxyPort}/api/resource`, {
1718
+ method: 'HEAD',
1719
+ });
1720
+ expect(result.status).toBe(200);
1721
+ expect(result.headers.get('content-type')).toBe('application/json');
1722
+ expect(result.headers.get('content-length')).toBe('100');
1723
+ }
1724
+ finally {
1725
+ targetServer.close();
1726
+ }
1727
+ });
1728
+ });
1729
+ it('Should handle 4xx client error responses', async () => {
1730
+ await usingAsync(new Injector(), async (injector) => {
1731
+ const proxyManager = injector.getInstance(ProxyManager);
1732
+ const targetPort = getPort();
1733
+ const proxyPort = getPort();
1734
+ const targetServer = await createEchoServer(targetPort, (_req, res) => {
1735
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1736
+ res.end(JSON.stringify({ error: 'Not Found' }));
1737
+ });
1738
+ try {
1739
+ await proxyManager.addProxy({
1740
+ sourceBaseUrl: '/api',
1741
+ targetBaseUrl: `http://localhost:${targetPort}`,
1742
+ sourcePort: proxyPort,
1743
+ });
1744
+ const result = await fetch(`http://127.0.0.1:${proxyPort}/api/nonexistent`);
1745
+ expect(result.status).toBe(404);
1746
+ const data = (await result.json());
1747
+ expect(data.error).toBe('Not Found');
1748
+ }
1749
+ finally {
1750
+ targetServer.close();
1751
+ }
1752
+ });
1753
+ });
1754
+ it('Should handle 5xx server error responses', async () => {
1755
+ await usingAsync(new Injector(), async (injector) => {
1756
+ const proxyManager = injector.getInstance(ProxyManager);
1757
+ const targetPort = getPort();
1758
+ const proxyPort = getPort();
1759
+ const targetServer = await createEchoServer(targetPort, (_req, res) => {
1760
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1761
+ res.end(JSON.stringify({ error: 'Internal Server Error' }));
1762
+ });
1763
+ try {
1764
+ await proxyManager.addProxy({
1765
+ sourceBaseUrl: '/api',
1766
+ targetBaseUrl: `http://localhost:${targetPort}`,
1767
+ sourcePort: proxyPort,
1768
+ });
1769
+ const result = await fetch(`http://127.0.0.1:${proxyPort}/api/error`);
1770
+ expect(result.status).toBe(500);
1771
+ const data = (await result.json());
1772
+ expect(data.error).toBe('Internal Server Error');
1773
+ }
1774
+ finally {
1775
+ targetServer.close();
1776
+ }
1777
+ });
1778
+ });
1779
+ });
1780
+ });
1781
+ //# sourceMappingURL=proxy-manager.spec.js.map