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:
		| @@ -1,5 +1,14 @@ | |||||||
| # Changelog | # 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) | ## 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. | Remove legacy certificate modules and Port80Handler; update documentation and route configurations to use SmartCertManager for certificate management. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -18,7 +18,6 @@ | |||||||
|     "@git.zone/tsbuild": "^2.5.1", |     "@git.zone/tsbuild": "^2.5.1", | ||||||
|     "@git.zone/tsrun": "^1.2.44", |     "@git.zone/tsrun": "^1.2.44", | ||||||
|     "@git.zone/tstest": "^1.9.0", |     "@git.zone/tstest": "^1.9.0", | ||||||
|     "@push.rocks/tapbundle": "^6.0.3", |  | ||||||
|     "@types/node": "^22.15.18", |     "@types/node": "^22.15.18", | ||||||
|     "typescript": "^5.8.3" |     "typescript": "^5.8.3" | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -45,8 +45,8 @@ tap.test('should handle static certificates', async () => { | |||||||
|         tls: { |         tls: { | ||||||
|           mode: 'terminate', |           mode: 'terminate', | ||||||
|           certificate: { |           certificate: { | ||||||
|             certFile: './test/fixtures/cert.pem', |             cert: '-----BEGIN CERTIFICATE-----\nMIIC...\n-----END CERTIFICATE-----', | ||||||
|             keyFile: './test/fixtures/key.pem' |             key: '-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----' | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -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(); |  | ||||||
| @@ -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 { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js'; | ||||||
| import type { IRouteContext } from '../ts/core/models/route-context.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 | // Declare variables for tests | ||||||
| let networkProxy: NetworkProxy; | let networkProxy: NetworkProxy; | ||||||
| let testServer: plugins.http.Server; | let testServer: plugins.http.Server; | ||||||
| @@ -14,7 +12,9 @@ let serverPort: number; | |||||||
| let serverPortHttp2: number; | let serverPortHttp2: number; | ||||||
|  |  | ||||||
| // Setup test environment | // 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 |   // Create simple HTTP server to respond to requests | ||||||
|   testServer = plugins.http.createServer((req, res) => { |   testServer = plugins.http.createServer((req, res) => { | ||||||
|     res.writeHead(200, { 'Content-Type': 'application/json' }); |     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 |   // Start the servers | ||||||
|   await new Promise<void>(resolve => { |   await new Promise<void>(resolve => { | ||||||
|     testServer.listen(0, () => { |     testServer.listen(0, () => { | ||||||
| @@ -318,21 +323,57 @@ tap.test('should support context-based routing with path', async () => { | |||||||
|  |  | ||||||
| // Cleanup test environment | // Cleanup test environment | ||||||
| tap.test('cleanup NetworkProxy function-based targets test environment', async () => { | tap.test('cleanup NetworkProxy function-based targets test environment', async () => { | ||||||
|   if (networkProxy) { |   // Skip cleanup if setup failed | ||||||
|     await networkProxy.stop(); |   if (!networkProxy && !testServer && !testServerHttp2) { | ||||||
|  |     console.log('Skipping cleanup - setup failed'); | ||||||
|  |     return; | ||||||
|   } |   } | ||||||
|    |    | ||||||
|  |   // Stop test servers first | ||||||
|   if (testServer) { |   if (testServer) { | ||||||
|     await new Promise<void>(resolve => { |     await new Promise<void>((resolve, reject) => { | ||||||
|       testServer.close(() => resolve()); |       testServer.close((err) => { | ||||||
|  |         if (err) { | ||||||
|  |           console.error('Error closing test server:', err); | ||||||
|  |           reject(err); | ||||||
|  |         } else { | ||||||
|  |           console.log('Test server closed successfully'); | ||||||
|  |           resolve(); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   if (testServerHttp2) { |   if (testServerHttp2) { | ||||||
|     await new Promise<void>(resolve => { |     await new Promise<void>((resolve, reject) => { | ||||||
|       testServerHttp2.close(() => resolve()); |       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 | // 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 | // Start the tests | ||||||
| export default tap.start(); | tap.start().then(() => { | ||||||
|  |   // Ensure process exits after tests complete | ||||||
|  |   process.exit(0); | ||||||
|  | }); | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| import * as plugins from '../ts/plugins.js'; | 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 { SmartCertManager } from '../ts/proxies/smart-proxy/certificate-manager.js'; | ||||||
| import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.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', |       name: 'test-acme-route', | ||||||
|       match: { |       match: { | ||||||
|         domains: ['test.example.com'] |         domains: ['test.example.com'], | ||||||
|  |         ports: [] | ||||||
|       }, |       }, | ||||||
|       action: { |       action: { | ||||||
|         type: 'proxy', |         type: 'forward', | ||||||
|         target: 'http://localhost:3000', |         target: {  | ||||||
|  |           host: 'localhost',  | ||||||
|  |           port: 3000  | ||||||
|  |         }, | ||||||
|         tls: { |         tls: { | ||||||
|           mode: 'terminate', |           mode: 'terminate', | ||||||
|           certificate: 'auto' |           certificate: 'auto', | ||||||
|         }, |           acme: { | ||||||
|         acme: { |             email: 'test@example.com' | ||||||
|           email: 'test@example.com' |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -3,6 +3,6 @@ | |||||||
|  */ |  */ | ||||||
| export const commitinfo = { | export const commitinfo = { | ||||||
|   name: '@push.rocks/smartproxy', |   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.' |   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.' | ||||||
| } | } | ||||||
|   | |||||||
| @@ -173,6 +173,13 @@ export class RouteManager extends plugins.EventEmitter { | |||||||
|     return this.portMap.get(port) || []; |     return this.portMap.get(port) || []; | ||||||
|   } |   } | ||||||
|    |    | ||||||
|  |   /** | ||||||
|  |    * Get all routes | ||||||
|  |    */ | ||||||
|  |   public getAllRoutes(): IRouteConfig[] { | ||||||
|  |     return [...this.routes]; | ||||||
|  |   } | ||||||
|  |    | ||||||
|   /** |   /** | ||||||
|    * Test if a pattern matches a domain using glob matching |    * Test if a pattern matches a domain using glob matching | ||||||
|    */ |    */ | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user