259 lines
7.9 KiB
TypeScript
259 lines
7.9 KiB
TypeScript
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import * as net from 'net';
|
|
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
|
import {
|
|
createPortMappingRoute,
|
|
createOffsetPortMappingRoute,
|
|
createDynamicRoute,
|
|
createSmartLoadBalancer,
|
|
createPortOffset
|
|
} from '../ts/proxies/smart-proxy/utils/route-helpers.js';
|
|
import type { IRouteConfig, IRouteContext } from '../ts/proxies/smart-proxy/models/route-types.js';
|
|
|
|
// Test server and client utilities
|
|
let testServers: Array<{ server: net.Server; port: number }> = [];
|
|
let smartProxy: SmartProxy;
|
|
|
|
const TEST_PORT_START = 4000;
|
|
const PROXY_PORT_START = 5000;
|
|
const TEST_DATA = 'Hello through dynamic port mapper!';
|
|
|
|
// Cleanup function to close all servers and proxies
|
|
function cleanup() {
|
|
console.log('Starting cleanup...');
|
|
const promises = [];
|
|
|
|
// Close test servers
|
|
for (const { server, port } of testServers) {
|
|
promises.push(new Promise<void>(resolve => {
|
|
console.log(`Closing test server on port ${port}`);
|
|
server.close(() => {
|
|
console.log(`Test server on port ${port} closed`);
|
|
resolve();
|
|
});
|
|
}));
|
|
}
|
|
|
|
// Stop SmartProxy
|
|
if (smartProxy) {
|
|
console.log('Stopping SmartProxy...');
|
|
promises.push(smartProxy.stop().then(() => {
|
|
console.log('SmartProxy stopped');
|
|
}));
|
|
}
|
|
|
|
return Promise.all(promises);
|
|
}
|
|
|
|
// Helper: Creates a test TCP server that listens on a given port
|
|
function createTestServer(port: number): Promise<net.Server> {
|
|
return new Promise((resolve) => {
|
|
const server = net.createServer((socket) => {
|
|
socket.on('data', (data) => {
|
|
// Echo the received data back with a server identifier
|
|
socket.write(`Server ${port} says: ${data.toString()}`);
|
|
});
|
|
socket.on('error', (error) => {
|
|
console.error(`[Test Server] Socket error on port ${port}:`, error);
|
|
});
|
|
});
|
|
|
|
server.listen(port, () => {
|
|
console.log(`[Test Server] Listening on port ${port}`);
|
|
testServers.push({ server, port });
|
|
resolve(server);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Helper: Creates a test client connection with timeout
|
|
function createTestClient(port: number, data: string): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const client = new net.Socket();
|
|
let response = '';
|
|
|
|
const timeout = setTimeout(() => {
|
|
client.destroy();
|
|
reject(new Error(`Client connection timeout to port ${port}`));
|
|
}, 5000);
|
|
|
|
client.connect(port, 'localhost', () => {
|
|
console.log(`[Test Client] Connected to server on port ${port}`);
|
|
client.write(data);
|
|
});
|
|
|
|
client.on('data', (chunk) => {
|
|
response += chunk.toString();
|
|
client.end();
|
|
});
|
|
|
|
client.on('end', () => {
|
|
clearTimeout(timeout);
|
|
resolve(response);
|
|
});
|
|
|
|
client.on('error', (error) => {
|
|
clearTimeout(timeout);
|
|
reject(error);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Set up test environment
|
|
tap.test('setup port mapping test environment', async () => {
|
|
// Create multiple test servers on different ports
|
|
await Promise.all([
|
|
createTestServer(TEST_PORT_START), // Server on port 4000
|
|
createTestServer(TEST_PORT_START + 1), // Server on port 4001
|
|
createTestServer(TEST_PORT_START + 2), // Server on port 4002
|
|
]);
|
|
|
|
// Create a SmartProxy with dynamic port mapping routes
|
|
smartProxy = new SmartProxy({
|
|
routes: [
|
|
// Simple function that returns the same port (identity mapping)
|
|
createPortMappingRoute({
|
|
sourcePortRange: PROXY_PORT_START,
|
|
targetHost: 'localhost',
|
|
portMapper: (context) => TEST_PORT_START,
|
|
name: 'Identity Port Mapping'
|
|
}),
|
|
|
|
// Offset port mapping from 5001 to 4001 (offset -1000)
|
|
createOffsetPortMappingRoute({
|
|
ports: PROXY_PORT_START + 1,
|
|
targetHost: 'localhost',
|
|
offset: -1000,
|
|
name: 'Offset Port Mapping (-1000)'
|
|
}),
|
|
|
|
// Dynamic route with conditional port mapping
|
|
createDynamicRoute({
|
|
ports: [PROXY_PORT_START + 2, PROXY_PORT_START + 3],
|
|
targetHost: (context) => {
|
|
// Dynamic host selection based on port
|
|
return context.port === PROXY_PORT_START + 2 ? 'localhost' : '127.0.0.1';
|
|
},
|
|
portMapper: (context) => {
|
|
// Port mapping logic based on incoming port
|
|
if (context.port === PROXY_PORT_START + 2) {
|
|
return TEST_PORT_START;
|
|
} else {
|
|
return TEST_PORT_START + 2;
|
|
}
|
|
},
|
|
name: 'Dynamic Host and Port Mapping'
|
|
}),
|
|
|
|
// Smart load balancer for domain-based routing
|
|
createSmartLoadBalancer({
|
|
ports: PROXY_PORT_START + 4,
|
|
domainTargets: {
|
|
'test1.example.com': 'localhost',
|
|
'test2.example.com': '127.0.0.1'
|
|
},
|
|
portMapper: (context) => {
|
|
// Use different backend ports based on domain
|
|
if (context.domain === 'test1.example.com') {
|
|
return TEST_PORT_START;
|
|
} else {
|
|
return TEST_PORT_START + 1;
|
|
}
|
|
},
|
|
defaultTarget: 'localhost',
|
|
name: 'Smart Domain Load Balancer'
|
|
})
|
|
]
|
|
});
|
|
|
|
// Start the SmartProxy
|
|
await smartProxy.start();
|
|
});
|
|
|
|
// Test 1: Simple identity port mapping (5000 -> 4000)
|
|
tap.test('should map port using identity function', async () => {
|
|
const response = await createTestClient(PROXY_PORT_START, TEST_DATA);
|
|
expect(response).toEqual(`Server ${TEST_PORT_START} says: ${TEST_DATA}`);
|
|
});
|
|
|
|
// Test 2: Offset port mapping (5001 -> 4001)
|
|
tap.test('should map port using offset function', async () => {
|
|
const response = await createTestClient(PROXY_PORT_START + 1, TEST_DATA);
|
|
expect(response).toEqual(`Server ${TEST_PORT_START + 1} says: ${TEST_DATA}`);
|
|
});
|
|
|
|
// Test 3: Dynamic port and host mapping (conditional logic)
|
|
tap.test('should map port using dynamic logic', async () => {
|
|
const response = await createTestClient(PROXY_PORT_START + 2, TEST_DATA);
|
|
expect(response).toEqual(`Server ${TEST_PORT_START} says: ${TEST_DATA}`);
|
|
});
|
|
|
|
// Test 4: Test reuse of createPortOffset helper
|
|
tap.test('should use createPortOffset helper for port mapping', async () => {
|
|
// Test the createPortOffset helper
|
|
const offsetFn = createPortOffset(-1000);
|
|
const context = {
|
|
port: PROXY_PORT_START + 1,
|
|
clientIp: '127.0.0.1',
|
|
serverIp: '127.0.0.1',
|
|
isTls: false,
|
|
timestamp: Date.now(),
|
|
connectionId: 'test-connection'
|
|
} as IRouteContext;
|
|
|
|
const mappedPort = offsetFn(context);
|
|
expect(mappedPort).toEqual(TEST_PORT_START + 1);
|
|
});
|
|
|
|
// Test 5: Test error handling for invalid port mapping functions
|
|
tap.test('should handle errors in port mapping functions', async () => {
|
|
// Create a route with a function that throws an error
|
|
const errorRoute: IRouteConfig = {
|
|
match: {
|
|
ports: PROXY_PORT_START + 5
|
|
},
|
|
action: {
|
|
type: 'forward',
|
|
target: {
|
|
host: 'localhost',
|
|
port: () => {
|
|
throw new Error('Test error in port mapping function');
|
|
}
|
|
}
|
|
},
|
|
name: 'Error Route'
|
|
};
|
|
|
|
// Add the route to SmartProxy
|
|
await smartProxy.updateRoutes([...smartProxy.settings.routes, errorRoute]);
|
|
|
|
// The connection should fail or timeout
|
|
try {
|
|
await createTestClient(PROXY_PORT_START + 5, TEST_DATA);
|
|
// Connection should not succeed
|
|
expect(false).toBeTrue();
|
|
} catch (error) {
|
|
// Connection failed as expected
|
|
expect(true).toBeTrue();
|
|
}
|
|
});
|
|
|
|
// Cleanup
|
|
tap.test('cleanup port mapping test environment', async () => {
|
|
// Add timeout to prevent hanging if SmartProxy shutdown has issues
|
|
const cleanupPromise = cleanup();
|
|
const timeoutPromise = new Promise((_, reject) =>
|
|
setTimeout(() => reject(new Error('Cleanup timeout after 5 seconds')), 5000)
|
|
);
|
|
|
|
try {
|
|
await Promise.race([cleanupPromise, timeoutPromise]);
|
|
} catch (error) {
|
|
console.error('Cleanup error:', error);
|
|
// Force cleanup even if there's an error
|
|
testServers = [];
|
|
smartProxy = null as any;
|
|
}
|
|
});
|
|
|
|
export default tap.start(); |