smartproxy/test/test.certificate-provisioning.ts
2025-05-29 11:30:42 +00:00

241 lines
6.9 KiB
TypeScript

import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
import { expect, tap } from '@git.zone/tstest/tapbundle';
const testProxy = new SmartProxy({
routes: [{
name: 'test-route',
match: { ports: 9443, domains: 'test.local' },
action: {
type: 'forward',
target: { host: 'localhost', port: 8080 },
tls: {
mode: 'terminate',
certificate: 'auto',
acme: {
email: 'test@test.local',
useProduction: false
}
}
}
}],
acme: {
port: 9080 // Use high port for ACME challenges
}
});
tap.test('should provision certificate automatically', async () => {
// Mock certificate manager to avoid real ACME initialization
const mockCertStatus = {
domain: 'test-route',
status: 'valid' as const,
source: 'acme' as const,
expiryDate: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000),
issueDate: new Date()
};
(testProxy as any).createCertificateManager = async function() {
return {
setUpdateRoutesCallback: () => {},
setHttpProxy: () => {},
setGlobalAcmeDefaults: () => {},
setAcmeStateManager: () => {},
initialize: async () => {},
provisionAllCertificates: async () => {},
stop: async () => {},
getAcmeOptions: () => ({ email: 'test@test.local', useProduction: false }),
getState: () => ({ challengeRouteActive: false }),
getCertificateStatus: () => mockCertStatus
};
};
(testProxy as any).getCertificateStatus = () => mockCertStatus;
await testProxy.start();
const status = testProxy.getCertificateStatus('test-route');
expect(status).toBeDefined();
expect(status.status).toEqual('valid');
expect(status.source).toEqual('acme');
await testProxy.stop();
});
tap.test('should handle static certificates', async () => {
const proxy = new SmartProxy({
routes: [{
name: 'static-route',
match: { ports: 9444, domains: 'static.example.com' },
action: {
type: 'forward',
target: { host: 'localhost', port: 8080 },
tls: {
mode: 'terminate',
certificate: {
cert: '-----BEGIN CERTIFICATE-----\nMIIC...\n-----END CERTIFICATE-----',
key: '-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----'
}
}
}
}]
});
await proxy.start();
const status = proxy.getCertificateStatus('static-route');
expect(status).toBeDefined();
expect(status.status).toEqual('valid');
expect(status.source).toEqual('static');
await proxy.stop();
});
tap.test('should handle ACME challenge routes', async () => {
const proxy = new SmartProxy({
routes: [{
name: 'auto-cert-route',
match: { ports: 9445, domains: 'acme.local' },
action: {
type: 'forward',
target: { host: 'localhost', port: 8080 },
tls: {
mode: 'terminate',
certificate: 'auto',
acme: {
email: 'acme@test.local',
useProduction: false,
challengePort: 9081
}
}
}
}, {
name: 'port-9081-route',
match: { ports: 9081, domains: 'acme.local' },
action: {
type: 'forward',
target: { host: 'localhost', port: 8080 }
}
}],
acme: {
port: 9081 // Use high port for ACME challenges
}
});
// Mock certificate manager to avoid real ACME initialization
(proxy as any).createCertificateManager = async function() {
return {
setUpdateRoutesCallback: () => {},
setHttpProxy: () => {},
setGlobalAcmeDefaults: () => {},
setAcmeStateManager: () => {},
initialize: async () => {},
provisionAllCertificates: async () => {},
stop: async () => {},
getAcmeOptions: () => ({ email: 'acme@test.local', useProduction: false }),
getState: () => ({ challengeRouteActive: false })
};
};
await proxy.start();
// Verify the proxy is configured with routes including the necessary port
const routes = proxy.settings.routes;
// Check that we have a route listening on the ACME challenge port
const acmeChallengePort = 9081;
const routesOnChallengePort = routes.filter((r: any) => {
const ports = Array.isArray(r.match.ports) ? r.match.ports : [r.match.ports];
return ports.includes(acmeChallengePort);
});
expect(routesOnChallengePort.length).toBeGreaterThan(0);
expect(routesOnChallengePort[0].name).toEqual('port-9081-route');
// Verify the main route has ACME configuration
const mainRoute = routes.find((r: any) => r.name === 'auto-cert-route');
expect(mainRoute).toBeDefined();
expect(mainRoute?.action.tls?.certificate).toEqual('auto');
expect(mainRoute?.action.tls?.acme?.email).toEqual('acme@test.local');
expect(mainRoute?.action.tls?.acme?.challengePort).toEqual(9081);
await proxy.stop();
});
tap.test('should renew certificates', async () => {
const proxy = new SmartProxy({
routes: [{
name: 'renew-route',
match: { ports: 9446, domains: 'renew.local' },
action: {
type: 'forward',
target: { host: 'localhost', port: 8080 },
tls: {
mode: 'terminate',
certificate: 'auto',
acme: {
email: 'renew@test.local',
useProduction: false,
renewBeforeDays: 30
}
}
}
}],
acme: {
port: 9082 // Use high port for ACME challenges
}
});
// Mock certificate manager with renewal capability
let renewCalled = false;
const mockCertStatus = {
domain: 'renew-route',
status: 'valid' as const,
source: 'acme' as const,
expiryDate: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000),
issueDate: new Date()
};
(proxy as any).certManager = {
renewCertificate: async (routeName: string) => {
renewCalled = true;
expect(routeName).toEqual('renew-route');
},
getCertificateStatus: () => mockCertStatus,
setUpdateRoutesCallback: () => {},
setHttpProxy: () => {},
setGlobalAcmeDefaults: () => {},
setAcmeStateManager: () => {},
initialize: async () => {},
provisionAllCertificates: async () => {},
stop: async () => {},
getAcmeOptions: () => ({ email: 'renew@test.local', useProduction: false }),
getState: () => ({ challengeRouteActive: false })
};
(proxy as any).createCertificateManager = async function() {
return this.certManager;
};
(proxy as any).getCertificateStatus = function(routeName: string) {
return this.certManager.getCertificateStatus(routeName);
};
(proxy as any).renewCertificate = async function(routeName: string) {
if (this.certManager) {
await this.certManager.renewCertificate(routeName);
}
};
await proxy.start();
// Force renewal
await proxy.renewCertificate('renew-route');
expect(renewCalled).toBeTrue();
const status = proxy.getCertificateStatus('renew-route');
expect(status).toBeDefined();
expect(status.status).toEqual('valid');
await proxy.stop();
});
tap.start();