570 lines
21 KiB
JavaScript
570 lines
21 KiB
JavaScript
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 storedProfile = (servers = [server]) => ({
|
|
id: 'primary',
|
|
label: 'Основной',
|
|
subscriptionUrl,
|
|
subscriptionConfig: { outbounds: [] },
|
|
servers,
|
|
userInfo: {},
|
|
fetchedAt: null,
|
|
desiredServerId: servers[0]?.id || '',
|
|
lastRefreshAttemptAt: null,
|
|
lastRefreshErrorCode: null,
|
|
});
|
|
const routeRules = [
|
|
{ type: 'domain', value: 'api.example.com', enabled: true, outbound: 'vpn' },
|
|
{ type: 'domain_suffix', value: 'example.com', enabled: true, outbound: 'direct' },
|
|
];
|
|
const canonicalState = (profiles = [storedProfile()]) => ({
|
|
revision: 10,
|
|
profiles,
|
|
desiredProfileId: profiles[0]?.id || '',
|
|
appliedProfileId: profiles[0]?.id || '',
|
|
appliedServerId: profiles[0]?.desiredServerId || '',
|
|
appliedServerSnapshot: profiles[0]?.servers[0] || null,
|
|
routeRules,
|
|
appliedRouteRules: routeRules,
|
|
routeRulesRevision: 0,
|
|
gatewayAutoEnabled: true,
|
|
});
|
|
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 ?? canonicalState());
|
|
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) => {
|
|
if (failures.configBuild) throw failures.configBuild;
|
|
return {
|
|
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;
|
|
},
|
|
stopCommand: async () => {
|
|
events.push('runtime.stop');
|
|
return overrides.stopCommandResult || { ok: true, mutationStarted: true };
|
|
},
|
|
},
|
|
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: { ...canonicalState([]), revision: 1 },
|
|
running: false,
|
|
});
|
|
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',
|
|
]);
|
|
assert.deepEqual(harness.snapshot().state.appliedRouteRules, routeRules);
|
|
});
|
|
|
|
test('gateway-auto running transitions publish the rules actually present in each mode', async () => {
|
|
const harness = createHarness({ running: true });
|
|
await harness.service.refresh();
|
|
assert.equal(harness.snapshot().gatewayAuto.mode, 'gateway-direct');
|
|
assert.deepEqual(harness.snapshot().state.appliedRouteRules, []);
|
|
|
|
await harness.service.setEnabled(false);
|
|
assert.equal(harness.snapshot().gatewayAuto.mode, 'local-vpn');
|
|
assert.deepEqual(harness.snapshot().state.appliedRouteRules, routeRules);
|
|
});
|
|
|
|
test('gateway loss safely stops when the applied server disappeared from refreshed config', async () => {
|
|
const state = canonicalState();
|
|
state.profiles[0].servers = [];
|
|
state.profiles[0].desiredServerId = '';
|
|
const buildError = Object.assign(new Error('server removed'), { code: 'SERVER_NOT_FOUND' });
|
|
const harness = createHarness({
|
|
state,
|
|
gatewayAuto: directState(),
|
|
network: networkB,
|
|
failures: { configBuild: buildError },
|
|
});
|
|
await assert.rejects(harness.service.refresh(), (error) => error === buildError);
|
|
const after = harness.snapshot();
|
|
assert.equal(after.gatewayAuto.mode, 'local-vpn');
|
|
assert.equal(after.state.connectionDesired, 'stopped');
|
|
assert.equal(after.state.appliedProfileId, '');
|
|
assert.equal(after.state.appliedServerId, '');
|
|
assert.equal(harness.events.includes('runtime.stop'), true);
|
|
});
|
|
|
|
test('Gateway discovery preserves a stale applied local runtime when direct config cannot build', async () => {
|
|
const state = canonicalState();
|
|
state.profiles[0].servers = [];
|
|
state.profiles[0].desiredServerId = '';
|
|
const buildError = Object.assign(new Error('server removed'), { code: 'SERVER_NOT_FOUND' });
|
|
const harness = createHarness({ state, failures: { configBuild: buildError } });
|
|
const before = harness.snapshot();
|
|
await assert.rejects(harness.service.refresh(), (error) => error === buildError);
|
|
assert.deepEqual(withoutRevision(harness.snapshot()), withoutRevision(before));
|
|
assert.equal(harness.events.includes('runtime.stop'), false);
|
|
});
|
|
|
|
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));
|
|
const changedState = staleSubscription.snapshot().state;
|
|
changedState.profiles[0].subscriptionUrl = `${subscriptionUrl}-new`;
|
|
staleSubscription.setState(changedState);
|
|
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['"]/);
|
|
});
|