import { expect, tap } from '@push.rocks/tapbundle'; import { SmartProxy } from '../ts/index.js'; tap.test('should handle concurrent route updates without race conditions', async (tools) => { tools.timeout(10000); const settings = { port: 6001, routes: [ { name: 'initial-route', match: { ports: 80 }, action: { type: 'forward' as const, targetUrl: 'http://localhost:3000' } } ], acme: { email: 'test@test.com', port: 80 } }; const proxy = new SmartProxy(settings); await proxy.start(); // Simulate concurrent route updates const updates = []; for (let i = 0; i < 5; i++) { updates.push(proxy.updateRoutes([ ...settings.routes, { name: `route-${i}`, match: { ports: [443] }, action: { type: 'forward' as const, targetUrl: `https://localhost:${3001 + i}`, tls: { mode: 'terminate' as const, certificate: 'auto' } } } ])); } // All updates should complete without errors await Promise.all(updates); // Verify final state const currentRoutes = proxy['settings'].routes; tools.expect(currentRoutes.length).toBeGreaterThan(1); await proxy.stop(); }); tap.test('should preserve certificate manager state during rapid updates', async (tools) => { tools.timeout(10000); const settings = { port: 6002, routes: [ { name: 'test-route', match: { ports: [443] }, action: { type: 'forward' as const, targetUrl: 'https://localhost:3001', tls: { mode: 'terminate' as const, certificate: 'auto' } } } ], acme: { email: 'test@test.com', port: 80 } }; const proxy = new SmartProxy(settings); await proxy.start(); // Get initial certificate manager reference const initialCertManager = proxy['certManager']; tools.expect(initialCertManager).not.toBeNull(); // Perform rapid route updates for (let i = 0; i < 3; i++) { await proxy.updateRoutes([ ...settings.routes, { name: `extra-route-${i}`, match: { ports: [8000 + i] }, action: { type: 'forward' as const, targetUrl: `http://localhost:${4000 + i}` } } ]); } // Certificate manager should be recreated but state preserved const finalCertManager = proxy['certManager']; tools.expect(finalCertManager).not.toBeNull(); tools.expect(finalCertManager).not.toEqual(initialCertManager); await proxy.stop(); }); tap.test('should handle challenge route state correctly across recreations', async (tools) => { tools.timeout(10000); let challengeRouteAddCount = 0; const settings = { port: 6003, routes: [ { name: 'acme-route', match: { ports: [443] }, action: { type: 'forward' as const, targetUrl: 'https://localhost:3001', tls: { mode: 'terminate' as const, certificate: 'auto' } } } ], acme: { email: 'test@test.com', port: 80 } }; const proxy = new SmartProxy(settings); // Mock the route update to count challenge route additions const originalUpdateRoutes = proxy['updateRoutes'].bind(proxy); proxy['updateRoutes'] = async (routes: any[]) => { if (routes.some(r => r.name === 'acme-challenge')) { challengeRouteAddCount++; } return originalUpdateRoutes(routes); }; await proxy.start(); // Multiple route updates for (let i = 0; i < 3; i++) { await proxy.updateRoutes([ ...settings.routes, { name: `dynamic-route-${i}`, match: { ports: [9000 + i] }, action: { type: 'forward' as const, targetUrl: `http://localhost:${5000 + i}` } } ]); } // Challenge route should only be added once during initial start tools.expect(challengeRouteAddCount).toEqual(1); await proxy.stop(); }); tap.test('should prevent port conflicts during certificate manager recreation', async (tools) => { tools.timeout(10000); const settings = { port: 6004, routes: [ { name: 'http-route', match: { ports: [80] }, action: { type: 'forward' as const, targetUrl: 'http://localhost:3000' } }, { name: 'https-route', match: { ports: [443] }, action: { type: 'forward' as const, targetUrl: 'https://localhost:3001', tls: { mode: 'terminate' as const, certificate: 'auto' } } } ], acme: { email: 'test@test.com', port: 80 // Same as user route } }; const proxy = new SmartProxy(settings); // Track port operations let port80AddCount = 0; const originalPortManager = proxy['portManager']; const originalAddPort = originalPortManager.addPort.bind(originalPortManager); originalPortManager.addPort = async (port: number) => { if (port === 80) { port80AddCount++; } return originalAddPort(port); }; await proxy.start(); // Update routes multiple times for (let i = 0; i < 3; i++) { await proxy.updateRoutes([ ...settings.routes, { name: `temp-route-${i}`, match: { ports: [7000 + i] }, action: { type: 'forward' as const, targetUrl: `http://localhost:${6000 + i}` } } ]); } // Port 80 should be maintained properly without conflicts tools.expect(port80AddCount).toBeGreaterThan(0); await proxy.stop(); }); tap.test('should handle mutex locking correctly', async (tools) => { tools.timeout(10000); const settings = { port: 6005, routes: [ { name: 'test-route', match: { ports: [80] }, action: { type: 'forward' as const, targetUrl: 'http://localhost:3000' } } ] }; const proxy = new SmartProxy(settings); await proxy.start(); let updateStartCount = 0; let updateEndCount = 0; // Wrap updateRoutes to track concurrent execution const originalUpdateRoutes = proxy['updateRoutes'].bind(proxy); proxy['updateRoutes'] = async (routes: any[]) => { updateStartCount++; const startCount = updateStartCount; const endCount = updateEndCount; // If mutex is working, start count should never be more than end count + 1 tools.expect(startCount).toBeLessThanOrEqual(endCount + 1); const result = await originalUpdateRoutes(routes); updateEndCount++; return result; }; // Trigger multiple concurrent updates const updates = []; for (let i = 0; i < 5; i++) { updates.push(proxy.updateRoutes([ ...settings.routes, { name: `concurrent-route-${i}`, match: { ports: [2000 + i] }, action: { type: 'forward' as const, targetUrl: `http://localhost:${3000 + i}` } } ])); } await Promise.all(updates); // All updates should have completed tools.expect(updateStartCount).toEqual(5); tools.expect(updateEndCount).toEqual(5); await proxy.stop(); }); export default tap;