import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as plugins from '../ts/plugins.js'; import { SmartProxy } from '../ts/index.js'; import * as net from 'net'; let smartProxyInstance: SmartProxy; let echoServer: net.Server; const echoServerPort = 9876; const proxyPort = 8080; // Create an echo server for testing tap.test('should create echo server for testing', async () => { echoServer = net.createServer((socket) => { socket.on('data', (data) => { socket.write(data); // Echo back the data }); }); await new Promise((resolve) => { echoServer.listen(echoServerPort, () => { console.log(`Echo server listening on port ${echoServerPort}`); resolve(); }); }); }); tap.test('should create SmartProxy instance with new metrics', async () => { smartProxyInstance = new SmartProxy({ routes: [{ id: 'test-route', // id is needed for per-route metrics tracking in Rust name: 'test-route', match: { ports: [proxyPort] // No domains — port-only route uses fast-path (no data peeking) }, action: { type: 'forward', targets: [{ host: 'localhost', port: echoServerPort }] // No TLS — plain TCP forwarding } }], metrics: { enabled: true, sampleIntervalMs: 100, // Sample every 100ms for faster testing retentionSeconds: 60 } }); await smartProxyInstance.start(); }); tap.test('should verify new metrics API structure', async () => { const metrics = smartProxyInstance.getMetrics(); // Check API structure expect(metrics).toHaveProperty('connections'); expect(metrics).toHaveProperty('throughput'); expect(metrics).toHaveProperty('requests'); expect(metrics).toHaveProperty('totals'); expect(metrics).toHaveProperty('percentiles'); // Check connections methods expect(metrics.connections).toHaveProperty('active'); expect(metrics.connections).toHaveProperty('total'); expect(metrics.connections).toHaveProperty('byRoute'); expect(metrics.connections).toHaveProperty('byIP'); expect(metrics.connections).toHaveProperty('topIPs'); // Check throughput methods expect(metrics.throughput).toHaveProperty('instant'); expect(metrics.throughput).toHaveProperty('recent'); expect(metrics.throughput).toHaveProperty('average'); expect(metrics.throughput).toHaveProperty('custom'); expect(metrics.throughput).toHaveProperty('history'); expect(metrics.throughput).toHaveProperty('byRoute'); expect(metrics.throughput).toHaveProperty('byIP'); }); tap.test('should track active connections', async (tools) => { const metrics = smartProxyInstance.getMetrics(); // Initial state - no connections expect(metrics.connections.active()).toEqual(0); // Create a test connection const client = new net.Socket(); await new Promise((resolve, reject) => { client.connect(proxyPort, 'localhost', () => { console.log('Connected to proxy'); resolve(); }); client.on('error', reject); }); // Send some data and wait for echo const testData = Buffer.from('Hello, World!'.repeat(100)); // ~1.3KB await new Promise((resolve) => { client.write(testData, () => resolve()); }); await new Promise((resolve) => { client.once('data', (data) => { console.log(`Received ${data.length} bytes back`); resolve(); }); }); // Wait for metrics to be polled await tools.delayFor(500); // Active connection count should be 1 expect(metrics.connections.active()).toEqual(1); // Total connections should be tracked expect(metrics.connections.total()).toBeGreaterThan(0); // Per-route tracking should show the connection const byRoute = metrics.connections.byRoute(); console.log('Connections by route:', Array.from(byRoute.entries())); expect(byRoute.get('test-route')).toEqual(1); // Clean up - close the connection client.destroy(); // Wait for connection cleanup for (let i = 0; i < 20; i++) { await tools.delayFor(100); if (metrics.connections.active() === 0) break; } expect(metrics.connections.active()).toEqual(0); }); tap.test('should track bytes after connection closes', async (tools) => { const metrics = smartProxyInstance.getMetrics(); // Create a connection, send data, then close it const client = new net.Socket(); await new Promise((resolve, reject) => { client.connect(proxyPort, 'localhost', () => resolve()); client.on('error', reject); }); // Send some data const testData = Buffer.from('Hello, World!'.repeat(100)); // ~1.3KB await new Promise((resolve) => { client.write(testData, () => resolve()); }); // Wait for echo await new Promise((resolve) => { client.once('data', () => resolve()); }); // Close the connection — Rust records bytes on connection close client.destroy(); // Wait for connection to fully close and metrics to poll for (let i = 0; i < 20; i++) { await tools.delayFor(100); if (metrics.connections.active() === 0 && metrics.totals.bytesIn() > 0) break; } // Now bytes should be recorded console.log('Total bytes in:', metrics.totals.bytesIn()); console.log('Total bytes out:', metrics.totals.bytesOut()); expect(metrics.totals.bytesIn()).toBeGreaterThan(0); expect(metrics.totals.bytesOut()).toBeGreaterThan(0); }); tap.test('should track multiple connections', async (tools) => { const metrics = smartProxyInstance.getMetrics(); // Ensure we start with 0 active connections for (let i = 0; i < 20; i++) { await tools.delayFor(100); if (metrics.connections.active() === 0) break; } // Create multiple connections const clients: net.Socket[] = []; const connectionCount = 5; for (let i = 0; i < connectionCount; i++) { const client = new net.Socket(); await new Promise((resolve, reject) => { client.connect(proxyPort, 'localhost', () => resolve()); client.on('error', reject); }); clients.push(client); } // Allow connections to be fully established and metrics polled await tools.delayFor(500); // Verify active connections console.log('Active connections:', metrics.connections.active()); expect(metrics.connections.active()).toEqual(connectionCount); // Per-route should track all connections const routeConnections = metrics.connections.byRoute(); console.log('Connections by route:', Array.from(routeConnections.entries())); expect(routeConnections.get('test-route')).toEqual(connectionCount); // Clean up all connections clients.forEach(client => client.destroy()); for (let i = 0; i < 20; i++) { await tools.delayFor(100); if (metrics.connections.active() === 0) break; } expect(metrics.connections.active()).toEqual(0); }); tap.test('should provide throughput data', async (tools) => { const metrics = smartProxyInstance.getMetrics(); // Create a connection and send data periodically const client = new net.Socket(); await new Promise((resolve, reject) => { client.connect(proxyPort, 'localhost', () => resolve()); client.on('error', reject); }); // Send data every 100ms for 1 second for (let i = 0; i < 10; i++) { const data = Buffer.from(`Packet ${i}: `.repeat(100)); client.write(data); await tools.delayFor(100); } // Close connection so bytes are recorded client.destroy(); // Wait for metrics to update for (let i = 0; i < 20; i++) { await tools.delayFor(100); if (metrics.totals.bytesIn() > 0) break; } // Verify different time windows are available (all return same data from Rust for now) const instant = metrics.throughput.instant(); const recent = metrics.throughput.recent(); const average = metrics.throughput.average(); console.log('Throughput windows:'); console.log(' Instant (1s):', instant); console.log(' Recent (10s):', recent); console.log(' Average (60s):', average); // Total bytes should have accumulated expect(metrics.totals.bytesIn()).toBeGreaterThan(0); expect(metrics.totals.bytesOut()).toBeGreaterThan(0); }); tap.test('should clean up resources', async () => { await smartProxyInstance.stop(); await new Promise((resolve) => { echoServer.close(() => { console.log('Echo server closed'); resolve(); }); }); }); export default tap.start();