From 41f7d09c520f55e4a21ec87c9104b995505c9b43 Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Sun, 18 May 2025 18:08:55 +0000 Subject: [PATCH] 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 --- changelog.md | 9 + package.json | 1 - test/test.certificate-provisioning.ts | 4 +- test/test.certprovisioner.unit.ts | 211 --------------------- test/test.networkproxy.function-targets.ts | 66 +++++-- test/test.smartacme-integration.ts | 20 +- ts/00_commitinfo_data.ts | 2 +- ts/proxies/smart-proxy/route-manager.ts | 7 + 8 files changed, 86 insertions(+), 234 deletions(-) delete mode 100644 test/test.certprovisioner.unit.ts diff --git a/changelog.md b/changelog.md index bbf0c0a..e44cc33 100644 --- a/changelog.md +++ b/changelog.md @@ -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. diff --git a/package.json b/package.json index 215b28e..d71a943 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/test/test.certificate-provisioning.ts b/test/test.certificate-provisioning.ts index 1eebb7f..23a6246 100644 --- a/test/test.certificate-provisioning.ts +++ b/test/test.certificate-provisioning.ts @@ -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-----' } } } diff --git a/test/test.certprovisioner.unit.ts b/test/test.certprovisioner.unit.ts deleted file mode 100644 index 77aa3d4..0000000 --- a/test/test.certprovisioner.unit.ts +++ /dev/null @@ -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 { - 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 => { - 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 => '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 => '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 => ({ - 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(); \ No newline at end of file diff --git a/test/test.networkproxy.function-targets.ts b/test/test.networkproxy.function-targets.ts index e264e27..8773c29 100644 --- a/test/test.networkproxy.function-targets.ts +++ b/test/test.networkproxy.function-targets.ts @@ -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(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(resolve => { - testServer.close(() => resolve()); + await new Promise((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(resolve => { - testServerHttp2.close(() => resolve()); + await new Promise((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(); \ No newline at end of file +// Start the tests +tap.start().then(() => { + // Ensure process exits after tests complete + process.exit(0); +}); \ No newline at end of file diff --git a/test/test.smartacme-integration.ts b/test/test.smartacme-integration.ts index 69a8f13..bc3e1c4 100644 --- a/test/test.smartacme-integration.ts +++ b/test/test.smartacme-integration.ts @@ -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' + } } } } diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index dbfd6f1..7272c97 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -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.' } diff --git a/ts/proxies/smart-proxy/route-manager.ts b/ts/proxies/smart-proxy/route-manager.ts index 2e90add..b4607c4 100644 --- a/ts/proxies/smart-proxy/route-manager.ts +++ b/ts/proxies/smart-proxy/route-manager.ts @@ -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 */