/** * Network Proxy 测试(v0.7.2 覆盖补齐 —— 此前零测试) * * 锁定 session 级代理应用契约(v0.6.4 P4-5 的回归防线): * 1. 配置 proxyUrl → Chromium 双分区(default + agent-browser)+ undici ProxyAgent * 2. 环境变量回退(HTTPS_PROXY/HTTP_PROXY) * 3. 双空 → 显式直连(direct mode + 直连 Agent) * 4. 失败语义:任一通道失败仅 WARN 不阻断主流程 */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; const sessionMocks = vi.hoisted(() => ({ defaultSetProxy: vi.fn(async (..._args: unknown[]) => undefined), partitionSetProxy: vi.fn(async (..._args: unknown[]) => undefined), throwOnDefaultSetProxy: false, throwOnPartitionSetProxy: false, })); vi.mock('electron', () => ({ session: { defaultSession: { setProxy: (...args: unknown[]) => { if (sessionMocks.throwOnDefaultSetProxy) { return Promise.reject(new Error('setProxy exploded')); } return sessionMocks.defaultSetProxy(...args); }, }, fromPartition: () => ({ setProxy: (...args: unknown[]) => { if (sessionMocks.throwOnPartitionSetProxy) { return Promise.reject(new Error('partition setProxy exploded')); } return sessionMocks.partitionSetProxy(...args); }, }), }, })); vi.mock('electron-log', () => ({ default: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, })); const undiciMocks = vi.hoisted(() => ({ setGlobalDispatcher: vi.fn(), proxyAgentCalls: [] as string[], agentCalls: 0, })); vi.mock('undici', () => ({ Agent: class { constructor() { undiciMocks.agentCalls++; } }, ProxyAgent: class { constructor(opts: { uri: string }) { undiciMocks.proxyAgentCalls.push(opts.uri); } }, setGlobalDispatcher: (...args: unknown[]) => undiciMocks.setGlobalDispatcher(...args), })); import { applySessionProxy, isProxyActive } from '../network-proxy'; beforeEach(() => { sessionMocks.defaultSetProxy.mockClear(); sessionMocks.partitionSetProxy.mockClear(); sessionMocks.throwOnDefaultSetProxy = false; sessionMocks.throwOnPartitionSetProxy = false; undiciMocks.setGlobalDispatcher.mockClear(); undiciMocks.proxyAgentCalls.length = 0; undiciMocks.agentCalls = 0; delete process.env['HTTPS_PROXY']; delete process.env['HTTP_PROXY']; }); afterEach(() => { delete process.env['HTTPS_PROXY']; delete process.env['HTTP_PROXY']; }); describe('applySessionProxy — 双通道应用', () => { it('配置代理 → default + agent-browser 分区均设置 proxyRules,undici 走 ProxyAgent', async () => { await applySessionProxy('http://127.0.0.1:7890'); const expected = { proxyRules: 'http://127.0.0.1:7890' }; expect(sessionMocks.defaultSetProxy).toHaveBeenCalledWith(expected); expect(sessionMocks.partitionSetProxy).toHaveBeenCalledWith(expected); expect(undiciMocks.proxyAgentCalls).toEqual(['http://127.0.0.1:7890']); expect(undiciMocks.setGlobalDispatcher).toHaveBeenCalledTimes(1); }); it('代理地址两侧空白被 trim', async () => { await applySessionProxy(' http://proxy.local:8080 '); expect(sessionMocks.defaultSetProxy).toHaveBeenCalledWith({ proxyRules: 'http://proxy.local:8080', }); }); }); describe('applySessionProxy — 环境变量回退链', () => { it('未配置 proxyUrl 时回退 HTTPS_PROXY 环境变量', async () => { process.env['HTTPS_PROXY'] = 'http://env-proxy:3128'; await applySessionProxy(null); expect(sessionMocks.defaultSetProxy).toHaveBeenCalledWith({ proxyRules: 'http://env-proxy:3128', }); }); it('HTTPS_PROXY 缺失时回退 HTTP_PROXY', async () => { process.env['HTTP_PROXY'] = 'http://env-http:3128'; await applySessionProxy(undefined); expect(sessionMocks.defaultSetProxy).toHaveBeenCalledWith({ proxyRules: 'http://env-http:3128', }); }); it('双空 → 显式直连(direct mode + 直连 Agent)', async () => { await applySessionProxy(null); expect(sessionMocks.defaultSetProxy).toHaveBeenCalledWith({ mode: 'direct' }); expect(undiciMocks.agentCalls).toBe(1); // 直连 Agent expect(undiciMocks.proxyAgentCalls).toHaveLength(0); }); it('配置空串视为未配置(回退环境变量)', async () => { process.env['HTTPS_PROXY'] = 'http://env-fallback:1'; await applySessionProxy(' '); expect(sessionMocks.defaultSetProxy).toHaveBeenCalledWith({ proxyRules: 'http://env-fallback:1', }); }); it('proxyUrl 与 HTTPS_PROXY 同时存在 → proxyUrl 优先(显式配置覆盖环境变量)', async () => { process.env['HTTPS_PROXY'] = 'http://env-ignored:3128'; await applySessionProxy('http://explicit:8080'); expect(sessionMocks.defaultSetProxy).toHaveBeenCalledWith({ proxyRules: 'http://explicit:8080', }); }); it('HTTPS_PROXY 为空串时回退 HTTP_PROXY', async () => { process.env['HTTPS_PROXY'] = ''; process.env['HTTP_PROXY'] = 'http://fallback:3128'; await applySessionProxy(null); expect(sessionMocks.defaultSetProxy).toHaveBeenCalledWith({ proxyRules: 'http://fallback:3128', }); }); }); describe('applySessionProxy — 失败语义(绝不阻断主流程)', () => { it('Chromium 通道失败 → 仅 WARN,undici 通道照常设置', async () => { sessionMocks.throwOnDefaultSetProxy = true; await expect(applySessionProxy('http://proxy:1')).resolves.toBeUndefined(); expect(undiciMocks.proxyAgentCalls).toEqual(['http://proxy:1']); }); it('agent-browser 分区失败 → 仅 WARN,default 与 undici 不受影响', async () => { sessionMocks.throwOnPartitionSetProxy = true; await expect(applySessionProxy('http://proxy:2')).resolves.toBeUndefined(); expect(sessionMocks.defaultSetProxy).toHaveBeenCalledWith({ proxyRules: 'http://proxy:2', }); expect(undiciMocks.proxyAgentCalls).toEqual(['http://proxy:2']); }); it('双通道均失败 → 不抛错(resolve undefined)', async () => { sessionMocks.throwOnDefaultSetProxy = true; sessionMocks.throwOnPartitionSetProxy = true; await expect(applySessionProxy('http://proxy:3')).resolves.toBeUndefined(); }); }); describe('isProxyActive — 代理激活标志(P2-1)', () => { it('配置代理后标志为 true', async () => { await applySessionProxy('http://proxy:8080'); expect(isProxyActive()).toBe(true); }); it('回退环境变量后标志为 true', async () => { process.env['HTTPS_PROXY'] = 'http://env:3128'; await applySessionProxy(null); expect(isProxyActive()).toBe(true); }); it('双空直连后标志为 false', async () => { await applySessionProxy(null); expect(isProxyActive()).toBe(false); }); it('先激活再切回直连 → 标志复位为 false', async () => { await applySessionProxy('http://proxy:1'); expect(isProxyActive()).toBe(true); await applySessionProxy(null); expect(isProxyActive()).toBe(false); }); });