Refactor VPN proxy components and update related behavior
This commit is contained in:
@@ -15,6 +15,30 @@ import { createGatewayAutoRoute } from '../../dist/server/http/routes/gatewayAut
|
||||
|
||||
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 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: [],
|
||||
routeRulesRevision: 0,
|
||||
gatewayAutoEnabled: true,
|
||||
});
|
||||
const networkA = {
|
||||
gateway: '192.168.50.111',
|
||||
interface: 'en0',
|
||||
@@ -51,17 +75,7 @@ function deferred() {
|
||||
}
|
||||
|
||||
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 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();
|
||||
@@ -109,11 +123,14 @@ function createHarness(overrides = {}) {
|
||||
readConfig: () => overrides.missingSubscription ? null : { outbounds: [] },
|
||||
},
|
||||
config: {
|
||||
build: (_subscription, selectedServerId, routeRules, gatewayAuto) => ({
|
||||
selectedServerId,
|
||||
routeRules,
|
||||
clientDirect: gatewayAuto.mode === 'gateway-direct',
|
||||
}),
|
||||
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');
|
||||
@@ -144,6 +161,10 @@ function createHarness(overrides = {}) {
|
||||
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),
|
||||
@@ -224,16 +245,8 @@ test('gateway-auto handles no-op and metadata-only discovery without config/runt
|
||||
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,
|
||||
},
|
||||
state: { ...canonicalState([]), revision: 1 },
|
||||
running: false,
|
||||
});
|
||||
await stateOnly.service.setEnabled(false);
|
||||
assert.equal(stateOnly.snapshot().gatewayAuto.mode, 'local-vpn');
|
||||
@@ -266,6 +279,38 @@ test('gateway-auto startup writes candidate config before publication without ap
|
||||
]);
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -382,7 +427,9 @@ test('gateway-auto discards stale subscription and route probe results', async (
|
||||
staleSubscription.service.set(directState());
|
||||
const refresh = staleSubscription.service.refresh();
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
staleSubscription.patchState({ subscriptionUrl: `${subscriptionUrl}-new` });
|
||||
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());
|
||||
|
||||
Reference in New Issue
Block a user