import { expect, tap } from '@git.zone/tstest/tapbundle'; import { SmartProxy, type IRouteConfig } from '../ts/index.js'; /** * Test that verifies mutex prevents race conditions during concurrent route updates */ 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, target: { host: 'localhost', port: 3001 + i }, tls: { mode: 'terminate' as const, certificate: 'auto' as const } } } ])); } // All updates should complete without errors await Promise.all(updates); // Verify final state const currentRoutes = proxy['settings'].routes; expect(currentRoutes.length).toEqual(2); // Initial route + last update await proxy.stop(); }); /** * Test that verifies mutex serializes route updates */ tap.test('should serialize route updates with mutex', async (tools) => { tools.timeout(10000); const settings = { port: 6002, 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; let maxConcurrent = 0; // Wrap updateRoutes to track concurrent execution const originalUpdateRoutes = proxy['updateRoutes'].bind(proxy); proxy['updateRoutes'] = async (routes: any[]) => { updateStartCount++; const concurrent = updateStartCount - updateEndCount; maxConcurrent = Math.max(maxConcurrent, concurrent); // If mutex is working, only one update should run at a time expect(concurrent).toEqual(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 expect(updateStartCount).toEqual(5); expect(updateEndCount).toEqual(5); expect(maxConcurrent).toEqual(1); // Mutex ensures only one at a time await proxy.stop(); }); /** * Test that challenge route state is preserved across certificate manager recreations */ tap.test('should preserve challenge route state during cert manager recreation', async (tools) => { tools.timeout(10000); const settings = { port: 6003, routes: [{ name: 'acme-route', match: { ports: [443] }, action: { type: 'forward' as const, target: { host: 'localhost', port: 3001 }, tls: { mode: 'terminate' as const, certificate: 'auto' as const } } }], acme: { email: 'test@test.com', port: 80 } }; const proxy = new SmartProxy(settings); // Track certificate manager recreations let certManagerCreationCount = 0; const originalCreateCertManager = proxy['createCertificateManager'].bind(proxy); proxy['createCertificateManager'] = async (...args: any[]) => { certManagerCreationCount++; return originalCreateCertManager(...args); }; await proxy.start(); // Initial creation expect(certManagerCreationCount).toEqual(1); // Multiple route updates for (let i = 0; i < 3; i++) { await proxy.updateRoutes([ ...settings.routes as IRouteConfig[], { name: `dynamic-route-${i}`, match: { ports: [9000 + i] }, action: { type: 'forward' as const, target: { host: 'localhost', port: 5000 + i } } } ]); } // Certificate manager should be recreated for each update expect(certManagerCreationCount).toEqual(4); // 1 initial + 3 updates // State should be preserved (challenge route active) const globalState = proxy['globalChallengeRouteActive']; expect(globalState).toBeDefined(); await proxy.stop(); }); export default tap.start();