/** * Tests for smart SNI requirement calculation * * These tests verify that the calculateSniRequirement() method correctly determines * when SNI (Server Name Indication) is required for routing decisions. */ import { expect, tap } from '@git.zone/tstest/tapbundle'; import { SmartProxy } from '../ts/proxies/smart-proxy/index.js'; import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js'; // Use unique high ports for each test to avoid conflicts let testPort = 20000; const getNextPort = () => testPort++; // --------------------------------- Single Route, No Domain Restriction --------------------------------- tap.test('SNI Requirement: Single passthrough, no domains, static target - should allow session tickets', async () => { const port = getNextPort(); const routes: IRouteConfig[] = [{ name: 'passthrough-no-domains', match: { ports: port }, action: { type: 'forward', targets: [{ host: 'backend-server', port: 9443 }], tls: { mode: 'passthrough' } } }]; const proxy = new SmartProxy({ routes }); await proxy.start(); const routesOnPort = proxy.routeManager.getRoutesForPort(port); expect(routesOnPort.length).toEqual(1); expect(routesOnPort[0].action.tls?.mode).toEqual('passthrough'); expect(routesOnPort[0].match.domains).toBeUndefined(); expect(typeof routesOnPort[0].action.targets?.[0].host).toEqual('string'); await proxy.stop(); }); tap.test('SNI Requirement: Single passthrough, domains: "*", static target - should allow session tickets', async () => { const port = getNextPort(); const routes: IRouteConfig[] = [{ name: 'passthrough-wildcard-domain', match: { ports: port, domains: '*' }, action: { type: 'forward', targets: [{ host: 'backend-server', port: 9443 }], tls: { mode: 'passthrough' } } }]; const proxy = new SmartProxy({ routes }); await proxy.start(); const routesOnPort = proxy.routeManager.getRoutesForPort(port); expect(routesOnPort.length).toEqual(1); expect(routesOnPort[0].match.domains).toEqual('*'); await proxy.stop(); }); tap.test('SNI Requirement: Single passthrough, domains: ["*"], static target - should allow session tickets', async () => { const port = getNextPort(); const routes: IRouteConfig[] = [{ name: 'passthrough-wildcard-array', match: { ports: port, domains: ['*'] }, action: { type: 'forward', targets: [{ host: 'backend-server', port: 9443 }], tls: { mode: 'passthrough' } } }]; const proxy = new SmartProxy({ routes }); await proxy.start(); const routesOnPort = proxy.routeManager.getRoutesForPort(port); expect(routesOnPort.length).toEqual(1); expect(routesOnPort[0].match.domains).toEqual(['*']); await proxy.stop(); }); // --------------------------------- Single Route, Specific Domain --------------------------------- tap.test('SNI Requirement: Single passthrough, specific domain - should require SNI (block session tickets)', async () => { const port = getNextPort(); const routes: IRouteConfig[] = [{ name: 'passthrough-specific-domain', match: { ports: port, domains: 'api.example.com' }, action: { type: 'forward', targets: [{ host: 'backend-server', port: 9443 }], tls: { mode: 'passthrough' } } }]; const proxy = new SmartProxy({ routes }); await proxy.start(); const routesOnPort = proxy.routeManager.getRoutesForPort(port); expect(routesOnPort.length).toEqual(1); expect(routesOnPort[0].match.domains).toEqual('api.example.com'); await proxy.stop(); }); tap.test('SNI Requirement: Single passthrough, multiple specific domains - should require SNI', async () => { const port = getNextPort(); const routes: IRouteConfig[] = [{ name: 'passthrough-multiple-domains', match: { ports: port, domains: ['a.example.com', 'b.example.com'] }, action: { type: 'forward', targets: [{ host: 'backend-server', port: 9443 }], tls: { mode: 'passthrough' } } }]; const proxy = new SmartProxy({ routes }); await proxy.start(); const routesOnPort = proxy.routeManager.getRoutesForPort(port); expect(routesOnPort.length).toEqual(1); expect(routesOnPort[0].match.domains).toEqual(['a.example.com', 'b.example.com']); await proxy.stop(); }); tap.test('SNI Requirement: Single passthrough, pattern domain - should require SNI', async () => { const port = getNextPort(); const routes: IRouteConfig[] = [{ name: 'passthrough-pattern-domain', match: { ports: port, domains: '*.example.com' }, action: { type: 'forward', targets: [{ host: 'backend-server', port: 9443 }], tls: { mode: 'passthrough' } } }]; const proxy = new SmartProxy({ routes }); await proxy.start(); const routesOnPort = proxy.routeManager.getRoutesForPort(port); expect(routesOnPort.length).toEqual(1); expect(routesOnPort[0].match.domains).toEqual('*.example.com'); await proxy.stop(); }); // --------------------------------- Single Route, Dynamic Target --------------------------------- tap.test('SNI Requirement: Single passthrough, dynamic host function - should require SNI', async () => { const port = getNextPort(); const routes: IRouteConfig[] = [{ name: 'passthrough-dynamic-host', match: { ports: port }, action: { type: 'forward', targets: [{ host: (context) => { if (context.domain === 'api.example.com') return 'api-backend'; return 'web-backend'; }, port: 9443 }], tls: { mode: 'passthrough' } } }]; const proxy = new SmartProxy({ routes }); await proxy.start(); const routesOnPort = proxy.routeManager.getRoutesForPort(port); expect(routesOnPort.length).toEqual(1); expect(typeof routesOnPort[0].action.targets?.[0].host).toEqual('function'); await proxy.stop(); }); // --------------------------------- Multiple Routes on Same Port --------------------------------- tap.test('SNI Requirement: Multiple passthrough routes on same port - should require SNI', async () => { const port = getNextPort(); const routes: IRouteConfig[] = [ { name: 'passthrough-api', match: { ports: port, domains: 'api.example.com' }, action: { type: 'forward', targets: [{ host: 'api-backend', port: 9443 }], tls: { mode: 'passthrough' } } }, { name: 'passthrough-web', match: { ports: port, domains: 'web.example.com' }, action: { type: 'forward', targets: [{ host: 'web-backend', port: 9443 }], tls: { mode: 'passthrough' } } } ]; const proxy = new SmartProxy({ routes }); await proxy.start(); const routesOnPort = proxy.routeManager.getRoutesForPort(port); expect(routesOnPort.length).toEqual(2); await proxy.stop(); }); // --------------------------------- TLS Termination Routes (route config only, no actual cert provisioning) --------------------------------- tap.test('SNI Requirement: Terminate route config is correctly identified', async () => { const port = getNextPort(); // Test route configuration without starting the proxy (avoids cert provisioning) const routes: IRouteConfig[] = [{ name: 'terminate-route', match: { ports: port, domains: 'secure.example.com' }, action: { type: 'forward', targets: [{ host: 'backend', port: 8080 }], tls: { mode: 'terminate', certificate: 'auto' } } }]; // Just verify route config is valid without starting (no ACME timeout) const proxy = new SmartProxy({ routes, acme: { email: 'test@example.com', useProduction: false } }); // Check route manager directly (before start) expect(routes[0].action.tls?.mode).toEqual('terminate'); expect(routes.length).toEqual(1); }); tap.test('SNI Requirement: Mixed terminate + passthrough config is correctly identified', async () => { const port = getNextPort(); const routes: IRouteConfig[] = [ { name: 'terminate-secure', match: { ports: port, domains: 'secure.example.com' }, action: { type: 'forward', targets: [{ host: 'secure-backend', port: 8080 }], tls: { mode: 'terminate', certificate: 'auto' } } }, { name: 'passthrough-raw', match: { ports: port, domains: 'passthrough.example.com' }, action: { type: 'forward', targets: [{ host: 'passthrough-backend', port: 9443 }], tls: { mode: 'passthrough' } } } ]; // Verify route configs without starting const hasTerminate = routes.some(r => r.action.tls?.mode === 'terminate'); const hasPassthrough = routes.some(r => r.action.tls?.mode === 'passthrough'); expect(hasTerminate).toBeTrue(); expect(hasPassthrough).toBeTrue(); expect(routes.length).toEqual(2); }); tap.test('SNI Requirement: terminate-and-reencrypt config is correctly identified', async () => { const port = getNextPort(); const routes: IRouteConfig[] = [{ name: 'reencrypt-route', match: { ports: port, domains: 'reencrypt.example.com' }, action: { type: 'forward', targets: [{ host: 'backend', port: 9443 }], tls: { mode: 'terminate-and-reencrypt', certificate: 'auto' } } }]; // Verify route config without starting expect(routes[0].action.tls?.mode).toEqual('terminate-and-reencrypt'); }); // --------------------------------- Edge Cases --------------------------------- tap.test('SNI Requirement: No routes on port - should not require SNI', async () => { const routePort = getNextPort(); const queryPort = getNextPort(); const routes: IRouteConfig[] = [{ name: 'different-port-route', match: { ports: routePort }, action: { type: 'forward', targets: [{ host: 'backend', port: 8080 }], tls: { mode: 'passthrough' } } }]; const proxy = new SmartProxy({ routes }); await proxy.start(); const routesOnQueryPort = proxy.routeManager.getRoutesForPort(queryPort); expect(routesOnQueryPort.length).toEqual(0); await proxy.stop(); }); tap.test('SNI Requirement: Multiple static targets in single route - should not require SNI', async () => { const port = getNextPort(); const routes: IRouteConfig[] = [{ name: 'multiple-static-targets', match: { ports: port }, action: { type: 'forward', targets: [ { host: 'backend1', port: 9443 }, { host: 'backend2', port: 9443 } ], tls: { mode: 'passthrough' } } }]; const proxy = new SmartProxy({ routes }); await proxy.start(); const routesOnPort = proxy.routeManager.getRoutesForPort(port); expect(routesOnPort.length).toEqual(1); expect(routesOnPort[0].action.targets?.length).toEqual(2); expect(typeof routesOnPort[0].action.targets?.[0].host).toEqual('string'); expect(typeof routesOnPort[0].action.targets?.[1].host).toEqual('string'); await proxy.stop(); }); tap.test('SNI Requirement: Host array (load balancing) is still static - should not require SNI', async () => { const port = getNextPort(); const routes: IRouteConfig[] = [{ name: 'host-array-static', match: { ports: port }, action: { type: 'forward', targets: [{ host: ['backend1', 'backend2', 'backend3'], port: 9443 }], tls: { mode: 'passthrough' } } }]; const proxy = new SmartProxy({ routes }); await proxy.start(); const routesOnPort = proxy.routeManager.getRoutesForPort(port); expect(routesOnPort.length).toEqual(1); expect(Array.isArray(routesOnPort[0].action.targets?.[0].host)).toBeTrue(); await proxy.stop(); }); export default tap.start();