update
This commit is contained in:
350
test/test.certificate-provision.ts
Normal file
350
test/test.certificate-provision.ts
Normal file
@ -0,0 +1,350 @@
|
||||
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 - the type is from plugins.tsclass.network.ICert
|
||||
// but we return a simple object with the required properties
|
||||
const testCertObject = {
|
||||
cert: testCert,
|
||||
key: testKey,
|
||||
ca: ''
|
||||
};
|
||||
|
||||
// Custom certificate store for testing
|
||||
const customCerts = new Map<string, typeof testCertObject>();
|
||||
customCerts.set('test.example.com', testCertObject);
|
||||
|
||||
// Create proxy with custom certificate provision
|
||||
testProxy = new SmartProxy({
|
||||
certProvisionFunction: async (domain: string): Promise<TSmartProxyCertProvisionObject> => {
|
||||
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)! as unknown as TSmartProxyCertProvisionObject;
|
||||
}
|
||||
|
||||
// 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<TSmartProxyCertProvisionObject> => {
|
||||
provisionCalled = true;
|
||||
provisionedDomains.push(domain);
|
||||
|
||||
// Return a test certificate using the loaded files
|
||||
// We need to return a proper cert object that satisfies the type
|
||||
return {
|
||||
cert: testCert,
|
||||
key: testKey,
|
||||
ca: ''
|
||||
} as unknown as TSmartProxyCertProvisionObject;
|
||||
},
|
||||
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<TSmartProxyCertProvisionObject> => {
|
||||
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<TSmartProxyCertProvisionObject> => {
|
||||
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<TSmartProxyCertProvisionObject> => {
|
||||
if (domain === 'known.example.com') {
|
||||
return {
|
||||
cert: testCert,
|
||||
key: testKey,
|
||||
ca: ''
|
||||
} as unknown as TSmartProxyCertProvisionObject;
|
||||
}
|
||||
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();
|
Reference in New Issue
Block a user