import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import * as plugins from '../ts/plugins.js'; // Import SmartProxy and configurations import { SmartProxy } from '../ts/index.js'; tap.test('should handle clients that connect and immediately disconnect without sending data', async () => { console.log('\n=== Testing Connect-Disconnect Cleanup ==='); // Create a SmartProxy instance const proxy = new SmartProxy({ ports: [8560], enableDetailedLogging: false, initialDataTimeout: 5000, // 5 second timeout for initial data routes: [{ name: 'test-route', match: { ports: 8560 }, action: { type: 'forward', target: { host: 'localhost', port: 9999 // Non-existent port } } }] }); // Start the proxy await proxy.start(); console.log('✓ Proxy started on port 8560'); // Helper to get active connection count const getActiveConnections = () => { const connectionManager = (proxy as any).connectionManager; return connectionManager ? connectionManager.getConnectionCount() : 0; }; const initialCount = getActiveConnections(); console.log(`Initial connection count: ${initialCount}`); // Test 1: Connect and immediately disconnect without sending data console.log('\n--- Test 1: Immediate disconnect ---'); const connectionCounts: number[] = []; for (let i = 0; i < 10; i++) { const client = new net.Socket(); // Connect and immediately destroy client.connect(8560, 'localhost', () => { // Connected - immediately destroy without sending data client.destroy(); }); // Wait a tiny bit await new Promise(resolve => setTimeout(resolve, 10)); const count = getActiveConnections(); connectionCounts.push(count); if ((i + 1) % 5 === 0) { console.log(`After ${i + 1} connect/disconnect cycles: ${count} active connections`); } } // Wait a bit for cleanup await new Promise(resolve => setTimeout(resolve, 500)); const afterImmediateDisconnect = getActiveConnections(); console.log(`After immediate disconnect test: ${afterImmediateDisconnect} active connections`); // Test 2: Connect, wait a bit, then disconnect without sending data console.log('\n--- Test 2: Delayed disconnect ---'); for (let i = 0; i < 5; i++) { const client = new net.Socket(); client.on('error', () => { // Ignore errors }); client.connect(8560, 'localhost', () => { // Wait 100ms then disconnect without sending data setTimeout(() => { if (!client.destroyed) { client.destroy(); } }, 100); }); } // Check count immediately const duringDelayed = getActiveConnections(); console.log(`During delayed disconnect test: ${duringDelayed} active connections`); // Wait for cleanup await new Promise(resolve => setTimeout(resolve, 1000)); const afterDelayedDisconnect = getActiveConnections(); console.log(`After delayed disconnect test: ${afterDelayedDisconnect} active connections`); // Test 3: Mix of immediate and delayed disconnects console.log('\n--- Test 3: Mixed disconnect patterns ---'); const promises = []; for (let i = 0; i < 20; i++) { promises.push(new Promise((resolve) => { const client = new net.Socket(); client.on('error', () => { resolve(); }); client.on('close', () => { resolve(); }); client.connect(8560, 'localhost', () => { if (i % 2 === 0) { // Half disconnect immediately client.destroy(); } else { // Half wait 50ms setTimeout(() => { if (!client.destroyed) { client.destroy(); } }, 50); } }); // Failsafe timeout setTimeout(() => resolve(), 200); })); } // Wait for all to complete await Promise.all(promises); const duringMixed = getActiveConnections(); console.log(`During mixed test: ${duringMixed} active connections`); // Final cleanup wait await new Promise(resolve => setTimeout(resolve, 1000)); const finalCount = getActiveConnections(); console.log(`\nFinal connection count: ${finalCount}`); // Stop the proxy await proxy.stop(); console.log('✓ Proxy stopped'); // Verify all connections were cleaned up expect(finalCount).toEqual(initialCount); expect(afterImmediateDisconnect).toEqual(initialCount); expect(afterDelayedDisconnect).toEqual(initialCount); // Check that connections didn't accumulate during the test const maxCount = Math.max(...connectionCounts); console.log(`\nMax connection count during immediate disconnect test: ${maxCount}`); expect(maxCount).toBeLessThan(3); // Should stay very low console.log('\n✅ PASS: Connect-disconnect cleanup working correctly!'); }); tap.test('should handle clients that error during connection', async () => { console.log('\n=== Testing Connection Error Cleanup ==='); const proxy = new SmartProxy({ ports: [8561], enableDetailedLogging: false, routes: [{ name: 'test-route', match: { ports: 8561 }, action: { type: 'forward', target: { host: 'localhost', port: 9999 } } }] }); await proxy.start(); console.log('✓ Proxy started on port 8561'); const getActiveConnections = () => { const connectionManager = (proxy as any).connectionManager; return connectionManager ? connectionManager.getConnectionCount() : 0; }; const initialCount = getActiveConnections(); console.log(`Initial connection count: ${initialCount}`); // Create connections that will error const promises = []; for (let i = 0; i < 10; i++) { promises.push(new Promise((resolve) => { const client = new net.Socket(); client.on('error', () => { resolve(); }); client.on('close', () => { resolve(); }); // Connect to proxy client.connect(8561, 'localhost', () => { // Force an error by writing invalid data then destroying try { client.write(Buffer.alloc(1024 * 1024)); // Large write client.destroy(); } catch (e) { // Ignore } }); // Timeout setTimeout(() => resolve(), 500); })); } await Promise.all(promises); console.log('✓ All error connections completed'); // Wait for cleanup await new Promise(resolve => setTimeout(resolve, 500)); const finalCount = getActiveConnections(); console.log(`Final connection count: ${finalCount}`); await proxy.stop(); console.log('✓ Proxy stopped'); expect(finalCount).toEqual(initialCount); console.log('\n✅ PASS: Connection error cleanup working correctly!'); }); tap.start();