Files
smartproxy/test/test.route-config.ts

773 lines
24 KiB
TypeScript

/**
* Tests for the unified route-based configuration system
*/
import { expect, tap } from '@git.zone/tstest/tapbundle';
// Import from core modules
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
// Import route utilities
import {
findMatchingRoutes,
findBestMatchingRoute,
routeMatchesDomain,
routeMatchesPort,
routeMatchesPath,
routeMatchesHeaders,
mergeRouteConfigs,
generateRouteId,
cloneRoute
} from '../ts/proxies/smart-proxy/utils/route-utils.js';
import {
validateRouteConfig,
validateRoutes,
isValidDomain,
isValidPort,
hasRequiredPropertiesForAction,
assertValidRoute
} from '../ts/proxies/smart-proxy/utils/route-validator.js';
import { SocketHandlers } from '../ts/proxies/smart-proxy/utils/socket-handlers.js';
// Import test helpers
import { loadTestCertificates } from './helpers/certificates.js';
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
// --------------------------------- Route Creation Tests ---------------------------------
tap.test('Routes: Should create basic HTTP route', async () => {
const httpRoute: IRouteConfig = {
match: { ports: 80, domains: 'example.com' },
action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] },
name: 'Basic HTTP Route'
};
expect(httpRoute.match.ports).toEqual(80);
expect(httpRoute.match.domains).toEqual('example.com');
expect(httpRoute.action.type).toEqual('forward');
expect(httpRoute.action.targets?.[0]?.host).toEqual('localhost');
expect(httpRoute.action.targets?.[0]?.port).toEqual(3000);
expect(httpRoute.name).toEqual('Basic HTTP Route');
});
tap.test('Routes: Should create HTTPS route with TLS termination', async () => {
const httpsRoute: IRouteConfig = {
match: { ports: 443, domains: 'secure.example.com' },
action: {
type: 'forward',
targets: [{ host: 'localhost', port: 8080 }],
tls: { mode: 'terminate', certificate: 'auto' }
},
name: 'HTTPS Route'
};
expect(httpsRoute.match.ports).toEqual(443);
expect(httpsRoute.match.domains).toEqual('secure.example.com');
expect(httpsRoute.action.type).toEqual('forward');
expect(httpsRoute.action.tls?.mode).toEqual('terminate');
expect(httpsRoute.action.tls?.certificate).toEqual('auto');
expect(httpsRoute.action.targets?.[0]?.host).toEqual('localhost');
expect(httpsRoute.action.targets?.[0]?.port).toEqual(8080);
expect(httpsRoute.name).toEqual('HTTPS Route');
});
tap.test('Routes: Should create HTTP to HTTPS redirect', async () => {
const redirectRoute: IRouteConfig = {
match: { ports: 80, domains: 'example.com' },
action: {
type: 'socket-handler',
socketHandler: SocketHandlers.httpRedirect('https://{domain}:443{path}', 301)
},
name: 'HTTP to HTTPS Redirect for example.com'
};
expect(redirectRoute.match.ports).toEqual(80);
expect(redirectRoute.match.domains).toEqual('example.com');
expect(redirectRoute.action.type).toEqual('socket-handler');
expect(redirectRoute.action.socketHandler).toBeDefined();
});
tap.test('Routes: Should create complete HTTPS server with redirects', async () => {
const routes: IRouteConfig[] = [
{
match: { ports: 443, domains: 'example.com' },
action: {
type: 'forward',
targets: [{ host: 'localhost', port: 8080 }],
tls: { mode: 'terminate', certificate: 'auto' }
},
name: 'HTTPS Terminate Route for example.com'
},
{
match: { ports: 80, domains: 'example.com' },
action: {
type: 'socket-handler',
socketHandler: SocketHandlers.httpRedirect('https://{domain}:443{path}', 301)
},
name: 'HTTP to HTTPS Redirect for example.com'
}
];
expect(routes.length).toEqual(2);
const httpsRoute = routes[0];
expect(httpsRoute.match.ports).toEqual(443);
expect(httpsRoute.match.domains).toEqual('example.com');
expect(httpsRoute.action.type).toEqual('forward');
expect(httpsRoute.action.tls?.mode).toEqual('terminate');
const redirectRoute = routes[1];
expect(redirectRoute.match.ports).toEqual(80);
expect(redirectRoute.action.type).toEqual('socket-handler');
expect(redirectRoute.action.socketHandler).toBeDefined();
});
tap.test('Routes: Should create load balancer route', async () => {
const lbRoute: IRouteConfig = {
match: { ports: 443, domains: 'app.example.com' },
action: {
type: 'forward',
targets: [{ host: ['10.0.0.1', '10.0.0.2', '10.0.0.3'], port: 8080 }],
tls: { mode: 'terminate', certificate: 'auto' },
loadBalancing: { algorithm: 'round-robin' }
},
name: 'Load Balanced Route'
};
expect(lbRoute.match.domains).toEqual('app.example.com');
expect(lbRoute.action.type).toEqual('forward');
expect(Array.isArray(lbRoute.action.targets?.[0]?.host)).toBeTrue();
expect((lbRoute.action.targets?.[0]?.host as string[]).length).toEqual(3);
expect((lbRoute.action.targets?.[0]?.host as string[])[0]).toEqual('10.0.0.1');
expect(lbRoute.action.targets?.[0]?.port).toEqual(8080);
expect(lbRoute.action.tls?.mode).toEqual('terminate');
});
tap.test('Routes: Should create API route with CORS', async () => {
const apiRoute: IRouteConfig = {
match: { ports: 443, domains: 'api.example.com', path: '/v1/*' },
action: {
type: 'forward',
targets: [{ host: 'localhost', port: 3000 }],
tls: { mode: 'terminate', certificate: 'auto' }
},
headers: {
response: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400'
}
},
priority: 100,
name: 'API Route'
};
expect(apiRoute.match.domains).toEqual('api.example.com');
expect(apiRoute.match.path).toEqual('/v1/*');
expect(apiRoute.action.type).toEqual('forward');
expect(apiRoute.action.tls?.mode).toEqual('terminate');
expect(apiRoute.action.targets?.[0]?.host).toEqual('localhost');
expect(apiRoute.action.targets?.[0]?.port).toEqual(3000);
expect(apiRoute.headers).toBeDefined();
if (apiRoute.headers?.response) {
expect(apiRoute.headers.response['Access-Control-Allow-Origin']).toEqual('*');
expect(apiRoute.headers.response['Access-Control-Allow-Methods']).toInclude('GET');
}
});
tap.test('Routes: Should create WebSocket route', async () => {
const wsRoute: IRouteConfig = {
match: { ports: 443, domains: 'ws.example.com', path: '/socket' },
action: {
type: 'forward',
targets: [{ host: 'localhost', port: 5000 }],
tls: { mode: 'terminate', certificate: 'auto' },
websocket: { enabled: true, pingInterval: 15000 }
},
priority: 100,
name: 'WebSocket Route'
};
expect(wsRoute.match.domains).toEqual('ws.example.com');
expect(wsRoute.match.path).toEqual('/socket');
expect(wsRoute.action.type).toEqual('forward');
expect(wsRoute.action.tls?.mode).toEqual('terminate');
expect(wsRoute.action.targets?.[0]?.host).toEqual('localhost');
expect(wsRoute.action.targets?.[0]?.port).toEqual(5000);
expect(wsRoute.action.websocket).toBeDefined();
if (wsRoute.action.websocket) {
expect(wsRoute.action.websocket.enabled).toBeTrue();
expect(wsRoute.action.websocket.pingInterval).toEqual(15000);
}
});
// Static file serving has been removed - should be handled by external servers
tap.test('SmartProxy: Should create instance with route-based config', async () => {
const certs = loadTestCertificates();
const proxy = new SmartProxy({
routes: [
{
match: { ports: 80, domains: 'example.com' },
action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] },
name: 'HTTP Route'
},
{
match: { ports: 443, domains: 'secure.example.com' },
action: {
type: 'forward',
targets: [{ host: 'localhost', port: 8443 }],
tls: {
mode: 'terminate',
certificate: { key: certs.privateKey, cert: certs.publicKey }
}
},
name: 'HTTPS Route'
}
],
defaults: {
target: {
host: 'localhost',
port: 8080
},
security: {
ipAllowList: ['127.0.0.1', '192.168.0.*'],
maxConnections: 100
}
},
initialDataTimeout: 10000,
inactivityTimeout: 300000,
enableDetailedLogging: true
});
expect(typeof proxy).toEqual('object');
expect(typeof proxy.start).toEqual('function');
expect(typeof proxy.stop).toEqual('function');
});
// --------------------------------- Edge Case Tests ---------------------------------
tap.test('Edge Case - Empty Routes Array', async () => {
const emptyRoutes: IRouteConfig[] = [];
const matches = findMatchingRoutes(emptyRoutes, { domain: 'example.com', port: 80 });
expect(matches).toBeInstanceOf(Array);
expect(matches.length).toEqual(0);
const bestMatch = findBestMatchingRoute(emptyRoutes, { domain: 'example.com', port: 80 });
expect(bestMatch).toBeUndefined();
});
tap.test('Edge Case - Multiple Matching Routes with Same Priority', async () => {
const route1: IRouteConfig = {
match: { ports: 80, domains: 'example.com' },
action: { type: 'forward', targets: [{ host: 'server1', port: 3000 }] },
name: 'HTTP Route for example.com'
};
const route2: IRouteConfig = {
match: { ports: 80, domains: 'example.com' },
action: { type: 'forward', targets: [{ host: 'server2', port: 3000 }] },
name: 'HTTP Route for example.com'
};
const route3: IRouteConfig = {
match: { ports: 80, domains: 'example.com' },
action: { type: 'forward', targets: [{ host: 'server3', port: 3000 }] },
name: 'HTTP Route for example.com'
};
route1.priority = 100;
route2.priority = 100;
route3.priority = 100;
const routes = [route1, route2, route3];
const matches = findMatchingRoutes(routes, { domain: 'example.com', port: 80 });
expect(matches.length).toEqual(3);
const bestMatch = findBestMatchingRoute(routes, { domain: 'example.com', port: 80 });
expect(bestMatch).not.toBeUndefined();
});
tap.test('Edge Case - Wildcard Domains and Path Matching', async () => {
const wildcardApiRoute: IRouteConfig = {
match: { ports: 443, domains: '*.example.com', path: '/api/*' },
action: {
type: 'forward',
targets: [{ host: 'api-server', port: 3000 }],
tls: { mode: 'terminate', certificate: 'auto' }
},
priority: 100,
name: 'API Route for *.example.com'
};
const exactApiRoute: IRouteConfig = {
match: { ports: 443, domains: 'api.example.com', path: '/api/*' },
action: {
type: 'forward',
targets: [{ host: 'specific-api-server', port: 3001 }],
tls: { mode: 'terminate', certificate: 'auto' }
},
priority: 200,
name: 'API Route for api.example.com'
};
const routes = [wildcardApiRoute, exactApiRoute];
const matches = findMatchingRoutes(routes, { domain: 'api.example.com', path: '/api/users', port: 443 });
expect(matches.length).toEqual(2);
const bestMatch = findBestMatchingRoute(routes, { domain: 'api.example.com', path: '/api/users', port: 443 });
expect(bestMatch).not.toBeUndefined();
if (bestMatch) {
expect(bestMatch.action.targets[0].port).toEqual(3001);
}
const otherMatches = findMatchingRoutes(routes, { domain: 'other.example.com', path: '/api/products', port: 443 });
expect(otherMatches.length).toEqual(1);
expect(otherMatches[0].action.targets[0].port).toEqual(3000);
});
tap.test('Edge Case - Disabled Routes', async () => {
const enabledRoute: IRouteConfig = {
match: { ports: 80, domains: 'example.com' },
action: { type: 'forward', targets: [{ host: 'server1', port: 3000 }] },
name: 'HTTP Route for example.com'
};
const disabledRoute: IRouteConfig = {
match: { ports: 80, domains: 'example.com' },
action: { type: 'forward', targets: [{ host: 'server2', port: 3001 }] },
name: 'HTTP Route for example.com'
};
disabledRoute.enabled = false;
const routes = [enabledRoute, disabledRoute];
const matches = findMatchingRoutes(routes, { domain: 'example.com', port: 80 });
expect(matches.length).toEqual(1);
expect(matches[0].action.targets[0].port).toEqual(3000);
});
tap.test('Edge Case - Complex Path and Headers Matching', async () => {
const complexRoute: IRouteConfig = {
match: {
domains: 'api.example.com',
ports: 443,
path: '/api/v2/*',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'valid-key'
}
},
action: {
type: 'forward',
targets: [{
host: 'internal-api',
port: 8080
}],
tls: {
mode: 'terminate',
certificate: 'auto'
}
},
name: 'Complex API Route'
};
const matchingPath = routeMatchesPath(complexRoute, '/api/v2/users');
expect(matchingPath).toBeTrue();
const matchingHeaders = routeMatchesHeaders(complexRoute, {
'Content-Type': 'application/json',
'X-API-Key': 'valid-key',
'Accept': 'application/json'
});
expect(matchingHeaders).toBeTrue();
const nonMatchingPath = routeMatchesPath(complexRoute, '/api/v1/users');
expect(nonMatchingPath).toBeFalse();
const nonMatchingHeaders = routeMatchesHeaders(complexRoute, {
'Content-Type': 'application/json',
'X-API-Key': 'invalid-key'
});
expect(nonMatchingHeaders).toBeFalse();
});
tap.test('Edge Case - Port Range Matching', async () => {
const portRangeRoute: IRouteConfig = {
match: {
domains: 'example.com',
ports: [{ from: 8000, to: 9000 }]
},
action: {
type: 'forward',
targets: [{
host: 'backend',
port: 3000
}]
},
name: 'Port Range Route'
};
expect(routeMatchesPort(portRangeRoute, 8000)).toBeTrue();
expect(routeMatchesPort(portRangeRoute, 8500)).toBeTrue();
expect(routeMatchesPort(portRangeRoute, 9000)).toBeTrue();
expect(routeMatchesPort(portRangeRoute, 7999)).toBeFalse();
expect(routeMatchesPort(portRangeRoute, 9001)).toBeFalse();
const multiRangeRoute: IRouteConfig = {
match: {
domains: 'example.com',
ports: [
{ from: 80, to: 90 },
{ from: 8000, to: 9000 }
]
},
action: {
type: 'forward',
targets: [{
host: 'backend',
port: 3000
}]
},
name: 'Multi Range Route'
};
expect(routeMatchesPort(multiRangeRoute, 85)).toBeTrue();
expect(routeMatchesPort(multiRangeRoute, 8500)).toBeTrue();
expect(routeMatchesPort(multiRangeRoute, 100)).toBeFalse();
});
// --------------------------------- Wildcard Domain Tests ---------------------------------
tap.test('Wildcard Domain Handling', async () => {
const simpleDomainRoute: IRouteConfig = {
match: { ports: 80, domains: 'example.com' },
action: { type: 'forward', targets: [{ host: 'server1', port: 3000 }] },
name: 'HTTP Route for example.com'
};
const wildcardSubdomainRoute: IRouteConfig = {
match: { ports: 80, domains: '*.example.com' },
action: { type: 'forward', targets: [{ host: 'server2', port: 3001 }] },
name: 'HTTP Route for *.example.com'
};
const specificSubdomainRoute: IRouteConfig = {
match: { ports: 80, domains: 'api.example.com' },
action: { type: 'forward', targets: [{ host: 'server3', port: 3002 }] },
name: 'HTTP Route for api.example.com'
};
specificSubdomainRoute.priority = 200;
wildcardSubdomainRoute.priority = 100;
simpleDomainRoute.priority = 50;
const routes = [simpleDomainRoute, wildcardSubdomainRoute, specificSubdomainRoute];
expect(routeMatchesDomain(simpleDomainRoute, 'example.com')).toBeTrue();
expect(routeMatchesDomain(simpleDomainRoute, 'sub.example.com')).toBeFalse();
expect(routeMatchesDomain(wildcardSubdomainRoute, 'any.example.com')).toBeTrue();
expect(routeMatchesDomain(wildcardSubdomainRoute, 'nested.sub.example.com')).toBeTrue();
expect(routeMatchesDomain(wildcardSubdomainRoute, 'example.com')).toBeFalse();
expect(routeMatchesDomain(specificSubdomainRoute, 'api.example.com')).toBeTrue();
expect(routeMatchesDomain(specificSubdomainRoute, 'other.example.com')).toBeFalse();
expect(routeMatchesDomain(specificSubdomainRoute, 'sub.api.example.com')).toBeFalse();
const specificSubdomainRequest = { domain: 'api.example.com', port: 80 };
const bestSpecificMatch = findBestMatchingRoute(routes, specificSubdomainRequest);
expect(bestSpecificMatch).not.toBeUndefined();
if (bestSpecificMatch) {
const matchedPort = bestSpecificMatch.action.targets[0].port;
console.log(`Matched route with port: ${matchedPort}`);
expect(bestSpecificMatch.priority).toEqual(200);
}
const otherSubdomainRequest = { domain: 'other.example.com', port: 80 };
const bestWildcardMatch = findBestMatchingRoute(routes, otherSubdomainRequest);
expect(bestWildcardMatch).not.toBeUndefined();
if (bestWildcardMatch) {
const matchedPort = bestWildcardMatch.action.targets[0].port;
console.log(`Matched route with port: ${matchedPort}`);
expect(bestWildcardMatch.priority).toEqual(100);
}
});
// --------------------------------- Integration Tests ---------------------------------
tap.test('Route Integration - Combining Multiple Route Types', async () => {
const routes: IRouteConfig[] = [
{
match: { ports: 443, domains: 'example.com' },
action: {
type: 'forward',
targets: [{ host: 'web-server', port: 8080 }],
tls: { mode: 'terminate', certificate: 'auto' }
},
name: 'HTTPS Terminate Route for example.com'
},
{
match: { ports: 80, domains: 'example.com' },
action: {
type: 'socket-handler',
socketHandler: SocketHandlers.httpRedirect('https://{domain}:443{path}', 301)
},
name: 'HTTP to HTTPS Redirect for example.com'
},
{
match: { ports: 443, domains: 'api.example.com', path: '/v1/*' },
action: {
type: 'forward',
targets: [{ host: 'api-server', port: 3000 }],
tls: { mode: 'terminate', certificate: 'auto' }
},
headers: {
response: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400'
}
},
priority: 100,
name: 'API Route for api.example.com'
},
{
match: { ports: 443, domains: 'ws.example.com', path: '/live' },
action: {
type: 'forward',
targets: [{ host: 'websocket-server', port: 5000 }],
tls: { mode: 'terminate', certificate: 'auto' },
websocket: { enabled: true }
},
priority: 100,
name: 'WebSocket Route for ws.example.com'
},
{
match: { ports: 443, domains: 'legacy.example.com' },
action: {
type: 'forward',
targets: [{ host: 'legacy-server', port: 443 }],
tls: { mode: 'passthrough' }
},
name: 'HTTPS Passthrough Route for legacy.example.com'
}
];
const validationResult = validateRoutes(routes);
expect(validationResult.valid).toBeTrue();
expect(validationResult.errors.length).toEqual(0);
const webServerMatch = findBestMatchingRoute(routes, { domain: 'example.com', port: 443 });
expect(webServerMatch).not.toBeUndefined();
if (webServerMatch) {
expect(webServerMatch.action.type).toEqual('forward');
expect(webServerMatch.action.targets[0].host).toEqual('web-server');
}
const webRedirectMatch = findBestMatchingRoute(routes, { domain: 'example.com', port: 80 });
expect(webRedirectMatch).not.toBeUndefined();
if (webRedirectMatch) {
expect(webRedirectMatch.action.type).toEqual('socket-handler');
}
const apiMatch = findBestMatchingRoute(routes, {
domain: 'api.example.com',
port: 443,
path: '/v1/users'
});
expect(apiMatch).not.toBeUndefined();
if (apiMatch) {
expect(apiMatch.action.type).toEqual('forward');
expect(apiMatch.action.targets[0].host).toEqual('api-server');
}
const wsMatch = findBestMatchingRoute(routes, {
domain: 'ws.example.com',
port: 443,
path: '/live'
});
expect(wsMatch).not.toBeUndefined();
if (wsMatch) {
expect(wsMatch.action.type).toEqual('forward');
expect(wsMatch.action.targets[0].host).toEqual('websocket-server');
expect(wsMatch.action.websocket?.enabled).toBeTrue();
}
const legacyMatch = findBestMatchingRoute(routes, {
domain: 'legacy.example.com',
port: 443
});
expect(legacyMatch).not.toBeUndefined();
if (legacyMatch) {
expect(legacyMatch.action.type).toEqual('forward');
expect(legacyMatch.action.tls?.mode).toEqual('passthrough');
}
});
// --------------------------------- Protocol Match Field Tests ---------------------------------
tap.test('Routes: Should accept protocol field on route match', async () => {
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',
};
const validation = validateRouteConfig(httpOnlyRoute);
expect(validation.valid).toBeTrue();
expect(httpOnlyRoute.match.protocol).toEqual('http');
});
tap.test('Routes: Should accept protocol tcp on route match', async () => {
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 () => {
const reencryptRoute: IRouteConfig = {
match: { ports: 443, domains: 'secure.example.com' },
action: {
type: 'forward',
targets: [{ host: 'backend', port: 443 }],
tls: { mode: 'terminate-and-reencrypt', certificate: 'auto' }
},
name: 'Reencrypt HTTP Route'
};
reencryptRoute.match.protocol = 'http';
const validation = validateRouteConfig(reencryptRoute);
expect(validation.valid).toBeTrue();
expect(reencryptRoute.action.tls?.mode).toEqual('terminate-and-reencrypt');
expect(reencryptRoute.match.protocol).toEqual('http');
});
tap.test('Routes: Protocol field should not affect domain/port matching', async () => {
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];
const matches = findMatchingRoutes(routes, { domain: 'example.com', port: 443 });
expect(matches.length).toEqual(2);
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);
expect(cloned.match.protocol).toEqual('http');
expect(cloned.action.tls?.mode).toEqual('terminate-and-reencrypt');
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',
};
const merged = mergeRouteConfigs(base, { name: 'Merged Route' });
expect(merged.match.protocol).toEqual('http');
expect(merged.name).toEqual('Merged Route');
});
export default tap.start();