import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as plugins from '../ts/plugins.js'; import { validateRouteConfig, validateRoutes, isValidDomain, isValidPort, validateRouteMatch, validateRouteAction, hasRequiredPropertiesForAction, assertValidRoute } from '../ts/proxies/smart-proxy/utils/route-validator.js'; import { mergeRouteConfigs, findMatchingRoutes, findBestMatchingRoute, routeMatchesDomain, routeMatchesPort, routeMatchesPath, routeMatchesHeaders, generateRouteId, cloneRoute } from '../ts/proxies/smart-proxy/utils/route-utils.js'; import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget, IRouteTls, TRouteActionType } from '../ts/proxies/smart-proxy/models/route-types.js'; // --------------------------------- Route Validation Tests --------------------------------- tap.test('Route Validation - isValidDomain', async () => { // Valid domains expect(isValidDomain('example.com')).toBeTrue(); expect(isValidDomain('sub.example.com')).toBeTrue(); expect(isValidDomain('*.example.com')).toBeTrue(); expect(isValidDomain('localhost')).toBeTrue(); expect(isValidDomain('*')).toBeTrue(); expect(isValidDomain('192.168.1.1')).toBeTrue(); // Single-word hostnames are valid (for internal network use) expect(isValidDomain('example')).toBeTrue(); // Invalid domains expect(isValidDomain('example.')).toBeFalse(); expect(isValidDomain('example..com')).toBeFalse(); expect(isValidDomain('-example.com')).toBeFalse(); expect(isValidDomain('')).toBeFalse(); }); tap.test('Route Validation - isValidPort', async () => { // Valid ports expect(isValidPort(80)).toBeTrue(); expect(isValidPort(443)).toBeTrue(); expect(isValidPort(8080)).toBeTrue(); expect(isValidPort([80, 443])).toBeTrue(); // Invalid ports expect(isValidPort(0)).toBeFalse(); expect(isValidPort(65536)).toBeFalse(); expect(isValidPort(-1)).toBeFalse(); expect(isValidPort([0, 80])).toBeFalse(); }); tap.test('Route Validation - validateRouteMatch', async () => { // Valid match configuration const validMatch: IRouteMatch = { ports: 80, domains: 'example.com' }; const validResult = validateRouteMatch(validMatch); expect(validResult.valid).toBeTrue(); expect(validResult.errors.length).toEqual(0); // Invalid match configuration (invalid domain) const invalidMatch: IRouteMatch = { ports: 80, domains: 'invalid..domain' }; const invalidResult = validateRouteMatch(invalidMatch); expect(invalidResult.valid).toBeFalse(); expect(invalidResult.errors.length).toBeGreaterThan(0); expect(invalidResult.errors[0]).toInclude('Invalid domain'); // Invalid match configuration (invalid port) const invalidPortMatch: IRouteMatch = { ports: 0, domains: 'example.com' }; const invalidPortResult = validateRouteMatch(invalidPortMatch); expect(invalidPortResult.valid).toBeFalse(); expect(invalidPortResult.errors.length).toBeGreaterThan(0); expect(invalidPortResult.errors[0]).toInclude('Invalid port'); // Test path validation const invalidPathMatch: IRouteMatch = { ports: 80, domains: 'example.com', path: 'invalid-path-without-slash' }; const invalidPathResult = validateRouteMatch(invalidPathMatch); expect(invalidPathResult.valid).toBeFalse(); expect(invalidPathResult.errors.length).toBeGreaterThan(0); expect(invalidPathResult.errors[0]).toInclude('starting with /'); }); tap.test('Route Validation - validateRouteAction', async () => { // Valid forward action const validForwardAction: IRouteAction = { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }; const validForwardResult = validateRouteAction(validForwardAction); expect(validForwardResult.valid).toBeTrue(); expect(validForwardResult.errors.length).toEqual(0); // Valid socket-handler action const validSocketAction: IRouteAction = { type: 'socket-handler', socketHandler: (socket, context) => { socket.end(); } }; const validSocketResult = validateRouteAction(validSocketAction); expect(validSocketResult.valid).toBeTrue(); expect(validSocketResult.errors.length).toEqual(0); // Invalid action (missing targets) const invalidAction: IRouteAction = { type: 'forward' }; const invalidResult = validateRouteAction(invalidAction); expect(invalidResult.valid).toBeFalse(); expect(invalidResult.errors.length).toBeGreaterThan(0); expect(invalidResult.errors[0]).toInclude('Targets array is required'); // Invalid action (missing socket handler) const invalidSocketAction: IRouteAction = { type: 'socket-handler' }; const invalidSocketResult = validateRouteAction(invalidSocketAction); expect(invalidSocketResult.valid).toBeFalse(); expect(invalidSocketResult.errors.length).toBeGreaterThan(0); expect(invalidSocketResult.errors[0]).toInclude('handler function is required'); }); tap.test('Route Validation - validateRouteConfig', async () => { // Valid route config const validRoute: IRouteConfig = { match: { ports: 80, domains: 'example.com' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, name: 'HTTP Route for example.com', }; const validResult = validateRouteConfig(validRoute); expect(validResult.valid).toBeTrue(); expect(validResult.errors.length).toEqual(0); // Invalid route config (missing targets) const invalidRoute: IRouteConfig = { match: { domains: 'example.com', ports: 80 }, action: { type: 'forward' }, name: 'Invalid Route' }; const invalidResult = validateRouteConfig(invalidRoute); expect(invalidResult.valid).toBeFalse(); expect(invalidResult.errors.length).toBeGreaterThan(0); }); tap.test('Route Validation - validateRoutes', async () => { // Create valid and invalid routes const routes = [ { match: { ports: 80, domains: 'example.com' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, name: 'HTTP Route for example.com', } as IRouteConfig, { match: { domains: 'invalid..domain', ports: 80 }, action: { type: 'forward', target: { host: 'localhost', port: 3000 } } } as IRouteConfig, { match: { ports: 443, domains: 'secure.example.com' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3001 }], tls: { mode: 'terminate', certificate: 'auto' } }, name: 'HTTPS Terminate Route for secure.example.com', } as IRouteConfig ]; const result = validateRoutes(routes); expect(result.valid).toBeFalse(); expect(result.errors.length).toEqual(1); expect(result.errors[0].index).toEqual(1); // The second route is invalid expect(result.errors[0].errors.length).toBeGreaterThan(0); expect(result.errors[0].errors[0]).toInclude('Invalid domain'); }); tap.test('Route Validation - hasRequiredPropertiesForAction', async () => { // Forward action const forwardRoute: IRouteConfig = { match: { ports: 80, domains: 'example.com' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, name: 'HTTP Route for example.com', }; expect(hasRequiredPropertiesForAction(forwardRoute, 'forward')).toBeTrue(); // Socket handler action const socketRoute: IRouteConfig = { match: { domains: 'socket.example.com', ports: 80 }, action: { type: 'socket-handler', socketHandler: (socket, context) => { socket.end(); } }, name: 'Socket Handler Route' }; expect(hasRequiredPropertiesForAction(socketRoute, 'socket-handler')).toBeTrue(); // Missing required properties const invalidForwardRoute: IRouteConfig = { match: { domains: 'example.com', ports: 80 }, action: { type: 'forward' }, name: 'Invalid Forward Route' }; expect(hasRequiredPropertiesForAction(invalidForwardRoute, 'forward')).toBeFalse(); }); tap.test('Route Validation - assertValidRoute', async () => { // Valid route const validRoute: IRouteConfig = { match: { ports: 80, domains: 'example.com' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, name: 'HTTP Route for example.com', }; expect(() => assertValidRoute(validRoute)).not.toThrow(); // Invalid route const invalidRoute: IRouteConfig = { match: { domains: 'example.com', ports: 80 }, action: { type: 'forward' }, name: 'Invalid Route' }; expect(() => assertValidRoute(invalidRoute)).toThrow(); }); // --------------------------------- Route Utilities Tests --------------------------------- tap.test('Route Utilities - mergeRouteConfigs', async () => { // Base route const baseRoute: IRouteConfig = { match: { ports: 80, domains: 'example.com' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, name: 'HTTP Route for example.com', }; // Override with different name and port const overrideRoute: Partial = { name: 'Merged Route', match: { ports: 8080 } }; // Merge configs const mergedRoute = mergeRouteConfigs(baseRoute, overrideRoute); // Check merged properties expect(mergedRoute.name).toEqual('Merged Route'); expect(mergedRoute.match.ports).toEqual(8080); expect(mergedRoute.match.domains).toEqual('example.com'); expect(mergedRoute.action.type).toEqual('forward'); // Test merging action properties const actionOverride: Partial = { action: { type: 'forward', targets: [{ host: 'new-host.local', port: 5000 }] } }; const actionMergedRoute = mergeRouteConfigs(baseRoute, actionOverride); expect(actionMergedRoute.action.targets?.[0]?.host).toEqual('new-host.local'); expect(actionMergedRoute.action.targets?.[0]?.port).toEqual(5000); // Test replacing action with socket handler const typeChangeOverride: Partial = { action: { type: 'socket-handler', socketHandler: (socket, context) => { socket.write('HTTP/1.1 301 Moved Permanently\r\n'); socket.write('Location: https://example.com\r\n'); socket.write('\r\n'); socket.end(); } } }; const typeChangedRoute = mergeRouteConfigs(baseRoute, typeChangeOverride); expect(typeChangedRoute.action.type).toEqual('socket-handler'); expect(typeChangedRoute.action.socketHandler).toBeDefined(); expect(typeChangedRoute.action.targets).toBeUndefined(); }); tap.test('Route Matching - routeMatchesDomain', async () => { // Create route with wildcard domain const wildcardRoute: IRouteConfig = { match: { ports: 80, domains: '*.example.com' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, name: 'HTTP Route for *.example.com', }; // Create route with exact domain const exactRoute: IRouteConfig = { match: { ports: 80, domains: 'example.com' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, name: 'HTTP Route for example.com', }; // Create route with multiple domains const multiDomainRoute: IRouteConfig = { match: { ports: 80, domains: ['example.com', 'example.org'] }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, name: 'HTTP Route for example.com,example.org', }; // Test wildcard domain matching expect(routeMatchesDomain(wildcardRoute, 'sub.example.com')).toBeTrue(); expect(routeMatchesDomain(wildcardRoute, 'another.example.com')).toBeTrue(); expect(routeMatchesDomain(wildcardRoute, 'example.com')).toBeFalse(); expect(routeMatchesDomain(wildcardRoute, 'example.org')).toBeFalse(); // Test exact domain matching expect(routeMatchesDomain(exactRoute, 'example.com')).toBeTrue(); expect(routeMatchesDomain(exactRoute, 'sub.example.com')).toBeFalse(); // Test multiple domains matching expect(routeMatchesDomain(multiDomainRoute, 'example.com')).toBeTrue(); expect(routeMatchesDomain(multiDomainRoute, 'example.org')).toBeTrue(); expect(routeMatchesDomain(multiDomainRoute, 'example.net')).toBeFalse(); // Test case insensitivity expect(routeMatchesDomain(exactRoute, 'Example.Com')).toBeTrue(); }); tap.test('Route Matching - routeMatchesPort', async () => { // Create routes with different port configurations const singlePortRoute: IRouteConfig = { match: { ports: 80, domains: 'example.com' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, name: 'HTTP Route for example.com', }; const multiPortRoute: IRouteConfig = { match: { domains: 'example.com', ports: [80, 8080] }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] } }; const portRangeRoute: IRouteConfig = { match: { domains: 'example.com', ports: [{ from: 8000, to: 9000 }] }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] } }; // Test single port matching expect(routeMatchesPort(singlePortRoute, 80)).toBeTrue(); expect(routeMatchesPort(singlePortRoute, 443)).toBeFalse(); // Test multi-port matching expect(routeMatchesPort(multiPortRoute, 80)).toBeTrue(); expect(routeMatchesPort(multiPortRoute, 8080)).toBeTrue(); expect(routeMatchesPort(multiPortRoute, 3000)).toBeFalse(); // Test port range matching 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(); }); tap.test('Route Matching - routeMatchesPath', async () => { // Create route with path configuration const exactPathRoute: IRouteConfig = { match: { domains: 'example.com', ports: 80, path: '/api' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] } }; // Test prefix matching with wildcard (not trailing slash) const prefixPathRoute: IRouteConfig = { match: { domains: 'example.com', ports: 80, path: '/api/*' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] } }; const wildcardPathRoute: IRouteConfig = { match: { domains: 'example.com', ports: 80, path: '/api/*' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] } }; // Test exact path matching expect(routeMatchesPath(exactPathRoute, '/api')).toBeTrue(); expect(routeMatchesPath(exactPathRoute, '/api/users')).toBeFalse(); expect(routeMatchesPath(exactPathRoute, '/app')).toBeFalse(); // Test prefix path matching with wildcard expect(routeMatchesPath(prefixPathRoute, '/api/')).toBeFalse(); // Wildcard requires content after /api/ expect(routeMatchesPath(prefixPathRoute, '/api/users')).toBeTrue(); expect(routeMatchesPath(prefixPathRoute, '/app/')).toBeFalse(); // Test wildcard path matching expect(routeMatchesPath(wildcardPathRoute, '/api/users')).toBeTrue(); expect(routeMatchesPath(wildcardPathRoute, '/api/products')).toBeTrue(); expect(routeMatchesPath(wildcardPathRoute, '/app/api')).toBeFalse(); }); tap.test('Route Matching - routeMatchesHeaders', async () => { // Create route with header matching const headerRoute: IRouteConfig = { match: { domains: 'example.com', ports: 80, headers: { 'Content-Type': 'application/json', 'X-Custom-Header': 'value' } }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] } }; // Test header matching expect(routeMatchesHeaders(headerRoute, { 'Content-Type': 'application/json', 'X-Custom-Header': 'value' })).toBeTrue(); expect(routeMatchesHeaders(headerRoute, { 'Content-Type': 'application/json', 'X-Custom-Header': 'value', 'Extra-Header': 'something' })).toBeTrue(); expect(routeMatchesHeaders(headerRoute, { 'Content-Type': 'application/json' })).toBeFalse(); expect(routeMatchesHeaders(headerRoute, { 'Content-Type': 'text/html', 'X-Custom-Header': 'value' })).toBeFalse(); // Route without header matching should match any headers const noHeaderRoute: IRouteConfig = { match: { ports: 80, domains: 'example.com' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, name: 'HTTP Route for example.com', }; expect(routeMatchesHeaders(noHeaderRoute, { 'Content-Type': 'application/json' })).toBeTrue(); }); tap.test('Route Finding - findMatchingRoutes', async () => { // Create multiple routes const routes: IRouteConfig[] = [ { match: { ports: 80, domains: 'example.com' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, name: 'HTTP Route for example.com', }, { match: { ports: 443, domains: 'secure.example.com' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3001 }], tls: { mode: 'terminate', certificate: 'auto' } }, name: 'HTTPS Route for secure.example.com', }, { match: { ports: 443, domains: 'api.example.com', path: '/v1/*' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3002 }], tls: { mode: 'terminate', certificate: 'auto' } }, name: 'API Route for api.example.com', }, { match: { ports: 443, domains: 'ws.example.com', path: '/socket' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3003 }], tls: { mode: 'terminate', certificate: 'auto' }, websocket: { enabled: true } }, name: 'WebSocket Route for ws.example.com', }, ]; // Set priorities routes[0].priority = 10; routes[1].priority = 20; routes[2].priority = 30; routes[3].priority = 40; // Find routes for different criteria const httpMatches = findMatchingRoutes(routes, { domain: 'example.com', port: 80 }); expect(httpMatches.length).toEqual(1); expect(httpMatches[0].name).toInclude('HTTP Route'); const httpsMatches = findMatchingRoutes(routes, { domain: 'secure.example.com', port: 443 }); expect(httpsMatches.length).toEqual(1); expect(httpsMatches[0].name).toInclude('HTTPS Route'); const apiMatches = findMatchingRoutes(routes, { domain: 'api.example.com', path: '/v1/users' }); expect(apiMatches.length).toEqual(1); expect(apiMatches[0].name).toInclude('API Route'); const wsMatches = findMatchingRoutes(routes, { domain: 'ws.example.com', path: '/socket' }); expect(wsMatches.length).toEqual(1); expect(wsMatches[0].name).toInclude('WebSocket Route'); // Test finding multiple routes that match same criteria const route1: IRouteConfig = { match: { ports: 80, domains: 'example.com' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, name: 'HTTP Route for example.com', }; route1.priority = 10; const route2: IRouteConfig = { match: { ports: 80, domains: 'example.com' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3001 }] }, name: 'HTTP Route for example.com', }; route2.priority = 20; route2.match.path = '/api'; const multiMatchRoutes = [route1, route2]; const multiMatches = findMatchingRoutes(multiMatchRoutes, { domain: 'example.com', port: 80 }); expect(multiMatches.length).toEqual(2); expect(multiMatches[0].priority).toEqual(20); // Higher priority should be first expect(multiMatches[1].priority).toEqual(10); // Test disabled routes const disabledRoute: IRouteConfig = { match: { ports: 80, domains: 'example.com' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, name: 'HTTP Route for example.com', }; disabledRoute.enabled = false; const enabledRoutes = findMatchingRoutes([disabledRoute], { domain: 'example.com', port: 80 }); expect(enabledRoutes.length).toEqual(0); }); tap.test('Route Finding - findBestMatchingRoute', async () => { // Create multiple routes with different priorities const route1: IRouteConfig = { match: { ports: 80, domains: 'example.com' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, name: 'HTTP Route for example.com', }; route1.priority = 10; const route2: IRouteConfig = { match: { ports: 80, domains: 'example.com' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3001 }] }, name: 'HTTP Route for example.com', }; route2.priority = 20; route2.match.path = '/api'; const route3: IRouteConfig = { match: { ports: 80, domains: 'example.com' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3002 }] }, name: 'HTTP Route for example.com', }; route3.priority = 30; route3.match.path = '/api/users'; const routes = [route1, route2, route3]; // Find best route for different criteria const bestGeneral = findBestMatchingRoute(routes, { domain: 'example.com', port: 80 }); expect(bestGeneral).not.toBeUndefined(); expect(bestGeneral?.priority).toEqual(30); // Test when no routes match const noMatch = findBestMatchingRoute(routes, { domain: 'unknown.com', port: 80 }); expect(noMatch).toBeUndefined(); }); tap.test('Route Utilities - generateRouteId', async () => { // Test ID generation for different route types const httpRoute: IRouteConfig = { match: { ports: 80, domains: 'example.com' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, name: 'HTTP Route for example.com', }; const httpId = generateRouteId(httpRoute); expect(httpId).toInclude('example-com'); expect(httpId).toInclude('80'); expect(httpId).toInclude('forward'); const httpsRoute: IRouteConfig = { match: { ports: 443, domains: 'secure.example.com' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3001 }], tls: { mode: 'terminate', certificate: 'auto' } }, name: 'HTTPS Terminate Route for secure.example.com', }; const httpsId = generateRouteId(httpsRoute); expect(httpsId).toInclude('secure-example-com'); expect(httpsId).toInclude('443'); expect(httpsId).toInclude('forward'); const multiDomainRoute: IRouteConfig = { match: { ports: 80, domains: ['example.com', 'example.org'] }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, name: 'HTTP Route for example.com,example.org', }; const multiDomainId = generateRouteId(multiDomainRoute); expect(multiDomainId).toInclude('example-com-example-org'); }); tap.test('Route Utilities - cloneRoute', async () => { // Create a route and clone it const originalRoute: IRouteConfig = { match: { ports: 443, domains: 'example.com' }, action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }], tls: { mode: 'terminate', certificate: 'auto' } }, name: 'Original Route', }; const clonedRoute = cloneRoute(originalRoute); // Check that the values are identical expect(clonedRoute.name).toEqual(originalRoute.name); expect(clonedRoute.match.domains).toEqual(originalRoute.match.domains); expect(clonedRoute.action.type).toEqual(originalRoute.action.type); expect(clonedRoute.action.targets?.[0]?.port).toEqual(originalRoute.action.targets?.[0]?.port); // Modify the clone and check that the original is unchanged clonedRoute.name = 'Modified Clone'; expect(originalRoute.name).toEqual('Original Route'); }); export default tap.start();