Spaces:
Paused
Paused
File size: 4,192 Bytes
94004a2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | // Unit tests for frontend/src/api/client.ts URL composition + error handling.
// Runs under node:test with a synthetic fetch mock so no backend is needed.
import { test, mock } from 'node:test';
import assert from 'node:assert/strict';
// bun/node strip .ts extension when type='module' is set in package.json;
// without that we load via bun's loader by requesting the .ts path.
const clientPath = new URL('../../frontend/src/api/client.ts', import.meta.url).pathname;
const { API, apiUrl, apiFetch, apiJson, apiPost, ApiError } = await import(clientPath);
test('apiUrl falls back to API root on empty input', () => {
assert.equal(apiUrl(), API);
assert.equal(apiUrl(''), API);
});
test('apiUrl prepends slash when missing', () => {
assert.equal(apiUrl('engines'), `${API}/engines`);
assert.equal(apiUrl('/engines'), `${API}/engines`);
});
test('apiUrl passes absolute URLs through untouched', () => {
assert.equal(apiUrl('https://example.com/foo'), 'https://example.com/foo');
assert.equal(apiUrl('http://localhost:9000/bar'), 'http://localhost:9000/bar');
});
test('ApiError carries status + detail', () => {
const err = new ApiError('boom', { status: 503, detail: { code: 'x' } });
assert.equal(err.name, 'ApiError');
assert.equal(err.message, 'boom');
assert.equal(err.status, 503);
assert.deepEqual(err.detail, { code: 'x' });
});
test('apiFetch resolves on 2xx', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = mock.fn(async () => new Response('ok', { status: 200 }));
try {
const res = await apiFetch('/ping');
assert.equal(res.status, 200);
} finally {
globalThis.fetch = originalFetch;
}
});
test('apiFetch throws ApiError with JSON detail on non-2xx', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = mock.fn(async () =>
new Response(JSON.stringify({ detail: 'Job not found' }), {
status: 404, statusText: 'Not Found',
headers: { 'Content-Type': 'application/json' },
}),
);
try {
await assert.rejects(
() => apiFetch('/dub/x'),
(err) => {
assert.ok(err instanceof ApiError);
assert.equal(err.status, 404);
assert.equal(err.detail, 'Job not found');
assert.match(err.message, /404/);
return true;
},
);
} finally {
globalThis.fetch = originalFetch;
}
});
test('apiJson parses 2xx body', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = mock.fn(async () =>
new Response(JSON.stringify({ ok: true, n: 42 }), {
status: 200, headers: { 'Content-Type': 'application/json' },
}),
);
try {
const body = await apiJson('/ping');
assert.deepEqual(body, { ok: true, n: 42 });
} finally {
globalThis.fetch = originalFetch;
}
});
test('apiPost json body sets Content-Type + stringified body', async () => {
const originalFetch = globalThis.fetch;
const calls = [];
globalThis.fetch = mock.fn(async (url, init) => {
calls.push({ url, init });
return new Response(JSON.stringify({ received: true }), {
status: 200, headers: { 'Content-Type': 'application/json' },
});
});
try {
await apiPost('/models/install', { repo_id: 'k2-fsa/OmniVoice' });
assert.equal(calls.length, 1);
const { init } = calls[0];
assert.equal(init.method, 'POST');
assert.equal(init.headers['Content-Type'], 'application/json');
assert.equal(init.body, JSON.stringify({ repo_id: 'k2-fsa/OmniVoice' }));
} finally {
globalThis.fetch = originalFetch;
}
});
test('apiPost passes FormData without stringify + no Content-Type override', async () => {
const originalFetch = globalThis.fetch;
const calls = [];
globalThis.fetch = mock.fn(async (url, init) => {
calls.push({ url, init });
return new Response(JSON.stringify({}), { status: 200 });
});
try {
const fd = new FormData();
fd.append('text', 'hello');
await apiPost('/generate', fd);
assert.equal(calls[0].init.body, fd);
// Browser sets multipart boundary; we must NOT force a JSON header.
assert.equal(calls[0].init.headers, undefined);
} finally {
globalThis.fetch = originalFetch;
}
});
|