BREAKING CHANGE(smart-proxy): remove route helper APIs and standardize route configuration on plain route objects
This commit is contained in:
@@ -6,7 +6,7 @@ import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
// Import from core modules
|
||||
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
||||
|
||||
// Import route utilities and helpers
|
||||
// Import route utilities
|
||||
import {
|
||||
findMatchingRoutes,
|
||||
findBestMatchingRoute,
|
||||
@@ -28,16 +28,7 @@ import {
|
||||
assertValidRoute
|
||||
} from '../ts/proxies/smart-proxy/utils/route-validator.js';
|
||||
|
||||
import {
|
||||
createHttpRoute,
|
||||
createHttpsTerminateRoute,
|
||||
createHttpsPassthroughRoute,
|
||||
createHttpToHttpsRedirect,
|
||||
createCompleteHttpsServer,
|
||||
createLoadBalancerRoute,
|
||||
createApiRoute,
|
||||
createWebSocketRoute
|
||||
} from '../ts/proxies/smart-proxy/utils/route-helpers.js';
|
||||
import { SocketHandlers } from '../ts/proxies/smart-proxy/utils/socket-handlers.js';
|
||||
|
||||
// Import test helpers
|
||||
import { loadTestCertificates } from './helpers/certificates.js';
|
||||
@@ -47,12 +38,12 @@ import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.
|
||||
// --------------------------------- Route Creation Tests ---------------------------------
|
||||
|
||||
tap.test('Routes: Should create basic HTTP route', async () => {
|
||||
// Create a simple HTTP route
|
||||
const httpRoute = createHttpRoute('example.com', { host: 'localhost', port: 3000 }, {
|
||||
const httpRoute: IRouteConfig = {
|
||||
match: { ports: 80, domains: 'example.com' },
|
||||
action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] },
|
||||
name: 'Basic HTTP Route'
|
||||
});
|
||||
};
|
||||
|
||||
// Validate the route configuration
|
||||
expect(httpRoute.match.ports).toEqual(80);
|
||||
expect(httpRoute.match.domains).toEqual('example.com');
|
||||
expect(httpRoute.action.type).toEqual('forward');
|
||||
@@ -62,14 +53,17 @@ tap.test('Routes: Should create basic HTTP route', async () => {
|
||||
});
|
||||
|
||||
tap.test('Routes: Should create HTTPS route with TLS termination', async () => {
|
||||
// Create an HTTPS route with TLS termination
|
||||
const httpsRoute = createHttpsTerminateRoute('secure.example.com', { host: 'localhost', port: 8080 }, {
|
||||
certificate: 'auto',
|
||||
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'
|
||||
});
|
||||
};
|
||||
|
||||
// Validate the route configuration
|
||||
expect(httpsRoute.match.ports).toEqual(443); // Default HTTPS port
|
||||
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');
|
||||
@@ -80,10 +74,15 @@ tap.test('Routes: Should create HTTPS route with TLS termination', async () => {
|
||||
});
|
||||
|
||||
tap.test('Routes: Should create HTTP to HTTPS redirect', async () => {
|
||||
// Create an HTTP to HTTPS redirect
|
||||
const redirectRoute = createHttpToHttpsRedirect('example.com', 443);
|
||||
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'
|
||||
};
|
||||
|
||||
// Validate the route configuration
|
||||
expect(redirectRoute.match.ports).toEqual(80);
|
||||
expect(redirectRoute.match.domains).toEqual('example.com');
|
||||
expect(redirectRoute.action.type).toEqual('socket-handler');
|
||||
@@ -91,22 +90,34 @@ tap.test('Routes: Should create HTTP to HTTPS redirect', async () => {
|
||||
});
|
||||
|
||||
tap.test('Routes: Should create complete HTTPS server with redirects', async () => {
|
||||
// Create a complete HTTPS server setup
|
||||
const routes = createCompleteHttpsServer('example.com', { host: 'localhost', port: 8080 }, {
|
||||
certificate: 'auto'
|
||||
});
|
||||
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'
|
||||
}
|
||||
];
|
||||
|
||||
// Validate that we got two routes (HTTPS route and HTTP redirect)
|
||||
expect(routes.length).toEqual(2);
|
||||
|
||||
// Validate HTTPS route
|
||||
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');
|
||||
|
||||
// Validate HTTP redirect route
|
||||
const redirectRoute = routes[1];
|
||||
expect(redirectRoute.match.ports).toEqual(80);
|
||||
expect(redirectRoute.action.type).toEqual('socket-handler');
|
||||
@@ -114,21 +125,17 @@ tap.test('Routes: Should create complete HTTPS server with redirects', async ()
|
||||
});
|
||||
|
||||
tap.test('Routes: Should create load balancer route', async () => {
|
||||
// Create a load balancer route
|
||||
const lbRoute = createLoadBalancerRoute(
|
||||
'app.example.com',
|
||||
['10.0.0.1', '10.0.0.2', '10.0.0.3'],
|
||||
8080,
|
||||
{
|
||||
tls: {
|
||||
mode: 'terminate',
|
||||
certificate: 'auto'
|
||||
},
|
||||
name: 'Load Balanced Route'
|
||||
}
|
||||
);
|
||||
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'
|
||||
};
|
||||
|
||||
// Validate the route configuration
|
||||
expect(lbRoute.match.domains).toEqual('app.example.com');
|
||||
expect(lbRoute.action.type).toEqual('forward');
|
||||
expect(Array.isArray(lbRoute.action.targets?.[0]?.host)).toBeTrue();
|
||||
@@ -139,23 +146,32 @@ tap.test('Routes: Should create load balancer route', async () => {
|
||||
});
|
||||
|
||||
tap.test('Routes: Should create API route with CORS', async () => {
|
||||
// Create an API route with CORS headers
|
||||
const apiRoute = createApiRoute('api.example.com', '/v1', { host: 'localhost', port: 3000 }, {
|
||||
useTls: true,
|
||||
certificate: 'auto',
|
||||
addCorsHeaders: true,
|
||||
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'
|
||||
});
|
||||
};
|
||||
|
||||
// Validate the route configuration
|
||||
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);
|
||||
|
||||
// Check CORS headers
|
||||
|
||||
expect(apiRoute.headers).toBeDefined();
|
||||
if (apiRoute.headers?.response) {
|
||||
expect(apiRoute.headers.response['Access-Control-Allow-Origin']).toEqual('*');
|
||||
@@ -164,23 +180,25 @@ tap.test('Routes: Should create API route with CORS', async () => {
|
||||
});
|
||||
|
||||
tap.test('Routes: Should create WebSocket route', async () => {
|
||||
// Create a WebSocket route
|
||||
const wsRoute = createWebSocketRoute('ws.example.com', '/socket', { host: 'localhost', port: 5000 }, {
|
||||
useTls: true,
|
||||
certificate: 'auto',
|
||||
pingInterval: 15000,
|
||||
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'
|
||||
});
|
||||
};
|
||||
|
||||
// Validate the route configuration
|
||||
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);
|
||||
|
||||
// Check WebSocket configuration
|
||||
|
||||
expect(wsRoute.action.websocket).toBeDefined();
|
||||
if (wsRoute.action.websocket) {
|
||||
expect(wsRoute.action.websocket.enabled).toBeTrue();
|
||||
@@ -191,22 +209,27 @@ tap.test('Routes: Should create WebSocket route', async () => {
|
||||
// Static file serving has been removed - should be handled by external servers
|
||||
|
||||
tap.test('SmartProxy: Should create instance with route-based config', async () => {
|
||||
// Create TLS certificates for testing
|
||||
const certs = loadTestCertificates();
|
||||
|
||||
// Create a SmartProxy instance with route-based configuration
|
||||
const proxy = new SmartProxy({
|
||||
routes: [
|
||||
createHttpRoute('example.com', { host: 'localhost', port: 3000 }, {
|
||||
{
|
||||
match: { ports: 80, domains: 'example.com' },
|
||||
action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] },
|
||||
name: 'HTTP Route'
|
||||
}),
|
||||
createHttpsTerminateRoute('secure.example.com', { host: 'localhost', port: 8443 }, {
|
||||
certificate: {
|
||||
key: certs.privateKey,
|
||||
cert: certs.publicKey
|
||||
},
|
||||
{
|
||||
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: {
|
||||
@@ -218,13 +241,11 @@ tap.test('SmartProxy: Should create instance with route-based config', async ()
|
||||
maxConnections: 100
|
||||
}
|
||||
},
|
||||
// Additional settings
|
||||
initialDataTimeout: 10000,
|
||||
inactivityTimeout: 300000,
|
||||
enableDetailedLogging: true
|
||||
});
|
||||
|
||||
// Simply verify the instance was created successfully
|
||||
expect(typeof proxy).toEqual('object');
|
||||
expect(typeof proxy.start).toEqual('function');
|
||||
expect(typeof proxy.stop).toEqual('function');
|
||||
@@ -233,94 +254,109 @@ tap.test('SmartProxy: Should create instance with route-based config', async ()
|
||||
// --------------------------------- Edge Case Tests ---------------------------------
|
||||
|
||||
tap.test('Edge Case - Empty Routes Array', async () => {
|
||||
// Attempting to find routes in an empty array
|
||||
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 () => {
|
||||
// Create multiple routes with identical priority but different targets
|
||||
const route1 = createHttpRoute('example.com', { host: 'server1', port: 3000 });
|
||||
const route2 = createHttpRoute('example.com', { host: 'server2', port: 3000 });
|
||||
const route3 = createHttpRoute('example.com', { host: 'server3', port: 3000 });
|
||||
|
||||
// Set all to the same priority
|
||||
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];
|
||||
|
||||
// Find matching routes
|
||||
|
||||
const matches = findMatchingRoutes(routes, { domain: 'example.com', port: 80 });
|
||||
|
||||
// Should find all three routes
|
||||
|
||||
expect(matches.length).toEqual(3);
|
||||
|
||||
// First match could be any of the routes since they have the same priority
|
||||
// But the implementation should be consistent (likely keep the original order)
|
||||
|
||||
const bestMatch = findBestMatchingRoute(routes, { domain: 'example.com', port: 80 });
|
||||
expect(bestMatch).not.toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('Edge Case - Wildcard Domains and Path Matching', async () => {
|
||||
// Create routes with wildcard domains and path patterns
|
||||
const wildcardApiRoute = createApiRoute('*.example.com', '/api', { host: 'api-server', port: 3000 }, {
|
||||
useTls: true,
|
||||
certificate: 'auto'
|
||||
});
|
||||
|
||||
const exactApiRoute = createApiRoute('api.example.com', '/api', { host: 'specific-api-server', port: 3001 }, {
|
||||
useTls: true,
|
||||
certificate: 'auto',
|
||||
priority: 200 // Higher priority
|
||||
});
|
||||
|
||||
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];
|
||||
|
||||
// Test with a specific subdomain that matches both routes
|
||||
|
||||
const matches = findMatchingRoutes(routes, { domain: 'api.example.com', path: '/api/users', port: 443 });
|
||||
|
||||
// Should match both routes
|
||||
|
||||
expect(matches.length).toEqual(2);
|
||||
|
||||
// The exact domain match should have higher priority
|
||||
|
||||
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); // Should match the exact domain route
|
||||
expect(bestMatch.action.targets[0].port).toEqual(3001);
|
||||
}
|
||||
|
||||
// Test with a different subdomain - should only match the wildcard route
|
||||
|
||||
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); // Should match the wildcard domain route
|
||||
expect(otherMatches[0].action.targets[0].port).toEqual(3000);
|
||||
});
|
||||
|
||||
tap.test('Edge Case - Disabled Routes', async () => {
|
||||
// Create enabled and disabled routes
|
||||
const enabledRoute = createHttpRoute('example.com', { host: 'server1', port: 3000 });
|
||||
const disabledRoute = createHttpRoute('example.com', { host: 'server2', port: 3001 });
|
||||
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];
|
||||
|
||||
// Find matching routes
|
||||
|
||||
const matches = findMatchingRoutes(routes, { domain: 'example.com', port: 80 });
|
||||
|
||||
// Should only find the enabled route
|
||||
|
||||
expect(matches.length).toEqual(1);
|
||||
expect(matches[0].action.targets[0].port).toEqual(3000);
|
||||
});
|
||||
|
||||
tap.test('Edge Case - Complex Path and Headers Matching', async () => {
|
||||
// Create route with complex path and headers matching
|
||||
const complexRoute: IRouteConfig = {
|
||||
match: {
|
||||
domains: 'api.example.com',
|
||||
@@ -344,22 +380,20 @@ tap.test('Edge Case - Complex Path and Headers Matching', async () => {
|
||||
},
|
||||
name: 'Complex API Route'
|
||||
};
|
||||
|
||||
// Test with matching criteria
|
||||
|
||||
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();
|
||||
|
||||
// Test with non-matching criteria
|
||||
|
||||
const nonMatchingPath = routeMatchesPath(complexRoute, '/api/v1/users');
|
||||
expect(nonMatchingPath).toBeFalse();
|
||||
|
||||
|
||||
const nonMatchingHeaders = routeMatchesHeaders(complexRoute, {
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-Key': 'invalid-key'
|
||||
@@ -368,7 +402,6 @@ tap.test('Edge Case - Complex Path and Headers Matching', async () => {
|
||||
});
|
||||
|
||||
tap.test('Edge Case - Port Range Matching', async () => {
|
||||
// Create route with port range matching
|
||||
const portRangeRoute: IRouteConfig = {
|
||||
match: {
|
||||
domains: 'example.com',
|
||||
@@ -383,17 +416,14 @@ tap.test('Edge Case - Port Range Matching', async () => {
|
||||
},
|
||||
name: 'Port Range Route'
|
||||
};
|
||||
|
||||
// Test with ports in the range
|
||||
expect(routeMatchesPort(portRangeRoute, 8000)).toBeTrue(); // Lower bound
|
||||
expect(routeMatchesPort(portRangeRoute, 8500)).toBeTrue(); // Middle
|
||||
expect(routeMatchesPort(portRangeRoute, 9000)).toBeTrue(); // Upper bound
|
||||
|
||||
// Test with ports outside the range
|
||||
expect(routeMatchesPort(portRangeRoute, 7999)).toBeFalse(); // Just below
|
||||
expect(routeMatchesPort(portRangeRoute, 9001)).toBeFalse(); // Just above
|
||||
|
||||
// Test with multiple port ranges
|
||||
|
||||
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',
|
||||
@@ -411,7 +441,7 @@ tap.test('Edge Case - Port Range Matching', async () => {
|
||||
},
|
||||
name: 'Multi Range Route'
|
||||
};
|
||||
|
||||
|
||||
expect(routeMatchesPort(multiRangeRoute, 85)).toBeTrue();
|
||||
expect(routeMatchesPort(multiRangeRoute, 8500)).toBeTrue();
|
||||
expect(routeMatchesPort(multiRangeRoute, 100)).toBeFalse();
|
||||
@@ -420,55 +450,56 @@ tap.test('Edge Case - Port Range Matching', async () => {
|
||||
// --------------------------------- Wildcard Domain Tests ---------------------------------
|
||||
|
||||
tap.test('Wildcard Domain Handling', async () => {
|
||||
// Create routes with different wildcard patterns
|
||||
const simpleDomainRoute = createHttpRoute('example.com', { host: 'server1', port: 3000 });
|
||||
const wildcardSubdomainRoute = createHttpRoute('*.example.com', { host: 'server2', port: 3001 });
|
||||
const specificSubdomainRoute = createHttpRoute('api.example.com', { host: 'server3', port: 3002 });
|
||||
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'
|
||||
};
|
||||
|
||||
// Set explicit priorities to ensure deterministic matching
|
||||
specificSubdomainRoute.priority = 200; // Highest priority for specific domain
|
||||
wildcardSubdomainRoute.priority = 100; // Medium priority for wildcard
|
||||
simpleDomainRoute.priority = 50; // Lowest priority for generic domain
|
||||
specificSubdomainRoute.priority = 200;
|
||||
wildcardSubdomainRoute.priority = 100;
|
||||
simpleDomainRoute.priority = 50;
|
||||
|
||||
const routes = [simpleDomainRoute, wildcardSubdomainRoute, specificSubdomainRoute];
|
||||
|
||||
// Test exact domain match
|
||||
expect(routeMatchesDomain(simpleDomainRoute, 'example.com')).toBeTrue();
|
||||
expect(routeMatchesDomain(simpleDomainRoute, 'sub.example.com')).toBeFalse();
|
||||
|
||||
// Test wildcard subdomain match
|
||||
expect(routeMatchesDomain(wildcardSubdomainRoute, 'any.example.com')).toBeTrue();
|
||||
expect(routeMatchesDomain(wildcardSubdomainRoute, 'nested.sub.example.com')).toBeTrue();
|
||||
expect(routeMatchesDomain(wildcardSubdomainRoute, 'example.com')).toBeFalse();
|
||||
|
||||
// Test specific subdomain match
|
||||
expect(routeMatchesDomain(specificSubdomainRoute, 'api.example.com')).toBeTrue();
|
||||
expect(routeMatchesDomain(specificSubdomainRoute, 'other.example.com')).toBeFalse();
|
||||
expect(routeMatchesDomain(specificSubdomainRoute, 'sub.api.example.com')).toBeFalse();
|
||||
|
||||
// Test finding best match when multiple domains match
|
||||
const specificSubdomainRequest = { domain: 'api.example.com', port: 80 };
|
||||
const bestSpecificMatch = findBestMatchingRoute(routes, specificSubdomainRequest);
|
||||
expect(bestSpecificMatch).not.toBeUndefined();
|
||||
if (bestSpecificMatch) {
|
||||
// Find which route was matched
|
||||
const matchedPort = bestSpecificMatch.action.targets[0].port;
|
||||
console.log(`Matched route with port: ${matchedPort}`);
|
||||
|
||||
// Verify it's the specific subdomain route (with highest priority)
|
||||
expect(bestSpecificMatch.priority).toEqual(200);
|
||||
}
|
||||
|
||||
// Test with a subdomain that matches wildcard but not specific
|
||||
const otherSubdomainRequest = { domain: 'other.example.com', port: 80 };
|
||||
const bestWildcardMatch = findBestMatchingRoute(routes, otherSubdomainRequest);
|
||||
expect(bestWildcardMatch).not.toBeUndefined();
|
||||
if (bestWildcardMatch) {
|
||||
// Find which route was matched
|
||||
const matchedPort = bestWildcardMatch.action.targets[0].port;
|
||||
console.log(`Matched route with port: ${matchedPort}`);
|
||||
|
||||
// Verify it's the wildcard subdomain route (with medium priority)
|
||||
expect(bestWildcardMatch.priority).toEqual(100);
|
||||
}
|
||||
});
|
||||
@@ -476,56 +507,83 @@ tap.test('Wildcard Domain Handling', async () => {
|
||||
// --------------------------------- Integration Tests ---------------------------------
|
||||
|
||||
tap.test('Route Integration - Combining Multiple Route Types', async () => {
|
||||
// Create a comprehensive set of routes for a full application
|
||||
const routes: IRouteConfig[] = [
|
||||
// Main website with HTTPS and HTTP redirect
|
||||
...createCompleteHttpsServer('example.com', { host: 'web-server', port: 8080 }, {
|
||||
certificate: 'auto'
|
||||
}),
|
||||
|
||||
// API endpoints
|
||||
createApiRoute('api.example.com', '/v1', { host: 'api-server', port: 3000 }, {
|
||||
useTls: true,
|
||||
certificate: 'auto',
|
||||
addCorsHeaders: true
|
||||
}),
|
||||
|
||||
// WebSocket for real-time updates
|
||||
createWebSocketRoute('ws.example.com', '/live', { host: 'websocket-server', port: 5000 }, {
|
||||
useTls: true,
|
||||
certificate: 'auto'
|
||||
}),
|
||||
|
||||
|
||||
// Legacy system with passthrough
|
||||
createHttpsPassthroughRoute('legacy.example.com', { host: 'legacy-server', port: 443 })
|
||||
{
|
||||
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'
|
||||
}
|
||||
];
|
||||
|
||||
// Validate all routes
|
||||
|
||||
const validationResult = validateRoutes(routes);
|
||||
expect(validationResult.valid).toBeTrue();
|
||||
expect(validationResult.errors.length).toEqual(0);
|
||||
|
||||
// Test route matching for different endpoints
|
||||
|
||||
// Web server (HTTPS)
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
// Web server (HTTP redirect via socket handler)
|
||||
|
||||
const webRedirectMatch = findBestMatchingRoute(routes, { domain: 'example.com', port: 80 });
|
||||
expect(webRedirectMatch).not.toBeUndefined();
|
||||
if (webRedirectMatch) {
|
||||
expect(webRedirectMatch.action.type).toEqual('socket-handler');
|
||||
}
|
||||
|
||||
// API server
|
||||
const apiMatch = findBestMatchingRoute(routes, {
|
||||
domain: 'api.example.com',
|
||||
|
||||
const apiMatch = findBestMatchingRoute(routes, {
|
||||
domain: 'api.example.com',
|
||||
port: 443,
|
||||
path: '/v1/users'
|
||||
});
|
||||
@@ -534,10 +592,9 @@ tap.test('Route Integration - Combining Multiple Route Types', async () => {
|
||||
expect(apiMatch.action.type).toEqual('forward');
|
||||
expect(apiMatch.action.targets[0].host).toEqual('api-server');
|
||||
}
|
||||
|
||||
// WebSocket server
|
||||
const wsMatch = findBestMatchingRoute(routes, {
|
||||
domain: 'ws.example.com',
|
||||
|
||||
const wsMatch = findBestMatchingRoute(routes, {
|
||||
domain: 'ws.example.com',
|
||||
port: 443,
|
||||
path: '/live'
|
||||
});
|
||||
@@ -547,12 +604,9 @@ tap.test('Route Integration - Combining Multiple Route Types', async () => {
|
||||
expect(wsMatch.action.targets[0].host).toEqual('websocket-server');
|
||||
expect(wsMatch.action.websocket?.enabled).toBeTrue();
|
||||
}
|
||||
|
||||
// Static assets route was removed - static file serving should be handled externally
|
||||
|
||||
// Legacy system
|
||||
const legacyMatch = findBestMatchingRoute(routes, {
|
||||
domain: 'legacy.example.com',
|
||||
|
||||
const legacyMatch = findBestMatchingRoute(routes, {
|
||||
domain: 'legacy.example.com',
|
||||
port: 443
|
||||
});
|
||||
expect(legacyMatch).not.toBeUndefined();
|
||||
@@ -565,7 +619,6 @@ 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,
|
||||
@@ -583,16 +636,13 @@ tap.test('Routes: Should accept protocol field on route match', async () => {
|
||||
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,
|
||||
@@ -616,28 +666,26 @@ tap.test('Routes: Should accept protocol tcp on route match', async () => {
|
||||
});
|
||||
|
||||
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' }
|
||||
);
|
||||
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'
|
||||
};
|
||||
|
||||
// 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,
|
||||
@@ -669,11 +717,9 @@ tap.test('Routes: Protocol field should not affect domain/port matching', async
|
||||
|
||||
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');
|
||||
@@ -696,11 +742,9 @@ tap.test('Routes: Protocol field preserved through route cloning', async () => {
|
||||
|
||||
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');
|
||||
});
|
||||
@@ -720,10 +764,9 @@ tap.test('Routes: Protocol field preserved through route merging', async () => {
|
||||
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();
|
||||
export default tap.start();
|
||||
|
||||
Reference in New Issue
Block a user