import * as plugins from '../ts/plugins.js'; import { SmartProxy } from '../ts/index.js'; import { tap, expect } from '@push.rocks/tapbundle'; let testProxy: SmartProxy; // Helper to check if a port is being listened on async function isPortListening(port: number): Promise { return new Promise((resolve) => { const server = plugins.net.createServer(); server.once('error', (err: any) => { if (err.code === 'EADDRINUSE') { // Port is already in use (being listened on) resolve(true); } else { resolve(false); } }); server.once('listening', () => { // Port is available (not being listened on) server.close(); resolve(false); }); server.listen(port); }); } // Helper to create test route const createRoute = (id: number, port: number = 8443) => ({ name: `test-route-${id}`, match: { ports: [port], domains: [`test${id}.example.com`] }, action: { type: 'forward' as const, target: { host: 'localhost', port: 3000 + id }, tls: { mode: 'terminate' as const, certificate: 'auto' as const } } }); tap.test('should add challenge route once during initialization', async () => { testProxy = new SmartProxy({ routes: [createRoute(1, 8443)], acme: { email: 'test@example.com', useProduction: false, port: 8080 // Use high port for testing } }); // Mock certificate manager initialization let challengeRouteAddCount = 0; const originalInitCertManager = (testProxy as any).initializeCertificateManager; (testProxy as any).initializeCertificateManager = async function() { // Track challenge route additions const mockCertManager = { addChallengeRoute: async function() { challengeRouteAddCount++; }, removeChallengeRoute: async function() { challengeRouteAddCount--; }, setUpdateRoutesCallback: function() {}, setNetworkProxy: function() {}, setGlobalAcmeDefaults: function() {}, initialize: async function() { // Simulate adding challenge route during init await this.addChallengeRoute(); }, stop: async function() { // Simulate removing challenge route during stop await this.removeChallengeRoute(); }, getAcmeOptions: function() { return { email: 'test@example.com' }; } }; (this as any).certManager = mockCertManager; }; await testProxy.start(); // Challenge route should be added exactly once expect(challengeRouteAddCount).toEqual(1); await testProxy.stop(); // Challenge route should be removed on stop expect(challengeRouteAddCount).toEqual(0); }); tap.test('should persist challenge route during multiple certificate provisioning', async () => { testProxy = new SmartProxy({ routes: [ createRoute(1, 8443), createRoute(2, 8444), createRoute(3, 8445) ], acme: { email: 'test@example.com', useProduction: false, port: 8080 } }); // Mock to track route operations let challengeRouteActive = false; let addAttempts = 0; let removeAttempts = 0; (testProxy as any).initializeCertificateManager = async function() { const mockCertManager = { challengeRouteActive: false, isProvisioning: false, addChallengeRoute: async function() { addAttempts++; if (this.challengeRouteActive) { console.log('Challenge route already active, skipping'); return; } this.challengeRouteActive = true; challengeRouteActive = true; }, removeChallengeRoute: async function() { removeAttempts++; if (!this.challengeRouteActive) { console.log('Challenge route not active, skipping removal'); return; } this.challengeRouteActive = false; challengeRouteActive = false; }, provisionAllCertificates: async function() { this.isProvisioning = true; // Simulate provisioning multiple certificates for (let i = 0; i < 3; i++) { // Would normally call provisionCertificate for each route // Challenge route should remain active throughout expect(this.challengeRouteActive).toEqual(true); } this.isProvisioning = false; }, setUpdateRoutesCallback: function() {}, setNetworkProxy: function() {}, setGlobalAcmeDefaults: function() {}, initialize: async function() { await this.addChallengeRoute(); await this.provisionAllCertificates(); }, stop: async function() { await this.removeChallengeRoute(); }, getAcmeOptions: function() { return { email: 'test@example.com' }; } }; (this as any).certManager = mockCertManager; }; await testProxy.start(); // Challenge route should be added once and remain active expect(addAttempts).toEqual(1); expect(challengeRouteActive).toEqual(true); await testProxy.stop(); // Challenge route should be removed once expect(removeAttempts).toEqual(1); expect(challengeRouteActive).toEqual(false); }); tap.test('should handle port conflicts gracefully', async () => { // Create a server that listens on port 8080 to create a conflict const conflictServer = plugins.net.createServer(); await new Promise((resolve) => { conflictServer.listen(8080, () => { resolve(); }); }); try { testProxy = new SmartProxy({ routes: [createRoute(1, 8443)], acme: { email: 'test@example.com', useProduction: false, port: 8080 // This port is already in use } }); let error: Error | null = null; (testProxy as any).initializeCertificateManager = async function() { const mockCertManager = { challengeRouteActive: false, addChallengeRoute: async function() { if (this.challengeRouteActive) { return; } // Simulate EADDRINUSE error const err = new Error('listen EADDRINUSE: address already in use :::8080'); (err as any).code = 'EADDRINUSE'; throw err; }, setUpdateRoutesCallback: function() {}, setNetworkProxy: function() {}, setGlobalAcmeDefaults: function() {}, initialize: async function() { try { await this.addChallengeRoute(); } catch (e) { error = e as Error; throw e; } }, stop: async function() {}, getAcmeOptions: function() { return { email: 'test@example.com' }; } }; (this as any).certManager = mockCertManager; }; try { await testProxy.start(); } catch (e) { error = e as Error; } // Should have caught the port conflict expect(error).toBeTruthy(); expect(error?.message).toContain('Port 8080 is already in use'); } finally { // Clean up conflict server conflictServer.close(); } }); tap.test('should prevent concurrent provisioning', async () => { // Mock the certificate manager with tracking let concurrentAttempts = 0; let maxConcurrent = 0; let currentlyProvisioning = 0; const mockProxy = { provisionCertificate: async function(route: any, allowConcurrent = false) { if (!allowConcurrent && currentlyProvisioning > 0) { console.log('Provisioning already in progress, skipping'); return; } concurrentAttempts++; currentlyProvisioning++; maxConcurrent = Math.max(maxConcurrent, currentlyProvisioning); // Simulate provisioning delay await new Promise(resolve => setTimeout(resolve, 10)); currentlyProvisioning--; } }; // Try to provision multiple certificates concurrently const promises = []; for (let i = 0; i < 5; i++) { promises.push(mockProxy.provisionCertificate({ name: `route-${i}` })); } await Promise.all(promises); // Should have rejected concurrent attempts expect(concurrentAttempts).toEqual(1); expect(maxConcurrent).toEqual(1); }); tap.test('should clean up properly even on errors', async () => { let challengeRouteActive = false; const mockCertManager = { challengeRouteActive: false, addChallengeRoute: async function() { this.challengeRouteActive = true; challengeRouteActive = true; throw new Error('Test error during add'); }, removeChallengeRoute: async function() { if (!this.challengeRouteActive) { return; } this.challengeRouteActive = false; challengeRouteActive = false; }, initialize: async function() { try { await this.addChallengeRoute(); } catch (error) { // Should still clean up await this.removeChallengeRoute(); throw error; } } }; try { await mockCertManager.initialize(); } catch (error) { // Expected error } // State should be cleaned up expect(challengeRouteActive).toEqual(false); expect(mockCertManager.challengeRouteActive).toEqual(false); }); tap.start();