import { expect, tap } from '@push.rocks/tapbundle'; import { NetworkProxy } from '../ts/proxies/network-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'; import * as http from 'http'; import * as https from 'https'; import * as http2 from 'http2'; const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); // Declare variables for tests let networkProxy: NetworkProxy; let testServer: http.Server; let testServerHttp2: http2.Http2Server; let serverPort: number; let serverPortHttp2: number; // Setup test environment tap.test('setup NetworkProxy function-based targets test environment', async () => { // Create simple HTTP server to respond to requests testServer = 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 = 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' })); }); // 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 NetworkProxy instance networkProxy = new NetworkProxy({ 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 networkProxy.start(); // Log the actual port being used const actualPort = networkProxy.getListeningPort(); console.log(`NetworkProxy actual listening port: ${actualPort}`); }); // Test static host/port routes tap.test('should support static host/port routes', async () => { const routes: IRouteConfig[] = [ { name: 'static-route', domain: 'example.com', priority: 100, match: { domain: 'example.com' }, action: { type: 'forward', target: { host: 'localhost', port: serverPort } } } ]; await networkProxy.updateRouteConfigs(routes); // Get proxy port using the improved getListeningPort() method const proxyPort = networkProxy.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', domain: 'function.example.com', priority: 100, match: { domain: 'function.example.com' }, action: { type: 'forward', target: { host: (context: IRouteContext) => { // Return localhost always in this test return 'localhost'; }, port: serverPort } } } ]; await networkProxy.updateRouteConfigs(routes); // Get proxy port using the improved getListeningPort() method const proxyPort = networkProxy.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', domain: 'function-port.example.com', priority: 100, match: { domain: 'function-port.example.com' }, action: { type: 'forward', target: { host: 'localhost', port: (context: IRouteContext) => { // Return test server port return serverPort; } } } } ]; await networkProxy.updateRouteConfigs(routes); // Get proxy port using the improved getListeningPort() method const proxyPort = networkProxy.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', domain: 'function-both.example.com', priority: 100, match: { domain: 'function-both.example.com' }, action: { type: 'forward', target: { host: (context: IRouteContext) => { return 'localhost'; }, port: (context: IRouteContext) => { return serverPort; } } } } ]; await networkProxy.updateRouteConfigs(routes); // Get proxy port using the improved getListeningPort() method const proxyPort = networkProxy.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', domain: 'context.example.com', priority: 100, match: { domain: 'context.example.com' }, 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 networkProxy.updateRouteConfigs(routes); // Get proxy port using the improved getListeningPort() method const proxyPort = networkProxy.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 NetworkProxy function-based targets test environment', async () => { if (networkProxy) { await networkProxy.stop(); } if (testServer) { await new Promise(resolve => { testServer.close(() => resolve()); }); } if (testServerHttp2) { await new Promise(resolve => { testServerHttp2.close(() => resolve()); }); } }); // Helper function to make HTTPS requests with self-signed certificate support async function makeRequest(options: http.RequestOptions): Promise<{ statusCode: number, headers: http.IncomingHttpHeaders, body: string }> { return new Promise((resolve, reject) => { // Use HTTPS with rejectUnauthorized: false to accept self-signed certificates const req = 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(); }); } // Export the test runner to start tests export default tap.start();