import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as plugins from '../ts/plugins.js'; import { HttpProxy } from '../ts/proxies/http-proxy/index.js'; import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js'; import type { IRouteContext } from '../ts/core/models/route-context.js'; // Declare variables for tests let httpProxy: HttpProxy; let testServer: plugins.http.Server; let testServerHttp2: plugins.http2.Http2Server; let serverPort: number; let serverPortHttp2: number; // Setup test environment tap.test('setup HttpProxy function-based targets test environment', async (tools) => { // Set a reasonable timeout for the test tools.timeout(30000); // 30 seconds // Create simple HTTP server to respond to requests testServer = plugins.http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ url: req.url, headers: req.headers, method: req.method, message: 'HTTP/1.1 Response' })); }); // Create simple HTTP/2 server to respond to requests testServerHttp2 = plugins.http2.createServer(); testServerHttp2.on('stream', (stream, headers) => { stream.respond({ 'content-type': 'application/json', ':status': 200 }); stream.end(JSON.stringify({ path: headers[':path'], headers, method: headers[':method'], message: 'HTTP/2 Response' })); }); // Handle HTTP/2 errors testServerHttp2.on('error', (err) => { console.error('HTTP/2 server error:', err); }); // Start the servers await new Promise(resolve => { testServer.listen(0, () => { const address = testServer.address() as { port: number }; serverPort = address.port; resolve(); }); }); await new Promise(resolve => { testServerHttp2.listen(0, () => { const address = testServerHttp2.address() as { port: number }; serverPortHttp2 = address.port; resolve(); }); }); // Create HttpProxy instance httpProxy = new HttpProxy({ port: 0, // Use dynamic port logLevel: 'info', // Use info level to see more logs // Disable ACME to avoid trying to bind to port 80 acme: { enabled: false } }); await httpProxy.start(); // Log the actual port being used const actualPort = httpProxy.getListeningPort(); console.log(`HttpProxy actual listening port: ${actualPort}`); }); // Test static host/port routes tap.test('should support static host/port routes', async () => { const routes: IRouteConfig[] = [ { name: 'static-route', priority: 100, match: { domains: 'example.com', ports: 0 }, action: { type: 'forward', target: { host: 'localhost', port: serverPort } } } ]; await httpProxy.updateRouteConfigs(routes); // Get proxy port using the improved getListeningPort() method const proxyPort = httpProxy.getListeningPort(); // Make request to proxy const response = await makeRequest({ hostname: 'localhost', port: proxyPort, path: '/test', method: 'GET', headers: { 'Host': 'example.com' } }); expect(response.statusCode).toEqual(200); const body = JSON.parse(response.body); expect(body.url).toEqual('/test'); expect(body.headers.host).toEqual(`localhost:${serverPort}`); }); // Test function-based host tap.test('should support function-based host', async () => { const routes: IRouteConfig[] = [ { name: 'function-host-route', priority: 100, match: { domains: 'function.example.com', ports: 0 }, action: { type: 'forward', target: { host: (context: IRouteContext) => { // Return localhost always in this test return 'localhost'; }, port: serverPort } } } ]; await httpProxy.updateRouteConfigs(routes); // Get proxy port using the improved getListeningPort() method const proxyPort = httpProxy.getListeningPort(); // Make request to proxy const response = await makeRequest({ hostname: 'localhost', port: proxyPort, path: '/function-host', method: 'GET', headers: { 'Host': 'function.example.com' } }); expect(response.statusCode).toEqual(200); const body = JSON.parse(response.body); expect(body.url).toEqual('/function-host'); expect(body.headers.host).toEqual(`localhost:${serverPort}`); }); // Test function-based port tap.test('should support function-based port', async () => { const routes: IRouteConfig[] = [ { name: 'function-port-route', priority: 100, match: { domains: 'function-port.example.com', ports: 0 }, action: { type: 'forward', target: { host: 'localhost', port: (context: IRouteContext) => { // Return test server port return serverPort; } } } } ]; await httpProxy.updateRouteConfigs(routes); // Get proxy port using the improved getListeningPort() method const proxyPort = httpProxy.getListeningPort(); // Make request to proxy const response = await makeRequest({ hostname: 'localhost', port: proxyPort, path: '/function-port', method: 'GET', headers: { 'Host': 'function-port.example.com' } }); expect(response.statusCode).toEqual(200); const body = JSON.parse(response.body); expect(body.url).toEqual('/function-port'); expect(body.headers.host).toEqual(`localhost:${serverPort}`); }); // Test function-based host AND port tap.test('should support function-based host AND port', async () => { const routes: IRouteConfig[] = [ { name: 'function-both-route', priority: 100, match: { domains: 'function-both.example.com', ports: 0 }, action: { type: 'forward', target: { host: (context: IRouteContext) => { return 'localhost'; }, port: (context: IRouteContext) => { return serverPort; } } } } ]; await httpProxy.updateRouteConfigs(routes); // Get proxy port using the improved getListeningPort() method const proxyPort = httpProxy.getListeningPort(); // Make request to proxy const response = await makeRequest({ hostname: 'localhost', port: proxyPort, path: '/function-both', method: 'GET', headers: { 'Host': 'function-both.example.com' } }); expect(response.statusCode).toEqual(200); const body = JSON.parse(response.body); expect(body.url).toEqual('/function-both'); expect(body.headers.host).toEqual(`localhost:${serverPort}`); }); // Test context-based routing with path tap.test('should support context-based routing with path', async () => { const routes: IRouteConfig[] = [ { name: 'context-path-route', priority: 100, match: { domains: 'context.example.com', ports: 0 }, action: { type: 'forward', target: { host: (context: IRouteContext) => { // Use path to determine host if (context.path?.startsWith('/api')) { return 'localhost'; } else { return '127.0.0.1'; // Another way to reference localhost } }, port: serverPort } } } ]; await httpProxy.updateRouteConfigs(routes); // Get proxy port using the improved getListeningPort() method const proxyPort = httpProxy.getListeningPort(); // Make request to proxy with /api path const apiResponse = await makeRequest({ hostname: 'localhost', port: proxyPort, path: '/api/test', method: 'GET', headers: { 'Host': 'context.example.com' } }); expect(apiResponse.statusCode).toEqual(200); const apiBody = JSON.parse(apiResponse.body); expect(apiBody.url).toEqual('/api/test'); // Make request to proxy with non-api path const nonApiResponse = await makeRequest({ hostname: 'localhost', port: proxyPort, path: '/web/test', method: 'GET', headers: { 'Host': 'context.example.com' } }); expect(nonApiResponse.statusCode).toEqual(200); const nonApiBody = JSON.parse(nonApiResponse.body); expect(nonApiBody.url).toEqual('/web/test'); }); // Cleanup test environment tap.test('cleanup HttpProxy function-based targets test environment', async () => { // Skip cleanup if setup failed if (!httpProxy && !testServer && !testServerHttp2) { console.log('Skipping cleanup - setup failed'); return; } // Stop test servers first if (testServer) { await new Promise((resolve, reject) => { testServer.close((err) => { if (err) { console.error('Error closing test server:', err); reject(err); } else { console.log('Test server closed successfully'); resolve(); } }); }); } if (testServerHttp2) { await new Promise((resolve, reject) => { testServerHttp2.close((err) => { if (err) { console.error('Error closing HTTP/2 test server:', err); reject(err); } else { console.log('HTTP/2 test server closed successfully'); resolve(); } }); }); } // Stop HttpProxy last if (httpProxy) { console.log('Stopping HttpProxy...'); await httpProxy.stop(); console.log('HttpProxy stopped successfully'); } // Force exit after a short delay to ensure cleanup const cleanupTimeout = setTimeout(() => { console.log('Cleanup completed, exiting'); }, 100); // Don't keep the process alive just for this timeout if (cleanupTimeout.unref) { cleanupTimeout.unref(); } }); // Helper function to make HTTPS requests with self-signed certificate support async function makeRequest(options: plugins.http.RequestOptions): Promise<{ statusCode: number, headers: plugins.http.IncomingHttpHeaders, body: string }> { return new Promise((resolve, reject) => { // Use HTTPS with rejectUnauthorized: false to accept self-signed certificates const req = plugins.https.request({ ...options, rejectUnauthorized: false, // Accept self-signed certificates }, (res) => { let body = ''; res.on('data', (chunk) => { body += chunk; }); res.on('end', () => { resolve({ statusCode: res.statusCode || 0, headers: res.headers, body }); }); }); req.on('error', (err) => { console.error(`Request error: ${err.message}`); reject(err); }); req.end(); }); } // Start the tests tap.start().then(() => { // Ensure process exits after tests complete process.exit(0); });