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, targetUrl: 'http://localhost:3000' } }, { name: 'secure-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 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 */ }, setNetworkProxy: 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 }, 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 tools.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, targetUrl: 'http://localhost:3000' } }, { name: 'secure-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 const mockPortManager = { addPort: async (port: number) => { if (!activePorts.has(port)) { activePorts.add(port); portAddHistory.push(port); } }, 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 */ }, setNetworkProxy: function() {}, setGlobalAcmeDefaults: function() {}, setAcmeStateManager: function() {}, initialize: async function() { // Simulate ACME route addition on different port const challengeRoute = { name: 'acme-challenge', priority: 1000, match: { ports: acmeOptions?.port || 80, path: '/.well-known/acme-challenge/*' }, action: { type: 'static' } }; }, 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 all expected ports were added tools.expect(portAddHistory).toContain(80); // User route tools.expect(portAddHistory).toContain(443); // TLS route tools.expect(portAddHistory).toContain(8080); // ACME challenge on different port await proxy.stop(); }); export default tap;