/** * 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();