smartproxy/test/test.race-conditions.node.ts

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;