242 lines
6.8 KiB
TypeScript
242 lines
6.8 KiB
TypeScript
|
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<void>((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<void>((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();
|