feat(RouteManager): Add getAllRoutes API to RouteManager and update test environment to improve timeouts, logging, and cleanup; remove deprecated test files and adjust devDependencies accordingly

This commit is contained in:
Philipp Kunz 2025-05-18 18:08:55 +00:00
parent 61ab1482e3
commit 41f7d09c52
8 changed files with 86 additions and 234 deletions

View File

@ -1,5 +1,14 @@
# Changelog
## 2025-05-18 - 19.1.0 - feat(RouteManager)
Add getAllRoutes API to RouteManager and update test environment to improve timeouts, logging, and cleanup; remove deprecated test files and adjust devDependencies accordingly
- Removed @push.rocks/tapbundle from devDependencies in package.json
- Deleted deprecated test.certprovisioner.unit.ts file
- Improved timeout handling and cleanup logic in test.networkproxy.function-targets.ts
- Added getAllRoutes public method to RouteManager to retrieve all routes
- Minor adjustments in SmartAcme integration tests with updated certificate fixture format
## 2025-05-18 - 19.0.0 - BREAKING CHANGE(certificates)
Remove legacy certificate modules and Port80Handler; update documentation and route configurations to use SmartCertManager for certificate management.

View File

@ -18,7 +18,6 @@
"@git.zone/tsbuild": "^2.5.1",
"@git.zone/tsrun": "^1.2.44",
"@git.zone/tstest": "^1.9.0",
"@push.rocks/tapbundle": "^6.0.3",
"@types/node": "^22.15.18",
"typescript": "^5.8.3"
},

View File

