ouroboros/web/tests/api_client_errors.test.js
Anton Razzhigaev 8059d69081 Report structured gateway errors instead of [object Object]
Several routes answer with `{error: {code, message}}`, and the client
interpolated that object straight into an Error message, so the owner's toast
read `[object Object]` — an alarm carrying no fact. Read the object's own
sentence, then its code, and serialize as a last resort so nothing is
swallowed. The toast also wraps long unbroken tokens now instead of
overflowing its own box.
2026-09-01 11:10:18 +00:00

64 lines
2.3 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { fetchJson } from '../modules/api_client.js';
function respond(status, body) {
const prior = globalThis.fetch;
globalThis.fetch = async () => ({
ok: status >= 200 && status < 300,
status,
json: async () => body,
});
return () => { globalThis.fetch = prior; };
}
test('a structured error body becomes a sentence, never [object Object]', async () => {
const restore = respond(400, { error: { code: 'skill_not_finalized', message: 'Skill review is not finalized.' } });
try {
await assert.rejects(() => fetchJson('/api/x'), (error) => {
assert.equal(error.message, 'Skill review is not finalized.');
assert.equal(error.status, 400);
return true;
});
} finally { restore(); }
});
test('a structured error with only a code still names the code', async () => {
const restore = respond(409, { error: { code: 'conflict' } });
try {
await assert.rejects(() => fetchJson('/api/x'), /conflict/);
} finally { restore(); }
});
test('a structured error with neither message nor code is serialized, not swallowed', async () => {
const restore = respond(500, { error: { fields: ['a'] } });
try {
await assert.rejects(() => fetchJson('/api/x'), (error) => {
assert.equal(error.message, '{"fields":["a"]}');
assert.equal(error.message.includes('[object Object]'), false);
return true;
});
} finally { restore(); }
});
test('a plain string error and a bare status are unchanged', async () => {
let restore = respond(422, { error: 'Path escapes file browser root.' });
try {
await assert.rejects(() => fetchJson('/api/x'), /Path escapes file browser root\./);
} finally { restore(); }
restore = respond(503, {});
try {
await assert.rejects(() => fetchJson('/api/x'), /HTTP 503/);
} finally { restore(); }
});
test('rejectOkFalse still reads the structured error of a 200 body', async () => {
const restore = respond(200, { ok: false, error: { message: 'Nothing to restore.' } });
try {
await assert.rejects(
() => fetchJson('/api/x', {}, { rejectOkFalse: true }),
/Nothing to restore\./,
);
} finally { restore(); }
});