test(cliproxy): add comprehensive proxy support unit tests

Add 34 unit tests covering:
- getProxyUrl: env var precedence (lowercase > uppercase > all_proxy)
- shouldBypassProxy: wildcard, exact match, suffix patterns, case-insensitivity
- getHostname: URL parsing with error handling
- getProxyAgent: proxy creation, NO_PROXY bypass, error handling

Export internal functions via __testExports for testability.
This commit is contained in:
kaitranntt
2026-02-04 17:05:24 -05:00
parent bcde5f4878
commit 713ee93606
2 changed files with 268 additions and 0 deletions
+8
View File
@@ -539,3 +539,11 @@ export async function fetchJson(
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Export internal functions for testing
export const __testExports = {
getProxyUrl,
shouldBypassProxy,
getHostname,
getProxyAgent,
};
@@ -0,0 +1,260 @@
/**
* Binary Downloader Proxy Support Tests
*
* Tests for proxy environment variable detection and NO_PROXY bypass logic.
*/
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
import { __testExports } from '../../../src/cliproxy/binary/downloader';
const { getProxyUrl, shouldBypassProxy, getHostname, getProxyAgent } = __testExports;
describe('Binary Downloader Proxy Support', () => {
// Store original env vars to restore after tests
const originalEnv: Record<string, string | undefined> = {};
const proxyEnvVars = [
'http_proxy',
'HTTP_PROXY',
'https_proxy',
'HTTPS_PROXY',
'all_proxy',
'ALL_PROXY',
'no_proxy',
'NO_PROXY',
];
beforeEach(() => {
// Save original values
proxyEnvVars.forEach((key) => {
originalEnv[key] = process.env[key];
delete process.env[key];
});
});
afterEach(() => {
// Restore original values
proxyEnvVars.forEach((key) => {
if (originalEnv[key] !== undefined) {
process.env[key] = originalEnv[key];
} else {
delete process.env[key];
}
});
});
describe('getProxyUrl', () => {
describe('HTTPS requests', () => {
it('should return undefined when no proxy env vars are set', () => {
expect(getProxyUrl(true)).toBeUndefined();
});
it('should prefer https_proxy (lowercase)', () => {
process.env.https_proxy = 'http://proxy1:8080';
process.env.HTTPS_PROXY = 'http://proxy2:8080';
expect(getProxyUrl(true)).toBe('http://proxy1:8080');
});
it('should fall back to HTTPS_PROXY (uppercase)', () => {
process.env.HTTPS_PROXY = 'http://proxy:8080';
expect(getProxyUrl(true)).toBe('http://proxy:8080');
});
it('should fall back to all_proxy', () => {
process.env.all_proxy = 'http://allproxy:8080';
expect(getProxyUrl(true)).toBe('http://allproxy:8080');
});
it('should fall back to ALL_PROXY', () => {
process.env.ALL_PROXY = 'http://allproxy:8080';
expect(getProxyUrl(true)).toBe('http://allproxy:8080');
});
});
describe('HTTP requests', () => {
it('should return undefined when no proxy env vars are set', () => {
expect(getProxyUrl(false)).toBeUndefined();
});
it('should prefer http_proxy (lowercase)', () => {
process.env.http_proxy = 'http://proxy1:8080';
process.env.HTTP_PROXY = 'http://proxy2:8080';
expect(getProxyUrl(false)).toBe('http://proxy1:8080');
});
it('should fall back to HTTP_PROXY (uppercase)', () => {
process.env.HTTP_PROXY = 'http://proxy:8080';
expect(getProxyUrl(false)).toBe('http://proxy:8080');
});
it('should fall back to all_proxy for HTTP', () => {
process.env.all_proxy = 'http://allproxy:8080';
expect(getProxyUrl(false)).toBe('http://allproxy:8080');
});
it('should not use https_proxy for HTTP requests', () => {
process.env.https_proxy = 'http://httpsproxy:8080';
expect(getProxyUrl(false)).toBeUndefined();
});
});
});
describe('shouldBypassProxy', () => {
describe('when NO_PROXY is not set', () => {
it('should return false', () => {
expect(shouldBypassProxy('example.com')).toBe(false);
});
});
describe('wildcard pattern', () => {
it('should bypass all hosts with *', () => {
process.env.NO_PROXY = '*';
expect(shouldBypassProxy('example.com')).toBe(true);
expect(shouldBypassProxy('any.host.here')).toBe(true);
});
});
describe('exact match', () => {
it('should match exact hostname', () => {
process.env.NO_PROXY = 'example.com';
expect(shouldBypassProxy('example.com')).toBe(true);
expect(shouldBypassProxy('other.com')).toBe(false);
});
it('should be case-insensitive', () => {
process.env.NO_PROXY = 'Example.COM';
expect(shouldBypassProxy('example.com')).toBe(true);
expect(shouldBypassProxy('EXAMPLE.COM')).toBe(true);
});
it('should not match subdomains for exact pattern', () => {
process.env.NO_PROXY = 'example.com';
expect(shouldBypassProxy('sub.example.com')).toBe(true); // matches as suffix
expect(shouldBypassProxy('notexample.com')).toBe(false);
});
});
describe('domain suffix pattern', () => {
it('should match subdomains with leading dot', () => {
process.env.NO_PROXY = '.example.com';
expect(shouldBypassProxy('sub.example.com')).toBe(true);
expect(shouldBypassProxy('deep.sub.example.com')).toBe(true);
expect(shouldBypassProxy('example.com')).toBe(true); // exact match without dot
});
it('should not match different domains', () => {
process.env.NO_PROXY = '.example.com';
expect(shouldBypassProxy('notexample.com')).toBe(false);
expect(shouldBypassProxy('example.org')).toBe(false);
});
});
describe('multiple patterns', () => {
it('should check comma-separated list', () => {
process.env.NO_PROXY = 'localhost,127.0.0.1,.internal.corp';
expect(shouldBypassProxy('localhost')).toBe(true);
expect(shouldBypassProxy('127.0.0.1')).toBe(true);
expect(shouldBypassProxy('api.internal.corp')).toBe(true);
expect(shouldBypassProxy('external.com')).toBe(false);
});
it('should handle whitespace in list', () => {
process.env.NO_PROXY = ' localhost , example.com , .corp ';
expect(shouldBypassProxy('localhost')).toBe(true);
expect(shouldBypassProxy('example.com')).toBe(true);
expect(shouldBypassProxy('host.corp')).toBe(true);
});
});
describe('lowercase no_proxy', () => {
it('should respect no_proxy (lowercase)', () => {
process.env.no_proxy = 'example.com';
expect(shouldBypassProxy('example.com')).toBe(true);
});
it('should prefer no_proxy over NO_PROXY', () => {
process.env.no_proxy = 'lower.com';
process.env.NO_PROXY = 'upper.com';
expect(shouldBypassProxy('lower.com')).toBe(true);
expect(shouldBypassProxy('upper.com')).toBe(false);
});
});
});
describe('getHostname', () => {
it('should extract hostname from HTTPS URL', () => {
expect(getHostname('https://example.com/path')).toBe('example.com');
});
it('should extract hostname from HTTP URL', () => {
expect(getHostname('http://api.github.com:443/repos')).toBe('api.github.com');
});
it('should return empty string for invalid URL', () => {
expect(getHostname('not-a-url')).toBe('');
expect(getHostname('')).toBe('');
});
it('should handle localhost', () => {
expect(getHostname('http://localhost:8080')).toBe('localhost');
});
it('should handle IP addresses', () => {
expect(getHostname('http://192.168.1.1:3000')).toBe('192.168.1.1');
});
});
describe('getProxyAgent', () => {
describe('when no proxy is configured', () => {
it('should return false for HTTPS URLs', () => {
expect(getProxyAgent('https://example.com')).toBe(false);
});
it('should return false for HTTP URLs', () => {
expect(getProxyAgent('http://example.com')).toBe(false);
});
});
describe('when proxy is configured', () => {
it('should return HttpsProxyAgent for HTTPS URLs', () => {
process.env.https_proxy = 'http://proxy:8080';
const agent = getProxyAgent('https://github.com/releases');
expect(agent).not.toBe(false);
expect(agent).toBeDefined();
});
it('should return HttpProxyAgent for HTTP URLs', () => {
process.env.http_proxy = 'http://proxy:8080';
const agent = getProxyAgent('http://example.com');
expect(agent).not.toBe(false);
expect(agent).toBeDefined();
});
});
describe('NO_PROXY bypass', () => {
it('should bypass proxy for NO_PROXY hosts', () => {
process.env.https_proxy = 'http://proxy:8080';
process.env.NO_PROXY = 'github.com';
expect(getProxyAgent('https://github.com/releases')).toBe(false);
});
it('should use proxy for non-bypassed hosts', () => {
process.env.https_proxy = 'http://proxy:8080';
process.env.NO_PROXY = 'internal.corp';
const agent = getProxyAgent('https://github.com/releases');
expect(agent).not.toBe(false);
});
});
describe('error handling', () => {
it('should return false for invalid proxy URL', () => {
process.env.https_proxy = 'not-a-valid-url';
expect(getProxyAgent('https://example.com')).toBe(false);
});
it('should return false for invalid target URL', () => {
process.env.https_proxy = 'http://proxy:8080';
expect(getProxyAgent('not-a-url')).toBe(false);
});
});
});
});