import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; import test from 'node:test'; const singboxUrl = pathToFileURL(path.resolve('dist/server/singbox.js')).href; const subscriptionConfig = { outbounds: [{ type: 'vless', tag: 'vpn', server: 'vpn.example.test', server_port: 443, uuid: '00000000-0000-4000-8000-000000000000', tls: { enabled: true }, }], }; function run(source, { component = 'dataplane', socket = '/tmp/harbor-test.sock' } = {}) { const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-gateway-native-config-')); const script = ` const { buildGatewayConfig, buildDualChannelGatewayConfig } = await import(${JSON.stringify(singboxUrl)}); const subscription = ${JSON.stringify(subscriptionConfig)}; process.stdout.write(JSON.stringify({ single: buildGatewayConfig(subscription, 'vpn'), dual: buildDualChannelGatewayConfig({ primary: { subscriptionConfig: subscription, selectedServerId: 'vpn' }, reserve: { subscriptionConfig: subscription, selectedServerId: 'vpn' }, }), })); `; try { return spawnSync(process.execPath, ['--input-type=module', '--eval', script], { cwd: path.resolve('.'), encoding: 'utf8', env: { ...process.env, APP_MODE: 'gateway', APP_COMPONENT: component, DATA_DIR: directory, SING_BOX_CACHE: path.join(directory, 'cache.db'), SING_BOX_TRAFFIC_SOURCE: source, DATAPLANE_SOCKET: socket, }, }); } finally { fs.rmSync(directory, { recursive: true, force: true }); } } test('Gateway snapshot keeps the 1.13-compatible config and Clash traffic API', () => { const result = run('snapshot', { component: '', socket: '' }); assert.equal(result.status, 0, result.stderr); const configs = JSON.parse(result.stdout); for (const config of Object.values(configs)) { assert.equal(config.services, undefined); assert.deepEqual(config.dns, { independent_cache: true }); assert.deepEqual(config.experimental.clash_api, { external_controller: '127.0.0.1:19090' }); } }); test('Gateway shadow and native add one secret-free loopback API to single and dual configs', () => { for (const source of ['shadow', 'native']) { const result = run(source); assert.equal(result.status, 0, result.stderr); const configs = JSON.parse(result.stdout); for (const config of Object.values(configs)) { assert.deepEqual(config.services, [{ type: 'api', listen: '127.0.0.1', listen_port: 19091, dashboard: false, }]); assert.equal(JSON.stringify(config).includes('secret'), false); assert.deepEqual(config.dns, {}); assert.deepEqual(config.experimental.clash_api, { external_controller: '127.0.0.1:19090' }); } } }); test('Gateway shadow and native reject combined or socket-less topology', () => { for (const options of [ { component: '', socket: '' }, { component: 'control', socket: '' }, { component: 'dataplane', socket: '' }, ]) { const result = run('native', options); assert.notEqual(result.status, 0); assert.match(result.stderr, /require split control\/dataplane topology/); } }); test('Gateway rejects unknown traffic sources', () => { const result = run('disabled'); assert.notEqual(result.status, 0); assert.match(result.stderr, /must be snapshot, shadow or native/); });