feat(routes): add protocol-based route matching and ensure terminate-and-reencrypt routes HTTP through the full HTTP proxy; update docs and tests

This commit is contained in:
2026-02-16 12:11:49 +00:00
parent 754d32fd34
commit 2e3cf515a4
5 changed files with 384 additions and 11 deletions

View File

@@ -562,4 +562,168 @@ tap.test('Route Integration - Combining Multiple Route Types', async () => {
}
});
// --------------------------------- Protocol Match Field Tests ---------------------------------
tap.test('Routes: Should accept protocol field on route match', async () => {
// Create a route with protocol: 'http'
const httpOnlyRoute: IRouteConfig = {
match: {
ports: 443,
domains: 'api.example.com',
protocol: 'http',
},
action: {
type: 'forward',
targets: [{ host: 'backend', port: 8080 }],
tls: {
mode: 'terminate',
certificate: 'auto',
},
},
name: 'HTTP-only Route',
};
// Validate the route - protocol field should not cause errors
const validation = validateRouteConfig(httpOnlyRoute);
expect(validation.valid).toBeTrue();
// Verify the protocol field is preserved
expect(httpOnlyRoute.match.protocol).toEqual('http');
});
tap.test('Routes: Should accept protocol tcp on route match', async () => {
// Create a route with protocol: 'tcp'
const tcpOnlyRoute: IRouteConfig = {
match: {
ports: 443,
domains: 'db.example.com',
protocol: 'tcp',
},
action: {
type: 'forward',
targets: [{ host: 'db-server', port: 5432 }],
tls: {
mode: 'passthrough',
},
},
name: 'TCP-only Route',
};
const validation = validateRouteConfig(tcpOnlyRoute);
expect(validation.valid).toBeTrue();
expect(tcpOnlyRoute.match.protocol).toEqual('tcp');
});
tap.test('Routes: Protocol field should work with terminate-and-reencrypt', async () => {
// Create a terminate-and-reencrypt route that only accepts HTTP
const reencryptRoute = createHttpsTerminateRoute(
'secure.example.com',
{ host: 'backend', port: 443 },
{ reencrypt: true, certificate: 'auto', name: 'Reencrypt HTTP Route' }
);
// Set protocol restriction to http
reencryptRoute.match.protocol = 'http';
// Validate the route
const validation = validateRouteConfig(reencryptRoute);
expect(validation.valid).toBeTrue();
// Verify TLS mode
expect(reencryptRoute.action.tls?.mode).toEqual('terminate-and-reencrypt');
// Verify protocol field is preserved
expect(reencryptRoute.match.protocol).toEqual('http');
});
tap.test('Routes: Protocol field should not affect domain/port matching', async () => {
// Routes with and without protocol field should both match the same domain/port
const routeWithProtocol: IRouteConfig = {
match: {
ports: 443,
domains: 'example.com',
protocol: 'http',
},
action: {
type: 'forward',
targets: [{ host: 'backend', port: 8080 }],
tls: { mode: 'terminate', certificate: 'auto' },
},
name: 'With Protocol',
priority: 10,
};
const routeWithoutProtocol: IRouteConfig = {
match: {
ports: 443,
domains: 'example.com',
},
action: {
type: 'forward',
targets: [{ host: 'fallback', port: 8081 }],
tls: { mode: 'terminate', certificate: 'auto' },
},
name: 'Without Protocol',
priority: 5,
};
const routes = [routeWithProtocol, routeWithoutProtocol];
// Both routes should match the domain/port (protocol is a hint for Rust-side matching)
const matches = findMatchingRoutes(routes, { domain: 'example.com', port: 443 });
expect(matches.length).toEqual(2);
// The one with higher priority should be first
const best = findBestMatchingRoute(routes, { domain: 'example.com', port: 443 });
expect(best).not.toBeUndefined();
expect(best!.name).toEqual('With Protocol');
});
tap.test('Routes: Protocol field preserved through route cloning', async () => {
const original: IRouteConfig = {
match: {
ports: 8443,
domains: 'clone-test.example.com',
protocol: 'http',
},
action: {
type: 'forward',
targets: [{ host: 'backend', port: 3000 }],
tls: { mode: 'terminate-and-reencrypt', certificate: 'auto' },
},
name: 'Clone Test',
};
const cloned = cloneRoute(original);
// Verify protocol is preserved in clone
expect(cloned.match.protocol).toEqual('http');
expect(cloned.action.tls?.mode).toEqual('terminate-and-reencrypt');
// Modify clone should not affect original
cloned.match.protocol = 'tcp';
expect(original.match.protocol).toEqual('http');
});
tap.test('Routes: Protocol field preserved through route merging', async () => {
const base: IRouteConfig = {
match: {
ports: 443,
domains: 'merge-test.example.com',
protocol: 'http',
},
action: {
type: 'forward',
targets: [{ host: 'backend', port: 3000 }],
tls: { mode: 'terminate-and-reencrypt', certificate: 'auto' },
},
name: 'Merge Base',
};
// Merge with override that changes name but not protocol
const merged = mergeRouteConfigs(base, { name: 'Merged Route' });
expect(merged.match.protocol).toEqual('http');
expect(merged.name).toEqual('Merged Route');
});
export default tap.start();