339 lines
11 KiB
TypeScript
339 lines
11 KiB
TypeScript
import * as plugins from '../ts/plugins.js';
|
|
import { SmartProxy } from '../ts/index.js';
|
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
|
|
let testProxy: SmartProxy;
|
|
|
|
// Create test routes using high ports to avoid permission issues
|
|
const createRoute = (id: number, domain: string, port: number = 8443) => ({
|
|
name: `test-route-${id}`,
|
|
match: {
|
|
ports: [port],
|
|
domains: [domain]
|
|
},
|
|
action: {
|
|
type: 'forward' as const,
|
|
target: {
|
|
host: 'localhost',
|
|
port: 3000 + id
|
|
},
|
|
tls: {
|
|
mode: 'terminate' as const,
|
|
certificate: 'auto' as const,
|
|
acme: {
|
|
email: 'test@testdomain.test',
|
|
useProduction: false
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
tap.test('should create SmartProxy instance', async () => {
|
|
testProxy = new SmartProxy({
|
|
routes: [createRoute(1, 'test1.testdomain.test', 8443)],
|
|
acme: {
|
|
email: 'test@testdomain.test',
|
|
useProduction: false,
|
|
port: 8080
|
|
}
|
|
});
|
|
expect(testProxy).toBeInstanceOf(SmartProxy);
|
|
});
|
|
|
|
tap.test('should preserve route update callback after updateRoutes', async () => {
|
|
// Mock the certificate manager to avoid actual ACME initialization
|
|
const originalInitializeCertManager = (testProxy as any).initializeCertificateManager;
|
|
let certManagerInitialized = false;
|
|
|
|
(testProxy as any).initializeCertificateManager = async function() {
|
|
certManagerInitialized = true;
|
|
// Create a minimal mock certificate manager
|
|
const mockCertManager = {
|
|
setUpdateRoutesCallback: function(callback: any) {
|
|
this.updateRoutesCallback = callback;
|
|
},
|
|
updateRoutesCallback: null,
|
|
setHttpProxy: function() {},
|
|
setGlobalAcmeDefaults: function() {},
|
|
setAcmeStateManager: function() {},
|
|
initialize: async function() {
|
|
// This is where the callback is actually set in the real implementation
|
|
return Promise.resolve();
|
|
},
|
|
provisionAllCertificates: async function() {
|
|
return Promise.resolve();
|
|
},
|
|
stop: async function() {},
|
|
getAcmeOptions: function() {
|
|
return { email: 'test@testdomain.test' };
|
|
},
|
|
getState: function() {
|
|
return { challengeRouteActive: false };
|
|
}
|
|
};
|
|
|
|
(this as any).certManager = mockCertManager;
|
|
|
|
// Simulate the real behavior where setUpdateRoutesCallback is called
|
|
mockCertManager.setUpdateRoutesCallback(async (routes: any) => {
|
|
await this.updateRoutes(routes);
|
|
});
|
|
};
|
|
|
|
// Start the proxy (with mocked cert manager)
|
|
await testProxy.start();
|
|
expect(certManagerInitialized).toEqual(true);
|
|
|
|
// Get initial certificate manager reference
|
|
const initialCertManager = (testProxy as any).certManager;
|
|
expect(initialCertManager).toBeTruthy();
|
|
expect(initialCertManager.updateRoutesCallback).toBeTruthy();
|
|
|
|
// Store the initial callback reference
|
|
const initialCallback = initialCertManager.updateRoutesCallback;
|
|
|
|
// Update routes - this should recreate the cert manager with callback
|
|
const newRoutes = [
|
|
createRoute(1, 'test1.testdomain.test', 8443),
|
|
createRoute(2, 'test2.testdomain.test', 8444)
|
|
];
|
|
|
|
// Mock the updateRoutes to simulate the real implementation
|
|
testProxy.updateRoutes = async function(routes) {
|
|
// Update settings
|
|
this.settings.routes = routes;
|
|
|
|
// Simulate what happens in the real code - recreate cert manager via createCertificateManager
|
|
if ((this as any).certManager) {
|
|
await (this as any).certManager.stop();
|
|
|
|
// Simulate createCertificateManager which creates a new cert manager
|
|
const newMockCertManager = {
|
|
setUpdateRoutesCallback: function(callback: any) {
|
|
this.updateRoutesCallback = callback;
|
|
},
|
|
updateRoutesCallback: null,
|
|
setHttpProxy: function() {},
|
|
setGlobalAcmeDefaults: function() {},
|
|
setAcmeStateManager: function() {},
|
|
initialize: async function() {},
|
|
provisionAllCertificates: async function() {},
|
|
stop: async function() {},
|
|
getAcmeOptions: function() {
|
|
return { email: 'test@testdomain.test' };
|
|
},
|
|
getState: function() {
|
|
return { challengeRouteActive: false };
|
|
}
|
|
};
|
|
|
|
// Set the callback as done in createCertificateManager
|
|
newMockCertManager.setUpdateRoutesCallback(async (routes: any) => {
|
|
await this.updateRoutes(routes);
|
|
});
|
|
|
|
(this as any).certManager = newMockCertManager;
|
|
await (this as any).certManager.initialize();
|
|
}
|
|
};
|
|
|
|
await testProxy.updateRoutes(newRoutes);
|
|
|
|
// Get new certificate manager reference
|
|
const newCertManager = (testProxy as any).certManager;
|
|
expect(newCertManager).toBeTruthy();
|
|
expect(newCertManager).not.toEqual(initialCertManager); // Should be a new instance
|
|
expect(newCertManager.updateRoutesCallback).toBeTruthy(); // Callback should be set
|
|
|
|
// Test that the callback works
|
|
const testChallengeRoute = {
|
|
name: 'acme-challenge',
|
|
match: {
|
|
ports: [8080],
|
|
path: '/.well-known/acme-challenge/*'
|
|
},
|
|
action: {
|
|
type: 'static' as const,
|
|
content: 'challenge-token'
|
|
}
|
|
};
|
|
|
|
// This should not throw "No route update callback set" error
|
|
let callbackWorked = false;
|
|
try {
|
|
// If callback is set, this should work
|
|
if (newCertManager.updateRoutesCallback) {
|
|
await newCertManager.updateRoutesCallback([...newRoutes, testChallengeRoute]);
|
|
callbackWorked = true;
|
|
}
|
|
} catch (error) {
|
|
throw new Error(`Route update callback failed: ${error.message}`);
|
|
}
|
|
|
|
expect(callbackWorked).toEqual(true);
|
|
console.log('Route update callback successfully preserved and invoked');
|
|
});
|
|
|
|
tap.test('should handle multiple sequential route updates', async () => {
|
|
// Continue with the mocked proxy from previous test
|
|
let updateCount = 0;
|
|
|
|
// Perform multiple route updates
|
|
for (let i = 1; i <= 3; i++) {
|
|
const routes = [];
|
|
for (let j = 1; j <= i; j++) {
|
|
routes.push(createRoute(j, `test${j}.testdomain.test`, 8440 + j));
|
|
}
|
|
|
|
await testProxy.updateRoutes(routes);
|
|
updateCount++;
|
|
|
|
// Verify cert manager is properly set up each time
|
|
const certManager = (testProxy as any).certManager;
|
|
expect(certManager).toBeTruthy();
|
|
expect(certManager.updateRoutesCallback).toBeTruthy();
|
|
|
|
console.log(`Route update ${i} callback is properly set`);
|
|
}
|
|
|
|
expect(updateCount).toEqual(3);
|
|
});
|
|
|
|
tap.test('should handle route updates when cert manager is not initialized', async () => {
|
|
// Create proxy without routes that need certificates
|
|
const proxyWithoutCerts = new SmartProxy({
|
|
routes: [{
|
|
name: 'no-cert-route',
|
|
match: {
|
|
ports: [9080]
|
|
},
|
|
action: {
|
|
type: 'forward' as const,
|
|
target: {
|
|
host: 'localhost',
|
|
port: 3000
|
|
}
|
|
}
|
|
}]
|
|
});
|
|
|
|
// Mock initializeCertificateManager to avoid ACME issues
|
|
(proxyWithoutCerts as any).initializeCertificateManager = async function() {
|
|
// Only create cert manager if routes need it
|
|
const autoRoutes = this.settings.routes.filter((r: any) =>
|
|
r.action.tls?.certificate === 'auto'
|
|
);
|
|
|
|
if (autoRoutes.length === 0) {
|
|
console.log('No routes require certificate management');
|
|
return;
|
|
}
|
|
|
|
// Create mock cert manager
|
|
const mockCertManager = {
|
|
setUpdateRoutesCallback: function(callback: any) {
|
|
this.updateRoutesCallback = callback;
|
|
},
|
|
updateRoutesCallback: null,
|
|
setHttpProxy: function() {},
|
|
initialize: async function() {},
|
|
provisionAllCertificates: async function() {},
|
|
stop: async function() {},
|
|
getAcmeOptions: function() {
|
|
return { email: 'test@testdomain.test' };
|
|
},
|
|
getState: function() {
|
|
return { challengeRouteActive: false };
|
|
}
|
|
};
|
|
|
|
(this as any).certManager = mockCertManager;
|
|
|
|
// Set the callback
|
|
mockCertManager.setUpdateRoutesCallback(async (routes: any) => {
|
|
await this.updateRoutes(routes);
|
|
});
|
|
};
|
|
|
|
await proxyWithoutCerts.start();
|
|
|
|
// This should not have a cert manager
|
|
const certManager = (proxyWithoutCerts as any).certManager;
|
|
expect(certManager).toBeFalsy();
|
|
|
|
// Update with routes that need certificates
|
|
await proxyWithoutCerts.updateRoutes([createRoute(1, 'cert-needed.testdomain.test', 9443)]);
|
|
|
|
// In the real implementation, cert manager is not created by updateRoutes if it doesn't exist
|
|
// This is the expected behavior - cert manager is only created during start() or re-created if already exists
|
|
const newCertManager = (proxyWithoutCerts as any).certManager;
|
|
expect(newCertManager).toBeFalsy(); // Should still be null
|
|
|
|
await proxyWithoutCerts.stop();
|
|
});
|
|
|
|
tap.test('should clean up properly', async () => {
|
|
await testProxy.stop();
|
|
});
|
|
|
|
tap.test('real code integration test - verify fix is applied', async () => {
|
|
// This test will start with routes that need certificates to test the fix
|
|
const realProxy = new SmartProxy({
|
|
routes: [createRoute(1, 'test.example.com', 9999)],
|
|
acme: {
|
|
email: 'test@example.com',
|
|
useProduction: false,
|
|
port: 18080
|
|
}
|
|
});
|
|
|
|
// Mock the certificate manager creation to track callback setting
|
|
let callbackSet = false;
|
|
(realProxy as any).createCertificateManager = async function(routes: any[], certDir: string, acmeOptions: any, initialState?: any) {
|
|
const mockCertManager = {
|
|
setUpdateRoutesCallback: function(callback: any) {
|
|
callbackSet = true;
|
|
this.updateRoutesCallback = callback;
|
|
},
|
|
updateRoutesCallback: null as any,
|
|
setHttpProxy: function() {},
|
|
setGlobalAcmeDefaults: function() {},
|
|
setAcmeStateManager: function() {},
|
|
initialize: async function() {},
|
|
provisionAllCertificates: async function() {},
|
|
stop: async function() {},
|
|
getAcmeOptions: function() {
|
|
return acmeOptions || { email: 'test@example.com', useProduction: false };
|
|
},
|
|
getState: function() {
|
|
return initialState || { challengeRouteActive: false };
|
|
}
|
|
};
|
|
|
|
// Always set up the route update callback for ACME challenges
|
|
mockCertManager.setUpdateRoutesCallback(async (routes) => {
|
|
await this.updateRoutes(routes);
|
|
});
|
|
|
|
return mockCertManager;
|
|
};
|
|
|
|
await realProxy.start();
|
|
|
|
// The callback should have been set during initialization
|
|
expect(callbackSet).toEqual(true);
|
|
callbackSet = false; // Reset for update test
|
|
|
|
// Update routes - this should recreate cert manager with callback preserved
|
|
const newRoute = createRoute(2, 'test2.example.com', 9999);
|
|
await realProxy.updateRoutes([createRoute(1, 'test.example.com', 9999), newRoute]);
|
|
|
|
// The callback should have been set again during update
|
|
expect(callbackSet).toEqual(true);
|
|
|
|
await realProxy.stop();
|
|
|
|
console.log('Real code integration test passed - fix is correctly applied!');
|
|
});
|
|
|
|
tap.start(); |