328 lines
7.4 KiB
TypeScript
328 lines
7.4 KiB
TypeScript
import { expect, tap } from '@push.rocks/tapbundle';
|
|
import { SmartProxy } from '../ts/index.js';
|
|
|
|
tap.test('should handle concurrent route updates without race conditions', async (tools) => {
|
|
tools.timeout(10000);
|
|
|
|
const settings = {
|
|
port: 6001,
|
|
routes: [
|
|
{
|
|
name: 'initial-route',
|
|
match: {
|
|
ports: 80
|
|
},
|
|
action: {
|
|
type: 'forward' as const,
|
|
targetUrl: 'http://localhost:3000'
|
|
}
|
|
}
|
|
],
|
|
acme: {
|
|
email: 'test@test.com',
|
|
port: 80
|
|
}
|
|
};
|
|
|
|
const proxy = new SmartProxy(settings);
|
|
await proxy.start();
|
|
|
|
// Simulate concurrent route updates
|
|
const updates = [];
|
|
for (let i = 0; i < 5; i++) {
|
|
updates.push(proxy.updateRoutes([
|
|
...settings.routes,
|
|
{
|
|
name: `route-${i}`,
|
|
match: {
|
|
ports: [443]
|
|
},
|
|
action: {
|
|
type: 'forward' as const,
|
|
targetUrl: `https://localhost:${3001 + i}`,
|
|
tls: {
|
|
mode: 'terminate' as const,
|
|
certificate: 'auto'
|
|
}
|
|
}
|
|
}
|
|
]));
|
|
}
|
|
|
|
// All updates should complete without errors
|
|
await Promise.all(updates);
|
|
|
|
// Verify final state
|
|
const currentRoutes = proxy['settings'].routes;
|
|
tools.expect(currentRoutes.length).toBeGreaterThan(1);
|
|
|
|
await proxy.stop();
|
|
});
|
|
|
|
tap.test('should preserve certificate manager state during rapid updates', async (tools) => {
|
|
tools.timeout(10000);
|
|
|
|
const settings = {
|
|
port: 6002,
|
|
routes: [
|
|
{
|
|
name: 'test-route',
|
|
match: {
|
|
ports: [443]
|
|
},
|
|
action: {
|
|
type: 'forward' as const,
|
|
targetUrl: 'https://localhost:3001',
|
|
tls: {
|
|
mode: 'terminate' as const,
|
|
certificate: 'auto'
|
|
}
|
|
}
|
|
}
|
|
],
|
|
acme: {
|
|
email: 'test@test.com',
|
|
port: 80
|
|
}
|
|
};
|
|
|
|
const proxy = new SmartProxy(settings);
|
|
await proxy.start();
|
|
|
|
// Get initial certificate manager reference
|
|
const initialCertManager = proxy['certManager'];
|
|
tools.expect(initialCertManager).not.toBeNull();
|
|
|
|
// Perform rapid route updates
|
|
for (let i = 0; i < 3; i++) {
|
|
await proxy.updateRoutes([
|
|
...settings.routes,
|
|
{
|
|
name: `extra-route-${i}`,
|
|
match: {
|
|
ports: [8000 + i]
|
|
},
|
|
action: {
|
|
type: 'forward' as const,
|
|
targetUrl: `http://localhost:${4000 + i}`
|
|
}
|
|
}
|
|
]);
|
|
}
|
|
|
|
// Certificate manager should be recreated but state preserved
|
|
const finalCertManager = proxy['certManager'];
|
|
tools.expect(finalCertManager).not.toBeNull();
|
|
tools.expect(finalCertManager).not.toEqual(initialCertManager);
|
|
|
|
await proxy.stop();
|
|
});
|
|
|
|
tap.test('should handle challenge route state correctly across recreations', async (tools) => {
|
|
tools.timeout(10000);
|
|
|
|
let challengeRouteAddCount = 0;
|
|
|
|
const settings = {
|
|
port: 6003,
|
|
routes: [
|
|
{
|
|
name: 'acme-route',
|
|
match: {
|
|
ports: [443]
|
|
},
|
|
action: {
|
|
type: 'forward' as const,
|
|
targetUrl: 'https://localhost:3001',
|
|
tls: {
|
|
mode: 'terminate' as const,
|
|
certificate: 'auto'
|
|
}
|
|
}
|
|
}
|
|
],
|
|
acme: {
|
|
email: 'test@test.com',
|
|
port: 80
|
|
}
|
|
};
|
|
|
|
const proxy = new SmartProxy(settings);
|
|
|
|
// Mock the route update to count challenge route additions
|
|
const originalUpdateRoutes = proxy['updateRoutes'].bind(proxy);
|
|
proxy['updateRoutes'] = async (routes: any[]) => {
|
|
if (routes.some(r => r.name === 'acme-challenge')) {
|
|
challengeRouteAddCount++;
|
|
}
|
|
return originalUpdateRoutes(routes);
|
|
};
|
|
|
|
await proxy.start();
|
|
|
|
// Multiple route updates
|
|
for (let i = 0; i < 3; i++) {
|
|
await proxy.updateRoutes([
|
|
...settings.routes,
|
|
{
|
|
name: `dynamic-route-${i}`,
|
|
match: {
|
|
ports: [9000 + i]
|
|
},
|
|
action: {
|
|
type: 'forward' as const,
|
|
targetUrl: `http://localhost:${5000 + i}`
|
|
}
|
|
}
|
|
]);
|
|
}
|
|
|
|
// Challenge route should only be added once during initial start
|
|
tools.expect(challengeRouteAddCount).toEqual(1);
|
|
|
|
await proxy.stop();
|
|
});
|
|
|
|
tap.test('should prevent port conflicts during certificate manager recreation', async (tools) => {
|
|
tools.timeout(10000);
|
|
|
|
const settings = {
|
|
port: 6004,
|
|
routes: [
|
|
{
|
|
name: 'http-route',
|
|
match: {
|
|
ports: [80]
|
|
},
|
|
action: {
|
|
type: 'forward' as const,
|
|
targetUrl: 'http://localhost:3000'
|
|
}
|
|
},
|
|
{
|
|
name: 'https-route',
|
|
match: {
|
|
ports: [443]
|
|
},
|
|
action: {
|
|
type: 'forward' as const,
|
|
targetUrl: 'https://localhost:3001',
|
|
tls: {
|
|
mode: 'terminate' as const,
|
|
certificate: 'auto'
|
|
}
|
|
}
|
|
}
|
|
],
|
|
acme: {
|
|
email: 'test@test.com',
|
|
port: 80 // Same as user route
|
|
}
|
|
};
|
|
|
|
const proxy = new SmartProxy(settings);
|
|
|
|
// Track port operations
|
|
let port80AddCount = 0;
|
|
const originalPortManager = proxy['portManager'];
|
|
const originalAddPort = originalPortManager.addPort.bind(originalPortManager);
|
|
originalPortManager.addPort = async (port: number) => {
|
|
if (port === 80) {
|
|
port80AddCount++;
|
|
}
|
|
return originalAddPort(port);
|
|
};
|
|
|
|
await proxy.start();
|
|
|
|
// Update routes multiple times
|
|
for (let i = 0; i < 3; i++) {
|
|
await proxy.updateRoutes([
|
|
...settings.routes,
|
|
{
|
|
name: `temp-route-${i}`,
|
|
match: {
|
|
ports: [7000 + i]
|
|
},
|
|
action: {
|
|
type: 'forward' as const,
|
|
targetUrl: `http://localhost:${6000 + i}`
|
|
}
|
|
}
|
|
]);
|
|
}
|
|
|
|
// Port 80 should be maintained properly without conflicts
|
|
tools.expect(port80AddCount).toBeGreaterThan(0);
|
|
|
|
await proxy.stop();
|
|
});
|
|
|
|
tap.test('should handle mutex locking correctly', async (tools) => {
|
|
tools.timeout(10000);
|
|
|
|
const settings = {
|
|
port: 6005,
|
|
routes: [
|
|
{
|
|
name: 'test-route',
|
|
match: {
|
|
ports: [80]
|
|
},
|
|
action: {
|
|
type: 'forward' as const,
|
|
targetUrl: 'http://localhost:3000'
|
|
}
|
|
}
|
|
]
|
|
};
|
|
|
|
const proxy = new SmartProxy(settings);
|
|
await proxy.start();
|
|
|
|
let updateStartCount = 0;
|
|
let updateEndCount = 0;
|
|
|
|
// Wrap updateRoutes to track concurrent execution
|
|
const originalUpdateRoutes = proxy['updateRoutes'].bind(proxy);
|
|
proxy['updateRoutes'] = async (routes: any[]) => {
|
|
updateStartCount++;
|
|
const startCount = updateStartCount;
|
|
const endCount = updateEndCount;
|
|
|
|
// If mutex is working, start count should never be more than end count + 1
|
|
tools.expect(startCount).toBeLessThanOrEqual(endCount + 1);
|
|
|
|
const result = await originalUpdateRoutes(routes);
|
|
updateEndCount++;
|
|
return result;
|
|
};
|
|
|
|
// Trigger multiple concurrent updates
|
|
const updates = [];
|
|
for (let i = 0; i < 5; i++) {
|
|
updates.push(proxy.updateRoutes([
|
|
...settings.routes,
|
|
{
|
|
name: `concurrent-route-${i}`,
|
|
match: {
|
|
ports: [2000 + i]
|
|
},
|
|
action: {
|
|
type: 'forward' as const,
|
|
targetUrl: `http://localhost:${3000 + i}`
|
|
}
|
|
}
|
|
]));
|
|
}
|
|
|
|
await Promise.all(updates);
|
|
|
|
// All updates should have completed
|
|
tools.expect(updateStartCount).toEqual(5);
|
|
tools.expect(updateEndCount).toEqual(5);
|
|
|
|
await proxy.stop();
|
|
});
|
|
|
|
export default tap; |