Refactor VPN proxy client implementation
This commit is contained in:
+103
-1
@@ -2,9 +2,10 @@ import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
api,
|
||||
HarborApiError,
|
||||
request,
|
||||
} from '../../src/web/api.js';
|
||||
} from '../../.test-dist/src/web/api/harborClient.js';
|
||||
|
||||
const response = (status, error) => ({
|
||||
ok: status >= 200 && status < 300,
|
||||
@@ -53,3 +54,104 @@ test('local unknown errors get a safe message and diagnostic reference', () => {
|
||||
assert.equal(typeof error.correlationId, 'string');
|
||||
assert.ok(error.correlationId.length >= 8);
|
||||
});
|
||||
|
||||
test('typed endpoint facade preserves exact request contracts and raw payload identity', async () => {
|
||||
const calls = [];
|
||||
const payload = { success: true, marker: 'raw' };
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (url, options) => {
|
||||
calls.push([url, options]);
|
||||
return { ok: true, status: 200, json: async () => payload };
|
||||
};
|
||||
const signal = new AbortController().signal;
|
||||
try {
|
||||
const cases = [
|
||||
[() => api.version(), '/api/version', {}],
|
||||
[() => api.subscription.validate('https://sub', { signal }), '/api/subscription/validate', {
|
||||
method: 'POST', body: JSON.stringify({ url: 'https://sub' }), signal,
|
||||
}],
|
||||
[() => api.subscription.fetch('https://sub'), '/api/subscription/fetch', {
|
||||
method: 'POST', body: JSON.stringify({ url: 'https://sub' }),
|
||||
}],
|
||||
[() => api.subscription.refresh(), '/api/subscription/refresh', { method: 'POST' }],
|
||||
[() => api.subscription.forget(), '/api/subscription', { method: 'DELETE' }],
|
||||
[() => api.apply('server-1'), '/api/apply', {
|
||||
method: 'POST', body: JSON.stringify({ serverId: 'server-1', selectedTag: 'server-1' }),
|
||||
}],
|
||||
[() => api.gatewayAuto.setEnabled(true), '/api/gateway-auto', {
|
||||
method: 'POST', body: JSON.stringify({ enabled: true }),
|
||||
}],
|
||||
[() => api.routeRules.update([{ type: 'domain', value: 'example.com' }], 7), '/api/route-rules', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
rules: [{ type: 'domain', value: 'example.com' }],
|
||||
expectedRulesRevision: 7,
|
||||
}),
|
||||
}],
|
||||
[() => api.devices.list(), '/api/devices', {}],
|
||||
[() => api.devices.refresh(), '/api/devices/refresh', { method: 'POST' }],
|
||||
[() => api.devices.update('dev_1', { alias: 'TV' }, 8), '/api/devices/dev_1', {
|
||||
method: 'PUT', body: JSON.stringify({ alias: 'TV', expectedRevision: 8 }),
|
||||
}],
|
||||
[() => api.devices.setPolicy('dev_1', 'direct', 9), '/api/devices/dev_1/policy', {
|
||||
method: 'PUT', body: JSON.stringify({ mode: 'direct', expectedRevision: 9 }),
|
||||
}],
|
||||
[() => api.diagnostics.connectivity(), '/api/diagnostics/connectivity', {
|
||||
method: 'POST', body: JSON.stringify({ services: [], target: null }),
|
||||
}],
|
||||
[() => api.singbox.stop(), '/api/singbox/stop', { method: 'POST' }],
|
||||
[() => api.singbox.restart(), '/api/singbox/restart', { method: 'POST' }],
|
||||
[() => api.servers.ping(['one', 'two']), '/api/servers/ping-all', {
|
||||
method: 'POST', body: JSON.stringify({ serverIds: ['one', 'two'] }),
|
||||
}],
|
||||
];
|
||||
|
||||
for (const [invoke, url, options] of cases) {
|
||||
assert.equal(await invoke(), payload);
|
||||
const [actualUrl, actualOptions] = calls.at(-1);
|
||||
assert.equal(actualUrl, url);
|
||||
assert.deepEqual(actualOptions, {
|
||||
...options,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('request preserves caller headers, AbortError identity and JSON fallbacks', async () => {
|
||||
let received;
|
||||
const value = { ok: 'raw' };
|
||||
assert.equal(await request('/api/test', {
|
||||
headers: { 'content-type': 'application/custom', 'x-harbor': 'yes' },
|
||||
}, async (url, options) => {
|
||||
received = [url, options];
|
||||
return { ok: true, status: 200, json: async () => value };
|
||||
}), value);
|
||||
assert.deepEqual(received, ['/api/test', {
|
||||
headers: { 'content-type': 'application/custom', 'x-harbor': 'yes' },
|
||||
}]);
|
||||
|
||||
const aborted = Object.assign(new Error('cancelled'), { name: 'AbortError' });
|
||||
await assert.rejects(
|
||||
request('/api/test', {}, async () => { throw aborted; }),
|
||||
(error) => error === aborted,
|
||||
);
|
||||
await assert.rejects(
|
||||
request('/api/test', {}, async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => { throw new Error('invalid json'); },
|
||||
})),
|
||||
(error) => error.code === 'UNKNOWN' && error.status === 500,
|
||||
);
|
||||
await assert.rejects(
|
||||
request('/api/test', {}, async () => ({
|
||||
ok: false,
|
||||
status: 503,
|
||||
json: async () => { throw new Error('invalid json'); },
|
||||
})),
|
||||
(error) => error.code === 'CONTROL_UNREACHABLE' && error.status === 503,
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user