@ -45,8 +45,8 @@ tap.test('should handle static certificates', async () => {
tls: {
mode: 'terminate',
certificate: {
certFile: './test/fixtures/cert.pem',
keyFile: './test/fixtures/key.pem'
cert: '-----BEGIN CERTIFICATE-----\nMIIC...\n-----END CERTIFICATE-----',
key: '-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----'
}
}
}

View File

@ -1,211 +0,0 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as plugins from '../ts/plugins.js';
import { CertProvisioner } from '../ts/certificate/providers/cert-provisioner.js';
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
import type { ICertificateData } from '../ts/certificate/models/certificate-types.js';
import type { TCertProvisionObject } from '../ts/certificate/providers/cert-provisioner.js';
// Fake Port80Handler stub
class FakePort80Handler extends plugins.EventEmitter {
public domainsAdded: string[] = [];
public renewCalled: string[] = [];
addDomain(opts: { domainName: string; sslRedirect: boolean; acmeMaintenance: boolean }) {
this.domainsAdded.push(opts.domainName);
}
async renewCertificate(domain: string): Promise<void> {
this.renewCalled.push(domain);
}
}
// Fake NetworkProxyBridge stub
class FakeNetworkProxyBridge {
public appliedCerts: ICertificateData[] = [];
applyExternalCertificate(cert: ICertificateData) {
this.appliedCerts.push(cert);
}
}
tap.test('CertProvisioner handles static provisioning', async () => {
const domain = 'static.com';
// Create route-based configuration for testing
const routeConfigs: IRouteConfig[] = [{
name: 'Static Route',
match: {
ports: 443,
domains: [domain]
},
action: {
type: 'forward',
target: { host: 'localhost', port: 443 },
tls: {
mode: 'terminate-and-reencrypt',
certificate: 'auto'
}
}
}];
const fakePort80 = new FakePort80Handler();
const fakeBridge = new FakeNetworkProxyBridge();
// certProvider returns static certificate
const certProvider = async (d: string): Promise<TCertProvisionObject> => {
expect(d).toEqual(domain);
return {
domainName: domain,
publicKey: 'CERT',
privateKey: 'KEY',
validUntil: Date.now() + 3600 * 1000,
created: Date.now(),
csr: 'CSR',
id: 'ID',
};
};
const prov = new CertProvisioner(
routeConfigs,
fakePort80 as any,
fakeBridge as any,
certProvider,
1, // low renew threshold
1, // short interval
false // disable auto renew for unit test
);
const events: any[] = [];
prov.on('certificate', (data) => events.push(data));
await prov.start();
// Static flow: no addDomain, certificate applied via bridge
expect(fakePort80.domainsAdded.length).toEqual(0);
expect(fakeBridge.appliedCerts.length).toEqual(1);
expect(events.length).toEqual(1);
const evt = events[0];
expect(evt.domain).toEqual(domain);
expect(evt.certificate).toEqual('CERT');
expect(evt.privateKey).toEqual('KEY');
expect(evt.isRenewal).toEqual(false);
expect(evt.source).toEqual('static');
expect(evt.routeReference).toBeTruthy();
expect(evt.routeReference.routeName).toEqual('Static Route');
});
tap.test('CertProvisioner handles http01 provisioning', async () => {
const domain = 'http01.com';
// Create route-based configuration for testing
const routeConfigs: IRouteConfig[] = [{
name: 'HTTP01 Route',
match: {
ports: 443,
domains: [domain]
},
action: {
type: 'forward',
target: { host: 'localhost', port: 80 },
tls: {
mode: 'terminate',
certificate: 'auto'
}
}
}];
const fakePort80 = new FakePort80Handler();
const fakeBridge = new FakeNetworkProxyBridge();
// certProvider returns http01 directive
const certProvider = async (): Promise<TCertProvisionObject> => 'http01';
const prov = new CertProvisioner(
routeConfigs,
fakePort80 as any,
fakeBridge as any,
certProvider,
1,
1,
false
);
const events: any[] = [];
prov.on('certificate', (data) => events.push(data));
await prov.start();
// HTTP-01 flow: addDomain called, no static cert applied
expect(fakePort80.domainsAdded).toEqual([domain]);
expect(fakeBridge.appliedCerts.length).toEqual(0);
expect(events.length).toEqual(0);
});
tap.test('CertProvisioner on-demand http01 renewal', async () => {
const domain = 'renew.com';
// Create route-based configuration for testing
const routeConfigs: IRouteConfig[] = [{
name: 'Renewal Route',
match: {
ports: 443,
domains: [domain]
},
action: {
type: 'forward',
target: { host: 'localhost', port: 80 },
tls: {
mode: 'terminate',
certificate: 'auto'
}
}
}];
const fakePort80 = new FakePort80Handler();
const fakeBridge = new FakeNetworkProxyBridge();
const certProvider = async (): Promise<TCertProvisionObject> => 'http01';
const prov = new CertProvisioner(
routeConfigs,
fakePort80 as any,
fakeBridge as any,
certProvider,
1,
1,
false
);
// requestCertificate should call renewCertificate
await prov.requestCertificate(domain);
expect(fakePort80.renewCalled).toEqual([domain]);
});
tap.test('CertProvisioner on-demand static provisioning', async () => {
const domain = 'ondemand.com';
// Create route-based configuration for testing
const routeConfigs: IRouteConfig[] = [{
name: 'On-Demand Route',
match: {
ports: 443,
domains: [domain]
},
action: {
type: 'forward',
target: { host: 'localhost', port: 443 },
tls: {
mode: 'terminate-and-reencrypt',
certificate: 'auto'
}
}
}];
const fakePort80 = new FakePort80Handler();
const fakeBridge = new FakeNetworkProxyBridge();
const certProvider = async (): Promise<TCertProvisionObject> => ({
domainName: domain,
publicKey: 'PKEY',
privateKey: 'PRIV',
validUntil: Date.now() + 1000,
created: Date.now(),
csr: 'CSR',
id: 'ID',
});
const prov = new CertProvisioner(
routeConfigs,
fakePort80 as any,
fakeBridge as any,
certProvider,
1,
1,
false
);
const events: any[] = [];
prov.on('certificate', (data) => events.push(data));
await prov.requestCertificate(domain);
expect(fakeBridge.appliedCerts.length).toEqual(1);
expect(events.length).toEqual(1);
expect(events[0].domain).toEqual(domain);
expect(events[0].source).toEqual('static');
expect(events[0].routeReference).toBeTruthy();
expect(events[0].routeReference.routeName).toEqual('On-Demand Route');
});
export default tap.start();

View File

@ -4,8 +4,6 @@ import { NetworkProxy } from '../ts/proxies/network-proxy/index.js';
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
import type { IRouteContext } from '../ts/core/models/route-context.js';
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
// Declare variables for tests
let networkProxy: NetworkProxy;
let testServer: plugins.http.Server;
@ -14,7 +12,9 @@ let serverPort: number;
let serverPortHttp2: number;
// Setup test environment
tap.test('setup NetworkProxy function-based targets test environment', async () => {
tap.test('setup NetworkProxy function-based targets test environment', async (tools) => {
// Set a reasonable timeout for the test
tools.timeout = 30000; // 30 seconds
// Create simple HTTP server to respond to requests
testServer = plugins.http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
@ -41,6 +41,11 @@ tap.test('setup NetworkProxy function-based targets test environment', async ()
}));
});
// Handle HTTP/2 errors
testServerHttp2.on('error', (err) => {
console.error('HTTP/2 server error:', err);
});
// Start the servers
await new Promise<void>(resolve => {
testServer.listen(0, () => {
@ -318,21 +323,57 @@ tap.test('should support context-based routing with path', async () => {
// Cleanup test environment
tap.test('cleanup NetworkProxy function-based targets test environment', async () => {
if (networkProxy) {
await networkProxy.stop();
// Skip cleanup if setup failed
if (!networkProxy && !testServer && !testServerHttp2) {
console.log('Skipping cleanup - setup failed');
return;
}
// Stop test servers first
if (testServer) {
await new Promise<void>(resolve => {
testServer.close(() => resolve());
await new Promise<void>((resolve, reject) => {
testServer.close((err) => {
if (err) {
console.error('Error closing test server:', err);
reject(err);
} else {
console.log('Test server closed successfully');
resolve();
}
});
});
}
if (testServerHttp2) {
await new Promise<void>(resolve => {
testServerHttp2.close(() => resolve());
await new Promise<void>((resolve, reject) => {
testServerHttp2.close((err) => {
if (err) {
console.error('Error closing HTTP/2 test server:', err);
reject(err);
} else {
console.log('HTTP/2 test server closed successfully');
resolve();
}
});
});
}
// Stop NetworkProxy last
if (networkProxy) {
console.log('Stopping NetworkProxy...');
await networkProxy.stop();
console.log('NetworkProxy stopped successfully');
}
// Force exit after a short delay to ensure cleanup
const cleanupTimeout = setTimeout(() => {
console.log('Cleanup completed, exiting');
}, 100);
// Don't keep the process alive just for this timeout
if (cleanupTimeout.unref) {
cleanupTimeout.unref();
}
});
// Helper function to make HTTPS requests with self-signed certificate support
@ -365,5 +406,8 @@ async function makeRequest(options: plugins.http.RequestOptions): Promise<{ stat
});
}
// Export the test runner to start tests
export default tap.start();
// Start the tests
tap.start().then(() => {
// Ensure process exits after tests complete
process.exit(0);
});

View File

@ -1,5 +1,5 @@
import * as plugins from '../ts/plugins.js';
import { tap } from '@push.rocks/tapbundle';
import { tap, expect } from '@push.rocks/tapbundle';
import { SmartCertManager } from '../ts/proxies/smart-proxy/certificate-manager.js';
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
@ -10,17 +10,21 @@ tap.test('should create a SmartCertManager instance', async () => {
{
name: 'test-acme-route',
match: {
domains: ['test.example.com']
domains: ['test.example.com'],
ports: []
},
action: {
type: 'proxy',
target: 'http://localhost:3000',
type: 'forward',
target: {
host: 'localhost',
port: 3000
},
tls: {
mode: 'terminate',
certificate: 'auto'
},
acme: {
email: 'test@example.com'
certificate: 'auto',
acme: {
email: 'test@example.com'
}
}
}
}

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartproxy',
version: '19.0.0',
version: '19.1.0',
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
}

View File

@ -173,6 +173,13 @@ export class RouteManager extends plugins.EventEmitter {
return this.portMap.get(port) || [];
}
/**
* Get all routes
*/
public getAllRoutes(): IRouteConfig[] {
return [...this.routes];
}
/**
* Test if a pattern matches a domain using glob matching
*/