Persist configurable connectivity diagnostics
Build and Deploy Gateway / build-and-push (push) Successful in 21s
Build and Deploy Gateway / deploy (push) Successful in 14s

This commit is contained in:
2026-08-19 13:38:19 +03:00
parent df865fbe3d
commit 416b2b294a
22 changed files with 407 additions and 131 deletions
+101 -33
View File
@@ -59,6 +59,7 @@ test('connectivity diagnostics endpoint is available in Connect and Gateway thro
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, /sendState: \(res\) => stateRoute\.send\(res\)/);
assert.match(server, /createConnectivityDiagnosticsUseCase\(\{/);
assert.match(server, /createConnectivityDiagnosticsRoute\(\{/);
assert.match(server, /connectivityDiagnosticsRoute\.handle\(req, res\)/);
@@ -69,6 +70,12 @@ test('connectivity diagnostics endpoint is available in Connect and Gateway thro
test('connectivity use case captures applied server before probes and preserves result fields', async () => {
const events = [];
let state = {
revision: 3,
diagnostics: {
configured: true,
customServices: [{ id: 'custom-saved', label: 'Saved', url: 'https://example.com/' }],
hiddenServiceIds: [],
},
desiredProfileId: 'primary',
appliedProfileId: 'primary',
appliedServerId: 'applied',
@@ -90,9 +97,12 @@ test('connectivity use case captures applied server before probes and preserves
assessment: { summary: 'available' },
};
const useCase = createConnectivityDiagnosticsUseCase({
readState: () => {
events.push('state');
return state;
state: {
read: () => {
events.push('state');
return state;
},
update: () => { throw new Error('unexpected update'); },
},
runDiagnostics: async (services, target) => {
events.push(['probe', services, target]);
@@ -100,12 +110,12 @@ test('connectivity use case captures applied server before probes and preserves
return sourceResult;
},
});
const resultPromise = useCase.run({ raw: true }, 42);
const resultPromise = useCase.run(42);
state.profiles[0].servers[0].label = 'Changed during probe';
releaseProbe();
const result = await resultPromise;
assert.deepEqual(events, ['state', ['probe', { raw: true }, 42]]);
assert.deepEqual(events, ['state', ['probe', state.diagnostics.customServices, 42]]);
assert.deepEqual(result, {
...sourceResult,
vpn: {
@@ -119,52 +129,98 @@ test('connectivity use case captures applied server before probes and preserves
test('connectivity use case keeps applied priority, selected fallback and error identity', async () => {
const result = { vpn: { available: false }, marker: true };
const selected = createConnectivityDiagnosticsUseCase({
readState: () => ({
desiredProfileId: 'primary',
appliedServerId: '',
profiles: [{
id: 'primary',
desiredServerId: 'selected',
servers: [{ id: 'selected', label: 'Selected' }],
}],
}),
state: {
read: () => ({
desiredProfileId: 'primary',
appliedServerId: '',
profiles: [{
id: 'primary',
desiredServerId: 'selected',
servers: [{ id: 'selected', label: 'Selected' }],
}],
}),
update: () => { throw new Error('unexpected update'); },
},
runDiagnostics: async () => result,
});
assert.deepEqual((await selected.run(null, null)).vpn.server, {
assert.deepEqual((await selected.run(null)).vpn.server, {
id: 'selected',
label: 'Selected',
});
const missingApplied = createConnectivityDiagnosticsUseCase({
readState: () => ({
desiredProfileId: 'primary',
appliedProfileId: 'primary',
appliedServerId: 'missing',
profiles: [{
id: 'primary',
desiredServerId: 'selected',
servers: [{ id: 'selected', label: 'Selected' }],
}],
}),
state: {
read: () => ({
desiredProfileId: 'primary',
appliedProfileId: 'primary',
appliedServerId: 'missing',
profiles: [{
id: 'primary',
desiredServerId: 'selected',
servers: [{ id: 'selected', label: 'Selected' }],
}],
}),
update: () => { throw new Error('unexpected update'); },
},
runDiagnostics: async () => result,
});
assert.equal((await missingApplied.run([], null)).vpn.server, null);
assert.equal((await missingApplied.run(null)).vpn.server, null);
const stateError = new Error('state failed');
let probes = 0;
const brokenState = createConnectivityDiagnosticsUseCase({
readState: () => { throw stateError; },
state: {
read: () => { throw stateError; },
update: () => { throw new Error('unexpected update'); },
},
runDiagnostics: async () => { probes += 1; return result; },
});
await assert.rejects(brokenState.run([], null), (error) => error === stateError);
await assert.rejects(brokenState.run(null), (error) => error === stateError);
assert.equal(probes, 0);
const probeError = new Error('probe failed');
const brokenProbe = createConnectivityDiagnosticsUseCase({
readState: () => ({ servers: [] }),
state: {
read: () => ({ profiles: [] }),
update: () => { throw new Error('unexpected update'); },
},
runDiagnostics: async () => { throw probeError; },
});
await assert.rejects(brokenProbe.run([], null), (error) => error === probeError);
await assert.rejects(brokenProbe.run(null), (error) => error === probeError);
});
test('diagnostics settings validate and replace one canonical revision', () => {
let state = {
revision: 7,
diagnostics: { configured: false, customServices: [], hiddenServiceIds: [] },
};
const useCase = createConnectivityDiagnosticsUseCase({
state: {
read: () => state,
update: (mutator) => {
state = { ...mutator(state), revision: state.revision + 1 };
return state;
},
},
runDiagnostics: async () => ({ vpn: {} }),
});
useCase.updateSettings({
customServices: [{ id: 'custom-status', label: 'Status', url: 'https://example.com/status' }],
hiddenServiceIds: ['speedtest'],
}, 7);
assert.deepEqual(state.diagnostics, {
configured: true,
customServices: [{ id: 'custom-status', label: 'Status', url: 'https://example.com/status' }],
hiddenServiceIds: ['speedtest'],
});
assert.equal(state.revision, 8);
assert.throws(() => useCase.updateSettings({ customServices: [], hiddenServiceIds: [] }, 7), {
code: 'STATE_CONFLICT',
});
assert.throws(() => useCase.updateSettings({
customServices: [{ id: 'bad', label: 'Router', url: 'http://router.local/' }],
hiddenServiceIds: [],
}, 8), { code: 'REQUEST_INVALID' });
});
function routeResponse() {
@@ -189,34 +245,46 @@ test('connectivity route preserves exact URL, defaults and raw response', async
calls.push(args);
return { checkedAt: 'now', vpn: { server: null } };
},
updateSettings: (...args) => calls.push(['settings', ...args]),
},
readBody: async () => {
bodyReads += 1;
return body;
},
sendState: async (response) => {
response.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
response.end(JSON.stringify({ success: true, state: {} }));
},
});
const res = routeResponse();
assert.equal(await route.handle({
method: 'POST',
url: '/api/diagnostics/connectivity',
}, res), true);
assert.deepEqual(calls, [[[], null]]);
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]);
assert.deepEqual(calls.at(-1), [17]);
body = { settings: { customServices: [], hiddenServiceIds: [] }, expectedRevision: 4 };
const settingsResponse = routeResponse();
assert.equal(await route.handle({ method: 'PUT', url: '/api/diagnostics/settings' }, settingsResponse), true);
assert.deepEqual(calls.at(-1), ['settings', body.settings, 4]);
assert.equal(settingsResponse.payload.success, true);
for (const [method, url] of [
['GET', '/api/diagnostics/connectivity'],
['POST', '/api/diagnostics/connectivity?target=all'],
['POST', '/api/diagnostics/settings'],
['POST', '/api/diagnostics/other'],
]) {
assert.equal(await route.handle({ method, url }, routeResponse()), false);
}
assert.equal(bodyReads, 2);
assert.equal(bodyReads, 3);
});
test('a targeted IP row uses three samples and keeps the majority address', async () => {