392 lines
12 KiB
TypeScript
392 lines
12 KiB
TypeScript
import { expect, tap } from '@push.rocks/tapbundle';
|
|
import * as tsclass from '@tsclass/tsclass';
|
|
import * as http from 'http';
|
|
import { ProxyRouter, type IRouterResult } from '../ts/classes.router.js';
|
|
|
|
// Test proxies and configurations
|
|
let router: ProxyRouter;
|
|
|
|
// 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 proxy configuration
|
|
function createProxyConfig(
|
|
hostname: string,
|
|
destinationIp: string = '10.0.0.1',
|
|
destinationPort: number = 8080
|
|
): tsclass.network.IReverseProxyConfig {
|
|
return {
|
|
hostName: hostname,
|
|
destinationIp,
|
|
destinationPort: destinationPort.toString(), // Convert to string for IReverseProxyConfig
|
|
publicKey: 'mock-cert',
|
|
privateKey: 'mock-key'
|
|
} as tsclass.network.IReverseProxyConfig;
|
|
}
|
|
|
|
// SETUP: Create a ProxyRouter instance
|
|
tap.test('setup proxy router test environment', async () => {
|
|
router = new ProxyRouter();
|
|
|
|
// Initialize with empty config
|
|
router.setNewProxyConfigs([]);
|
|
});
|
|
|
|
// Test basic routing by hostname
|
|
tap.test('should route requests by hostname', async () => {
|
|
const config = createProxyConfig(TEST_DOMAIN);
|
|
router.setNewProxyConfigs([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 = createProxyConfig(TEST_DOMAIN);
|
|
router.setNewProxyConfigs([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 = createProxyConfig(TEST_DOMAIN.toLowerCase());
|
|
router.setNewProxyConfigs([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 = createProxyConfig(TEST_DOMAIN);
|
|
router.setNewProxyConfigs([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 = createProxyConfig(TEST_DOMAIN);
|
|
router.setNewProxyConfigs([config]);
|
|
|
|
// Add a path pattern to the config
|
|
router.setPathPattern(config, '/api/users');
|
|
|
|
// Test that path matches
|
|
const req1 = createMockRequest(TEST_DOMAIN, '/api/users');
|
|
const result1 = router.routeReqWithDetails(req1);
|
|
|
|
expect(result1).toBeTruthy();
|
|
expect(result1.config).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 = createProxyConfig(TEST_DOMAIN);
|
|
router.setNewProxyConfigs([config]);
|
|
|
|
router.setPathPattern(config, '/api/*');
|
|
|
|
// 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.config).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 = createProxyConfig(TEST_DOMAIN);
|
|
router.setNewProxyConfigs([config]);
|
|
|
|
router.setPathPattern(config, '/users/:id/profile');
|
|
|
|
const req = createMockRequest(TEST_DOMAIN, '/users/123/profile');
|
|
const result = router.routeReqWithDetails(req);
|
|
|
|
expect(result).toBeTruthy();
|
|
expect(result.config).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 = createProxyConfig(TEST_DOMAIN, '10.0.0.1', 8001);
|
|
const webConfig = createProxyConfig(TEST_DOMAIN, '10.0.0.2', 8002);
|
|
|
|
// Add both configs
|
|
router.setNewProxyConfigs([apiConfig, webConfig]);
|
|
|
|
// Set different path patterns
|
|
router.setPathPattern(apiConfig, '/api');
|
|
router.setPathPattern(webConfig, '/web');
|
|
|
|
// 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 = createProxyConfig(TEST_WILDCARD);
|
|
router.setNewProxyConfigs([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 = createProxyConfig('example.*');
|
|
router.setNewProxyConfigs([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 = createProxyConfig('*.lossless*');
|
|
router.setNewProxyConfigs([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 = createProxyConfig('*');
|
|
const specificConfig = createProxyConfig(TEST_DOMAIN);
|
|
|
|
router.setNewProxyConfigs([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 = createProxyConfig(TEST_WILDCARD);
|
|
const exactConfig = createProxyConfig(TEST_SUBDOMAIN);
|
|
|
|
router.setNewProxyConfigs([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.setNewProxyConfigs([]);
|
|
|
|
// Add a config
|
|
const config = createProxyConfig(TEST_DOMAIN);
|
|
router.addProxyConfig(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
|
|
const removed = router.removeProxyConfig(TEST_DOMAIN);
|
|
expect(removed).toBeTrue();
|
|
|
|
result = router.routeReq(req);
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
// Test path pattern specificity
|
|
tap.test('should prioritize more specific path patterns', async () => {
|
|
const genericConfig = createProxyConfig(TEST_DOMAIN, '10.0.0.1', 8001);
|
|
const specificConfig = createProxyConfig(TEST_DOMAIN, '10.0.0.2', 8002);
|
|
|
|
router.setNewProxyConfigs([genericConfig, specificConfig]);
|
|
|
|
router.setPathPattern(genericConfig, '/api/*');
|
|
router.setPathPattern(specificConfig, '/api/users');
|
|
|
|
// 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 getHostnames method
|
|
tap.test('should retrieve all configured hostnames', async () => {
|
|
router.setNewProxyConfigs([
|
|
createProxyConfig(TEST_DOMAIN),
|
|
createProxyConfig(TEST_SUBDOMAIN)
|
|
]);
|
|
|
|
const hostnames = router.getHostnames();
|
|
|
|
expect(hostnames.length).toEqual(2);
|
|
expect(hostnames).toContain(TEST_DOMAIN.toLowerCase());
|
|
expect(hostnames).toContain(TEST_SUBDOMAIN.toLowerCase());
|
|
});
|
|
|
|
// Test handling missing host header
|
|
tap.test('should handle missing host header', async () => {
|
|
const defaultConfig = createProxyConfig('*');
|
|
router.setNewProxyConfigs([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 = createProxyConfig(TEST_DOMAIN);
|
|
router.setNewProxyConfigs([config]);
|
|
|
|
router.setPathPattern(config, '/api/:version/users/:userId/posts/:postId');
|
|
|
|
const req = createMockRequest(TEST_DOMAIN, '/api/v1/users/123/posts/456');
|
|
const result = router.routeReqWithDetails(req);
|
|
|
|
expect(result).toBeTruthy();
|
|
expect(result.config).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(createProxyConfig(`host-${i}.example.com`));
|
|
}
|
|
|
|
router.setNewProxyConfigs(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.setNewProxyConfigs([]);
|
|
|
|
// Verify empty state
|
|
expect(router.getHostnames().length).toEqual(0);
|
|
expect(router.getProxyConfigs().length).toEqual(0);
|
|
});
|
|
|
|
export default tap.start(); |