import { expect, tap } from '@git.zone/tstest/tapbundle'; import { SmartProxy } from '../ts/index.js'; import type { TSmartProxyCertProvisionObject } from '../ts/index.js'; import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); let testProxy: SmartProxy; // Load test certificates from helpers const testCert = fs.readFileSync(path.join(__dirname, 'helpers/test-cert.pem'), 'utf8'); const testKey = fs.readFileSync(path.join(__dirname, 'helpers/test-key.pem'), 'utf8'); // Helper to create a fully mocked certificate manager that doesn't contact ACME servers function createMockCertManager(options: { onProvisionAll?: () => void; onGetCertForDomain?: (domain: string) => void; } = {}) { return { setUpdateRoutesCallback: function(callback: any) { this.updateRoutesCallback = callback; }, updateRoutesCallback: null as any, setHttpProxy: function() {}, setGlobalAcmeDefaults: function() {}, setAcmeStateManager: function() {}, setRoutes: function(routes: any) {}, initialize: async function() {}, provisionAllCertificates: async function() { if (options.onProvisionAll) { options.onProvisionAll(); } }, stop: async function() {}, getAcmeOptions: function() { return { email: 'test@example.com', useProduction: false }; }, getState: function() { return { challengeRouteActive: false }; }, smartAcme: { getCertificateForDomain: async (domain: string) => { if (options.onGetCertForDomain) { options.onGetCertForDomain(domain); } throw new Error('Mocked ACME - not calling real servers'); } } }; } tap.test('SmartProxy should support custom certificate provision function', async () => { // Create test certificate object matching ICert interface const testCertObject = { id: 'test-cert-1', domainName: 'test.example.com', created: Date.now(), validUntil: Date.now() + 90 * 24 * 60 * 60 * 1000, // 90 days privateKey: testKey, publicKey: testCert, csr: '' }; // Custom certificate store for testing const customCerts = new Map(); customCerts.set('test.example.com', testCertObject); // Create proxy with custom certificate provision testProxy = new SmartProxy({ certProvisionFunction: async (domain: string): Promise => { console.log(`Custom cert provision called for domain: ${domain}`); // Return custom cert for known domains if (customCerts.has(domain)) { console.log(`Returning custom certificate for ${domain}`); return customCerts.get(domain)!; } // Fallback to Let's Encrypt for other domains console.log(`Falling back to Let's Encrypt for ${domain}`); return 'http01'; }, certProvisionFallbackToAcme: true, acme: { email: 'test@example.com', useProduction: false }, routes: [ { name: 'test-route', match: { ports: [443], domains: ['test.example.com'] }, action: { type: 'forward', targets: [{ host: 'localhost', port: 8080 }], tls: { mode: 'terminate', certificate: 'auto' } } } ] }); expect(testProxy).toBeInstanceOf(SmartProxy); }); tap.test('Custom certificate provision function should be called', async () => { let provisionCalled = false; const provisionedDomains: string[] = []; const testProxy2 = new SmartProxy({ certProvisionFunction: async (domain: string): Promise => { provisionCalled = true; provisionedDomains.push(domain); // Return a test certificate matching ICert interface return { id: `test-cert-${domain}`, domainName: domain, created: Date.now(), validUntil: Date.now() + 90 * 24 * 60 * 60 * 1000, privateKey: testKey, publicKey: testCert, csr: '' }; }, acme: { email: 'test@example.com', useProduction: false, port: 9080 }, routes: [ { name: 'custom-cert-route', match: { ports: [9443], domains: ['custom.example.com'] }, action: { type: 'forward', targets: [{ host: 'localhost', port: 8080 }], tls: { mode: 'terminate', certificate: 'auto' } } } ] }); // Fully mock the certificate manager to avoid ACME server contact let certManagerCalled = false; (testProxy2 as any).createCertificateManager = async function() { const mockCertManager = createMockCertManager({ onProvisionAll: () => { certManagerCalled = true; // Simulate calling the provision function testProxy2.settings.certProvisionFunction?.('custom.example.com'); } }); // Set callback as in real implementation mockCertManager.setUpdateRoutesCallback(async (routes: any) => { await this.updateRoutes(routes); }); return mockCertManager; }; // Start the proxy (this will trigger certificate provisioning) await testProxy2.start(); expect(certManagerCalled).toBeTrue(); expect(provisionCalled).toBeTrue(); expect(provisionedDomains).toContain('custom.example.com'); await testProxy2.stop(); }); tap.test('Should fallback to ACME when custom provision fails', async () => { const failedDomains: string[] = []; let acmeAttempted = false; const testProxy3 = new SmartProxy({ certProvisionFunction: async (domain: string): Promise => { failedDomains.push(domain); throw new Error('Custom provision failed for testing'); }, certProvisionFallbackToAcme: true, acme: { email: 'test@example.com', useProduction: false, port: 9080 }, routes: [ { name: 'fallback-route', match: { ports: [9444], domains: ['fallback.example.com'] }, action: { type: 'forward', targets: [{ host: 'localhost', port: 8080 }], tls: { mode: 'terminate', certificate: 'auto' } } } ] }); // Fully mock the certificate manager to avoid ACME server contact (testProxy3 as any).createCertificateManager = async function() { const mockCertManager = createMockCertManager({ onProvisionAll: async () => { // Simulate the provision logic: first try custom function, then ACME try { await testProxy3.settings.certProvisionFunction?.('fallback.example.com'); } catch (e) { // Custom provision failed, try ACME acmeAttempted = true; } } }); // Set callback as in real implementation mockCertManager.setUpdateRoutesCallback(async (routes: any) => { await this.updateRoutes(routes); }); return mockCertManager; }; // Start the proxy await testProxy3.start(); // Custom provision should have failed expect(failedDomains).toContain('fallback.example.com'); // ACME should have been attempted as fallback expect(acmeAttempted).toBeTrue(); await testProxy3.stop(); }); tap.test('Should not fallback when certProvisionFallbackToAcme is false', async () => { let errorThrown = false; let errorMessage = ''; const testProxy4 = new SmartProxy({ certProvisionFunction: async (_domain: string): Promise => { throw new Error('Custom provision failed for testing'); }, certProvisionFallbackToAcme: false, acme: { email: 'test@example.com', useProduction: false, port: 9082 }, routes: [ { name: 'no-fallback-route', match: { ports: [9449], domains: ['no-fallback.example.com'] }, action: { type: 'forward', targets: [{ host: 'localhost', port: 8080 }], tls: { mode: 'terminate', certificate: 'auto' } } } ] }); // Fully mock the certificate manager to avoid ACME server contact (testProxy4 as any).createCertificateManager = async function() { const mockCertManager = createMockCertManager({ onProvisionAll: async () => { // Simulate the provision logic with no fallback try { await testProxy4.settings.certProvisionFunction?.('no-fallback.example.com'); } catch (e: any) { errorThrown = true; errorMessage = e.message; // With certProvisionFallbackToAcme=false, the error should propagate if (!testProxy4.settings.certProvisionFallbackToAcme) { throw e; } } } }); // Set callback as in real implementation mockCertManager.setUpdateRoutesCallback(async (routes: any) => { await this.updateRoutes(routes); }); return mockCertManager; }; try { await testProxy4.start(); } catch (e) { // Expected to fail } expect(errorThrown).toBeTrue(); expect(errorMessage).toInclude('Custom provision failed for testing'); await testProxy4.stop(); }); tap.test('Should return http01 for unknown domains', async () => { let returnedHttp01 = false; let acmeAttempted = false; const testProxy5 = new SmartProxy({ certProvisionFunction: async (domain: string): Promise => { if (domain === 'known.example.com') { return { id: `test-cert-${domain}`, domainName: domain, created: Date.now(), validUntil: Date.now() + 90 * 24 * 60 * 60 * 1000, privateKey: testKey, publicKey: testCert, csr: '' }; } returnedHttp01 = true; return 'http01'; }, acme: { email: 'test@example.com', useProduction: false, port: 9081 }, routes: [ { name: 'unknown-domain-route', match: { ports: [9446], domains: ['unknown.example.com'] }, action: { type: 'forward', targets: [{ host: 'localhost', port: 8080 }], tls: { mode: 'terminate', certificate: 'auto' } } } ] }); // Fully mock the certificate manager to avoid ACME server contact (testProxy5 as any).createCertificateManager = async function() { const mockCertManager = createMockCertManager({ onProvisionAll: async () => { // Simulate the provision logic: call provision function first const result = await testProxy5.settings.certProvisionFunction?.('unknown.example.com'); if (result === 'http01') { // http01 means use ACME acmeAttempted = true; } } }); // Set callback as in real implementation mockCertManager.setUpdateRoutesCallback(async (routes: any) => { await this.updateRoutes(routes); }); return mockCertManager; }; await testProxy5.start(); // Should have returned http01 for unknown domain expect(returnedHttp01).toBeTrue(); // ACME should have been attempted expect(acmeAttempted).toBeTrue(); await testProxy5.stop(); }); tap.test('cleanup', async () => { // Clean up any test proxies if (testProxy) { await testProxy.stop(); } }); export default tap.start();