import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import * as tls from 'tls'; import { SmartProxy } from '../ts/index.js'; let testProxy: SmartProxy; let targetServer: net.Server; // Create a simple echo server as target tap.test('setup test environment', async () => { // Create target server that echoes data back targetServer = net.createServer((socket) => { console.log('Target server: client connected'); // Echo data back socket.on('data', (data) => { console.log(`Target server received: ${data.toString().trim()}`); socket.write(data); }); socket.on('close', () => { console.log('Target server: client disconnected'); }); }); await new Promise((resolve) => { targetServer.listen(9876, () => { console.log('Target server listening on port 9876'); resolve(); }); }); // Create proxy with simple TCP forwarding (no TLS) testProxy = new SmartProxy({ routes: [{ name: 'tcp-forward-test', match: { ports: 8888 // Plain TCP port }, action: { type: 'forward', target: { host: 'localhost', port: 9876 } // No TLS configuration - just plain TCP forwarding } }], defaults: { target: { host: 'localhost', port: 9876 } }, enableDetailedLogging: true, keepAliveTreatment: 'extended', // Allow long-lived connections inactivityTimeout: 3600000, // 1 hour socketTimeout: 3600000, // 1 hour keepAlive: true, keepAliveInitialDelay: 1000 }); await testProxy.start(); }); tap.test('should keep WebSocket-like connection open for extended period', async (tools) => { tools.timeout(65000); // 65 second test timeout const client = new net.Socket(); let messagesReceived = 0; let connectionClosed = false; // Connect to proxy await new Promise((resolve, reject) => { client.connect(8888, 'localhost', () => { console.log('Client connected to proxy'); resolve(); }); client.on('error', reject); }); // Set up data handler client.on('data', (data) => { console.log(`Client received: ${data.toString().trim()}`); messagesReceived++; }); client.on('close', () => { console.log('Client connection closed'); connectionClosed = true; }); // Send initial handshake-like data client.write('HELLO\n'); // Wait for response await new Promise(resolve => setTimeout(resolve, 100)); expect(messagesReceived).toEqual(1); // Simulate WebSocket-like keep-alive pattern // Send periodic messages over 60 seconds const startTime = Date.now(); const pingInterval = setInterval(() => { if (!connectionClosed && Date.now() - startTime < 60000) { console.log('Sending ping...'); client.write('PING\n'); } else { clearInterval(pingInterval); } }, 10000); // Every 10 seconds // Wait for 61 seconds await new Promise(resolve => setTimeout(resolve, 61000)); // Clean up interval clearInterval(pingInterval); // Connection should still be open expect(connectionClosed).toEqual(false); // Should have received responses (1 hello + 6 pings) expect(messagesReceived).toBeGreaterThan(5); // Close connection gracefully client.end(); // Wait for close await new Promise(resolve => setTimeout(resolve, 100)); expect(connectionClosed).toEqual(true); }); tap.test('should support half-open connections', async () => { const client = new net.Socket(); const serverSocket = await new Promise((resolve) => { targetServer.once('connection', resolve); client.connect(8888, 'localhost'); }); let clientClosed = false; let serverClosed = false; let serverReceivedData = false; client.on('close', () => { clientClosed = true; }); serverSocket.on('close', () => { serverClosed = true; }); serverSocket.on('data', () => { serverReceivedData = true; }); // Client sends data then closes write side client.write('HALF-OPEN TEST\n'); client.end(); // Close write side only // Wait a bit await new Promise(resolve => setTimeout(resolve, 500)); // Server should still be able to send data expect(serverClosed).toEqual(false); serverSocket.write('RESPONSE\n'); // Wait for data await new Promise(resolve => setTimeout(resolve, 100)); // Now close server side serverSocket.end(); // Wait for full close await new Promise(resolve => setTimeout(resolve, 500)); expect(clientClosed).toEqual(true); expect(serverClosed).toEqual(true); expect(serverReceivedData).toEqual(true); }); tap.test('cleanup', async () => { await testProxy.stop(); await new Promise((resolve) => { targetServer.close(() => { console.log('Target server closed'); resolve(); }); }); }); export default tap.start();