import { tap, expect } from '@git.zone/tstest/tapbundle'; import { SmartProxy } from '../ts/index.js'; import * as net from 'net'; // Test that certificate provisioning waits for ports to be ready tap.test('should defer certificate provisioning until after ports are listening', async (tapTest) => { // Track the order of operations const operationLog: string[] = []; // Create a mock server to verify ports are listening let port80Listening = false; const testServer = net.createServer(() => { // We don't need to handle connections, just track that we're listening }); // Try to use port 8080 instead of 80 to avoid permission issues in testing const acmePort = 8080; // Create proxy with ACME certificate requirement const proxy = new SmartProxy({ useHttpProxy: [acmePort], httpProxyPort: 8844, acme: { email: 'test@test.local', useProduction: false, port: acmePort }, routes: [{ name: 'test-acme-route', match: { ports: 8443, domains: ['test.local'] }, action: { type: 'forward', target: { host: 'localhost', port: 8181 }, tls: { mode: 'terminate', certificate: 'auto', acme: { email: 'test@test.local', useProduction: false } } } }] }); // Mock some internal methods to track operation order const originalAddPorts = proxy['portManager'].addPorts; proxy['portManager'].addPorts = async function(ports: number[]) { operationLog.push('Starting port listeners'); const result = await originalAddPorts.call(this, ports); operationLog.push('Port listeners started'); port80Listening = true; return result; }; // Track certificate provisioning const originalProvisionAll = proxy['certManager'] ? proxy['certManager']['provisionAllCertificates'] : null; if (proxy['certManager']) { proxy['certManager']['provisionAllCertificates'] = async function() { operationLog.push('Starting certificate provisioning'); // Check if port 80 is listening if (!port80Listening) { operationLog.push('ERROR: Certificate provisioning started before ports ready'); } // Don't actually provision certificates in the test operationLog.push('Certificate provisioning completed'); }; } // Mock certificate manager to avoid real ACME initialization (proxy as any).createCertificateManager = async function() { operationLog.push('Creating certificate manager'); const mockCertManager = { setUpdateRoutesCallback: () => {}, setHttpProxy: () => {}, setGlobalAcmeDefaults: () => {}, setAcmeStateManager: () => {}, initialize: async () => { operationLog.push('Starting certificate provisioning'); if (!port80Listening) { operationLog.push('ERROR: Certificate provisioning started before ports ready'); } operationLog.push('Certificate provisioning completed'); }, provisionAllCertificates: async () => {}, stop: async () => {}, getAcmeOptions: () => ({ email: 'test@test.local', useProduction: false }), getState: () => ({ challengeRouteActive: false }) }; return mockCertManager; }; // Start the proxy await proxy.start(); // Verify the order of operations expect(operationLog).toContain('Starting port listeners'); expect(operationLog).toContain('Port listeners started'); expect(operationLog).toContain('Starting certificate provisioning'); // Ensure port listeners started before certificate provisioning const portStartIndex = operationLog.indexOf('Port listeners started'); const certStartIndex = operationLog.indexOf('Starting certificate provisioning'); expect(portStartIndex).toBeLessThan(certStartIndex); expect(operationLog).not.toContain('ERROR: Certificate provisioning started before ports ready'); await proxy.stop(); }); // Test that ACME challenge route is available when certificate is requested tap.test('should have ACME challenge route ready before certificate provisioning', async (tapTest) => { let challengeRouteActive = false; let certificateProvisioningStarted = false; const proxy = new SmartProxy({ useHttpProxy: [8080], httpProxyPort: 8844, acme: { email: 'test@test.local', useProduction: false, port: 8080 }, routes: [{ name: 'test-route', match: { ports: 8443, domains: ['test.example.com'] }, action: { type: 'forward', target: { host: 'localhost', port: 8181 }, tls: { mode: 'terminate', certificate: 'auto' } } }] }); // Mock the certificate manager to track operations const originalInitialize = proxy['certManager'] ? proxy['certManager'].initialize : null; if (proxy['certManager']) { const certManager = proxy['certManager']; // Track when challenge route is added const originalAddChallenge = certManager['addChallengeRoute']; certManager['addChallengeRoute'] = async function() { await originalAddChallenge.call(this); challengeRouteActive = true; }; // Track when certificate provisioning starts const originalProvisionAcme = certManager['provisionAcmeCertificate']; certManager['provisionAcmeCertificate'] = async function(...args: any[]) { certificateProvisioningStarted = true; // Verify challenge route is active expect(challengeRouteActive).toEqual(true); // Don't actually provision in test return; }; } // Mock certificate manager to avoid real ACME initialization (proxy as any).createCertificateManager = async function() { const mockCertManager = { setUpdateRoutesCallback: () => {}, setHttpProxy: () => {}, setGlobalAcmeDefaults: () => {}, setAcmeStateManager: () => {}, initialize: async () => { challengeRouteActive = true; }, provisionAllCertificates: async () => { certificateProvisioningStarted = true; expect(challengeRouteActive).toEqual(true); }, stop: async () => {}, getAcmeOptions: () => ({ email: 'test@test.local', useProduction: false }), getState: () => ({ challengeRouteActive: false }), addChallengeRoute: async () => { challengeRouteActive = true; }, provisionAcmeCertificate: async () => { certificateProvisioningStarted = true; expect(challengeRouteActive).toEqual(true); } }; return mockCertManager; }; await proxy.start(); // Give it a moment to complete initialization await new Promise(resolve => setTimeout(resolve, 100)); // Verify challenge route was added before any certificate provisioning expect(challengeRouteActive).toEqual(true); await proxy.stop(); }); tap.start();