fix(smartproxy): improve certificate manager mocking in tests, enhance IPv6 validation, and record initial bytes for connection metrics
This commit is contained in:
@@ -14,6 +14,44 @@ let testProxy: SmartProxy;
|
||||
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 = {
|
||||
@@ -25,22 +63,22 @@ tap.test('SmartProxy should support custom certificate provision function', asyn
|
||||
publicKey: testCert,
|
||||
csr: ''
|
||||
};
|
||||
|
||||
|
||||
// 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)!;
|
||||
}
|
||||
|
||||
|
||||
// Fallback to Let's Encrypt for other domains
|
||||
console.log(`Falling back to Let's Encrypt for ${domain}`);
|
||||
return 'http01';
|
||||
@@ -71,19 +109,19 @@ tap.test('SmartProxy should support custom certificate provision function', asyn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
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 matching ICert interface
|
||||
return {
|
||||
id: `test-cert-${domain}`,
|
||||
@@ -121,37 +159,40 @@ tap.test('Custom certificate provision function should be called', async () => {
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Mock the certificate manager to test our custom provision function
|
||||
|
||||
// Fully mock the certificate manager to avoid ACME server contact
|
||||
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;
|
||||
(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<TSmartProxyCertProvisionObject> => {
|
||||
failedDomains.push(domain);
|
||||
@@ -184,49 +225,60 @@ tap.test('Should fallback to ACME when custom provision fails', async () => {
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 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');
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return certManager;
|
||||
});
|
||||
|
||||
// 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<TSmartProxyCertProvisionObject> => {
|
||||
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: [9445],
|
||||
ports: [9449],
|
||||
domains: ['no-fallback.example.com']
|
||||
},
|
||||
action: {
|
||||
@@ -243,43 +295,49 @@ tap.test('Should not fallback when certProvisionFallbackToAcme is false', async
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return certManager;
|
||||
});
|
||||
|
||||
// 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<TSmartProxyCertProvisionObject> => {
|
||||
if (domain === 'known.example.com') {
|
||||
@@ -322,31 +380,36 @@ tap.test('Should return http01 for unknown domains', async () => {
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 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');
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return certManager;
|
||||
});
|
||||
|
||||
// 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();
|
||||
});
|
||||
|
||||
@@ -357,4 +420,4 @@ tap.test('cleanup', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
export default tap.start();
|
||||
|
||||
Reference in New Issue
Block a user