413 lines
11 KiB
TypeScript
413 lines
11 KiB
TypeScript
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<void>(resolve => {
|
|
testServer.listen(0, () => {
|
|
const address = testServer.address() as { port: number };
|
|
serverPort = address.port;
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
await new Promise<void>(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<void>((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<void>((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);
|
|
}); |