/** * 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, })); 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[]) => 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 } from '../network-proxy'; beforeEach(() => { sessionMocks.defaultSetProxy.mockClear(); sessionMocks.partitionSetProxy.mockClear(); sessionMocks.throwOnDefaultSetProxy = 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', }); }); }); 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']); }); });