import { expect, tap } from '@git.zone/tstest/tapbundle'; import { SmartProxy } from '../ts/index.js'; /** * Test that verifies port 80 is not double-registered when both * user routes and ACME challenges use the same port */ 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, target: { host: 'localhost', port: 3000 } } }, { name: 'secure-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 // ACME on same port as user route } }; const proxy = new SmartProxy(settings); // Mock the port manager to track port additions const mockPortManager = { addPort: async (port: number) => { if (activePorts.has(port)) { return; // Simulate deduplication } activePorts.add(port); if (port === 80) { port80AddCount++; } }, addPorts: async (ports: number[]) => { for (const port of ports) { await mockPortManager.addPort(port); } }, updatePorts: async (requiredPorts: Set) => { for (const port of requiredPorts) { await mockPortManager.addPort(port); } }, setShuttingDown: () => {}, closeAll: async () => { activePorts.clear(); }, stop: async () => { await mockPortManager.closeAll(); } }; // Inject mock (proxy as any).portManager = mockPortManager; // Mock certificate manager to prevent ACME calls (proxy as any).createCertificateManager = async function(routes: any[], certDir: string, acmeOptions: any, initialState?: any) { const mockCertManager = { setUpdateRoutesCallback: function(callback: any) { /* noop */ }, setHttpProxy: function() {}, setGlobalAcmeDefaults: function() {}, setAcmeStateManager: function() {}, initialize: async function() { // Simulate ACME route addition const challengeRoute = { name: 'acme-challenge', priority: 1000, match: { ports: acmeOptions?.port || 80, path: '/.well-known/acme-challenge/*' }, action: { type: 'static' } }; // This would trigger route update in real implementation }, provisionAllCertificates: async function() { // Mock implementation to satisfy the call in SmartProxy.start() // Add the ACME challenge port here too in case initialize was skipped const challengePort = acmeOptions?.port || 80; await mockPortManager.addPort(challengePort); console.log(`Added ACME challenge port from provisionAllCertificates: ${challengePort}`); }, getAcmeOptions: () => acmeOptions, getState: () => ({ challengeRouteActive: false }), stop: async () => {} }; return mockCertManager; }; // Mock NFTables (proxy as any).nftablesManager = { ensureNFTablesSetup: async () => {}, stop: async () => {} }; // Mock admin server (proxy as any).startAdminServer = async function() { (this as any).servers.set(this.settings.port, { port: this.settings.port, close: async () => {} }); }; await proxy.start(); // Verify that port 80 was added only once expect(port80AddCount).toEqual(1); await proxy.stop(); }); /** * Test that verifies ACME can use a different port than user routes */ 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, target: { host: 'localhost', port: 3000 } } }, { name: 'secure-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: 8080 // ACME on different port than user routes } }; const proxy = new SmartProxy(settings); // Mock the port manager const mockPortManager = { addPort: async (port: number) => { console.log(`Attempting to add port: ${port}`); if (!activePorts.has(port)) { activePorts.add(port); portAddHistory.push(port); console.log(`Port ${port} added to history`); } else { console.log(`Port ${port} already active, not adding to history`); } }, addPorts: async (ports: number[]) => { for (const port of ports) { await mockPortManager.addPort(port); } }, updatePorts: async (requiredPorts: Set) => { for (const port of requiredPorts) { await mockPortManager.addPort(port); } }, setShuttingDown: () => {}, closeAll: async () => { activePorts.clear(); }, stop: async () => { await mockPortManager.closeAll(); } }; // Inject mocks (proxy as any).portManager = mockPortManager; // Mock certificate manager (proxy as any).createCertificateManager = async function(routes: any[], certDir: string, acmeOptions: any, initialState?: any) { const mockCertManager = { setUpdateRoutesCallback: function(callback: any) { /* noop */ }, setHttpProxy: function() {}, setGlobalAcmeDefaults: function() {}, setAcmeStateManager: function() {}, initialize: async function() { // Simulate ACME route addition on different port const challengePort = acmeOptions?.port || 80; const challengeRoute = { name: 'acme-challenge', priority: 1000, match: { ports: challengePort, path: '/.well-known/acme-challenge/*' }, action: { type: 'static' } }; // Add the ACME port to our port tracking await mockPortManager.addPort(challengePort); // For debugging console.log(`Added ACME challenge port: ${challengePort}`); }, provisionAllCertificates: async function() { // Mock implementation to satisfy the call in SmartProxy.start() // Add the ACME challenge port here too in case initialize was skipped const challengePort = acmeOptions?.port || 80; await mockPortManager.addPort(challengePort); console.log(`Added ACME challenge port from provisionAllCertificates: ${challengePort}`); }, getAcmeOptions: () => acmeOptions, getState: () => ({ challengeRouteActive: false }), stop: async () => {} }; return mockCertManager; }; // Mock NFTables (proxy as any).nftablesManager = { ensureNFTablesSetup: async () => {}, stop: async () => {} }; // Mock admin server (proxy as any).startAdminServer = async function() { (this as any).servers.set(this.settings.port, { port: this.settings.port, close: async () => {} }); }; await proxy.start(); // Log the port history for debugging console.log('Port add history:', portAddHistory); // Verify that all expected ports were added expect(portAddHistory.includes(80)).toBeTrue(); // User route expect(portAddHistory.includes(443)).toBeTrue(); // TLS route expect(portAddHistory.includes(8080)).toBeTrue(); // ACME challenge on different port await proxy.stop(); }); export default tap.start();