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'); 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', target: { 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', target: { host: 'localhost', port: 8080 }, tls: { mode: 'terminate', certificate: 'auto' } } } ] }); // Mock the certificate manager to test our custom provision function let certManagerCalled = false; const origCreateCertManager = (testProxy2 as any).createCertificateManager; (testProxy2 as any).createCertificateManager = async function(...args: any[]) { const certManager = await origCreateCertManager.apply(testProxy2, args); // Override provisionAllCertificates to track calls const origProvisionAll = certManager.provisionAllCertificates; certManager.provisionAllCertificates = async function() { certManagerCalled = true; await origProvisionAll.call(certManager); }; return certManager; }; // 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', target: { host: 'localhost', port: 8080 }, tls: { mode: 'terminate', certificate: 'auto' } } } ] }); // Mock to track ACME attempts const origCreateCertManager = (testProxy3 as any).createCertificateManager; (testProxy3 as any).createCertificateManager = async function(...args: any[]) { const certManager = await origCreateCertManager.apply(testProxy3, args); // Mock SmartAcme to avoid real ACME calls (certManager as any).smartAcme = { getCertificateForDomain: async () => { acmeAttempted = true; throw new Error('Mocked ACME failure'); } }; return certManager; }; // 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, routes: [ { name: 'no-fallback-route', match: { ports: [9445], domains: ['no-fallback.example.com'] }, action: { type: 'forward', target: { host: 'localhost', port: 8080 }, tls: { mode: 'terminate', certificate: 'auto' } } } ] }); // Mock certificate manager to capture errors const origCreateCertManager = (testProxy4 as any).createCertificateManager; (testProxy4 as any).createCertificateManager = async function(...args: any[]) { const certManager = await origCreateCertManager.apply(testProxy4, args); // Override provisionAllCertificates to capture errors const origProvisionAll = certManager.provisionAllCertificates; certManager.provisionAllCertificates = async function() { try { await origProvisionAll.call(certManager); } catch (e) { errorThrown = true; errorMessage = e.message; throw e; } }; return certManager; }; 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', target: { host: 'localhost', port: 8080 }, tls: { mode: 'terminate', certificate: 'auto' } } } ] }); // Mock to track ACME attempts const origCreateCertManager = (testProxy5 as any).createCertificateManager; (testProxy5 as any).createCertificateManager = async function(...args: any[]) { const certManager = await origCreateCertManager.apply(testProxy5, args); // Mock SmartAcme to track attempts (certManager as any).smartAcme = { getCertificateForDomain: async () => { acmeAttempted = true; throw new Error('Mocked ACME failure'); } }; return certManager; }; 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();