403 lines
13 KiB
TypeScript
403 lines
13 KiB
TypeScript
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import * as http from 'http';
|
|
import { HttpRouter, type RouterResult } from '../ts/routing/router/http-router.js';
|
|
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
|
|
|
|
// Test proxies and configurations
|
|
let router: HttpRouter;
|
|
|
|
// Sample hostname for testing
|
|
const TEST_DOMAIN = 'example.com';
|
|
const TEST_SUBDOMAIN = 'api.example.com';
|
|
const TEST_WILDCARD = '*.example.com';
|
|
|
|
// Helper: Creates a mock HTTP request for testing
|
|
function createMockRequest(host: string, url: string = '/'): http.IncomingMessage {
|
|
const req = {
|
|
headers: { host },
|
|
url,
|
|
socket: {
|
|
remoteAddress: '127.0.0.1'
|
|
}
|
|
} as any;
|
|
return req;
|
|
}
|
|
|
|
// Helper: Creates a test route configuration
|
|
function createRouteConfig(
|
|
hostname: string,
|
|
destinationIp: string = '10.0.0.1',
|
|
destinationPort: number = 8080
|
|
): IRouteConfig {
|
|
return {
|
|
name: `route-${hostname}`,
|
|
match: {
|
|
domains: [hostname],
|
|
ports: 443
|
|
},
|
|
action: {
|
|
type: 'forward',
|
|
target: {
|
|
host: destinationIp,
|
|
port: destinationPort
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// SETUP: Create an HttpRouter instance
|
|
tap.test('setup http router test environment', async () => {
|
|
router = new HttpRouter();
|
|
|
|
// Initialize with empty config
|
|
router.updateRoutes([]);
|
|
});
|
|
|
|
// Test basic routing by hostname
|
|
tap.test('should route requests by hostname', async () => {
|
|
const config = createRouteConfig(TEST_DOMAIN);
|
|
router.updateRoutes([config]);
|
|
|
|
const req = createMockRequest(TEST_DOMAIN);
|
|
const result = router.routeReq(req);
|
|
|
|
expect(result).toBeTruthy();
|
|
expect(result).toEqual(config);
|
|
});
|
|
|
|
// Test handling of hostname with port number
|
|
tap.test('should handle hostname with port number', async () => {
|
|
const config = createRouteConfig(TEST_DOMAIN);
|
|
router.updateRoutes([config]);
|
|
|
|
const req = createMockRequest(`${TEST_DOMAIN}:443`);
|
|
const result = router.routeReq(req);
|
|
|
|
expect(result).toBeTruthy();
|
|
expect(result).toEqual(config);
|
|
});
|
|
|
|
// Test case-insensitive hostname matching
|
|
tap.test('should perform case-insensitive hostname matching', async () => {
|
|
const config = createRouteConfig(TEST_DOMAIN.toLowerCase());
|
|
router.updateRoutes([config]);
|
|
|
|
const req = createMockRequest(TEST_DOMAIN.toUpperCase());
|
|
const result = router.routeReq(req);
|
|
|
|
expect(result).toBeTruthy();
|
|
expect(result).toEqual(config);
|
|
});
|
|
|
|
// Test handling of unmatched hostnames
|
|
tap.test('should return undefined for unmatched hostnames', async () => {
|
|
const config = createRouteConfig(TEST_DOMAIN);
|
|
router.updateRoutes([config]);
|
|
|
|
const req = createMockRequest('unknown.domain.com');
|
|
const result = router.routeReq(req);
|
|
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
// Test adding path patterns
|
|
tap.test('should match requests using path patterns', async () => {
|
|
const config = createRouteConfig(TEST_DOMAIN);
|
|
config.match.path = '/api/users';
|
|
router.updateRoutes([config]);
|
|
|
|
// Test that path matches
|
|
const req1 = createMockRequest(TEST_DOMAIN, '/api/users');
|
|
const result1 = router.routeReqWithDetails(req1);
|
|
|
|
expect(result1).toBeTruthy();
|
|
expect(result1.route).toEqual(config);
|
|
expect(result1.pathMatch).toEqual('/api/users');
|
|
|
|
// Test that non-matching path doesn't match
|
|
const req2 = createMockRequest(TEST_DOMAIN, '/web/users');
|
|
const result2 = router.routeReqWithDetails(req2);
|
|
|
|
expect(result2).toBeUndefined();
|
|
});
|
|
|
|
// Test handling wildcard patterns
|
|
tap.test('should support wildcard path patterns', async () => {
|
|
const config = createRouteConfig(TEST_DOMAIN);
|
|
config.match.path = '/api/*';
|
|
router.updateRoutes([config]);
|
|
|
|
// Test with path that matches the wildcard pattern
|
|
const req = createMockRequest(TEST_DOMAIN, '/api/users/123');
|
|
const result = router.routeReqWithDetails(req);
|
|
|
|
expect(result).toBeTruthy();
|
|
expect(result.route).toEqual(config);
|
|
expect(result.pathMatch).toEqual('/api');
|
|
|
|
// Print the actual value to diagnose issues
|
|
console.log('Path remainder value:', result.pathRemainder);
|
|
expect(result.pathRemainder).toBeTruthy();
|
|
expect(result.pathRemainder).toEqual('/users/123');
|
|
});
|
|
|
|
// Test extracting path parameters
|
|
tap.test('should extract path parameters from URL', async () => {
|
|
const config = createRouteConfig(TEST_DOMAIN);
|
|
config.match.path = '/users/:id/profile';
|
|
router.updateRoutes([config]);
|
|
|
|
const req = createMockRequest(TEST_DOMAIN, '/users/123/profile');
|
|
const result = router.routeReqWithDetails(req);
|
|
|
|
expect(result).toBeTruthy();
|
|
expect(result.route).toEqual(config);
|
|
expect(result.pathParams).toBeTruthy();
|
|
expect(result.pathParams.id).toEqual('123');
|
|
});
|
|
|
|
// Test multiple configs for same hostname with different paths
|
|
tap.test('should support multiple configs for same hostname with different paths', async () => {
|
|
const apiConfig = createRouteConfig(TEST_DOMAIN, '10.0.0.1', 8001);
|
|
apiConfig.match.path = '/api';
|
|
apiConfig.name = 'api-route';
|
|
|
|
const webConfig = createRouteConfig(TEST_DOMAIN, '10.0.0.2', 8002);
|
|
webConfig.match.path = '/web';
|
|
webConfig.name = 'web-route';
|
|
|
|
// Add both configs
|
|
router.updateRoutes([apiConfig, webConfig]);
|
|
|
|
// Test API path routes to API config
|
|
const apiReq = createMockRequest(TEST_DOMAIN, '/api/users');
|
|
const apiResult = router.routeReq(apiReq);
|
|
|
|
expect(apiResult).toEqual(apiConfig);
|
|
|
|
// Test web path routes to web config
|
|
const webReq = createMockRequest(TEST_DOMAIN, '/web/dashboard');
|
|
const webResult = router.routeReq(webReq);
|
|
|
|
expect(webResult).toEqual(webConfig);
|
|
|
|
// Test unknown path returns undefined
|
|
const unknownReq = createMockRequest(TEST_DOMAIN, '/unknown');
|
|
const unknownResult = router.routeReq(unknownReq);
|
|
|
|
expect(unknownResult).toBeUndefined();
|
|
});
|
|
|
|
// Test wildcard subdomains
|
|
tap.test('should match wildcard subdomains', async () => {
|
|
const wildcardConfig = createRouteConfig(TEST_WILDCARD);
|
|
router.updateRoutes([wildcardConfig]);
|
|
|
|
// Test that subdomain.example.com matches *.example.com
|
|
const req = createMockRequest('subdomain.example.com');
|
|
const result = router.routeReq(req);
|
|
|
|
expect(result).toBeTruthy();
|
|
expect(result).toEqual(wildcardConfig);
|
|
});
|
|
|
|
// Test TLD wildcards (example.*)
|
|
tap.test('should match TLD wildcards', async () => {
|
|
const tldWildcardConfig = createRouteConfig('example.*');
|
|
router.updateRoutes([tldWildcardConfig]);
|
|
|
|
// Test that example.com matches example.*
|
|
const req1 = createMockRequest('example.com');
|
|
const result1 = router.routeReq(req1);
|
|
expect(result1).toBeTruthy();
|
|
expect(result1).toEqual(tldWildcardConfig);
|
|
|
|
// Test that example.org matches example.*
|
|
const req2 = createMockRequest('example.org');
|
|
const result2 = router.routeReq(req2);
|
|
expect(result2).toBeTruthy();
|
|
expect(result2).toEqual(tldWildcardConfig);
|
|
|
|
// Test that subdomain.example.com doesn't match example.*
|
|
const req3 = createMockRequest('subdomain.example.com');
|
|
const result3 = router.routeReq(req3);
|
|
expect(result3).toBeUndefined();
|
|
});
|
|
|
|
// Test complex pattern matching (*.lossless*)
|
|
tap.test('should match complex wildcard patterns', async () => {
|
|
const complexWildcardConfig = createRouteConfig('*.lossless*');
|
|
router.updateRoutes([complexWildcardConfig]);
|
|
|
|
// Test that sub.lossless.com matches *.lossless*
|
|
const req1 = createMockRequest('sub.lossless.com');
|
|
const result1 = router.routeReq(req1);
|
|
expect(result1).toBeTruthy();
|
|
expect(result1).toEqual(complexWildcardConfig);
|
|
|
|
// Test that api.lossless.org matches *.lossless*
|
|
const req2 = createMockRequest('api.lossless.org');
|
|
const result2 = router.routeReq(req2);
|
|
expect(result2).toBeTruthy();
|
|
expect(result2).toEqual(complexWildcardConfig);
|
|
|
|
// Test that losslessapi.com matches *.lossless*
|
|
const req3 = createMockRequest('losslessapi.com');
|
|
const result3 = router.routeReq(req3);
|
|
expect(result3).toBeUndefined(); // Should not match as it doesn't have a subdomain
|
|
});
|
|
|
|
// Test default configuration fallback
|
|
tap.test('should fall back to default configuration', async () => {
|
|
const defaultConfig = createRouteConfig('*');
|
|
const specificConfig = createRouteConfig(TEST_DOMAIN);
|
|
|
|
router.updateRoutes([defaultConfig, specificConfig]);
|
|
|
|
// Test specific domain routes to specific config
|
|
const specificReq = createMockRequest(TEST_DOMAIN);
|
|
const specificResult = router.routeReq(specificReq);
|
|
|
|
expect(specificResult).toEqual(specificConfig);
|
|
|
|
// Test unknown domain falls back to default config
|
|
const unknownReq = createMockRequest('unknown.com');
|
|
const unknownResult = router.routeReq(unknownReq);
|
|
|
|
expect(unknownResult).toEqual(defaultConfig);
|
|
});
|
|
|
|
// Test priority between exact and wildcard matches
|
|
tap.test('should prioritize exact hostname over wildcard', async () => {
|
|
const wildcardConfig = createRouteConfig(TEST_WILDCARD);
|
|
const exactConfig = createRouteConfig(TEST_SUBDOMAIN);
|
|
|
|
router.updateRoutes([wildcardConfig, exactConfig]);
|
|
|
|
// Test that exact match takes priority
|
|
const req = createMockRequest(TEST_SUBDOMAIN);
|
|
const result = router.routeReq(req);
|
|
|
|
expect(result).toEqual(exactConfig);
|
|
});
|
|
|
|
// Test adding and removing configurations
|
|
tap.test('should manage configurations correctly', async () => {
|
|
router.updateRoutes([]);
|
|
|
|
// Add a config
|
|
const config = createRouteConfig(TEST_DOMAIN);
|
|
router.updateRoutes([config]);
|
|
|
|
// Verify routing works
|
|
const req = createMockRequest(TEST_DOMAIN);
|
|
let result = router.routeReq(req);
|
|
|
|
expect(result).toEqual(config);
|
|
|
|
// Remove the config and verify it no longer routes
|
|
router.updateRoutes([]);
|
|
|
|
result = router.routeReq(req);
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
// Test path pattern specificity
|
|
tap.test('should prioritize more specific path patterns', async () => {
|
|
const genericConfig = createRouteConfig(TEST_DOMAIN, '10.0.0.1', 8001);
|
|
genericConfig.match.path = '/api/*';
|
|
genericConfig.name = 'generic-api';
|
|
|
|
const specificConfig = createRouteConfig(TEST_DOMAIN, '10.0.0.2', 8002);
|
|
specificConfig.match.path = '/api/users';
|
|
specificConfig.name = 'specific-api';
|
|
specificConfig.priority = 10; // Higher priority
|
|
|
|
router.updateRoutes([genericConfig, specificConfig]);
|
|
|
|
// The more specific '/api/users' should match before the '/api/*' wildcard
|
|
const req = createMockRequest(TEST_DOMAIN, '/api/users');
|
|
const result = router.routeReq(req);
|
|
|
|
expect(result).toEqual(specificConfig);
|
|
});
|
|
|
|
// Test multiple hostnames
|
|
tap.test('should handle multiple configured hostnames', async () => {
|
|
const routes = [
|
|
createRouteConfig(TEST_DOMAIN),
|
|
createRouteConfig(TEST_SUBDOMAIN)
|
|
];
|
|
router.updateRoutes(routes);
|
|
|
|
// Test first domain routes correctly
|
|
const req1 = createMockRequest(TEST_DOMAIN);
|
|
const result1 = router.routeReq(req1);
|
|
expect(result1).toEqual(routes[0]);
|
|
|
|
// Test second domain routes correctly
|
|
const req2 = createMockRequest(TEST_SUBDOMAIN);
|
|
const result2 = router.routeReq(req2);
|
|
expect(result2).toEqual(routes[1]);
|
|
});
|
|
|
|
// Test handling missing host header
|
|
tap.test('should handle missing host header', async () => {
|
|
const defaultConfig = createRouteConfig('*');
|
|
router.updateRoutes([defaultConfig]);
|
|
|
|
const req = createMockRequest('');
|
|
req.headers.host = undefined;
|
|
|
|
const result = router.routeReq(req);
|
|
|
|
expect(result).toEqual(defaultConfig);
|
|
});
|
|
|
|
// Test complex path parameters
|
|
tap.test('should handle complex path parameters', async () => {
|
|
const config = createRouteConfig(TEST_DOMAIN);
|
|
config.match.path = '/api/:version/users/:userId/posts/:postId';
|
|
router.updateRoutes([config]);
|
|
|
|
const req = createMockRequest(TEST_DOMAIN, '/api/v1/users/123/posts/456');
|
|
const result = router.routeReqWithDetails(req);
|
|
|
|
expect(result).toBeTruthy();
|
|
expect(result.route).toEqual(config);
|
|
expect(result.pathParams).toBeTruthy();
|
|
expect(result.pathParams.version).toEqual('v1');
|
|
expect(result.pathParams.userId).toEqual('123');
|
|
expect(result.pathParams.postId).toEqual('456');
|
|
});
|
|
|
|
// Performance test
|
|
tap.test('should handle many configurations efficiently', async () => {
|
|
const configs = [];
|
|
|
|
// Create many configs with different hostnames
|
|
for (let i = 0; i < 100; i++) {
|
|
configs.push(createRouteConfig(`host-${i}.example.com`));
|
|
}
|
|
|
|
router.updateRoutes(configs);
|
|
|
|
// Test middle of the list to avoid best/worst case
|
|
const req = createMockRequest('host-50.example.com');
|
|
const result = router.routeReq(req);
|
|
|
|
expect(result).toEqual(configs[50]);
|
|
});
|
|
|
|
// Test cleanup
|
|
tap.test('cleanup proxy router test environment', async () => {
|
|
// Clear all configurations
|
|
router.updateRoutes([]);
|
|
|
|
// Verify empty state by testing that no routes match
|
|
const req = createMockRequest(TEST_DOMAIN);
|
|
const result = router.routeReq(req);
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
export default tap.start(); |