Unify Harbor error handling across server and client
This commit is contained in:
@@ -29,13 +29,18 @@ async function freePort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
async function request(port, pathname, method = 'GET', body) {
|
||||
async function rawRequest(port, pathname, method = 'GET', body) {
|
||||
const response = await fetch(`http://127.0.0.1:${port}${pathname}`, {
|
||||
method,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
});
|
||||
const payload = await response.json();
|
||||
return { response, payload };
|
||||
}
|
||||
|
||||
async function request(port, pathname, method = 'GET', body) {
|
||||
const { response, payload } = await rawRequest(port, pathname, method, body);
|
||||
assert.equal(response.ok, true, JSON.stringify(payload));
|
||||
return payload;
|
||||
}
|
||||
@@ -54,7 +59,7 @@ async function waitForState(port, child, stderr) {
|
||||
|
||||
test('state v1 normalizes legacy storage and validates the canonical snapshot', () => {
|
||||
const stored = normalizeStoredState({
|
||||
subscriptionUrl: 'https://provider.example/subscription/full-secret-token',
|
||||
subscriptionUrl: 'https://provider.example/subscription/test',
|
||||
selectedTag: ' legacy ',
|
||||
servers: [{ tag: ' legacy ', type: 'vless', server: 'vpn.example', server_port: 443 }],
|
||||
});
|
||||
@@ -95,14 +100,20 @@ test('GET and domain mutations return one state shape with monotonic revisions',
|
||||
}],
|
||||
};
|
||||
fs.mkdirSync(binDir);
|
||||
fs.writeFileSync(path.join(binDir, 'sing-box'), `#!/usr/bin/env node
|
||||
const singboxPath = path.join(binDir, 'sing-box');
|
||||
const workingSingbox = `#!/usr/bin/env node
|
||||
if (process.argv[2] === 'check') process.exit(0);
|
||||
process.on('SIGTERM', () => process.exit(0));
|
||||
setInterval(() => {}, 60_000);
|
||||
`);
|
||||
fs.chmodSync(path.join(binDir, 'sing-box'), 0o755);
|
||||
`;
|
||||
fs.writeFileSync(singboxPath, workingSingbox);
|
||||
fs.chmodSync(singboxPath, 0o755);
|
||||
|
||||
const subscriptionServer = http.createServer((req, res) => {
|
||||
if (req.url === '/unavailable') {
|
||||
res.writeHead(503);
|
||||
return res.end('unavailable');
|
||||
}
|
||||
res.writeHead(200, {
|
||||
'content-type': 'application/json',
|
||||
'subscription-userinfo': 'upload=10; download=20; total=100',
|
||||
@@ -110,7 +121,7 @@ setInterval(() => {}, 60_000);
|
||||
res.end(JSON.stringify(config));
|
||||
});
|
||||
const subscriptionPort = await listen(subscriptionServer);
|
||||
const subscriptionUrl = `http://127.0.0.1:${subscriptionPort}/subscription/full-secret-token`;
|
||||
const subscriptionUrl = `http://127.0.0.1:${subscriptionPort}/subscription/test`;
|
||||
fs.writeFileSync(path.join(dir, 'state.json'), JSON.stringify({
|
||||
subscriptionUrl,
|
||||
selectedTag: 'test-vpn',
|
||||
@@ -150,6 +161,42 @@ setInterval(() => {}, 60_000);
|
||||
const stateKeys = Object.keys(initial).sort();
|
||||
let revision = initial.revision;
|
||||
|
||||
const invalidSubscription = await rawRequest(
|
||||
port,
|
||||
'/api/subscription/validate',
|
||||
'POST',
|
||||
{ url: 'not-a-url' },
|
||||
);
|
||||
assert.equal(invalidSubscription.response.status, 400);
|
||||
assert.deepEqual(
|
||||
{
|
||||
code: invalidSubscription.payload.error.code,
|
||||
retryable: invalidSubscription.payload.error.retryable,
|
||||
},
|
||||
{ code: 'SUBSCRIPTION_INVALID', retryable: false },
|
||||
);
|
||||
assert.equal(typeof invalidSubscription.payload.error.correlationId, 'string');
|
||||
|
||||
const providerUnavailable = await rawRequest(
|
||||
port,
|
||||
'/api/subscription/validate',
|
||||
'POST',
|
||||
{ url: `http://127.0.0.1:${subscriptionPort}/unavailable` },
|
||||
);
|
||||
assert.equal(providerUnavailable.response.status, 502);
|
||||
assert.equal(providerUnavailable.payload.error.code, 'PROVIDER_UNAVAILABLE');
|
||||
assert.equal(providerUnavailable.payload.error.retryable, true);
|
||||
|
||||
const missingServer = await rawRequest(
|
||||
port,
|
||||
'/api/apply',
|
||||
'POST',
|
||||
{ selectedTag: 'missing-server' },
|
||||
);
|
||||
assert.equal(missingServer.response.status, 404);
|
||||
assert.equal(missingServer.payload.error.code, 'SERVER_NOT_FOUND');
|
||||
assert.equal((await request(port, '/api/state')).selection.desiredServerId, 'test-vpn');
|
||||
|
||||
async function stateResponse(pathname, method = 'POST', body) {
|
||||
const result = await request(port, pathname, method, body);
|
||||
assert.deepEqual(Object.keys(result.state).sort(), stateKeys);
|
||||
@@ -174,10 +221,30 @@ setInterval(() => {}, 60_000);
|
||||
assert.equal(applied.state.connection.process, 'running');
|
||||
assert.equal((await mutation('/api/singbox/stop')).state.connection.desired, 'stopped');
|
||||
assert.equal((await mutation('/api/singbox/restart')).state.connection.desired, 'running');
|
||||
|
||||
fs.writeFileSync(singboxPath, `#!/usr/bin/env node
|
||||
if (process.argv[2] === 'check') {
|
||||
require('node:fs').unlinkSync(process.argv[1]);
|
||||
process.exit(0);
|
||||
}
|
||||
`);
|
||||
fs.chmodSync(singboxPath, 0o755);
|
||||
const processFailure = await rawRequest(port, '/api/singbox/restart', 'POST');
|
||||
assert.equal(processFailure.response.status, 503);
|
||||
assert.equal(processFailure.payload.error.code, 'PROCESS_START_FAILED');
|
||||
assert.equal(processFailure.payload.error.retryable, true);
|
||||
fs.writeFileSync(singboxPath, workingSingbox);
|
||||
fs.chmodSync(singboxPath, 0o755);
|
||||
|
||||
await mutation('/api/subscription/refresh');
|
||||
assert.equal((await mutation('/api/gateway-auto', 'POST', { enabled: false })).state.gatewayAuto.enabled, false);
|
||||
const forgotten = await mutation('/api/subscription', 'DELETE');
|
||||
assert.equal(forgotten.state.subscription.status, 'missing');
|
||||
assert.equal(forgotten.state.servers.length, 0);
|
||||
assert.deepEqual((await stateResponse('/api/servers/ping-all')).results, []);
|
||||
|
||||
const missingConfig = await rawRequest(port, '/api/singbox/restart', 'POST');
|
||||
assert.equal(missingConfig.response.status, 422);
|
||||
assert.equal(missingConfig.payload.error.code, 'CONFIG_INVALID');
|
||||
assert.equal(missingConfig.payload.error.retryable, false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user