197 lines
4.8 KiB
TypeScript
197 lines
4.8 KiB
TypeScript
import { expect, tap } from '@push.rocks/tapbundle';
|
|
import { SmartProxy } from '../ts/index.js';
|
|
|
|
/**
|
|
* Test that verifies mutex prevents race conditions during concurrent route updates
|
|
*/
|
|
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).toEqual(2); // Initial route + last update
|
|
|
|
await proxy.stop();
|
|
});
|
|
|
|
/**
|
|
* Test that verifies mutex serializes route updates
|
|
*/
|
|
tap.test('should serialize route updates with mutex', async (tools) => {
|
|
tools.timeout(10000);
|
|
|
|
const settings = {
|
|
port: 6002,
|
|
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;
|
|
let maxConcurrent = 0;
|
|
|
|
// Wrap updateRoutes to track concurrent execution
|
|
const originalUpdateRoutes = proxy['updateRoutes'].bind(proxy);
|
|
proxy['updateRoutes'] = async (routes: any[]) => {
|
|
updateStartCount++;
|
|
const concurrent = updateStartCount - updateEndCount;
|
|
maxConcurrent = Math.max(maxConcurrent, concurrent);
|
|
|
|
// If mutex is working, only one update should run at a time
|
|
tools.expect(concurrent).toEqual(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);
|
|
tools.expect(maxConcurrent).toEqual(1); // Mutex ensures only one at a time
|
|
|
|
await proxy.stop();
|
|
});
|
|
|
|
/**
|
|
* Test that challenge route state is preserved across certificate manager recreations
|
|
*/
|
|
tap.test('should preserve challenge route state during cert manager recreation', async (tools) => {
|
|
tools.timeout(10000);
|
|
|
|
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);
|
|
|
|
// Track certificate manager recreations
|
|
let certManagerCreationCount = 0;
|
|
const originalCreateCertManager = proxy['createCertificateManager'].bind(proxy);
|
|
proxy['createCertificateManager'] = async (...args: any[]) => {
|
|
certManagerCreationCount++;
|
|
return originalCreateCertManager(...args);
|
|
};
|
|
|
|
await proxy.start();
|
|
|
|
// Initial creation
|
|
tools.expect(certManagerCreationCount).toEqual(1);
|
|
|
|
// 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}`
|
|
}
|
|
}
|
|
]);
|
|
}
|
|
|
|
// Certificate manager should be recreated for each update
|
|
tools.expect(certManagerCreationCount).toEqual(4); // 1 initial + 3 updates
|
|
|
|
// State should be preserved (challenge route active)
|
|
const globalState = proxy['globalChallengeRouteActive'];
|
|
tools.expect(globalState).toBeDefined();
|
|
|
|
await proxy.stop();
|
|
});
|
|
|
|
export default tap; |