fokemal / server /src /__tests__ /lib /proxy-restore.test.ts
automindy's picture
Sync to upstream v0.13.7, keep HF Space adaptations
dcedb25
Raw History Blame Contribute Delete
9.7 kB
import { describe, it, expect, vi, beforeEach, afterAll } from 'vitest';
import { initDb, getDb, setSetting } from '../../db/index.js';
import {
restoreProxySettings,
getProxyUrl,
getProxyMode,
getFetchRelayToken,
isProxyEnabled,
getProxyBypassPlatforms,
getNoProxyRules,
applyProxyUrl,
applyProxyMode,
applyProxyEnabled,
applyProxyBypass,
applyFetchRelayToken,
encodeFetchRelayToken,
} from '../../lib/proxy.js';
// #838 reads the OS-wide proxy (scutil/reg/gsettings) as a last-resort
// fallback when neither the env nor the DB configures one. A developer
// machine can genuinely have that set (macOS System Settings → Proxies),
// which fails every "no proxy configured" case below for the same reason
// clearProxyEnv() exists — the ambient system, not the code under test.
// '' parses as "no proxy" in every platform branch, so the suite sees a
// clean machine regardless of the host's system settings.
vi.mock('node:child_process', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
execFileSync: () => '',
}));
// #949: the desktop embedder builds the app without server/src/index.ts, so
// the proxy state it starts with is whatever the module defaults are — an
// empty URL. The URL the user saved through PUT /api/settings/proxy sits in
// the settings table, ignored until the next re-save. restoreProxySettings()
// is the single hydration step both entry points now call after initDb; this
// test pins that the DB value actually reaches the process state.
const PROXY_ENV_VARS = ['PROXY_URL', 'PROXY_MODE', 'FETCH_RELAY_TOKEN', 'ALL_PROXY', 'HTTPS_PROXY', 'HTTP_PROXY', 'NO_PROXY'];
function clearProxyEnv(): void {
for (const name of PROXY_ENV_VARS) {
delete process.env[name];
delete process.env[name.toLowerCase()];
}
}
let closed = false;
beforeEach(() => {
clearProxyEnv();
// Reset to the module defaults so each case starts from "fresh process".
applyProxyUrl('');
applyProxyMode('forward');
applyFetchRelayToken('');
applyProxyEnabled(true);
applyProxyBypass('');
});
afterAll(() => {
if (!closed) {
getDb().close();
closed = true;
}
});
describe('restoreProxySettings (desktop embedder hydration, #949)', () => {
it('loads a saved proxy URL, enabled flag and bypass list from the settings table', () => {
process.env.ENCRYPTION_KEY = '0'.repeat(64);
initDb(':memory:');
setSetting('proxy_url', 'socks5h://127.0.0.1:9050');
setSetting('proxy_enabled', '1');
setSetting('proxy_bypass', 'groq,openrouter');
restoreProxySettings();
expect(getProxyUrl()).toBe('socks5h://127.0.0.1:9050');
expect(isProxyEnabled()).toBe(true);
expect(getProxyBypassPlatforms()).toEqual(['groq', 'openrouter']);
});
it('respects a disabled proxy saved across a restart', () => {
process.env.ENCRYPTION_KEY = '0'.repeat(64);
initDb(':memory:');
setSetting('proxy_url', 'http://127.0.0.1:3128');
setSetting('proxy_enabled', '0');
restoreProxySettings();
expect(getProxyUrl()).toBe('http://127.0.0.1:3128');
expect(isProxyEnabled()).toBe(false);
});
it('keeps the defaults when nothing was ever saved', () => {
process.env.ENCRYPTION_KEY = '0'.repeat(64);
initDb(':memory:');
restoreProxySettings();
expect(getProxyUrl()).toBe('');
expect(getProxyMode()).toBe('forward');
expect(isProxyEnabled()).toBe(true);
expect(getProxyBypassPlatforms()).toEqual([]);
});
it('lets the PROXY_URL env var outrank the saved value, as before', () => {
process.env.ENCRYPTION_KEY = '0'.repeat(64);
initDb(':memory:');
setSetting('proxy_url', 'http://saved:3128');
process.env.PROXY_URL = 'http://env:8080';
restoreProxySettings();
expect(getProxyUrl()).toBe('http://env:8080');
expect(getProxyMode()).toBe('forward');
});
it('restores an explicitly saved Fetch Relay mode', () => {
process.env.ENCRYPTION_KEY = '0'.repeat(64);
initDb(':memory:');
setSetting('proxy_url', 'https://relay.example.test/secret');
setSetting('proxy_mode', 'fetch-relay');
restoreProxySettings();
expect(getProxyUrl()).toBe('https://relay.example.test/secret');
expect(getProxyMode()).toBe('fetch-relay');
});
it('restores a saved Relay token and lets FETCH_RELAY_TOKEN override it', () => {
process.env.ENCRYPTION_KEY = '0'.repeat(64);
initDb(':memory:');
setSetting('fetch_relay_token', encodeFetchRelayToken('saved-token'));
restoreProxySettings();
expect(getFetchRelayToken()).toBe('saved-token');
process.env.FETCH_RELAY_TOKEN = 'environment-token';
restoreProxySettings();
expect(getFetchRelayToken()).toBe('environment-token');
});
it('lets PROXY_MODE opt an environment PROXY_URL into Fetch Relay', () => {
process.env.ENCRYPTION_KEY = '0'.repeat(64);
initDb(':memory:');
process.env.PROXY_URL = 'https://relay.example.test/secret';
process.env.PROXY_MODE = 'fetch-relay';
restoreProxySettings();
expect(getProxyMode()).toBe('fetch-relay');
});
});
// The env tiers below are resolveProxySource()'s job, but restore time is the
// only moment they are consulted on a desktop start: the embedder hydrates
// once and then nothing re-reads the environment. A regression that skipped a
// tier here would strand a user who has only ever exported HTTPS_PROXY.
describe('restoreProxySettings + the standard env fallbacks (#353 x #949)', () => {
function freshDb(): void {
process.env.ENCRYPTION_KEY = '0'.repeat(64);
initDb(':memory:');
}
it('falls back to ALL_PROXY when nothing was saved in the dashboard', () => {
freshDb();
process.env.ALL_PROXY = 'socks5://all:1080';
restoreProxySettings();
expect(getProxyUrl()).toBe('socks5://all:1080');
});
it('falls back to HTTPS_PROXY', () => {
freshDb();
process.env.HTTPS_PROXY = 'http://https-tier:8443';
restoreProxySettings();
expect(getProxyUrl()).toBe('http://https-tier:8443');
});
it('falls back to HTTP_PROXY', () => {
freshDb();
process.env.HTTP_PROXY = 'http://http-tier:8080';
restoreProxySettings();
expect(getProxyUrl()).toBe('http://http-tier:8080');
});
it('prefers ALL_PROXY, then HTTPS_PROXY, then HTTP_PROXY', () => {
freshDb();
process.env.ALL_PROXY = 'http://all:1';
process.env.HTTPS_PROXY = 'http://https:2';
process.env.HTTP_PROXY = 'http://http:3';
restoreProxySettings();
expect(getProxyUrl()).toBe('http://all:1');
});
it('reads the lower-case spelling too — the one curl/git users actually export', () => {
freshDb();
process.env.https_proxy = 'http://lower:8443';
restoreProxySettings();
expect(getProxyUrl()).toBe('http://lower:8443');
});
it('does NOT let the ambient env override a proxy the user typed into the dashboard', () => {
freshDb();
setSetting('proxy_url', 'http://saved:3128');
process.env.ALL_PROXY = 'http://ambient:1080';
process.env.HTTPS_PROXY = 'http://ambient:8443';
process.env.HTTP_PROXY = 'http://ambient:8080';
restoreProxySettings();
expect(getProxyUrl()).toBe('http://saved:3128');
});
it('still lets PROXY_URL outrank the ambient vars', () => {
freshDb();
process.env.PROXY_URL = 'http://explicit:9090';
process.env.ALL_PROXY = 'http://ambient:1080';
restoreProxySettings();
expect(getProxyUrl()).toBe('http://explicit:9090');
});
it('leaves the proxy empty when neither the DB nor any env var has one', () => {
freshDb();
restoreProxySettings();
expect(getProxyUrl()).toBe('');
});
});
// NO_PROXY is read from the environment inside applyProxyUrl, so on the
// desktop path it is parsed exactly once: during restore. If restore stopped
// going through applyProxyUrl the saved URL might still land while the direct
// list silently emptied, quietly proxying hosts the user excluded.
describe('restoreProxySettings + NO_PROXY (#949)', () => {
function freshDb(): void {
process.env.ENCRYPTION_KEY = '0'.repeat(64);
initDb(':memory:');
}
it('parses NO_PROXY at restore time, lower-cased and trimmed', () => {
freshDb();
setSetting('proxy_url', 'http://127.0.0.1:3128');
process.env.NO_PROXY = 'localhost, .Internal.Corp ,, 10.0.0.1';
restoreProxySettings();
expect(getNoProxyRules()).toEqual(['localhost', '.internal.corp', '10.0.0.1']);
});
it('normalises the `*.domain` spelling to the leading-dot form', () => {
freshDb();
setSetting('proxy_url', 'http://127.0.0.1:3128');
process.env.NO_PROXY = '*.example.com';
restoreProxySettings();
expect(getNoProxyRules()).toEqual(['.example.com']);
});
it('honours the lower-case no_proxy spelling', () => {
freshDb();
setSetting('proxy_url', 'http://127.0.0.1:3128');
process.env.no_proxy = 'example.com';
restoreProxySettings();
expect(getNoProxyRules()).toEqual(['example.com']);
});
it('applies NO_PROXY even when the proxy itself came from the env tier', () => {
freshDb();
process.env.HTTPS_PROXY = 'http://ambient:8443';
process.env.NO_PROXY = 'internal.corp';
restoreProxySettings();
expect(getProxyUrl()).toBe('http://ambient:8443');
expect(getNoProxyRules()).toEqual(['internal.corp']);
});
it('clears stale rules when the env no longer sets NO_PROXY', () => {
freshDb();
setSetting('proxy_url', 'http://127.0.0.1:3128');
process.env.NO_PROXY = 'example.com';
restoreProxySettings();
expect(getNoProxyRules()).toEqual(['example.com']);
delete process.env.NO_PROXY;
restoreProxySettings();
expect(getNoProxyRules()).toEqual([]);
});
});