import { expect, tap } from '@push.rocks/tapbundle'; import { SmartProxy } from '../ts/index.js'; tap.test('should not double-register port 80 when user route and ACME use same port', async (tools) => { tools.timeout(5000); let port80AddCount = 0; const activePorts = new Set(); const settings = { port: 9901, routes: [ { name: 'user-route', match: { ports: [80] }, action: { type: 'forward' as const, targetUrl: 'http://localhost:3000' } }, { 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 // ACME on same port as user route } }; const proxy = new SmartProxy(settings); // Mock the port manager to track port additions (proxy as any).portManager = { addPort: async (port: number) => { if (activePorts.has(port)) { // This is the deduplication behavior we're testing return; } activePorts.add(port); if (port === 80) { port80AddCount++; } }, addPorts: async (ports: number[]) => { for (const port of ports) { await (proxy as any).portManager.addPort(port); } }, removePort: async (port: number) => { activePorts.delete(port); }, updatePorts: async (requiredPorts: Set) => { const portsToRemove = []; for (const port of activePorts) { if (!requiredPorts.has(port)) { portsToRemove.push(port); } } const portsToAdd = []; for (const port of requiredPorts) { if (!activePorts.has(port)) { portsToAdd.push(port); } } for (const port of portsToRemove) { await (proxy as any).portManager.removePort(port); } for (const port of portsToAdd) { await (proxy as any).portManager.addPort(port); } }, setShuttingDown: () => {}, getPortForRoutes: () => new Map(), closeAll: async () => { activePorts.clear(); }, stop: async () => { await (proxy as any).portManager.closeAll(); } }; // Mock NFTables (proxy as any).nftablesManager = { ensureNFTablesSetup: async () => {}, stop: async () => {} }; // Mock certificate manager to prevent ACME (proxy as any).createCertificateManager = async function(routes: any[], certDir: string, acmeOptions: any) { const certManager = { routes: routes, globalAcmeDefaults: acmeOptions, updateRoutesCallback: null as any, challengeRouteActive: false, setUpdateRoutesCallback: function(callback: any) { this.updateRoutesCallback = callback; }, setNetworkProxy: function() {}, setGlobalAcmeDefaults: function(defaults: any) { this.globalAcmeDefaults = defaults; }, initialize: async function() { const hasAcmeRoutes = routes.some((r: any) => r.action.tls?.certificate === 'auto' ); if (hasAcmeRoutes && acmeOptions?.email) { const challengeRoute = { name: 'acme-challenge', priority: 1000, match: { ports: acmeOptions.port || 80, path: '/.well-known/acme-challenge/*' }, action: { type: 'static', handler: async () => ({ status: 200, body: 'challenge' }) } }; const updatedRoutes = [...routes, challengeRoute]; if (this.updateRoutesCallback) { await this.updateRoutesCallback(updatedRoutes); } this.challengeRouteActive = true; } }, getAcmeOptions: function() { return acmeOptions; }, stop: async function() {} }; certManager.setUpdateRoutesCallback(async (routes: any[]) => { await this.updateRoutes(routes); }); await certManager.initialize(); return certManager; }; // Mock admin server to prevent binding (proxy as any).startAdminServer = async function() { this.servers.set(this.settings.port, { port: this.settings.port, close: async () => {} }); }; try { await proxy.start(); // Verify that port 80 was added only once tools.expect(port80AddCount).toEqual(1); } finally { await proxy.stop(); } }); tap.test('should handle ACME on different port than user routes', async (tools) => { tools.timeout(5000); const portAddHistory: number[] = []; const activePorts = new Set(); const settings = { port: 9902, routes: [ { name: 'user-route', match: { ports: [80] }, action: { type: 'forward' as const, targetUrl: 'http://localhost:3000' } }, { 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: 8080 // ACME on different port than user routes } }; const proxy = new SmartProxy(settings); // Mock the port manager (proxy as any).portManager = { addPort: async (port: number) => { if (!activePorts.has(port)) { activePorts.add(port); portAddHistory.push(port); } }, addPorts: async (ports: number[]) => { for (const port of ports) { await (proxy as any).portManager.addPort(port); } }, removePort: async (port: number) => { activePorts.delete(port); }, updatePorts: async (requiredPorts: Set) => { for (const port of requiredPorts) { await (proxy as any).portManager.addPort(port); } }, setShuttingDown: () => {}, getPortForRoutes: () => new Map(), closeAll: async () => { activePorts.clear(); }, stop: async () => { await (proxy as any).portManager.closeAll(); } }; // Mock NFTables (proxy as any).nftablesManager = { ensureNFTablesSetup: async () => {}, stop: async () => {} }; // Mock certificate manager (proxy as any).createCertificateManager = async function(routes: any[], certDir: string, acmeOptions: any) { const certManager = { routes: routes, globalAcmeDefaults: acmeOptions, updateRoutesCallback: null as any, challengeRouteActive: false, setUpdateRoutesCallback: function(callback: any) { this.updateRoutesCallback = callback; }, setNetworkProxy: function() {}, setGlobalAcmeDefaults: function(defaults: any) { this.globalAcmeDefaults = defaults; }, initialize: async function() { const hasAcmeRoutes = routes.some((r: any) => r.action.tls?.certificate === 'auto' ); if (hasAcmeRoutes && acmeOptions?.email) { const challengeRoute = { name: 'acme-challenge', priority: 1000, match: { ports: acmeOptions.port || 80, path: '/.well-known/acme-challenge/*' }, action: { type: 'static', handler: async () => ({ status: 200, body: 'challenge' }) } }; const updatedRoutes = [...routes, challengeRoute]; if (this.updateRoutesCallback) { await this.updateRoutesCallback(updatedRoutes); } this.challengeRouteActive = true; } }, getAcmeOptions: function() { return acmeOptions; }, stop: async function() {} }; certManager.setUpdateRoutesCallback(async (routes: any[]) => { await this.updateRoutes(routes); }); await certManager.initialize(); return certManager; }; // Mock admin server (proxy as any).startAdminServer = async function() { this.servers.set(this.settings.port, { port: this.settings.port, close: async () => {} }); }; try { await proxy.start(); // Verify that all expected ports were added tools.expect(portAddHistory).toInclude(80); // User route tools.expect(portAddHistory).toInclude(443); // TLS route tools.expect(portAddHistory).toInclude(8080); // ACME challenge } finally { await proxy.stop(); } }); export default tap;