import assert from "node:assert/strict"; import { readFileSync } from 'node:fs'; import test from "node:test"; const { buildSharedProxyInfo, } = await import("../../dist/server/sharedProxy.js"); const { createSharedProxyRoute, } = await import('../../dist/server/http/routes/sharedProxyRoute.js'); function response() { return { writeHead(status, headers) { this.status = status; this.headers = headers; }, end(payload) { this.rawPayload = payload; this.payload = JSON.parse(payload); }, }; } function createHarness(overrides = {}) { const events = []; const refreshError = overrides.refreshError; const route = createSharedProxyRoute({ appMode: overrides.appMode ?? 'gateway', proxyPort: overrides.proxyPort ?? 8080, sharedProxyHost: overrides.sharedProxyHost ?? '', refreshRuntime: async () => { events.push('refresh'); if (refreshError) throw refreshError; return { running: overrides.running ?? true }; }, }); return { events, route }; } test("gateway shared proxy info exposes host and socks proxy when running", () => { const info = buildSharedProxyInfo({ appMode: "gateway", proxyPort: 8080, running: true, hostHeader: "192.168.50.111:3456", }); assert.equal(info.available, true); assert.deepEqual(info.proxy, { host: "192.168.50.111", port: 8080, protocol: "socks5", httpUrl: "http://192.168.50.111:8080", socksUrl: "socks5://192.168.50.111:8080", }); }); test('shared proxy builder preserves host precedence, IPv6, port and unavailable quirks', () => { const configured = buildSharedProxyInfo({ appMode: 'gateway', proxyPort: '9000px', running: true, hostHeader: 'ignored.example:1234', sharedProxyHost: ' proxy.example ', }); assert.deepEqual(configured.proxy, { host: 'proxy.example', port: 9000, protocol: 'socks5', httpUrl: 'http://proxy.example:9000', socksUrl: 'socks5://proxy.example:9000', }); const ipv6 = buildSharedProxyInfo({ appMode: 'gateway', proxyPort: 1080, running: true, hostHeader: '[2001:db8::1]:443', }); assert.equal(ipv6.proxy.host, '2001:db8::1'); assert.equal(buildSharedProxyInfo({ appMode: 'gateway', proxyPort: 1080, running: true, hostHeader: '', }).available, ''); assert.equal(buildSharedProxyInfo({ appMode: 'gateway', proxyPort: 65536, running: true, hostHeader: 'gateway.local', }).available, false); }); test('shared proxy route refreshes once before reading Host and preserves raw JSON response', async () => { const harness = createHarness(); const res = response(); const headers = {}; Object.defineProperty(headers, 'host', { get() { harness.events.push('host'); return '192.168.50.111:3456'; }, }); assert.equal(await harness.route.handle({ method: 'GET', url: '/api/shared-proxy', headers, }, res), true); assert.deepEqual(harness.events, ['refresh', 'host']); assert.equal(res.status, 200); assert.equal(res.headers['content-type'], 'application/json; charset=utf-8'); assert.equal(res.rawPayload.endsWith('\n'), false); assert.deepEqual(res.payload, { success: true, available: true, mode: 'gateway', proxy: { host: '192.168.50.111', port: 8080, protocol: 'socks5', httpUrl: 'http://192.168.50.111:8080', socksUrl: 'socks5://192.168.50.111:8080', }, }); }); test('shared proxy route still refreshes for client and unavailable responses', async () => { for (const options of [ { appMode: 'client' }, { running: false }, { proxyPort: 'invalid' }, ]) { const harness = createHarness(options); const res = response(); await harness.route.handle({ method: 'GET', url: '/api/shared-proxy', headers: { host: 'gateway.local' }, }, res); assert.deepEqual(harness.events, ['refresh']); assert.equal(res.payload.available, false); assert.equal(res.payload.proxy, null); } }); test('shared proxy route exact guard avoids refresh and errors propagate unchanged', async () => { for (const [method, url] of [ ['POST', '/api/shared-proxy'], ['GET', '/api/shared-proxy?source=client'], ['GET', '/api/shared-proxy/'], ['GET', '/api/other'], ]) { const harness = createHarness(); assert.equal(await harness.route.handle({ method, url, headers: {} }, response()), false); assert.deepEqual(harness.events, []); } const refreshError = new Error('refresh failed'); const brokenRefresh = createHarness({ refreshError }); await assert.rejects( brokenRefresh.route.handle({ method: 'GET', url: '/api/shared-proxy', headers: { host: 'gateway.local' }, }, response()), (error) => error === refreshError, ); const builderError = new Error('builder failed'); const brokenBuilder = createHarness({ sharedProxyHost: { toString() { throw builderError; } }, }); await assert.rejects( brokenBuilder.route.handle({ method: 'GET', url: '/api/shared-proxy', headers: { host: 'gateway.local' }, }, response()), (error) => error === builderError, ); }); test('shared proxy route is the sole HTTP owner', () => { const index = readFileSync(new URL('../../src/server/index.ts', import.meta.url), 'utf8'); const route = readFileSync( new URL('../../src/server/http/routes/sharedProxyRoute.ts', import.meta.url), 'utf8', ); assert.match(index, /createSharedProxyRoute\(\{/); assert.match(index, /sharedProxyRoute\.handle\(req, res\)/); assert.doesNotMatch(index, /['"]\/api\/shared-proxy['"]|buildSharedProxyInfo/); assert.match(route, /req\.url !== '\/api\/shared-proxy'/); assert.match(route, /buildSharedProxyInfo\(\{/); });