Refactor VPN proxy client implementation
Build and Deploy Gateway / build-and-push (push) Failing after 2s
Build and Deploy Gateway / deploy (push) Has been skipped

This commit is contained in:
2026-08-09 00:41:52 +03:00
parent 32be4380a3
commit 34d8b681ad
190 changed files with 20064 additions and 9761 deletions
+155
View File
@@ -0,0 +1,155 @@
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import http from 'node:http';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
const root = path.resolve(import.meta.dirname, '../..');
function listen(server) {
return new Promise((resolve) => server.listen(0, '127.0.0.1', () => resolve(server.address().port)));
}
async function freePort() {
const server = http.createServer();
const port = await listen(server);
await new Promise((resolve) => server.close(resolve));
return port;
}
function socketRequest(socketPath, pathname) {
return new Promise((resolve, reject) => {
const request = http.request({ socketPath, path: pathname }, (response) => {
const chunks = [];
response.on('data', (chunk) => chunks.push(chunk));
response.on('end', () => {
try {
resolve({ status: response.statusCode, body: JSON.parse(Buffer.concat(chunks).toString('utf8')) });
} catch (error) {
reject(error);
}
});
});
request.on('error', reject);
request.end();
});
}
async function httpText(url) {
const response = await fetch(url);
return { status: response.status, type: response.headers.get('content-type'), body: await response.text() };
}
async function waitFor(probe, child, stderr) {
for (let attempt = 0; attempt < 100; attempt += 1) {
if (child.exitCode !== null) throw new Error(`Compiled Harbor exited early: ${stderr()}`);
try {
const result = await probe();
if (result) return result;
} catch {
// The disposable listener is still starting.
}
await new Promise((resolve) => setTimeout(resolve, 20));
}
throw new Error(`Compiled Harbor did not become ready: ${stderr()}`);
}
async function stop(child) {
child.kill('SIGTERM');
if (child.exitCode === null) await new Promise((resolve) => child.once('exit', resolve));
assert.equal(child.exitCode, 0);
}
test('compiled dispatcher starts and stops control and dataplane contracts', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-compiled-'));
const controlData = path.join(directory, 'control');
const dataplaneData = path.join(directory, 'dataplane');
const socketPath = path.join(directory, 'dataplane.sock');
const port = await freePort();
fs.mkdirSync(controlData);
fs.mkdirSync(dataplaneData);
const children = [];
t.after(async () => {
for (const child of children) {
if (child.exitCode === null) await stop(child);
}
fs.rmSync(directory, { recursive: true, force: true });
});
const start = (env) => {
const child = spawn(process.execPath, ['dist/server/main.js'], {
cwd: root,
env: { ...process.env, ...env },
stdio: ['ignore', 'ignore', 'pipe'],
});
let stderr = '';
child.stderr.on('data', (chunk) => { stderr += chunk; });
children.push(child);
return { child, stderr: () => stderr };
};
const control = start({
APP_COMPONENT: 'control',
APP_MODE: 'client',
DATA_DIR: controlData,
DIST_DIR: '',
HARBOR_HOST_NETWORK_STATE: path.join(directory, 'missing-network.json'),
PORT: String(port),
SING_BOX_CACHE: path.join(controlData, 'cache.db'),
});
const state = await waitFor(async () => {
const response = await fetch(`http://127.0.0.1:${port}/api/state`);
return response.ok ? response.json() : null;
}, control.child, control.stderr);
assert.equal(state.apiVersion, 1);
assert.equal(state.mode, 'client');
const page = await httpText(`http://127.0.0.1:${port}/`);
assert.equal(page.status, 200);
assert.match(page.type, /^text\/html/);
assert.match(page.body, /<div id="root"><\/div>/);
await stop(control.child);
const dataplane = start({
APP_COMPONENT: 'dataplane',
APP_MODE: 'gateway',
DATA_DIR: dataplaneData,
DATAPLANE_SOCKET: socketPath,
SING_BOX_CACHE: path.join(dataplaneData, 'cache.db'),
SING_BOX_CONFIG: path.join(dataplaneData, 'missing-config.json'),
});
const status = await waitFor(async () => {
const response = await socketRequest(socketPath, '/status');
return response.status === 200 ? response.body : null;
}, dataplane.child, dataplane.stderr);
assert.equal(status.ready, true);
await stop(dataplane.child);
});
test('production paths use only the compiled dispatcher', () => {
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
const packageJson = JSON.parse(read('package.json'));
const main = read('src/server/main.ts');
const gatewayEntrypoint = read('entrypoint.sh');
const clientEntrypoint = read('entrypoint.client.sh');
const workflow = read('.gitea/workflows/gateway-build.yml');
const legacyBuild = read('scripts/build-on-107-deploy-111.sh');
const dockerignore = read('.dockerignore');
assert.equal(packageJson.scripts['build:production'], 'npm run build && npm run build:server');
assert.equal(packageJson.scripts.prestart, 'npm run build:production');
assert.equal(packageJson.scripts.start, 'node dist/server/main.js');
assert.match(main, /APP_COMPONENT === 'dataplane'/);
assert.match(main, /process\.env\.DIST_DIR \|\|= path\.resolve\('dist'\)/);
assert.match(main, /import\('\.\/dataplane\.js'\)/);
assert.match(main, /import\('\.\/index\.js'\)/);
for (const entrypoint of [gatewayEntrypoint, clientEntrypoint]) {
assert.match(entrypoint, /node \/app\/dist\/server\/main\.js/);
assert.doesNotMatch(entrypoint, /\/app\/src/);
}
assert.match(workflow, /npm run build:production/);
assert.match(workflow, /NODE_BUILD_IMAGE: node:20\.19-alpine/);
assert.match(workflow, /docker image inspect "\$\{\{ env\.NODE_BUILD_IMAGE \}\}"[\s\S]*validating inside \$\{\{ env\.NODE_BUILD_IMAGE \}\}/);
assert.match(legacyBuild, /npm run build:production && docker build/);
assert.match(dockerignore, /^dist$/m);
});
+486
View File
@@ -0,0 +1,486 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import {
captureRuntimeCommand,
createConnectionService,
} from '../../dist/server/features/connection/index.js';
import { createConnectionRuntimeRoute } from '../../dist/server/http/routes/connectionRuntimeRoute.js';
import { createServerApplyRoute } from '../../dist/server/http/routes/serverApplyRoute.js';
import { HarborError } from '../../dist/shared/errors.js';
const serverA = { id: 'a', label: 'Alpha', host: 'a.example', port: 1, protocol: 'vless' };
const serverB = { id: 'b', label: 'Beta', host: 'b.example', port: 2, protocol: 'vless' };
const duplicateA = { id: 'a2', label: 'Alpha', host: 'a2.example', port: 3, protocol: 'vless' };
const rules = [{ type: 'domain_suffix', value: 'example', enabled: true }];
function createHarness(overrides = {}) {
let state = structuredClone(overrides.state ?? {
revision: 5,
servers: [serverA, serverB],
selectedServerId: serverA.id,
appliedServerId: serverA.id,
routeRules: rules,
appliedRouteRules: rules,
connectionDesired: 'running',
});
let config = Object.hasOwn(overrides, 'config') ? overrides.config : 'old-config';
let running = overrides.running ?? true;
let tail = Promise.resolve();
let active = 0;
let peak = 0;
let stateUpdateIndex = 0;
const events = [];
const failures = {
...overrides.failures,
stateUpdates: [...(overrides.failures?.stateUpdates || [])],
runtimeStarts: [...(overrides.failures?.runtimeStarts || [])],
runtimeStops: [...(overrides.failures?.runtimeStops || [])],
runtimeRestarts: [...(overrides.failures?.runtimeRestarts || [])],
};
function applyState(mutator) {
const failure = failures.stateUpdates[stateUpdateIndex++];
if (failure instanceof Error) throw failure;
const revision = state.revision + 1;
state = { ...structuredClone(mutator(structuredClone(state))), revision };
events.push(stateUpdateIndex === 1 ? 'state.desired' : stateUpdateIndex === 2 ? 'state.applied' : 'state.restore');
if (failure?.after) throw failure.after;
return structuredClone(state);
}
const serialize = (operation) => {
const run = async () => {
active += 1;
peak = Math.max(peak, active);
try {
return await operation();
} finally {
active -= 1;
}
};
const result = tail.then(run, run);
tail = result.then(() => undefined, () => undefined);
return result;
};
const stopRuntime = async () => {
events.push('runtime.stop');
const failure = failures.runtimeStops.shift();
if (failure) {
running = false;
throw failure;
}
running = false;
};
const restartRuntime = async () => {
events.push('runtime.restart');
const failure = failures.runtimeRestarts.shift();
if (failure) {
if (failure.code !== 'CONFIG_INVALID') running = false;
throw failure;
}
running = true;
};
const service = createConnectionService({
state: {
read: () => structuredClone(state),
update: applyState,
},
subscription: {
readConfig: () => overrides.missingSubscription ? null : { outbounds: [] },
},
config: {
exists: () => config !== null,
build: (_subscription, selectedServerId, routeRules) => ({ selectedServerId, routeRules }),
read: () => config,
write: (value) => {
events.push('config.write');
if (failures.configWrite) throw failures.configWrite;
config = JSON.stringify(value);
if (failures.configWriteAfter) throw failures.configWriteAfter;
},
restore: (value) => {
events.push('config.restore');
if (failures.configRestore) throw failures.configRestore;
config = value;
},
remove: () => {
events.push('config.remove');
if (failures.configRemove) throw failures.configRemove;
config = null;
},
},
runtime: {
isRunning: async () => {
if (failures.runtimeStatus) throw failures.runtimeStatus;
return running;
},
start: async () => {
events.push('runtime.start');
const failure = failures.runtimeStarts.shift();
if (failure) {
running = false;
throw failure;
}
if (overrides.startGate) await overrides.startGate();
running = true;
},
stop: stopRuntime,
stopCommand: () => captureRuntimeCommand(stopRuntime),
restartCommand: () => captureRuntimeCommand(
restartRuntime,
{ preMutationErrorCodes: ['CONFIG_INVALID'] },
),
},
serialize,
now: () => new Date('2026-08-08T12:00:00.000Z'),
});
return {
service,
events,
enqueue: serialize,
setState: (value) => { state = structuredClone(value); },
snapshot: () => structuredClone({ state, config, running, peak }),
};
}
function assertDomainRestored(actual, expected) {
const actualDomain = structuredClone(actual);
const expectedDomain = structuredClone(expected);
delete actualDomain.state.revision;
delete expectedDomain.state.revision;
delete actualDomain.peak;
delete expectedDomain.peak;
assert.deepEqual(actualDomain, expectedDomain);
assert.ok(actual.state.revision >= expected.state.revision);
}
test('apply resolves ID inside the queue and commits desired, config, runtime, then applied state', async () => {
const harness = createHarness({ state: {
revision: 5,
servers: [serverA, serverB, duplicateA],
selectedServerId: serverA.id,
appliedServerId: serverA.id,
routeRules: rules,
appliedRouteRules: rules,
} });
assert.deepEqual(await harness.service.apply(' b ', 'Alpha'), { serverId: 'b', selectedTag: 'Beta' });
const snapshot = harness.snapshot();
assert.equal(snapshot.state.selectedServerId, 'b');
assert.equal(snapshot.state.appliedServerId, 'b');
assert.equal(snapshot.state.connectionDesired, 'running');
assert.equal(snapshot.state.appliedAt, '2026-08-08T12:00:00.000Z');
assert.deepEqual(snapshot.state.appliedRouteRules, rules);
assert.deepEqual(harness.events, ['state.desired', 'config.write', 'runtime.start', 'state.applied']);
await assert.rejects(harness.service.apply('', 'Alpha'), (error) => error.code === 'SERVER_NOT_FOUND');
await assert.rejects(harness.service.apply('missing', 'Beta'), (error) => error.code === 'SERVER_NOT_FOUND');
});
test('apply accepts one legacy label and rejects missing config before mutation', async () => {
const unique = createHarness();
assert.deepEqual(await unique.service.apply('', ' Beta '), { serverId: 'b', selectedTag: 'Beta' });
const missing = createHarness({ missingSubscription: true });
const before = missing.snapshot();
await assert.rejects(missing.service.apply('b', ''), (error) => error.code === 'CONFIG_INVALID');
assertDomainRestored(missing.snapshot(), before);
assert.deepEqual(missing.events, []);
});
test('apply restores previous running domain/config/runtime after mutation failures', async () => {
const failureCases = [
{ failures: { stateUpdates: [new Error('desired')] }, expected: 'desired' },
{ failures: { configWriteAfter: new Error('config') }, expected: 'config' },
{ failures: { runtimeStarts: [new Error('start')] }, expected: 'start' },
{ failures: { stateUpdates: [null, { after: new Error('applied') }] }, expected: 'applied' },
];
for (const { failures, expected } of failureCases) {
const harness = createHarness({ failures });
const before = harness.snapshot();
await assert.rejects(harness.service.apply('b', ''), new RegExp(expected));
assertDomainRestored(harness.snapshot(), before);
}
});
test('apply compensates a desired state update that persists and then throws', async () => {
const failure = new Error('desired persisted then failed');
const harness = createHarness({ failures: { stateUpdates: [{ after: failure }] } });
const before = harness.snapshot();
await assert.rejects(harness.service.apply('b', ''), (error) => error === failure);
const after = harness.snapshot();
assertDomainRestored(after, before);
assert.ok(after.state.revision > before.state.revision);
});
test('apply restores previous stopped state and prior config absence', async () => {
const failure = new Error('start');
const harness = createHarness({ config: null, running: false, failures: { runtimeStarts: [failure] } });
const before = harness.snapshot();
await assert.rejects(harness.service.apply('b', ''), (error) => error === failure);
assertDomainRestored(harness.snapshot(), before);
assert.ok(harness.events.includes('config.remove'));
assert.ok(harness.events.includes('runtime.stop'));
});
test('apply rollback continues after restore failures and marks runtime rollback failure', async () => {
const original = new Error('apply failed');
const configFailure = new Error('config restore failed');
const nonRuntime = createHarness({
failures: { runtimeStarts: [original], configRestore: configFailure },
});
await assert.rejects(nonRuntime.service.apply('b', ''), (error) => {
assert.ok(error instanceof AggregateError);
assert.deepEqual(error.errors, [original, configFailure]);
return true;
});
assert.ok(nonRuntime.events.filter((event) => event === 'runtime.start').length >= 2);
assert.ok(nonRuntime.events.filter((event) => event.startsWith('state.')).length >= 2);
const rollbackFailure = new Error('runtime rollback failed');
const runtimeBroken = createHarness({ failures: { runtimeStarts: [original, rollbackFailure] } });
await assert.rejects(runtimeBroken.service.apply('b', ''), (error) => {
assert.equal(error.code, 'PROCESS_START_FAILED');
assert.deepEqual(error.cause.errors, [original, rollbackFailure]);
return true;
});
assert.ok(runtimeBroken.events.filter((event) => event.startsWith('state.')).length >= 2);
});
test('queued apply resolves latest state and concurrent valid applies remain serial', async () => {
let release;
const blocker = new Promise((resolve) => { release = resolve; });
const stale = createHarness();
const blocked = stale.enqueue(() => blocker);
const apply = stale.service.apply('b', '');
stale.setState({ ...stale.snapshot().state, servers: [serverA] });
release();
await blocked;
await assert.rejects(apply, (error) => error.code === 'SERVER_NOT_FOUND');
const serial = createHarness();
const [first, second] = await Promise.all([
serial.service.apply('a', ''),
serial.service.apply('b', ''),
]);
assert.equal(first.serverId, 'a');
assert.equal(second.serverId, 'b');
assert.equal(serial.snapshot().state.appliedServerId, 'b');
assert.equal(serial.snapshot().peak, 1);
});
test('server apply route preserves defaults, operation kind, response, and single ownership', async () => {
const calls = [];
const sent = [];
const route = createServerApplyRoute({
connection: {
apply: async (serverId, selectedTag) => {
calls.push([serverId, selectedTag]);
return { serverId: 'resolved', selectedTag: 'Resolved' };
},
},
readBody: async () => ({}),
withOperation: async (kind, operation) => {
calls.push(kind);
return operation();
},
sendState: async (_res, extra) => { sent.push(extra); },
});
const response = {};
assert.equal(await route.handle({ method: 'GET', url: '/api/apply' }, response), false);
assert.equal(await route.handle({ method: 'POST', url: '/api/apply' }, response), true);
assert.deepEqual(calls, ['apply-server', ['', '']]);
assert.deepEqual(sent, [{ serverId: 'resolved', selectedTag: 'Resolved' }]);
const source = readFileSync(new URL('../../src/server/index.ts', import.meta.url), 'utf8');
assert.match(source, /createServerApplyRoute\(\{/);
assert.doesNotMatch(source, /function applySelectedServer|req\.url === ['"]\/api\/apply['"]/);
});
test('stop always cleans runtime and commits only desired stopped state', async () => {
for (const running of [true, false]) {
const harness = createHarness({ running });
const before = harness.snapshot();
await harness.service.stop();
const after = harness.snapshot();
assert.equal(after.running, false);
assert.equal(after.state.connectionDesired, 'stopped');
assert.equal(after.state.selectedServerId, before.state.selectedServerId);
assert.equal(after.state.appliedServerId, before.state.appliedServerId);
assert.deepEqual(after.state.appliedRouteRules, before.state.appliedRouteRules);
assert.equal(harness.events.filter((event) => event === 'runtime.stop').length, 1);
}
});
test('restart validates config inside the queue and commits applied runtime fields', async () => {
const missing = createHarness({ config: null });
const beforeMissing = missing.snapshot();
await assert.rejects(missing.service.restart(), (error) => error.code === 'CONFIG_INVALID');
assertDomainRestored(missing.snapshot(), beforeMissing);
assert.deepEqual(missing.events, []);
for (const running of [true, false]) {
const harness = createHarness({ running, state: {
revision: 1,
servers: [serverA, serverB],
selectedServerId: serverB.id,
appliedServerId: serverA.id,
routeRules: rules,
appliedRouteRules: [],
appliedAt: 'preserved',
connectionDesired: 'stopped',
} });
await harness.service.restart();
const after = harness.snapshot();
assert.equal(after.running, true);
assert.equal(after.state.appliedServerId, serverB.id);
assert.equal(after.state.connectionDesired, 'running');
assert.deepEqual(after.state.appliedRouteRules, rules);
assert.equal(after.state.appliedAt, 'preserved');
}
});
test('stop and restart compensate runtime and pre/post-write state failures', async () => {
const statusFailure = new Error('status failed');
for (const command of ['stop', 'restart']) {
const status = createHarness({ failures: { runtimeStatus: statusFailure } });
await status.service[command]();
assert.ok(status.events.includes(`runtime.${command}`));
}
for (const stateFailure of [new Error('state before'), { after: new Error('state after') }]) {
const stopHarness = createHarness({ failures: { stateUpdates: [stateFailure] } });
const stopBefore = stopHarness.snapshot();
await assert.rejects(stopHarness.service.stop(), /state/);
assertDomainRestored(stopHarness.snapshot(), stopBefore);
const restartHarness = createHarness({
running: false,
failures: { stateUpdates: [stateFailure] },
});
const restartBefore = restartHarness.snapshot();
await assert.rejects(restartHarness.service.restart(), /state/);
assertDomainRestored(restartHarness.snapshot(), restartBefore);
}
const stopFailure = new Error('stop failed');
const stopped = createHarness({ failures: { runtimeStops: [stopFailure] } });
const stoppedBefore = stopped.snapshot();
await assert.rejects(stopped.service.stop(), (error) => error === stopFailure);
assertDomainRestored(stopped.snapshot(), stoppedBefore);
const restartFailure = new Error('restart failed');
const restarted = createHarness({ running: false, failures: { runtimeRestarts: [restartFailure] } });
const restartedBefore = restarted.snapshot();
await assert.rejects(restarted.service.restart(), (error) => error === restartFailure);
assertDomainRestored(restarted.snapshot(), restartedBefore);
});
test('restart preserves CONFIG_INVALID when validation fails before runtime mutation', async () => {
const invalid = new HarborError('CONFIG_INVALID');
const harness = createHarness({ failures: { runtimeRestarts: [invalid] } });
const before = harness.snapshot();
await assert.rejects(harness.service.restart(), (error) => error === invalid);
assertDomainRestored(harness.snapshot(), before);
assert.equal(harness.events.filter((event) => event === 'runtime.restart').length, 1);
assert.equal(harness.events.includes('runtime.start'), false);
});
test('runtime command adapter makes local and remote mutation phase explicit', async () => {
const localFailure = new HarborError('CONFIG_INVALID');
assert.deepEqual(
await captureRuntimeCommand(
async () => { throw localFailure; },
{ preMutationErrorCodes: ['CONFIG_INVALID'] },
),
{ ok: false, mutationStarted: false, error: localFailure },
);
const remoteFailure = new HarborError('PROCESS_START_FAILED');
assert.deepEqual(
await captureRuntimeCommand(async () => { throw remoteFailure; }),
{ ok: false, mutationStarted: true, error: remoteFailure },
);
});
test('runtime command rollback continues through state errors and maps runtime restore failure', async () => {
const original = new Error('stop state failed');
const stateRestore = new Error('state restore failed');
const aggregate = createHarness({
failures: { stateUpdates: [{ after: original }, stateRestore] },
});
await assert.rejects(aggregate.service.stop(), (error) => {
assert.ok(error instanceof AggregateError);
assert.deepEqual(error.errors, [original, stateRestore]);
return true;
});
assert.equal(aggregate.snapshot().running, true);
const stopFailure = new Error('stop failed');
const runtimeRestore = new Error('start rollback failed');
const broken = createHarness({
failures: { runtimeStops: [stopFailure], runtimeStarts: [runtimeRestore] },
});
await assert.rejects(broken.service.stop(), (error) => {
assert.equal(error.code, 'PROCESS_START_FAILED');
assert.deepEqual(error.cause.errors, [stopFailure, runtimeRestore]);
return true;
});
});
test('apply, stop, and restart share one deterministic connection queue', async () => {
const applyThenStop = createHarness();
await Promise.all([
applyThenStop.service.apply('b', ''),
applyThenStop.service.stop(),
]);
assert.equal(applyThenStop.snapshot().state.connectionDesired, 'stopped');
assert.equal(applyThenStop.snapshot().peak, 1);
const stopThenRestart = createHarness();
await Promise.all([
stopThenRestart.service.stop(),
stopThenRestart.service.restart(),
]);
assert.equal(stopThenRestart.snapshot().state.connectionDesired, 'running');
assert.equal(stopThenRestart.snapshot().running, true);
assert.equal(stopThenRestart.snapshot().peak, 1);
});
test('connection runtime route preserves operation kinds, response extras, and single ownership', async () => {
const calls = [];
const sent = [];
const route = createConnectionRuntimeRoute({
connection: {
stop: async () => { calls.push('service.stop'); },
restart: async () => { calls.push('service.restart'); },
},
withOperation: async (kind, operation) => {
calls.push(kind);
return operation();
},
sendState: async (_res, extra) => { sent.push(extra); },
});
const response = {};
assert.equal(await route.handle({ method: 'GET', url: '/api/singbox/stop' }, response), false);
assert.equal(await route.handle({ method: 'POST', url: '/api/singbox/stop' }, response), true);
assert.equal(await route.handle({ method: 'POST', url: '/api/singbox/restart' }, response), true);
assert.deepEqual(calls, ['stop', 'service.stop', 'start', 'service.restart']);
assert.deepEqual(sent, [{ singboxRunning: false }, { singboxRunning: true }]);
const source = readFileSync(new URL('../../src/server/index.ts', import.meta.url), 'utf8');
assert.match(source, /createConnectionRuntimeRoute\(\{/);
assert.doesNotMatch(source, /req\.url === ['"]\/api\/singbox\/(?:stop|restart)['"]/);
});
+149 -3
View File
@@ -5,9 +5,11 @@ import test from 'node:test';
import {
createConnectivityDiagnosticsService,
CURL_META_MARKER,
} from '../../src/server/services/connectivityDiagnosticsService.js';
} from '../../dist/server/services/connectivityDiagnosticsService.js';
import { createConnectivityDiagnosticsUseCase } from '../../dist/server/features/diagnostics/index.js';
import { createConnectivityDiagnosticsRoute } from '../../dist/server/http/routes/connectivityDiagnosticsRoute.js';
const server = fs.readFileSync(path.resolve(import.meta.dirname, '../../src/server/index.js'), 'utf8');
const server = fs.readFileSync(path.resolve(import.meta.dirname, '../../src/server/index.ts'), 'utf8');
function response(body = '', overrides = {}) {
return {
@@ -52,10 +54,154 @@ test('connectivity diagnostics force separate direct and VPN paths', async () =>
assert.ok(calls.some((args) => args.includes('--proxy') && args.includes('http://127.0.0.1:18080')));
});
test('connectivity diagnostics endpoint is available in Connect and Gateway', () => {
test('connectivity diagnostics endpoint is available in Connect and Gateway through one owner', () => {
assert.match(server, /const localConnectivityDiagnostics = !remoteDataplane/);
assert.doesNotMatch(server, /settings\.appMode !== 'gateway'[\s\S]{0,120}ENDPOINT_NOT_FOUND/);
assert.match(server, /runConnectivityDiagnostics\(services, target\)/);
assert.match(server, /createConnectivityDiagnosticsUseCase\(\{/);
assert.match(server, /createConnectivityDiagnosticsRoute\(\{/);
assert.match(server, /connectivityDiagnosticsRoute\.handle\(req, res\)/);
assert.doesNotMatch(server, /['"]\/api\/diagnostics\/connectivity['"]/);
assert.doesNotMatch(server, /appliedServerId\s*\|\|\s*state\.selectedServerId/);
});
test('connectivity use case captures applied server before probes and preserves result fields', async () => {
const events = [];
let state = {
appliedServerId: 'applied',
selectedServerId: 'selected',
servers: [
{ id: 'applied', label: 'Applied before probe' },
{ id: 'selected', label: 'Selected' },
],
};
let releaseProbe;
const probe = new Promise((resolve) => { releaseProbe = resolve; });
const sourceResult = {
checkedAt: 'now',
direct: { available: true },
vpn: { available: true, server: { id: 'stale', label: 'Stale' }, detail: 7 },
assessment: { summary: 'available' },
};
const useCase = createConnectivityDiagnosticsUseCase({
readState: () => {
events.push('state');
return state;
},
runDiagnostics: async (services, target) => {
events.push(['probe', services, target]);
await probe;
return sourceResult;
},
});
const resultPromise = useCase.run({ raw: true }, 42);
state.servers[0].label = 'Changed during probe';
releaseProbe();
const result = await resultPromise;
assert.deepEqual(events, ['state', ['probe', { raw: true }, 42]]);
assert.deepEqual(result, {
...sourceResult,
vpn: {
...sourceResult.vpn,
server: { id: 'applied', label: 'Applied before probe' },
},
});
assert.deepEqual(sourceResult.vpn.server, { id: 'stale', label: 'Stale' });
});
test('connectivity use case keeps applied priority, selected fallback and error identity', async () => {
const result = { vpn: { available: false }, marker: true };
const selected = createConnectivityDiagnosticsUseCase({
readState: () => ({
appliedServerId: '',
selectedServerId: 'selected',
servers: [{ id: 'selected', label: 'Selected' }],
}),
runDiagnostics: async () => result,
});
assert.deepEqual((await selected.run(null, null)).vpn.server, {
id: 'selected',
label: 'Selected',
});
const missingApplied = createConnectivityDiagnosticsUseCase({
readState: () => ({
appliedServerId: 'missing',
selectedServerId: 'selected',
servers: [{ id: 'selected', label: 'Selected' }],
}),
runDiagnostics: async () => result,
});
assert.equal((await missingApplied.run([], null)).vpn.server, null);
const stateError = new Error('state failed');
let probes = 0;
const brokenState = createConnectivityDiagnosticsUseCase({
readState: () => { throw stateError; },
runDiagnostics: async () => { probes += 1; return result; },
});
await assert.rejects(brokenState.run([], null), (error) => error === stateError);
assert.equal(probes, 0);
const probeError = new Error('probe failed');
const brokenProbe = createConnectivityDiagnosticsUseCase({
readState: () => ({ servers: [] }),
runDiagnostics: async () => { throw probeError; },
});
await assert.rejects(brokenProbe.run([], null), (error) => error === probeError);
});
function routeResponse() {
return {
writeHead(status, headers) {
this.status = status;
this.headers = headers;
},
end(payload) {
this.payload = JSON.parse(payload);
},
};
}
test('connectivity route preserves exact URL, defaults and raw response', async () => {
const calls = [];
let body = {};
let bodyReads = 0;
const route = createConnectivityDiagnosticsRoute({
diagnostics: {
run: async (...args) => {
calls.push(args);
return { checkedAt: 'now', vpn: { server: null } };
},
},
readBody: async () => {
bodyReads += 1;
return body;
},
});
const res = routeResponse();
assert.equal(await route.handle({
method: 'POST',
url: '/api/diagnostics/connectivity',
}, res), true);
assert.deepEqual(calls, [[[], null]]);
assert.equal(res.status, 200);
assert.equal(res.headers['content-type'], 'application/json; charset=utf-8');
assert.deepEqual(res.payload, { checkedAt: 'now', vpn: { server: null } });
body = { services: null, target: 17 };
await route.handle({ method: 'POST', url: '/api/diagnostics/connectivity' }, routeResponse());
assert.deepEqual(calls.at(-1), [null, 17]);
for (const [method, url] of [
['GET', '/api/diagnostics/connectivity'],
['POST', '/api/diagnostics/connectivity?target=all'],
['POST', '/api/diagnostics/other'],
]) {
assert.equal(await route.handle({ method, url }, routeResponse()), false);
}
assert.equal(bodyReads, 2);
});
test('a targeted IP row uses three samples and keeps the majority address', async () => {
+1 -1
View File
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createDataplaneClient } from '../../src/server/dataplaneClient.js';
import { createDataplaneClient } from '../../dist/server/dataplaneClient.js';
test('control uses the dataplane socket protocol', async () => {
const requests = [];
+17 -6
View File
@@ -7,6 +7,7 @@ const root = path.resolve(import.meta.dirname, '../..');
const compose = fs.readFileSync(path.join(root, 'docker-compose.gateway.yml'), 'utf8');
const deploy = fs.readFileSync(path.join(root, 'scripts/deploy-gateway.sh'), 'utf8');
const workflow = fs.readFileSync(path.join(root, '.gitea/workflows/gateway-build.yml'), 'utf8');
const clientDockerfile = fs.readFileSync(path.join(root, 'Dockerfile.client'), 'utf8');
const dockerfiles = ['Dockerfile', 'Dockerfile.client']
.map((file) => fs.readFileSync(path.join(root, file), 'utf8'));
@@ -16,14 +17,24 @@ test('gateway deploy updates control without recreating dataplane', () => {
assert.match(compose, /DATAPLANE_SOCKET: \/run\/vpn-proxy\/dataplane\.sock/);
assert.match(deploy, /up -d --no-deps --wait[^\n]+vpn-proxy-control/);
assert.match(workflow, /UPDATE_DATAPLANE="\$\{UPDATE_DATAPLANE\}"/);
assert.match(workflow, /src\/server\/\(config\|dataplane\|gatewayRouting\|singbox\|singboxRuntime\|version\)/);
assert.match(workflow, /src\/server\/\(adapters\/neighbors\|services\/\(connectivityDiagnosticsService\|deviceTrafficService\|devicePolicyService\)\)/);
assert.match(workflow, /src\/shared\/\(connectivityDiagnostics\|errors\)/);
assert.doesNotMatch(workflow, /dataplaneClient/);
assert.match(workflow, /node scripts\/runtime-impact\.mjs --stdin/);
assert.match(workflow, /Affected components: \$\{AFFECTED_COMPONENTS\}/);
assert.match(workflow, /Restart scope: \$\{RESTART_SCOPE\}/);
assert.match(workflow, /git diff --no-renames --name-only "\$BEFORE_SHA"/);
assert.match(workflow, /git diff-tree --no-renames/);
assert.match(workflow, /git cat-file -e "\$\{BEFORE_SHA\}\^\{commit\}"/);
assert.match(workflow, /npm test[\s\S]*Image build and push skipped: no Gateway runtime impact\.[\s\S]*exit 0[\s\S]*docker login/);
assert.match(workflow, /Image build and push skipped: no Gateway runtime impact\.[\s\S]*exit 0[\s\S]*\.\/scripts\/build-runtime-base\.sh/);
assert.match(workflow, /Deploy skipped: no Gateway runtime impact\.[\s\S]*exit 0[\s\S]*bash scripts\/deploy-gateway\.sh/);
assert.doesNotMatch(workflow, /grep -Eq/);
});
test('runtime images include shared server modules', () => {
test('runtime images contain only compiled application modules', () => {
for (const dockerfile of dockerfiles) {
assert.match(dockerfile, /COPY src\/shared \/app\/src\/shared/);
assert.match(dockerfile, /RUN npm run build:production/);
assert.match(dockerfile, /COPY --from=build \/src\/dist \/app\/dist/);
assert.doesNotMatch(dockerfile, /\/app\/src/);
}
assert.match(clientDockerfile, /COPY index\.html vite\.config\.ts/);
assert.doesNotMatch(clientDockerfile, /vite\.config\.js/);
});
+72 -4
View File
@@ -3,15 +3,16 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { parseNeighborSnapshot, readNeighborSnapshot } from '../../src/server/adapters/neighbors.js';
import { parseNeighborSnapshot, readNeighborSnapshot } from '../../dist/server/adapters/neighbors.js';
import {
createDeviceInventoryService,
createVendorLookup,
DEVICE_INVENTORY_SCHEMA_VERSION,
deviceId,
migrateDeviceInventoryState,
} from '../../src/server/services/deviceInventoryService.js';
import { fingerprintDirectDevices } from '../../src/server/services/devicePolicyService.js';
import { createJsonStore } from '../../src/server/services/stateStore.js';
} from '../../dist/server/services/deviceInventoryService.js';
import { fingerprintDirectDevices } from '../../dist/server/services/devicePolicyService.js';
import { createJsonStore } from '../../dist/server/services/stateStore.js';
test('container neighbors are hidden while a real 172 LAN device remains valid', () => {
const observedAt = '2026-08-07T12:00:00.000Z';
@@ -47,6 +48,73 @@ test('container neighbors are hidden while a real 172 LAN device remains valid',
assert.deepEqual(migrated.devices.map(({ ip }) => ip), ['172.20.0.7']);
});
test('malformed persisted devices and remote observations cannot enter canonical inventory', async (t) => {
const observedAt = '2026-08-08T12:00:00.000Z';
const mac = '00:11:22:33:44:55';
const persisted = {
id: 'legacy-device-id',
alias: 42,
pinned: 'yes',
hostname: 42,
manufacturer: [],
mac,
ip: '192.168.50.7',
interface: 'eth0',
firstSeenAt: observedAt,
lastSeenAt: observedAt,
source: null,
confidence: 'untrusted',
};
const migrated = migrateDeviceInventoryState({
schemaVersion: DEVICE_INVENTORY_SCHEMA_VERSION,
devices: [
persisted,
{ ...persisted, mac: 'invalid' },
{ ...persisted, mac: '00:11:22:33:44:66', ip: 'not-an-ip' },
{ ...persisted, mac: '00:11:22:33:44:77', lastSeenAt: 'not-a-date' },
],
});
assert.deepEqual(migrated.devices, [{
...persisted,
id: deviceId(mac),
alias: '',
pinned: false,
hostname: null,
manufacturer: null,
source: 'neighbor',
confidence: 'high',
}]);
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-device-guard-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const store = createJsonStore({
filePath: path.join(directory, 'devices.json'),
defaultValue: {},
migrate: migrateDeviceInventoryState,
});
store.remove();
const service = createDeviceInventoryService({
store,
observe: () => ({
observedAt,
error: null,
observations: [
{ ip: '192.168.50.8', mac: '00:11:22:33:44:88', interface: 'eth0', observedAt, active: true },
{ ip: 'bad', mac: '00:11:22:33:44:99', interface: 'eth0', observedAt, active: true },
{ ip: '192.168.50.9', mac: 'invalid', interface: 'eth0', observedAt, active: true },
{ ip: '192.168.50.10', mac: '00:11:22:33:44:aa', interface: 'docker0', observedAt, active: true },
{ ip: '192.168.50.11', mac: '00:11:22:33:44:bb', interface: 'eth0', observedAt, active: 'yes' },
{ ip: '192.168.50.12', mac: '00:11:22:33:44:cc', interface: 'eth0', observedAt: 'bad', active: true },
],
}),
now: () => new Date(observedAt),
});
const snapshot = await service.refresh();
assert.deepEqual(snapshot.devices.map(({ mac: deviceMac, ip }) => [deviceMac, ip]), [
['00:11:22:33:44:88', '192.168.50.8'],
]);
});
test('device inventory discovers, merges, persists metadata and expires anonymous devices', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-devices-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
+1 -1
View File
@@ -5,7 +5,7 @@ import {
createDevicePolicyService,
fingerprintDirectDevices,
normalizeDirectDevices,
} from '../../src/server/services/devicePolicyService.js';
} from '../../dist/server/services/devicePolicyService.js';
const directDevice = {
id: 'dev_0011223344556677',
+191
View File
@@ -0,0 +1,191 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import { createDeviceInventoryRoute } from '../../dist/server/http/routes/deviceInventoryRoute.js';
const deviceId = 'dev_0123456789abcdef';
function response() {
return {
writeHead(status, headers) {
this.status = status;
this.headers = headers;
},
end(payload) {
this.payload = JSON.parse(payload);
},
};
}
function createHarness({ inventory = {}, body = {} } = {}) {
const calls = [];
let bodyReads = 0;
const deviceInventory = inventory === null ? null : {
snapshot: () => {
calls.push(['snapshot']);
return inventory.snapshot ?? { revision: 1, devices: [] };
},
refresh: async () => {
calls.push(['refresh']);
return inventory.refresh ?? { revision: 2, devices: [] };
},
update: (...args) => {
calls.push(['update', ...args]);
return inventory.update ?? { revision: 3 };
},
setPolicy: async (...args) => {
calls.push(['setPolicy', ...args]);
return inventory.setPolicy ?? { revision: 4 };
},
};
const route = createDeviceInventoryRoute({
deviceInventory,
readBody: async () => {
bodyReads += 1;
return body;
},
});
return { route, calls, bodyReads: () => bodyReads };
}
test('device route forwards list and refresh query paths as raw JSON responses', async () => {
const harness = createHarness({
inventory: {
snapshot: { revision: 11, devices: [{ id: deviceId }] },
refresh: { revision: 12, devices: [] },
},
});
const listResponse = response();
assert.equal(await harness.route.handle({
method: 'GET',
url: '/api/devices?source=ui',
}, listResponse), true);
assert.equal(listResponse.status, 200);
assert.equal(listResponse.headers['content-type'], 'application/json; charset=utf-8');
assert.deepEqual(listResponse.payload, { revision: 11, devices: [{ id: deviceId }] });
const refreshResponse = response();
assert.equal(await harness.route.handle({
method: 'POST',
url: '/api/devices/refresh?source=ui',
}, refreshResponse), true);
assert.deepEqual(refreshResponse.payload, { revision: 12, devices: [] });
assert.deepEqual(harness.calls, [['snapshot'], ['refresh']]);
});
test('device route forwards metadata patch and policy arguments without coercion', async () => {
const patch = { expectedRevision: 7, alias: 'Desk', pinned: false, extra: 0 };
const metadata = createHarness({ body: patch });
const metadataResponse = response();
assert.equal(await metadata.route.handle({
method: 'PUT',
url: `/api/devices/${deviceId}?source=ui`,
}, metadataResponse), true);
assert.deepEqual(metadata.calls, [[
'update',
deviceId,
{ alias: 'Desk', pinned: false, extra: 0 },
7,
]]);
assert.deepEqual(metadataResponse.payload, { revision: 3 });
const policy = createHarness({ body: { mode: 42, expectedRevision: '8', ignored: true } });
const policyResponse = response();
assert.equal(await policy.route.handle({
method: 'PUT',
url: `/api/devices/${deviceId}/policy?source=ui`,
}, policyResponse), true);
assert.deepEqual(policy.calls, [['setPolicy', deviceId, 42, '8']]);
assert.deepEqual(policyResponse.payload, { revision: 4 });
});
test('device route preserves endpoint gating and strict lowercase IDs', async () => {
for (const [method, url] of [
['POST', '/api/devices'],
['GET', '/api/devices/refresh'],
['GET', `/api/devices/${deviceId}`],
['POST', `/api/devices/${deviceId}/policy`],
]) {
const harness = createHarness();
await assert.rejects(
harness.route.handle({ method, url }, response()),
(error) => error.code === 'ENDPOINT_NOT_FOUND',
);
assert.deepEqual(harness.calls, []);
assert.equal(harness.bodyReads(), 0);
}
for (const url of [
'/api/other',
'/api/devices/dev_0123456789ABCDEF',
'/api/devices/dev_short',
]) {
const harness = createHarness();
assert.equal(await harness.route.handle({ method: 'PUT', url }, response()), false);
assert.deepEqual(harness.calls, []);
assert.equal(harness.bodyReads(), 0);
}
for (const [method, url] of [
['GET', '/api/devices'],
['POST', '/api/devices/refresh'],
['PUT', `/api/devices/${deviceId}`],
['PUT', `/api/devices/${deviceId}/policy`],
]) {
const client = createHarness({ inventory: null });
await assert.rejects(
client.route.handle({ method, url }, response()),
(error) => error.code === 'ENDPOINT_NOT_FOUND',
);
assert.equal(client.bodyReads(), 0);
}
});
test('device route propagates synchronous and asynchronous service errors unchanged', async () => {
const syncError = new Error('snapshot failed');
const syncRoute = createDeviceInventoryRoute({
deviceInventory: {
snapshot: () => { throw syncError; },
refresh: async () => ({}),
update: () => ({}),
setPolicy: async () => ({}),
},
readBody: async () => ({}),
});
await assert.rejects(
syncRoute.handle({ method: 'GET', url: '/api/devices' }, response()),
(error) => error === syncError,
);
const asyncError = new Error('refresh failed');
const asyncRoute = createDeviceInventoryRoute({
deviceInventory: {
snapshot: () => ({}),
refresh: async () => { throw asyncError; },
update: () => ({}),
setPolicy: async () => ({}),
},
readBody: async () => ({}),
});
await assert.rejects(
asyncRoute.handle({ method: 'POST', url: '/api/devices/refresh' }, response()),
(error) => error === asyncError,
);
});
test('device route is the only HTTP owner while lifecycle stays in composition', () => {
const index = readFileSync(new URL('../../src/server/index.ts', import.meta.url), 'utf8');
const route = readFileSync(
new URL('../../src/server/http/routes/deviceInventoryRoute.ts', import.meta.url),
'utf8',
);
assert.match(index, /createDeviceInventoryRoute\(\{/);
assert.match(index, /deviceInventoryRoute\.handle\(req, res\)/);
assert.doesNotMatch(index, /\/api\/devices/);
assert.doesNotMatch(index, /deviceInventory\.(?:snapshot|update|setPolicy)\(/);
assert.match(index, /deviceInventory\.reconcilePolicies\(\)/);
assert.match(index, /deviceInventory\.refresh\(\)/);
assert.match(route, /DEVICE_PATH/);
assert.match(route, /DEVICE_POLICY_PATH/);
});
+2 -2
View File
@@ -7,12 +7,12 @@ import {
createDeviceTrafficService,
parseTrafficCounters,
selectTrafficDevices,
} from '../../src/server/services/deviceTrafficService.js';
} from '../../dist/server/services/deviceTrafficService.js';
const uploadChain = 'VPN_PROXY_TRAFFIC_UP';
const downloadChain = 'VPN_PROXY_TRAFFIC_DOWN';
const dataplaneSource = fs.readFileSync(
path.resolve(import.meta.dirname, '../../src/server/dataplane.js'),
path.resolve(import.meta.dirname, '../../src/server/dataplane.ts'),
'utf8',
);
const observation = (ip, mac = '00:11:22:33:44:55', deviceInterface = 'eth0') => ({
+2 -2
View File
@@ -3,8 +3,8 @@ import test from 'node:test';
import {
classifyDomain,
createDomainTrafficService,
} from '../../src/server/services/domainTrafficService.js';
import { deviceId } from '../../src/server/services/deviceInventoryService.js';
} from '../../dist/server/services/domainTrafficService.js';
import { deviceId } from '../../dist/server/services/deviceInventoryService.js';
const mac = '00:11:22:33:44:55';
const id = deviceId(mac);
+4 -3
View File
@@ -31,7 +31,8 @@ test('gateway keeps direct forwarding active while TProxy interception is switch
test('control bypasses host routing while dataplane owns it', () => {
assert.match(entrypoint, /APP_COMPONENT.*control/);
assert.match(entrypoint, /exec node \/app\/src\/server\/index\.js/);
assert.match(entrypoint, /APP_COMPONENT.*dataplane/);
assert.match(entrypoint, /node \/app\/src\/server\/dataplane\.js/);
assert.match(entrypoint, /exec node \/app\/dist\/server\/main\.js/);
assert.match(entrypoint, /fi[\s\S]*setup_gateway_forwarding[\s\S]*node \/app\/dist\/server\/main\.js/);
assert.match(entrypoint, /node \/app\/dist\/server\/main\.js/);
assert.doesNotMatch(entrypoint, /\/app\/src/);
});
+1 -1
View File
@@ -5,7 +5,7 @@ import {
ERROR_DEFINITIONS,
HarborError,
normalizeHarborError,
} from '../../src/shared/errors.js';
} from '../../dist/shared/errors.js';
test('every Harbor error code has stable Russian copy and retry policy', () => {
for (const [code, definition] of Object.entries(ERROR_DEFINITIONS)) {
+506
View File
@@ -0,0 +1,506 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import {
createGatewayAutoService,
} from '../../dist/server/features/routing/index.js';
import {
applyGatewayPreference,
createGatewayAutoState,
nextGatewayAutoState,
sameGatewayRoute,
} from '../../dist/server/gatewayPresence.js';
import { createGatewayAutoRoute } from '../../dist/server/http/routes/gatewayAutoRoute.js';
const subscriptionUrl = 'https://subscription.example/0123456789abcdef0123456789abcdef';
const server = { id: 'server', label: 'Server', host: 'server.example', port: 443, protocol: 'vless' };
const networkA = {
gateway: '192.168.50.111',
interface: 'en0',
mac: 'aa:bb:cc:dd:ee:ff',
observedAt: Date.now(),
};
const networkB = {
gateway: '192.168.60.111',
interface: 'en1',
mac: '11:22:33:44:55:66',
observedAt: Date.now(),
};
const verifiedA = {
gatewayId: 'gateway-a',
uiOrigin: 'http://192.168.50.111:3456',
verifiedAt: '2026-08-08T12:00:00.000Z',
};
function directState(network = networkA, verified = verifiedA) {
return nextGatewayAutoState(createGatewayAutoState(), {
network,
verifiedGateway: verified,
});
}
function deferred() {
let resolve;
let reject;
const promise = new Promise((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
return { promise, resolve, reject };
}
function createHarness(overrides = {}) {
let state = structuredClone(overrides.state ?? {
revision: 10,
servers: [server],
selectedServerId: server.id,
appliedServerId: server.id,
routeRules: [],
appliedRouteRules: [],
routeRulesRevision: 0,
subscriptionUrl,
gatewayAutoEnabled: true,
});
let config = Object.hasOwn(overrides, 'config') ? overrides.config : 'old-config';
let network = Object.hasOwn(overrides, 'network') ? overrides.network : networkA;
let tail = Promise.resolve();
let active = 0;
let peak = 0;
let stateUpdateIndex = 0;
let timerCallback = null;
let timerUnrefs = 0;
let timerClears = 0;
const events = [];
const warnings = [];
const failures = {
stateUpdates: [...(overrides.failures?.stateUpdates || [])],
...overrides.failures,
};
const serialize = (operation) => {
const run = async () => {
active += 1;
peak = Math.max(peak, active);
try { return await operation(); } finally { active -= 1; }
};
const result = tail.then(run, run);
tail = result.then(() => undefined, () => undefined);
return result;
};
const service = createGatewayAutoService({
appMode: overrides.appMode || 'client',
state: {
read: () => structuredClone(state),
update: (mutator) => {
const failure = failures.stateUpdates[stateUpdateIndex++];
if (failure instanceof Error) throw failure;
state = {
...structuredClone(mutator(structuredClone(state))),
revision: state.revision + 1,
};
events.push('state.update');
if (failure?.after) throw failure.after;
return structuredClone(state);
},
},
subscription: {
readConfig: () => overrides.missingSubscription ? null : { outbounds: [] },
},
config: {
build: (_subscription, selectedServerId, routeRules, gatewayAuto) => ({
selectedServerId,
routeRules,
clientDirect: gatewayAuto.mode === 'gateway-direct',
}),
read: () => config,
write: (value) => {
events.push('config.write');
if (failures.configWrite) throw failures.configWrite;
config = JSON.stringify(value);
if (failures.configWriteAfter) throw failures.configWriteAfter;
},
restore: (value) => {
events.push('config.restore');
if (failures.configRestore) throw failures.configRestore;
config = value;
},
remove: () => {
events.push('config.remove');
if (failures.configRemove) throw failures.configRemove;
config = null;
},
},
runtime: {
isRunning: () => overrides.running ?? true,
applyCommand: async () => {
events.push('runtime.apply');
return typeof overrides.commandResult === 'function'
? overrides.commandResult()
: overrides.commandResult || { ok: true, mutationStarted: true };
},
restoreRunning: async () => {
events.push('runtime.restore');
if (failures.runtimeRestore) throw failures.runtimeRestore;
},
},
discovery: {
readHostNetwork: () => structuredClone(network),
probeGateway: async (input) => {
events.push('discovery.probe');
if (overrides.probe) return overrides.probe(input);
return verifiedA;
},
},
transition: {
createInitial: createGatewayAutoState,
applyPreference: applyGatewayPreference,
next: nextGatewayAutoState,
sameRoute: sameGatewayRoute,
},
serialize,
scheduler: {
setInterval: (callback, intervalMs) => {
events.push(`timer.set:${intervalMs}`);
timerCallback = callback;
return { unref: () => { timerUnrefs += 1; } };
},
clearInterval: () => {
events.push('timer.clear');
timerClears += 1;
timerCallback = null;
},
},
onRouteChange: (gatewayAuto) => events.push(`route:${gatewayAuto.mode}`),
onDiscoveryWarning: (reason) => warnings.push(reason),
onTimerError: (error) => warnings.push(error.message),
});
if (overrides.gatewayAuto) service.set(structuredClone(overrides.gatewayAuto));
return {
service,
events,
warnings,
enqueue: serialize,
setNetwork: (value) => { network = value; },
setState: (value) => { state = structuredClone(value); },
patchState: (value) => { state = { ...state, ...structuredClone(value) }; },
fireTimer: () => timerCallback?.(),
snapshot: () => structuredClone({
state,
gatewayAuto: service.read(),
config,
peak,
timerUnrefs,
timerClears,
timerActive: Boolean(timerCallback),
}),
};
}
function withoutRevision(value) {
const copy = structuredClone(value);
delete copy.state.revision;
delete copy.peak;
return copy;
}
test('gateway-auto handles no-op and metadata-only discovery without config/runtime mutation', async () => {
const noOp = createHarness({ appMode: 'gateway' });
const before = noOp.snapshot();
await noOp.service.refresh();
assert.deepEqual(noOp.snapshot(), before);
assert.deepEqual(noOp.events, []);
const metadata = createHarness({ probe: async () => { throw new Error('offline'); } });
await metadata.service.refresh();
assert.equal(metadata.snapshot().gatewayAuto.mode, 'local-vpn');
assert.equal(metadata.snapshot().gatewayAuto.failures, 1);
assert.deepEqual(metadata.events, ['discovery.probe', 'state.update']);
assert.deepEqual(metadata.warnings, ['offline']);
});
test('gateway-auto mode changes separate state-only, stopped and running paths', async () => {
const stateOnly = createHarness({
gatewayAuto: directState(),
state: {
revision: 1,
servers: [],
selectedServerId: '',
appliedServerId: '',
routeRules: [],
appliedRouteRules: [],
routeRulesRevision: 0,
gatewayAutoEnabled: true,
},
});
await stateOnly.service.setEnabled(false);
assert.equal(stateOnly.snapshot().gatewayAuto.mode, 'local-vpn');
assert.deepEqual(stateOnly.events, ['state.update', 'state.update', 'route:local-vpn']);
const stopped = createHarness({ gatewayAuto: directState(), running: false });
await stopped.service.setEnabled(false);
assert.equal(stopped.events.includes('config.write'), true);
assert.equal(stopped.events.includes('runtime.apply'), false);
const running = createHarness({ gatewayAuto: directState(), running: true });
await running.service.setEnabled(false);
assert.deepEqual(running.events.slice(0, 4), [
'config.write',
'runtime.apply',
'state.update',
'state.update',
]);
});
test('gateway-auto startup writes candidate config before publication without applying runtime', async () => {
const harness = createHarness();
await harness.service.refresh({ reconfigure: false });
assert.equal(harness.snapshot().gatewayAuto.mode, 'gateway-direct');
assert.deepEqual(harness.events, [
'discovery.probe',
'config.write',
'state.update',
'route:gateway-direct',
]);
});
test('disable and re-enable preserve verified Gateway identity and persisted preference commits', async () => {
const harness = createHarness({ gatewayAuto: directState() });
await harness.service.setEnabled(false);
const disabled = harness.snapshot();
assert.equal(disabled.gatewayAuto.mode, 'local-vpn');
assert.equal(disabled.gatewayAuto.gatewayId, verifiedA.gatewayId);
assert.equal(disabled.state.gatewayAutoEnabled, false);
await harness.service.setEnabled(true);
const enabled = harness.snapshot();
assert.equal(enabled.gatewayAuto.mode, 'gateway-direct');
assert.equal(enabled.gatewayAuto.gatewayId, verifiedA.gatewayId);
assert.equal(enabled.state.gatewayAutoEnabled, true);
const revision = enabled.state.revision;
await harness.service.setEnabled(true);
assert.equal(harness.snapshot().state.revision, revision + 1);
});
test('gateway-auto rollback restores config, transient owner, runtime and domain state', async () => {
const original = new Error('state failed');
const harness = createHarness({
gatewayAuto: directState(),
failures: { stateUpdates: [{ after: original }] },
});
const before = harness.snapshot();
await assert.rejects(harness.service.setEnabled(false), (error) => error === original);
const after = harness.snapshot();
assert.deepEqual(withoutRevision(after), withoutRevision(before));
assert.ok(after.state.revision >= before.state.revision);
assert.deepEqual(harness.events, [
'config.write',
'runtime.apply',
'state.update',
'config.restore',
'runtime.restore',
'state.update',
]);
});
test('gateway-auto rollback honors explicit runtime mutation phase and write-after-failure', async () => {
const configFailure = new Error('config write after');
const config = createHarness({
gatewayAuto: directState(),
failures: { configWriteAfter: configFailure },
});
await assert.rejects(config.service.setEnabled(false), (error) => error === configFailure);
assert.equal(config.events.includes('config.restore'), true);
assert.equal(config.events.includes('runtime.apply'), false);
const preMutationError = new Error('invalid config');
const local = createHarness({
gatewayAuto: directState(),
commandResult: { ok: false, mutationStarted: false, error: preMutationError },
});
await assert.rejects(local.service.setEnabled(false), (error) => error === preMutationError);
assert.equal(local.events.includes('runtime.restore'), false);
const postMutationError = new Error('remote failed');
const remote = createHarness({
gatewayAuto: directState(),
commandResult: { ok: false, mutationStarted: true, error: postMutationError },
});
await assert.rejects(remote.service.setEnabled(false), (error) => error === postMutationError);
assert.equal(remote.events.includes('runtime.restore'), true);
});
test('gateway-auto rollback continues after restore failures and classifies runtime restore', async () => {
const original = new Error('state failed');
const configRestore = new Error('config restore failed');
const aggregate = createHarness({
gatewayAuto: directState(),
failures: { stateUpdates: [original], configRestore },
});
await assert.rejects(aggregate.service.setEnabled(false), (error) => {
assert.ok(error instanceof AggregateError);
assert.equal(error.message, 'Gateway auto rollback failed');
assert.deepEqual(error.errors, [original, configRestore]);
return true;
});
assert.equal(aggregate.events.includes('runtime.restore'), true);
assert.ok(aggregate.events.filter((event) => event === 'state.update').length >= 1);
const runtimeRestore = new Error('runtime restore failed');
const brokenRuntime = createHarness({
gatewayAuto: directState(),
failures: { stateUpdates: [original], runtimeRestore },
});
await assert.rejects(brokenRuntime.service.setEnabled(false), (error) => {
assert.equal(error.code, 'PROCESS_START_FAILED');
assert.deepEqual(error.cause.errors, [original, runtimeRestore]);
return true;
});
});
test('gateway-auto refresh coalesces while queued and never overlaps the shared serializer', async () => {
const blocker = deferred();
const probe = deferred();
const harness = createHarness({ probe: () => probe.promise });
const queued = harness.enqueue(() => blocker.promise);
const first = harness.service.refresh();
const second = harness.service.refresh();
assert.strictEqual(first, second);
blocker.resolve();
await queued;
await new Promise((resolve) => setImmediate(resolve));
probe.resolve(verifiedA);
await first;
assert.equal(harness.snapshot().peak, 1);
});
test('gateway-auto discards stale subscription and route probe results', async () => {
const gate = deferred();
const staleSubscription = createHarness({ probe: () => gate.promise });
staleSubscription.service.set(directState());
const refresh = staleSubscription.service.refresh();
await new Promise((resolve) => setImmediate(resolve));
staleSubscription.patchState({ subscriptionUrl: `${subscriptionUrl}-new` });
gate.resolve(verifiedA);
await refresh;
assert.deepEqual(staleSubscription.snapshot().gatewayAuto, createGatewayAutoState());
const routeGate = deferred();
const staleRoute = createHarness({ probe: () => routeGate.promise });
const routeRefresh = staleRoute.service.refresh();
await new Promise((resolve) => setImmediate(resolve));
staleRoute.setNetwork(networkB);
routeGate.resolve(verifiedA);
await routeRefresh;
assert.deepEqual(staleRoute.snapshot().gatewayAuto, createGatewayAutoState());
});
test('gateway-auto demotes a changed route before probing and retains verified identity on transient failure', async () => {
let routeHarness;
routeHarness = createHarness({
gatewayAuto: directState(networkA),
network: networkB,
probe: async () => {
assert.equal(routeHarness.service.read().mode, 'local-vpn');
assert.equal(routeHarness.service.read().gatewayId, '');
return { ...verifiedA, gatewayId: 'gateway-b', uiOrigin: 'http://192.168.60.111:3456' };
},
});
await routeHarness.service.refresh();
assert.equal(routeHarness.snapshot().gatewayAuto.gatewayId, 'gateway-b');
assert.deepEqual(
routeHarness.events.filter((event) => event.startsWith('route:')),
['route:local-vpn', 'route:gateway-direct'],
);
const transient = createHarness({
gatewayAuto: directState(),
probe: async () => { throw new Error('timeout'); },
});
await transient.service.refresh();
const state = transient.snapshot().gatewayAuto;
assert.equal(state.mode, 'gateway-direct');
assert.equal(state.gatewayId, verifiedA.gatewayId);
assert.equal(state.failures, 1);
assert.equal(state.lastError, 'timeout');
const structural = createHarness({
probe: async () => { throw { message: 'cross-realm failure' }; },
});
await structural.service.refresh();
assert.equal(structural.snapshot().gatewayAuto.lastError, 'cross-realm failure');
assert.deepEqual(structural.warnings, ['cross-realm failure']);
});
test('gateway-auto discovery timer is single, unrefed, reports errors and stops idempotently', async () => {
const harness = createHarness({ probe: async () => { throw new Error('timer failure'); } });
harness.service.startDiscovery(5_000);
harness.service.startDiscovery(9_000);
assert.deepEqual(harness.events, ['timer.set:5000']);
assert.equal(harness.snapshot().timerUnrefs, 1);
harness.fireTimer();
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(harness.warnings, ['timer failure']);
harness.service.stopDiscovery();
harness.service.stopDiscovery();
assert.equal(harness.snapshot().timerClears, 1);
assert.equal(harness.snapshot().timerActive, false);
});
test('gateway-auto route preserves validation, operation and exact response envelope', async () => {
const calls = [];
let body = { enabled: false };
const route = createGatewayAutoRoute({
appMode: 'client',
gatewayAuto: { setEnabled: async (enabled) => { calls.push(['enabled', enabled]); } },
readBody: async () => body,
withOperation: async (kind, operation) => {
calls.push(['operation', kind]);
return operation();
},
readStatePayload: async () => ({ gatewayAuto: { mode: 'local-vpn' }, revision: 7 }),
});
const response = {
writeHead: (status, headers) => { response.status = status; response.headers = headers; },
end: (payload) => { response.payload = JSON.parse(payload); },
};
assert.equal(await route.handle({ method: 'GET', url: '/api/gateway-auto' }, response), false);
assert.equal(await route.handle({ method: 'POST', url: '/api/gateway-auto' }, response), true);
assert.deepEqual(calls, [['operation', 'gateway-auto'], ['enabled', false]]);
assert.deepEqual(response.payload, {
success: true,
gatewayAuto: { mode: 'local-vpn' },
state: { gatewayAuto: { mode: 'local-vpn' }, revision: 7 },
});
body = { enabled: 'false' };
await assert.rejects(
route.handle({ method: 'POST', url: '/api/gateway-auto' }, response),
(error) => error.code === 'REQUEST_INVALID',
);
const gatewayRoute = createGatewayAutoRoute({
appMode: 'gateway',
gatewayAuto: { setEnabled: async () => {} },
readBody: async () => ({ enabled: true }),
withOperation: async (_kind, operation) => operation(),
readStatePayload: async () => ({}),
});
await assert.rejects(
gatewayRoute.handle({ method: 'POST', url: '/api/gateway-auto' }, response),
(error) => error.code === 'REQUEST_INVALID',
);
});
test('gateway-auto route and service are the only displaced owners', () => {
const source = readFileSync(new URL('../../src/server/index.ts', import.meta.url), 'utf8');
assert.match(source, /createGatewayAutoService\(\{/);
assert.match(source, /createGatewayAutoRoute\(\{/);
assert.doesNotMatch(source, /let gatewayAutoState/);
assert.doesNotMatch(source, /gatewayDiscoveryPromise/);
assert.doesNotMatch(source, /applyGatewayAutoState/);
assert.doesNotMatch(source, /refreshGatewayAutoMode/);
assert.doesNotMatch(source, /req\.url\s*===\s*['"]\/api\/gateway-auto['"]/);
});
+141
View File
@@ -0,0 +1,141 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import { createGatewayPresenceRoute } from '../../dist/server/http/routes/gatewayPresenceRoute.js';
const subscriptionUrl = 'https://subscription.example/0123456789abcdef0123456789abcdef';
const nonce = '0123456789abcdef0123456789abcdef';
function response() {
return {
writeHead(status, headers) {
this.status = status;
this.headers = headers;
},
end(payload) {
this.payload = JSON.parse(payload);
},
};
}
function createHarness(overrides = {}) {
const events = [];
const route = createGatewayPresenceRoute({
appMode: overrides.appMode || 'gateway',
readState: () => {
events.push('state');
if (overrides.stateError) throw overrides.stateError;
return { subscriptionUrl: overrides.subscriptionUrl ?? subscriptionUrl };
},
getHwid: () => {
events.push('hwid');
if (overrides.hwidError) throw overrides.hwidError;
return overrides.gatewayId ?? 'gateway-1';
},
});
return { route, events };
}
test('Gateway presence route preserves query nonce, read order and raw response', async () => {
const harness = createHarness();
const res = response();
assert.equal(await harness.route.handle({
method: 'GET',
url: `/api/gateway-presence?source=client&nonce=${nonce}&nonce=ignored`,
}, res), true);
assert.deepEqual(harness.events, ['state', 'hwid']);
assert.equal(res.status, 200);
assert.equal(res.headers['content-type'], 'application/json; charset=utf-8');
assert.deepEqual({
...res.payload,
proof: '[proof]',
}, {
success: true,
available: true,
product: 'harbor',
role: 'gateway',
protocolVersion: 1,
gatewayId: 'gateway-1',
transparentRouting: true,
proof: '[proof]',
});
assert.match(res.payload.proof, /^[a-f0-9]{64}$/);
});
test('Gateway presence route preserves unavailable client and missing identity responses', async () => {
for (const options of [
{ appMode: 'client' },
{ gatewayId: '' },
{ subscriptionUrl: 'https://subscription.example/public-feed' },
]) {
const harness = createHarness(options);
const res = response();
await harness.route.handle({
method: 'GET',
url: `/api/gateway-presence?nonce=${nonce}`,
}, res);
assert.deepEqual(res.payload, {
success: true,
available: false,
product: 'harbor',
role: options.appMode || 'gateway',
protocolVersion: 1,
});
}
});
test('Gateway presence route reads state and HWID before preserving nonce validation errors', async () => {
for (const value of [null, 'ABCDEF0123456789ABCDEF0123456789', 'short']) {
const harness = createHarness();
const url = value === null
? '/api/gateway-presence'
: `/api/gateway-presence?nonce=${value}`;
await assert.rejects(
harness.route.handle({ method: 'GET', url }, response()),
(error) => error.code === 'REQUEST_INVALID',
);
assert.deepEqual(harness.events, ['state', 'hwid']);
}
});
test('Gateway presence route guards before dependencies and propagates dependency errors', async () => {
for (const [method, url] of [
['POST', `/api/gateway-presence?nonce=${nonce}`],
['GET', `/api/gateway-presence/extra?nonce=${nonce}`],
['GET', `/api/other?nonce=${nonce}`],
]) {
const harness = createHarness();
assert.equal(await harness.route.handle({ method, url }, response()), false);
assert.deepEqual(harness.events, []);
}
const stateError = new Error('state failed');
const brokenState = createHarness({ stateError });
await assert.rejects(
brokenState.route.handle({ method: 'GET', url: `/api/gateway-presence?nonce=${nonce}` }, response()),
(error) => error === stateError,
);
assert.deepEqual(brokenState.events, ['state']);
const hwidError = new Error('hwid failed');
const brokenHwid = createHarness({ hwidError });
await assert.rejects(
brokenHwid.route.handle({ method: 'GET', url: `/api/gateway-presence?nonce=${nonce}` }, response()),
(error) => error === hwidError,
);
assert.deepEqual(brokenHwid.events, ['state', 'hwid']);
});
test('Gateway presence 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/gatewayPresenceRoute.ts', import.meta.url),
'utf8',
);
assert.match(index, /createGatewayPresenceRoute\(\{/);
assert.match(index, /gatewayPresenceRoute\.handle\(req, res\)/);
assert.doesNotMatch(index, /\/api\/gateway-presence|buildGatewayPresence|searchParams\.get\(['"]nonce/);
assert.match(route, /buildGatewayPresence\(\{/);
assert.match(route, /searchParams\.get\(['"]nonce['"]\)/);
});
+2 -2
View File
@@ -12,8 +12,8 @@ import {
probeGatewayPresence,
readHostNetworkState,
verifyGatewayPresence,
} from '../../src/server/gatewayPresence.js';
import { createStateSnapshot } from '../../src/shared/contracts/state.js';
} from '../../dist/server/gatewayPresence.js';
import { createStateSnapshot } from '../../dist/shared/contracts/state.js';
const subscriptionUrl = 'https://subscription.example/0123456789abcdef0123456789abcdef';
const nonce = '0123456789abcdef0123456789abcdef';
+1 -1
View File
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { setGatewayInterception } from '../../src/server/gatewayRouting.js';
import { setGatewayInterception } from '../../dist/server/gatewayRouting.js';
test('gateway switches only the TProxy PREROUTING jump', () => {
const calls = [];
+97 -1
View File
@@ -1,9 +1,11 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import {
renderPrometheusMetrics,
sendPrometheusMetrics,
} from '../../src/server/prometheusMetrics.js';
} from '../../dist/server/prometheusMetrics.js';
import { createPrometheusMetricsRoute } from '../../dist/server/http/routes/prometheusMetricsRoute.js';
const observedAt = '2026-08-08T10:00:00.000Z';
const snapshot = {
@@ -98,3 +100,97 @@ test('invalid canonical counters fail the scrape instead of publishing corrupt v
/Invalid Prometheus counter/,
);
});
function routeResponse() {
return {
writeHead(status, headers) {
this.status = status;
this.headers = headers;
},
end(body) {
this.body = body;
},
};
}
test('Prometheus route reads one current snapshot and preserves the exact text contract', async () => {
let reads = 0;
let refreshes = 0;
const route = createPrometheusMetricsRoute({
deviceInventory: {
metricsSnapshot: () => {
reads += 1;
return snapshot;
},
refresh: () => { refreshes += 1; },
},
});
const res = routeResponse();
assert.equal(await route.handle({ method: 'GET', url: '/metrics?source=prometheus' }, res), true);
assert.equal(reads, 1);
assert.equal(refreshes, 0);
assert.equal(res.status, 200);
assert.equal(res.headers['content-type'], 'text/plain; version=0.0.4; charset=utf-8');
assert.equal(res.body, renderPrometheusMetrics(snapshot));
assert.equal(res.body.endsWith('\n'), true);
});
test('Prometheus route preserves path, method and nullable inventory gating', async () => {
let reads = 0;
const route = createPrometheusMetricsRoute({
deviceInventory: { metricsSnapshot: () => { reads += 1; return snapshot; } },
});
for (const url of ['/metrics/', '/api/metrics', '/']) {
assert.equal(await route.handle({ method: 'GET', url }, routeResponse()), false);
}
await assert.rejects(
route.handle({ method: 'POST', url: '/metrics' }, routeResponse()),
(error) => error.code === 'ENDPOINT_NOT_FOUND',
);
assert.equal(reads, 0);
const clientRoute = createPrometheusMetricsRoute({ deviceInventory: null });
await assert.rejects(
clientRoute.handle({ method: 'GET', url: '/metrics' }, routeResponse()),
(error) => error.code === 'ENDPOINT_NOT_FOUND',
);
});
test('Prometheus route propagates snapshot and renderer failures unchanged', async () => {
const snapshotError = new Error('snapshot failed');
const snapshotRoute = createPrometheusMetricsRoute({
deviceInventory: { metricsSnapshot: () => { throw snapshotError; } },
});
await assert.rejects(
snapshotRoute.handle({ method: 'GET', url: '/metrics' }, routeResponse()),
(error) => error === snapshotError,
);
const rendererRoute = createPrometheusMetricsRoute({
deviceInventory: {
metricsSnapshot: () => ({ traffic: { gatewayBytes: 'broken', proxyBytes: '0' } }),
},
});
await assert.rejects(
rendererRoute.handle({ method: 'GET', url: '/metrics' }, routeResponse()),
/Invalid Prometheus counter/,
);
});
test('Prometheus route is the sole HTTP owner before API and static fallback', () => {
const index = readFileSync(new URL('../../src/server/index.ts', import.meta.url), 'utf8');
const route = readFileSync(
new URL('../../src/server/http/routes/prometheusMetricsRoute.ts', import.meta.url),
'utf8',
);
assert.match(index, /createPrometheusMetricsRoute\(\{ deviceInventory \}\)/);
const delegation = index.indexOf('prometheusMetricsRoute.handle(req, res)');
const apiDispatch = index.indexOf("requestUrl.pathname.startsWith('/api/')");
const staticFallback = index.indexOf(': serveStatic(req, res)');
assert.ok(delegation >= 0 && delegation < apiDispatch && apiDispatch < staticFallback);
assert.doesNotMatch(index, /['"]\/metrics['"]/);
assert.doesNotMatch(index, /metricsSnapshot\(\)|sendPrometheusMetrics/);
assert.match(route, /pathname !== '\/metrics'/);
assert.match(route, /deviceInventory\.metricsSnapshot\(\)/);
assert.doesNotMatch(route, /\.refresh\(/);
});
+56
View File
@@ -0,0 +1,56 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { finishRollback } from '../../dist/server/services/rollback.js';
test('shared rollback rethrows the original error after successful ordered restores', async () => {
const original = new Error('original');
const events = [];
await assert.rejects(
finishRollback(original, [
{ run: () => { events.push('first'); } },
{ run: async () => { events.push('second'); } },
], 'Rollback failed'),
(error) => error === original,
);
assert.deepEqual(events, ['first', 'second']);
});
test('shared rollback continues, preserves aggregate order and message', async () => {
const original = new Error('original');
const first = new Error('first restore');
const second = new Error('second restore');
const events = [];
await assert.rejects(
finishRollback(original, [
{ run: () => { events.push('first'); throw first; } },
{ run: () => { events.push('middle'); } },
{ run: () => { events.push('second'); throw second; } },
], 'Expected rollback message'),
(error) => {
assert.ok(error instanceof AggregateError);
assert.equal(error.message, 'Expected rollback message');
assert.deepEqual(error.errors, [original, first, second]);
return true;
},
);
assert.deepEqual(events, ['first', 'middle', 'second']);
});
test('shared rollback maps a runtime restore failure to PROCESS_START_FAILED', async () => {
const original = new Error('original');
const config = new Error('config restore');
const runtime = new Error('runtime restore');
await assert.rejects(
finishRollback(original, [
{ run: () => { throw config; } },
{ run: () => { throw runtime; }, runtime: true },
], 'Runtime rollback failed'),
(error) => {
assert.equal(error.code, 'PROCESS_START_FAILED');
assert.equal(error.cause.message, 'Runtime rollback failed');
assert.deepEqual(error.cause.errors, [original, config, runtime]);
return true;
},
);
});
+228
View File
@@ -0,0 +1,228 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import { createRouteRulesService } from '../../dist/server/features/routing/index.js';
import { createRouteRulesRoute } from '../../dist/server/http/routes/routeRulesRoute.js';
const oldRules = [{ type: 'domain_suffix', value: 'old.example', enabled: true }];
const newRules = [{ type: 'domain_suffix', value: 'new.example', enabled: true }];
const server = { id: 'server', label: 'Server', host: 'server.example', port: 443, protocol: 'vless' };
function createHarness(overrides = {}) {
let state = structuredClone(overrides.state ?? {
revision: 10,
routeRulesRevision: 2,
routeRules: oldRules,
appliedRouteRules: oldRules,
servers: [server],
selectedServerId: server.id,
appliedServerId: server.id,
});
let config = Object.hasOwn(overrides, 'config') ? overrides.config : 'old-config';
const failures = {
stateUpdates: [...(overrides.failures?.stateUpdates || [])],
...overrides.failures,
};
let stateUpdateIndex = 0;
let tail = Promise.resolve();
let active = 0;
let peak = 0;
const events = [];
const serialize = (operation) => {
const run = async () => {
active += 1;
peak = Math.max(peak, active);
try { return await operation(); } finally { active -= 1; }
};
const result = tail.then(run, run);
tail = result.then(() => undefined, () => undefined);
return result;
};
const service = createRouteRulesService({
state: {
read: () => structuredClone(state),
update: (mutator) => {
const failure = failures.stateUpdates[stateUpdateIndex++];
if (failure instanceof Error) throw failure;
const revision = state.revision + 1;
state = { ...structuredClone(mutator(structuredClone(state))), revision };
events.push('state.update');
if (failure?.after) throw failure.after;
return structuredClone(state);
},
},
subscription: {
readConfig: () => overrides.missingSubscription ? null : { outbounds: [] },
},
config: {
build: (_subscription, selectedServerId, routeRules) => ({ selectedServerId, routeRules }),
read: () => config,
write: (value) => {
events.push('config.write');
if (failures.configWrite) throw failures.configWrite;
config = JSON.stringify(value);
if (failures.configWriteAfter) throw failures.configWriteAfter;
},
restore: (value) => {
events.push('config.restore');
if (failures.configRestore) throw failures.configRestore;
config = value;
},
remove: () => {
events.push('config.remove');
if (failures.configRemove) throw failures.configRemove;
config = null;
},
},
runtime: {
isRunning: async () => overrides.running ?? true,
applyCommand: async () => {
events.push('runtime.apply');
return overrides.commandResult || { ok: true, mutationStarted: true };
},
restoreRunning: async () => {
events.push('runtime.restore');
if (failures.runtimeRestore) throw failures.runtimeRestore;
},
},
serialize,
runOperation: async (operation) => {
events.push('operation');
return operation();
},
});
return {
service,
events,
snapshot: () => structuredClone({ state, config, peak }),
};
}
function assertDomainRestored(actual, expected) {
const actualDomain = structuredClone(actual);
const expectedDomain = structuredClone(expected);
delete actualDomain.state.revision;
delete expectedDomain.state.revision;
delete actualDomain.peak;
delete expectedDomain.peak;
assert.deepEqual(actualDomain, expectedDomain);
assert.ok(actual.state.revision >= expected.state.revision);
}
test('route rules validate strictly, conflict before no-op, and preserve no-op revisions', async () => {
const harness = createHarness();
assert.throws(() => harness.service.update('bad', 2, undefined), (error) => error.code === 'REQUEST_INVALID');
assert.throws(() => harness.service.update(newRules, -1, undefined), (error) => error.code === 'REQUEST_INVALID');
await assert.rejects(harness.service.update(oldRules, 1, undefined), (error) => error.code === 'STATE_CONFLICT');
const before = harness.snapshot();
await harness.service.update(oldRules, 2, undefined);
assert.deepEqual(harness.snapshot(), before);
assert.deepEqual(harness.events, []);
});
test('route rules support explicit domain revision and legacy global revision with normalization', async () => {
const explicit = createHarness();
await explicit.service.update([
{ type: 'domain_suffix', value: 'NEW.EXAMPLE', enabled: true },
{ type: 'domain_suffix', value: 'new.example', enabled: true },
], 2, undefined);
assert.deepEqual(explicit.snapshot().state.routeRules, newRules);
assert.equal(explicit.snapshot().state.routeRulesRevision, 3);
const legacy = createHarness();
await legacy.service.update(newRules, undefined, 10);
assert.deepEqual(legacy.snapshot().state.routeRules, newRules);
});
test('route rules state-only path leaves config and applied rules unchanged', async () => {
for (const state of [
{ revision: 1, routeRulesRevision: 0, routeRules: oldRules, appliedRouteRules: oldRules, servers: [], selectedServerId: '', appliedServerId: '' },
{ revision: 1, routeRulesRevision: 0, routeRules: oldRules, appliedRouteRules: oldRules, servers: [server], selectedServerId: server.id, appliedServerId: server.id },
]) {
const harness = createHarness({ state, missingSubscription: Boolean(state.selectedServerId) });
await harness.service.update(newRules, 0, undefined);
assert.deepEqual(harness.snapshot().state.routeRules, newRules);
assert.deepEqual(harness.snapshot().state.appliedRouteRules, oldRules);
assert.equal(harness.snapshot().config, 'old-config');
assert.deepEqual(harness.events, ['operation', 'state.update']);
}
});
test('route rules running apply updates active rules while stopped leaves them pending', async () => {
const running = createHarness({ running: true });
await running.service.update(newRules, 2, undefined);
assert.deepEqual(running.snapshot().state.appliedRouteRules, newRules);
assert.deepEqual(running.events, ['operation', 'config.write', 'runtime.apply', 'state.update']);
const stopped = createHarness({ running: false });
await stopped.service.update(newRules, 2, undefined);
assert.deepEqual(stopped.snapshot().state.appliedRouteRules, oldRules);
assert.equal(stopped.events.includes('runtime.apply'), false);
});
test('route rules rollback restores config/domain and honors runtime mutation phase', async () => {
for (const failures of [
{ configWriteAfter: new Error('config') },
{ stateUpdates: [{ after: new Error('state') }] },
]) {
const harness = createHarness({ failures });
const before = harness.snapshot();
await assert.rejects(harness.service.update(newRules, 2, undefined));
assertDomainRestored(harness.snapshot(), before);
}
const preMutation = new Error('invalid config');
const local = createHarness({ commandResult: { ok: false, mutationStarted: false, error: preMutation } });
await assert.rejects(local.service.update(newRules, 2, undefined), (error) => error === preMutation);
assert.equal(local.events.includes('runtime.restore'), false);
const postMutation = new Error('remote failed');
const remote = createHarness({ commandResult: { ok: false, mutationStarted: true, error: postMutation } });
await assert.rejects(remote.service.update(newRules, 2, undefined), (error) => error === postMutation);
assert.equal(remote.events.includes('runtime.restore'), true);
});
test('route rules rollback continues and classifies runtime restore failure', async () => {
const original = new Error('state failed');
const configRestore = new Error('config restore failed');
const aggregate = createHarness({
failures: { stateUpdates: [original], configRestore },
});
await assert.rejects(aggregate.service.update(newRules, 2, undefined), (error) => {
assert.ok(error instanceof AggregateError);
assert.deepEqual(error.errors, [original, configRestore]);
return true;
});
const runtimeRestore = new Error('runtime restore failed');
const broken = createHarness({
failures: { stateUpdates: [original], runtimeRestore },
});
await assert.rejects(broken.service.update(newRules, 2, undefined), (error) => {
assert.equal(error.code, 'PROCESS_START_FAILED');
assert.deepEqual(error.cause.errors, [original, runtimeRestore]);
return true;
});
assert.ok(broken.events.filter((event) => event === 'state.update').length >= 1);
});
test('route rules route preserves one adapter and state-only response', async () => {
const calls = [];
const route = createRouteRulesRoute({
routeRules: { update: async (...args) => { calls.push(args); } },
readBody: async () => ({ rules: newRules, expectedRulesRevision: 3, expectedRevision: 99 }),
sendState: async () => { calls.push('sent'); },
});
const response = {};
assert.equal(await route.handle({ method: 'POST', url: '/api/route-rules' }, response), false);
assert.equal(await route.handle({ method: 'PUT', url: '/api/route-rules' }, response), true);
assert.deepEqual(calls, [[newRules, 3, 99], 'sent']);
const source = readFileSync(new URL('../../src/server/index.ts', import.meta.url), 'utf8');
assert.match(source, /createRouteRulesRoute\(\{/);
assert.doesNotMatch(source, /function applyRouteRules|req\.url === ['"]\/api\/route-rules['"]/);
});
+54 -1
View File
@@ -1,7 +1,13 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { checkServerHealth } from '../../src/server/serverHealth.js';
import { readFileSync } from 'node:fs';
import {
checkServerHealth,
createServerHealthService,
} from '../../dist/server/features/servers/index.js';
import { createServerHealthRoute } from '../../dist/server/http/routes/serverHealthRoute.js';
test('server health checks cap count and concurrency', async () => {
const servers = Array.from({ length: 300 }, (_, index) => ({
@@ -24,3 +30,50 @@ test('server health checks cap count and concurrency', async () => {
assert.equal(peak, 4);
assert.deepEqual(results.map(({ id }) => id), servers.slice(0, 30).map(({ id }) => id));
});
test('server health selection coerces IDs, deduplicates, ignores unknowns, and keeps canonical order', async () => {
const servers = [
{ id: '1', label: 'One', host: 'one.example', port: 1, protocol: 'vless' },
{ id: '2', label: 'Two', host: 'two.example', port: 2, protocol: 'vless' },
{ id: '3', label: 'Three', host: 'three.example', port: 3, protocol: 'vless' },
];
const checked = [];
const service = createServerHealthService({
readServers: () => servers,
ping: async (host, port) => {
checked.push([host, port]);
return { ok: true };
},
});
assert.deepEqual((await service.check(['3', 1, '3', 'missing'])).map(({ id }) => id), ['1', '3']);
assert.deepEqual(checked, [['one.example', 1], ['three.example', 3]]);
assert.deepEqual((await service.check(undefined)).map(({ id }) => id), ['1', '2', '3']);
assert.deepEqual((await service.check('2')).map(({ id }) => id), ['1', '2', '3']);
assert.deepEqual((await service.check([])).map(({ id }) => id), ['1', '2', '3']);
assert.deepEqual(await service.check(['missing']), []);
const failure = new Error('ping failed');
await assert.rejects(
createServerHealthService({ readServers: () => servers, ping: async () => { throw failure; } }).check([]),
(error) => error === failure,
);
});
test('server health route is the only endpoint adapter', async () => {
const sent = [];
const route = createServerHealthRoute({
serverHealth: { check: async (ids) => [{ ids }] },
readBody: async () => ({ serverIds: ['chosen'] }),
sendState: async (_res, extra) => { sent.push(extra); },
});
const response = {};
assert.equal(await route.handle({ method: 'GET', url: '/api/servers/ping-all' }, response), false);
assert.equal(await route.handle({ method: 'POST', url: '/api/servers/ping-all' }, response), true);
assert.deepEqual(sent, [{ results: [{ ids: ['chosen'] }] }]);
const source = readFileSync(new URL('../../src/server/index.ts', import.meta.url), 'utf8');
assert.match(source, /createServerHealthRoute\(\{/);
assert.doesNotMatch(source, /req\.url === ['"]\/api\/servers\/ping-all['"]/);
});
+175 -1
View File
@@ -1,9 +1,42 @@
import assert from "node:assert/strict";
import { readFileSync } from 'node:fs';
import test from "node:test";
const {
buildSharedProxyInfo,
} = await import("../../src/server/sharedProxy.js");
} = 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({
@@ -22,3 +55,144 @@ test("gateway shared proxy info exposes host and socks proxy when running", () =
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\(\{/);
});
+1 -1
View File
@@ -8,7 +8,7 @@ process.env.APP_MODE = 'client';
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'vpn-proxy-client-test-'));
process.env.SING_BOX_CACHE = path.join(process.env.DATA_DIR, 'cache.db');
const { buildGatewayConfig } = await import(`../../src/server/singbox.js?client=${Date.now()}`);
const { buildGatewayConfig } = await import(`../../dist/server/singbox.js?client=${Date.now()}`);
const subscriptionConfig = {
outbounds: [{
+1 -1
View File
@@ -8,7 +8,7 @@ process.env.APP_MODE = 'gateway';
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'vpn-proxy-gateway-test-'));
process.env.SING_BOX_CACHE = path.join(process.env.DATA_DIR, 'cache.db');
const { buildGatewayConfig } = await import(`../../src/server/singbox.js?gateway=${Date.now()}`);
const { buildGatewayConfig } = await import(`../../dist/server/singbox.js?gateway=${Date.now()}`);
const subscriptionConfig = {
outbounds: [{
+1 -1
View File
@@ -3,7 +3,7 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { createSingboxRuntime } from '../../src/server/singboxRuntime.js';
import { createSingboxRuntime } from '../../dist/server/singboxRuntime.js';
async function waitForStarts(filePath, count) {
for (let attempt = 0; attempt < 100; attempt += 1) {
+28 -5
View File
@@ -10,9 +10,9 @@ import {
assertStateSnapshot,
createStateSnapshot,
normalizeStoredState,
} from '../../src/shared/contracts/state.js';
import { createServerId } from '../../src/shared/serverIdentity.js';
import { HARBOR_VERSIONS } from '../../src/shared/versions.js';
} from '../../dist/shared/contracts/state.js';
import { createServerId } from '../../dist/shared/serverIdentity.js';
import { HARBOR_VERSIONS } from '../../dist/shared/versions.js';
const root = path.resolve(import.meta.dirname, '../..');
@@ -113,7 +113,7 @@ test('startup discards a rejected cached subscription and returns to first-run',
}));
fs.writeFileSync(path.join(dir, 'sing-box-config.json'), '{}');
const child = spawn(process.execPath, ['src/server/index.js'], {
const child = spawn(process.execPath, ['dist/server/main.js'], {
cwd: root,
env: {
...process.env,
@@ -254,7 +254,7 @@ setInterval(() => {}, 60_000);
}));
const port = await freePort();
const child = spawn(process.execPath, ['src/server/index.js'], {
const child = spawn(process.execPath, ['dist/server/main.js'], {
cwd: root,
env: {
...process.env,
@@ -294,6 +294,29 @@ setInterval(() => {}, 60_000);
assert.equal(initial.route.localRulesPendingRestart, false);
assert.equal(JSON.stringify(initial).includes(subscriptionUrl), false);
const stateKeys = Object.keys(initial).sort();
assert.deepEqual(stateKeys, [
'apiVersion',
'configExists',
'connection',
'fetchedAt',
'gatewayAuto',
'generatedAt',
'hasSubscription',
'mode',
'operation',
'port',
'proxyPort',
'revision',
'route',
'selectedTag',
'selection',
'servers',
'singboxRunning',
'singboxStartedAt',
'subscription',
'subscriptionHost',
'userInfo',
]);
let revision = initial.revision;
let rulesRevision = initial.route.localRulesRevision;
+12 -2
View File
@@ -8,8 +8,8 @@ import {
atomicWriteJson,
createStateStore,
STATE_SCHEMA_VERSION,
} from '../../src/server/services/stateStore.js';
import { createServerId } from '../../src/shared/serverIdentity.js';
} from '../../dist/server/services/stateStore.js';
import { createServerId } from '../../dist/shared/serverIdentity.js';
const fixture = (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-state-store-'));
@@ -17,6 +17,16 @@ const fixture = (t) => {
return path.join(directory, 'state.json');
};
test('raw JSON stores stay typed unknown until a migrator validates them', () => {
const source = fs.readFileSync(
new URL('../../src/server/services/stateStore.ts', import.meta.url),
'utf8',
);
assert.match(source, /createJsonStore\(options: RawJsonStoreOptions\): JsonStore<unknown>/);
assert.match(source, /const migrate = options\.migrate \|\| \(\(value: unknown\) => value\)/);
assert.doesNotMatch(source, /value as T|migrate = \(value\) => value as/);
});
test('data invariant: failure before rename preserves the last successful file', (t) => {
const filePath = fixture(t);
atomicWriteJson(filePath, { revision: 1 });
+539
View File
@@ -0,0 +1,539 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import { createSubscriptionService } from '../../dist/server/features/subscription/index.js';
import { createSubscriptionMutationRoute } from '../../dist/server/http/routes/subscriptionMutationRoute.js';
import { HarborError } from '../../dist/shared/errors.js';
const oldServer = {
id: 'srv_old',
label: 'Old',
host: 'old.example',
port: 443,
protocol: 'vless',
};
const nextServer = {
id: 'srv_next',
label: 'Next',
host: 'next.example',
port: 443,
protocol: 'vless',
};
const routeRules = [{ type: 'domain_suffix', value: 'example', enabled: true }];
function parsed(servers = [nextServer]) {
return {
config: { normalized: true },
sourceConfig: { source: true },
servers,
userInfo: { total: 100 },
fetchedAt: '2026-08-08T10:00:00.000Z',
};
}
function createHarness(overrides = {}) {
let state = structuredClone(overrides.state ?? {
revision: 3,
subscriptionUrl: 'https://old.example/sub',
servers: [oldServer],
selectedServerId: oldServer.id,
appliedServerId: oldServer.id,
routeRules,
gatewayAutoEnabled: false,
connectionDesired: 'running',
});
let cache = structuredClone(overrides.cache ?? { url: 'old', config: { old: true } });
let config = overrides.config ?? 'old-config-bytes';
let gatewayAuto = structuredClone(overrides.gatewayAuto ?? { mode: 'gateway-direct', gatewayId: 'old' });
let running = overrides.running ?? true;
let serialized = Promise.resolve();
const calls = [];
const failures = { ...(overrides.failures || {}) };
const timers = [];
const failOnce = (name) => {
const failure = failures[name];
if (!failure) return;
delete failures[name];
throw failure;
};
const dependencies = {
provider: {
fetchSubscription: overrides.fetchSubscription || (async () => parsed()),
selectRefreshedServer: overrides.selectRefreshedServer || ((current, _before, after) => (
after.some((server) => server.id === current) ? current : ''
)),
},
state: {
read: () => structuredClone(state),
update: (mutator) => {
failOnce('stateUpdate');
const revision = state.revision + 1;
state = { ...structuredClone(mutator(structuredClone(state))), revision };
calls.push('state.update');
failOnce('stateUpdateAfter');
return structuredClone(state);
},
},
cache: {
read: () => structuredClone(cache),
write: (value) => {
failOnce('cacheWrite');
cache = structuredClone(value);
calls.push('cache.write');
},
remove: () => {
failOnce('cacheRemove');
cache = null;
calls.push('cache.remove');
},
},
config: {
build: (_value, selectedServerId, rules) => ({ selectedServerId, rules }),
read: () => config,
write: (value) => {
failOnce('configWrite');
config = JSON.stringify(value);
calls.push('config.write');
},
restore: (value) => {
failOnce('configRestore');
config = value;
calls.push('config.restore');
},
remove: () => {
failOnce('configRemove');
config = null;
calls.push('config.remove');
},
},
runtime: {
isRunning: async () => running,
stop: async () => {
calls.push('runtime.stop');
failOnce('runtimeStop');
running = false;
},
start: async () => {
calls.push('runtime.start');
const failure = Array.isArray(failures.runtimeStart)
? failures.runtimeStart.shift()
: failures.runtimeStart;
if (failure) {
if (!Array.isArray(failures.runtimeStart)) delete failures.runtimeStart;
running = false;
throw failure;
}
running = true;
},
},
gatewayAuto: {
read: () => structuredClone(gatewayAuto),
set: (value) => {
gatewayAuto = structuredClone(value);
calls.push('gateway.set');
},
createInitial: () => ({ mode: 'local-vpn' }),
},
serialize: (operation) => {
const result = serialized.then(operation, operation);
serialized = result.then(() => undefined, () => undefined);
return result;
},
scheduler: {
setInterval: (callback, intervalMs) => {
const timer = {
callback,
intervalMs,
unrefCalls: 0,
clearCalls: 0,
unref() { this.unrefCalls += 1; },
};
timers.push(timer);
return timer;
},
clearInterval: (timer) => { timer.clearCalls += 1; },
},
onRefreshError: overrides.onRefreshError || (() => {}),
};
return {
service: createSubscriptionService(dependencies),
calls,
timers,
snapshot: () => structuredClone({ state, cache, config, gatewayAuto, running }),
};
}
function assertRestoredSnapshot(actual, expected, label) {
const actualRevision = actual.state.revision;
const expectedRevision = expected.state.revision;
const actualDomain = structuredClone(actual);
const expectedDomain = structuredClone(expected);
delete actualDomain.state.revision;
delete expectedDomain.state.revision;
assert.deepEqual(actualDomain, expectedDomain, label);
assert.ok(actualRevision >= expectedRevision, `${label}: revision moved backwards`);
}
test('subscription import commits source config, clears selection, and leaves runtime stopped', async () => {
const harness = createHarness();
const result = await harness.service.importSubscription('https://new.example/sub');
const snapshot = harness.snapshot();
assert.equal(result.success, true);
assert.equal(result.selectedServerId, '');
assert.equal(snapshot.state.subscriptionUrl, 'https://new.example/sub');
assert.deepEqual(snapshot.state.routeRules, routeRules);
assert.equal(snapshot.state.gatewayAutoEnabled, false);
assert.equal(snapshot.state.connectionDesired, 'stopped');
assert.deepEqual(snapshot.cache.config, { source: true });
assert.equal(snapshot.config, null);
assert.deepEqual(snapshot.gatewayAuto, { mode: 'local-vpn' });
assert.equal(snapshot.running, false);
assert.equal(harness.calls.filter((call) => call === 'runtime.stop').length, 1);
assert.equal(harness.calls.filter((call) => call === 'runtime.start').length, 0);
});
test('subscription refresh retains selection and restarts only a previously running runtime', async () => {
const retained = { ...oldServer, label: 'Renamed' };
const harness = createHarness({ fetchSubscription: async () => parsed([retained, nextServer]) });
const result = await harness.service.refreshSavedSubscription();
const snapshot = harness.snapshot();
assert.equal(result.selectedServerId, oldServer.id);
assert.equal(snapshot.state.selectedServerId, oldServer.id);
assert.match(snapshot.config, /srv_old/);
assert.equal(snapshot.running, true);
assert.equal(harness.calls.filter((call) => call === 'runtime.stop').length, 0);
assert.equal(harness.calls.filter((call) => call === 'runtime.start').length, 1);
});
test('subscription import restores every snapshot on pre-commit failures', async () => {
for (const failurePoint of ['runtimeStop', 'configRemove', 'cacheWrite', 'stateUpdate', 'stateUpdateAfter']) {
const failure = new Error(failurePoint);
const harness = createHarness({ failures: { [failurePoint]: failure } });
const before = harness.snapshot();
await assert.rejects(
harness.service.importSubscription('https://new.example/sub'),
(error) => error === failure,
failurePoint,
);
assertRestoredSnapshot(harness.snapshot(), before, failurePoint);
}
});
test('subscription refresh restores snapshots and reapplies the old config on pre-commit failures', async () => {
for (const failurePoint of ['configWrite', 'cacheWrite', 'stateUpdate', 'stateUpdateAfter']) {
const failure = new Error(failurePoint);
const harness = createHarness({
fetchSubscription: async () => parsed([oldServer]),
failures: { [failurePoint]: failure },
});
const before = harness.snapshot();
await assert.rejects(
harness.service.refreshSavedSubscription(),
(error) => error === failure,
failurePoint,
);
assertRestoredSnapshot(harness.snapshot(), before, failurePoint);
assert.equal(
harness.calls.filter((call) => call === 'runtime.start').length,
failurePoint === 'stateUpdate' || failurePoint === 'stateUpdateAfter' ? 2 : 0,
failurePoint,
);
}
});
test('subscription reset restores cache, config, gateway, state, and runtime on removal or commit failure', async () => {
for (const failurePoint of ['configRemove', 'cacheRemove', 'stateUpdate', 'stateUpdateAfter']) {
const failure = new Error(failurePoint);
const harness = createHarness({ failures: { [failurePoint]: failure } });
const before = harness.snapshot();
await assert.rejects(
harness.service.resetSavedSubscription(),
(error) => error === failure,
failurePoint,
);
assertRestoredSnapshot(harness.snapshot(), before, failurePoint);
}
const noRuntime = createHarness({ failures: { stateUpdate: new Error('commit') } });
await assert.rejects(noRuntime.service.resetSavedSubscription({ stopRuntime: false }));
assert.equal(noRuntime.calls.includes('runtime.stop'), false);
assert.equal(noRuntime.calls.includes('runtime.start'), false);
const stoppedCleanup = createHarness({ running: false });
await stoppedCleanup.service.resetSavedSubscription();
assert.equal(stoppedCleanup.calls.filter((call) => call === 'runtime.stop').length, 1);
assert.equal(stoppedCleanup.calls.includes('runtime.start'), false);
});
test('subscription rollback attempts remaining restores and aggregates restoration failures', async () => {
const commitFailure = new Error('commit failed');
const configRestoreFailure = new Error('config restore failed');
const configBroken = createHarness({
fetchSubscription: async () => parsed([oldServer]),
failures: { stateUpdate: commitFailure, configRestore: configRestoreFailure },
});
const beforeConfigBroken = configBroken.snapshot();
await assert.rejects(configBroken.service.refreshSavedSubscription(), (error) => {
assert.ok(error instanceof AggregateError);
assert.deepEqual(error.errors, [commitFailure, configRestoreFailure]);
return true;
});
assert.equal(configBroken.snapshot().cache.url, beforeConfigBroken.cache.url);
assert.deepEqual(configBroken.snapshot().gatewayAuto, beforeConfigBroken.gatewayAuto);
assert.equal(configBroken.snapshot().running, true);
assert.equal(configBroken.calls.filter((call) => call === 'runtime.start').length, 2);
const removeFailure = new Error('remove failed');
const cacheRestoreFailure = new Error('cache restore failed');
const cacheBroken = createHarness({
failures: { configRemove: removeFailure, cacheWrite: cacheRestoreFailure },
});
const beforeCacheBroken = cacheBroken.snapshot();
await assert.rejects(cacheBroken.service.importSubscription('https://new.example/sub'), (error) => {
assert.ok(error instanceof AggregateError);
assert.deepEqual(error.errors, [removeFailure, cacheRestoreFailure]);
return true;
});
assert.equal(cacheBroken.snapshot().config, beforeCacheBroken.config);
assert.deepEqual(cacheBroken.snapshot().gatewayAuto, beforeCacheBroken.gatewayAuto);
assert.equal(cacheBroken.snapshot().running, true);
assert.equal(cacheBroken.calls.filter((call) => call === 'runtime.start').length, 1);
});
test('failed refresh start rolls back, and failed rollback start reports both causes', async () => {
const applyFailure = new Error('apply failed');
const recovered = createHarness({
fetchSubscription: async () => parsed([oldServer]),
failures: { runtimeStart: [applyFailure] },
});
const before = recovered.snapshot();
await assert.rejects(
recovered.service.refreshSavedSubscription(),
(error) => error === applyFailure,
);
assert.deepEqual(recovered.snapshot(), before);
assert.equal(recovered.calls.filter((call) => call === 'runtime.start').length, 2);
const rollbackFailure = new Error('rollback failed');
const broken = createHarness({
fetchSubscription: async () => parsed([oldServer]),
failures: { runtimeStart: [applyFailure, rollbackFailure] },
});
await assert.rejects(broken.service.refreshSavedSubscription(), (error) => {
assert.equal(error.code, 'PROCESS_START_FAILED');
assert.ok(error.cause instanceof AggregateError);
assert.deepEqual(error.cause.errors, [applyFailure, rollbackFailure]);
return true;
});
});
test('subscription refresh coalesces provider work and rejects a stale late result', async () => {
let releaseOld;
const oldResult = new Promise((resolve) => { releaseOld = resolve; });
let providerCalls = 0;
const harness = createHarness({
fetchSubscription: async (url) => {
providerCalls += 1;
return url.includes('old') ? oldResult : parsed();
},
});
const first = harness.service.refreshSavedSubscription();
const second = harness.service.refreshSavedSubscription();
assert.equal(first, second);
assert.equal(providerCalls, 1);
await harness.service.importSubscription('https://new.example/sub');
releaseOld(parsed([oldServer]));
await assert.rejects(first, (error) => error.code === 'STATE_CONFLICT');
assert.equal(harness.snapshot().state.subscriptionUrl, 'https://new.example/sub');
assert.equal(providerCalls, 2);
});
test('subscription generation rejects late success after a newer import of the same URL', async () => {
let releaseRefresh;
const delayed = new Promise((resolve) => { releaseRefresh = resolve; });
let providerCalls = 0;
const harness = createHarness({
fetchSubscription: async () => {
providerCalls += 1;
return providerCalls === 1 ? delayed : parsed([nextServer]);
},
});
const stale = harness.service.refreshSavedSubscription();
await harness.service.importSubscription('https://old.example/sub');
releaseRefresh(parsed([oldServer]));
await assert.rejects(stale, (error) => error.code === 'STATE_CONFLICT');
assert.deepEqual(harness.snapshot().state.servers, [nextServer]);
assert.equal(harness.snapshot().state.subscriptionUrl, 'https://old.example/sub');
});
test('subscription generation prevents a late terminal refresh from forgetting a newer import', async () => {
let rejectRefresh;
const delayed = new Promise((_resolve, reject) => { rejectRefresh = reject; });
let providerCalls = 0;
const harness = createHarness({
fetchSubscription: async () => {
providerCalls += 1;
return providerCalls === 1 ? delayed : parsed([nextServer]);
},
});
const stale = harness.service.refreshSavedSubscription();
await harness.service.importSubscription('https://old.example/sub');
rejectRefresh(new HarborError('SUBSCRIPTION_EXPIRED'));
await assert.rejects(stale, (error) => error.code === 'STATE_CONFLICT');
assert.equal(harness.snapshot().state.subscriptionUrl, 'https://old.example/sub');
assert.deepEqual(harness.snapshot().state.servers, [nextServer]);
assert.ok(harness.snapshot().cache);
});
test('subscription generation prevents a delayed import from resurrecting a forgotten subscription', async () => {
let releaseImport;
const delayed = new Promise((resolve) => { releaseImport = resolve; });
const harness = createHarness({ fetchSubscription: async () => delayed });
const staleImport = harness.service.importSubscription('https://new.example/sub');
await harness.service.resetSavedSubscription();
releaseImport(parsed());
await assert.rejects(staleImport, (error) => error.code === 'STATE_CONFLICT');
assert.equal(Boolean(harness.snapshot().state.subscriptionUrl), false);
assert.equal(harness.snapshot().cache, null);
assert.equal(harness.snapshot().config, null);
});
test('subscription generation lets only the first completed concurrent import commit', async () => {
let releaseFirst;
let releaseSecond;
const firstResult = new Promise((resolve) => { releaseFirst = resolve; });
const secondResult = new Promise((resolve) => { releaseSecond = resolve; });
const harness = createHarness({
fetchSubscription: async (url) => url.includes('first') ? firstResult : secondResult,
});
const first = harness.service.importSubscription('https://first.example/sub');
const second = harness.service.importSubscription('https://second.example/sub');
releaseSecond(parsed([nextServer]));
await second;
releaseFirst(parsed([oldServer]));
await assert.rejects(first, (error) => error.code === 'STATE_CONFLICT');
assert.equal(harness.snapshot().state.subscriptionUrl, 'https://second.example/sub');
assert.deepEqual(harness.snapshot().state.servers, [nextServer]);
});
test('only terminal subscription refresh failures forget saved data', async () => {
const cases = [
['SUBSCRIPTION_EXPIRED', true],
['SUBSCRIPTION_DISABLED', true],
['SUBSCRIPTION_REJECTED', true],
['SUBSCRIPTION_TRAFFIC_EXHAUSTED', false],
['SUBSCRIPTION_INVALID', false],
['PROVIDER_UNAVAILABLE', false],
];
for (const [code, resets] of cases) {
const harness = createHarness({
running: false,
fetchSubscription: async () => { throw new HarborError(code); },
});
await assert.rejects(harness.service.refreshSavedSubscription(), (error) => error.code === code);
assert.equal(Boolean(harness.snapshot().state.subscriptionUrl), !resets, code);
assert.equal(Boolean(harness.snapshot().cache), !resets, code);
}
});
test('subscription scheduler is unrefed, skips empty state, reports errors, and stops idempotently', async () => {
const errors = [];
let providerCalls = 0;
const harness = createHarness({
state: { revision: 0, servers: [], selectedServerId: '', appliedServerId: '', routeRules: [] },
fetchSubscription: async () => {
providerCalls += 1;
throw new HarborError('PROVIDER_UNAVAILABLE');
},
onRefreshError: (error) => errors.push(error.code),
});
harness.service.startAutoRefresh(1234);
harness.service.startAutoRefresh(1234);
assert.equal(harness.timers.length, 1);
assert.equal(harness.timers[0].intervalMs, 1234);
assert.equal(harness.timers[0].unrefCalls, 1);
harness.timers[0].callback();
await new Promise(setImmediate);
assert.equal(providerCalls, 0);
const stateful = createHarness({
fetchSubscription: async () => {
providerCalls += 1;
throw new HarborError('PROVIDER_UNAVAILABLE');
},
onRefreshError: (error) => errors.push(error.code),
});
stateful.service.startAutoRefresh(10);
stateful.timers[0].callback();
await new Promise(setImmediate);
await new Promise(setImmediate);
assert.equal(providerCalls, 1);
assert.deepEqual(errors, ['PROVIDER_UNAVAILABLE']);
stateful.service.stopAutoRefresh();
stateful.service.stopAutoRefresh();
assert.equal(stateful.timers[0].clearCalls, 1);
});
test('subscription mutation route preserves methods, operation kinds, and response fields', async () => {
const operations = [];
const sent = [];
const service = {
importSubscription: async (url) => ({ success: true, imported: url }),
refreshSavedSubscription: async () => ({ success: true, refreshed: true }),
resetSavedSubscription: async () => { operations.push('reset'); },
};
const route = createSubscriptionMutationRoute({
subscriptionService: service,
readBody: async () => ({ url: ' https://new.example/sub ' }),
withOperation: async (kind, operation) => {
operations.push(kind);
return operation();
},
sendState: async (_res, extra = {}) => { sent.push(extra); },
});
const response = {};
assert.equal(await route.handle({ method: 'GET', url: '/api/subscription/fetch' }, response), false);
assert.equal(await route.handle({ method: 'POST', url: '/api/subscription/fetch' }, response), true);
assert.equal(await route.handle({ method: 'POST', url: '/api/subscription/refresh' }, response), true);
assert.equal(await route.handle({ method: 'DELETE', url: '/api/subscription' }, response), true);
assert.deepEqual(operations, ['subscription-import', 'subscription-refresh', 'subscription-forget', 'reset']);
assert.deepEqual(sent, [
{ success: true, imported: 'https://new.example/sub' },
{ refreshed: true },
{},
]);
});
test('composition root contains no displaced subscription mutation owner', () => {
const source = readFileSync(new URL('../../src/server/index.ts', import.meta.url), 'utf8');
assert.match(source, /createSubscriptionMutationRoute\(\{/);
assert.match(source, /subscriptionService\.startAutoRefresh\(SUBSCRIPTION_REFRESH_INTERVAL_MS\)/);
assert.doesNotMatch(source, /subscriptionRefreshPromise|subscriptionRefreshTimer|TERMINAL_SUBSCRIPTION_CODES/);
assert.doesNotMatch(source, /req\.url === ['"]\/api\/subscription\/(?:fetch|refresh)['"]/);
assert.doesNotMatch(source, /req\.method === ['"]DELETE['"] && req\.url === ['"]\/api\/subscription['"]/);
});
@@ -0,0 +1,39 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import { createValidateSubscription } from '../../dist/server/features/subscription/index.js';
import { createSubscriptionValidationRoute } from '../../dist/server/http/routes/subscriptionValidationRoute.js';
test('subscription validation trims once and returns only the provider server count', async () => {
const calls = [];
const validate = createValidateSubscription(async (url) => {
calls.push(url);
return { servers: [{ id: 'one' }, { id: 'two' }], privateConfig: 'not returned' };
});
assert.deepEqual(await validate(' https://provider.example/sub '), { servers: 2 });
assert.deepEqual(await validate(null), { servers: 2 });
assert.deepEqual(calls, ['https://provider.example/sub', 'null']);
const failure = new Error('provider failed');
await assert.rejects(createValidateSubscription(async () => { throw failure; })('url'), (error) => error === failure);
});
test('subscription validation route is the only method/path adapter', async () => {
const sent = [];
const route = createSubscriptionValidationRoute({
validateSubscription: async (url) => ({ servers: String(url).length }),
readBody: async () => ({ url: 'trimmed' }),
sendState: async (_res, extra) => { sent.push(extra); },
});
const response = {};
assert.equal(await route.handle({ method: 'GET', url: '/api/subscription/validate' }, response), false);
assert.equal(await route.handle({ method: 'POST', url: '/api/subscription/validate' }, response), true);
assert.deepEqual(sent, [{ servers: 7 }]);
const compositionSource = readFileSync(new URL('../../src/server/index.ts', import.meta.url), 'utf8');
assert.match(compositionSource, /createSubscriptionValidationRoute\(\{/);
assert.doesNotMatch(compositionSource, /req\.url === ['"]\/api\/subscription\/validate['"]/);
});
+11 -1
View File
@@ -4,7 +4,7 @@ import test from 'node:test';
import {
parseSubscriptionBody,
selectRefreshedServer,
} from '../../src/server/subscription.js';
} from '../../dist/server/subscription.js';
const parse = (outbounds) => parseSubscriptionBody(JSON.stringify({ outbounds }));
const outbound = (tag, server, server_port = 443) => ({
@@ -76,3 +76,13 @@ test('provider placeholders never become selectable servers', () => {
assert.deepEqual(parsed.servers.map((server) => server.label), ['Amsterdam']);
assert.equal(parsed.config.outbounds.length, 1);
});
test('subscription parser rejects JSON primitives and arrays', () => {
for (const value of [null, [], 42, true, 'provider rejected']) {
assert.throws(
() => parseSubscriptionBody(JSON.stringify(value)),
(error) => error.code === 'SUBSCRIPTION_INVALID',
`expected ${JSON.stringify(value)} to be rejected`,
);
}
});
+115 -2
View File
@@ -1,8 +1,30 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import { buildGatewayVersionInfo, buildVersionInfo } from '../../src/server/version.js';
import { HARBOR_VERSIONS, versionCompatibility } from '../../src/shared/versions.js';
import { buildGatewayVersionInfo, buildVersionInfo } from '../../dist/server/version.js';
import { createVersionRoute } from '../../dist/server/http/routes/versionRoute.js';
import { HARBOR_VERSIONS, versionCompatibility } from '../../dist/shared/versions.js';
function response() {
return {
writeHead(status, headers) {
this.status = status;
this.headers = headers;
},
end(payload) {
this.rawPayload = payload;
this.payload = JSON.parse(payload);
},
};
}
const controlVersionInfo = {
apiVersion: 1,
location: 'gateway',
components: { gatewayBackend: '0.21.17' },
runtime: { singBox: '1.12.13' },
};
test('component versions enforce one Harbor major and one Gateway major.minor', () => {
assert.deepEqual(versionCompatibility(HARBOR_VERSIONS), {
@@ -49,3 +71,94 @@ test('runtime version info reports the installed sing-box binary', () => {
runtime: { singBox: '1.12.13' },
});
});
test('version route returns prebuilt local info without a dataplane refresh', async () => {
const res = response();
const route = createVersionRoute({
versionInfo: controlVersionInfo,
refreshDataplaneRuntime: null,
});
assert.equal(await route.handle({ method: 'GET', url: '/api/version' }, res), true);
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, controlVersionInfo);
});
test('version route refreshes split Gateway once and preserves remote runtime overlay', async () => {
let refreshes = 0;
const route = createVersionRoute({
versionInfo: controlVersionInfo,
refreshDataplaneRuntime: async () => {
refreshes += 1;
return {
gatewayBackendVersion: '0.21.16',
singBoxVersion: '1.12.14',
};
},
});
const res = response();
assert.equal(await route.handle({ method: 'GET', url: '/api/version' }, res), true);
assert.equal(refreshes, 1);
assert.deepEqual(res.payload, {
apiVersion: 1,
location: 'gateway',
components: { gatewayBackend: '0.21.17' },
runtime: { dataplaneVersion: '0.21.16', singBox: '1.12.14' },
});
});
test('version route keeps remote null fallbacks', async () => {
const route = createVersionRoute({
versionInfo: controlVersionInfo,
refreshDataplaneRuntime: async () => ({}),
});
const res = response();
await route.handle({ method: 'GET', url: '/api/version' }, res);
assert.deepEqual(res.payload.runtime, { dataplaneVersion: null, singBox: null });
});
test('version route exact guard avoids refresh and refresh errors propagate unchanged', async () => {
let refreshes = 0;
const refreshError = new Error('status failed');
const route = createVersionRoute({
versionInfo: controlVersionInfo,
refreshDataplaneRuntime: async () => {
refreshes += 1;
throw refreshError;
},
});
for (const [method, url] of [
['POST', '/api/version'],
['GET', '/api/version?source=client'],
['GET', '/api/version/'],
['GET', '/api/other'],
]) {
assert.equal(await route.handle({ method, url }, response()), false);
}
assert.equal(refreshes, 0);
const res = response();
await assert.rejects(
route.handle({ method: 'GET', url: '/api/version' }, res),
(error) => error === refreshError,
);
assert.equal(refreshes, 1);
assert.equal(res.status, undefined);
});
test('version 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/versionRoute.ts', import.meta.url),
'utf8',
);
assert.match(index, /createVersionRoute\(\{/);
assert.match(index, /versionRoute\.handle\(req, res\)/);
assert.doesNotMatch(index, /['"]\/api\/version['"]|buildGatewayVersionInfo|sendJson/);
assert.match(route, /req\.url !== '\/api\/version'/);
assert.match(route, /buildGatewayVersionInfo\(/);
});