import assert from 'node:assert/strict' import { readFile } from 'node:fs/promises' import test from 'node:test' import ts from 'typescript' const clientPath = new URL('../src/api/client.ts', import.meta.url) const source = (await readFile(clientPath, 'utf8')).replaceAll('import.meta.env', '{}') const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2023, }, fileName: 'client.ts', }).outputText const client = await import(`data:text/javascript;base64,${Buffer.from(output).toString('base64')}`) const { ApiError, apiRequest, normalizeBaseUrl, setApiBaseUrl } = client const originalFetch = globalThis.fetch test.beforeEach(() => { setApiBaseUrl('http://api.test') }) test.after(() => { globalThis.fetch = originalFetch }) test('empty 200 response rejects with a stable invalid_response ApiError', async () => { globalThis.fetch = async () => new Response('', { status: 200 }) await assert.rejects(apiRequest('/empty'), invalidResponseError) }) test('non-JSON 200 response rejects with a stable invalid_response ApiError', async () => { globalThis.fetch = async () => new Response('bad gateway', { status: 200 }) await assert.rejects(apiRequest('/html'), invalidResponseError) }) test('interrupted successful response body rejects with a stable invalid_response ApiError', async () => { globalThis.fetch = async () => ({ ok: true, status: 200, text: async () => { throw new Error('read interrupted') }, }) await assert.rejects(apiRequest('/interrupted'), invalidResponseError) }) test('204 response returns undefined without reading a body', async () => { globalThis.fetch = async () => new Response(null, { status: 204 }) assert.equal(await apiRequest('/no-content'), undefined) }) test('explicit void response returns undefined for a non-204 response', async () => { globalThis.fetch = async () => new Response('ignored', { status: 200 }) assert.equal(await apiRequest('/accepted', { responseType: 'void' }), undefined) }) test('network failure rejects with network_error ApiError', async () => { globalThis.fetch = async () => { throw new Error('socket closed') } await assert.rejects(apiRequest('/network'), (error) => { assert.equal(error instanceof ApiError, true) assert.equal(error.status, 0) assert.equal(error.code, 'network_error') return true }) }) test('standard error envelope preserves status, code, and Chinese message', async () => { globalThis.fetch = async () => Response.json( { error: { code: 'invalid_request', message: '请求参数无效' } }, { status: 400 }, ) await assert.rejects(apiRequest('/standard-error'), (error) => { assert.equal(error instanceof ApiError, true) assert.equal(error.status, 400) assert.equal(error.code, 'invalid_request') assert.equal(error.message, '请求参数无效') return true }) }) test('GET without a body does not send Content-Type', async () => { let requestInit globalThis.fetch = async (_url, init) => { requestInit = init return Response.json({ ok: true }) } await apiRequest('/read') assert.equal(requestInit.headers['Content-Type'], undefined) }) test('JSON body sends application/json Content-Type', async () => { let requestInit globalThis.fetch = async (_url, init) => { requestInit = init return Response.json({ ok: true }) } await apiRequest('/write', { method: 'POST', body: { title: '测试' } }) assert.equal(requestInit.headers['Content-Type'], 'application/json') }) test('FormData body leaves Content-Type unset so fetch provides the boundary', async () => { let requestInit globalThis.fetch = async (_url, init) => { requestInit = init return Response.json({ ok: true }) } await apiRequest('/upload', { method: 'POST', body: new FormData() }) assert.equal(requestInit.headers['Content-Type'], undefined) }) test('request forwards its AbortSignal to fetch', async () => { let requestInit const controller = new AbortController() globalThis.fetch = async (_url, init) => { requestInit = init return Response.json({ ok: true }) } await apiRequest('/search', { signal: controller.signal }) assert.equal(requestInit.signal, controller.signal) }) test('base URL normalizer trims whitespace and all trailing slashes', () => { assert.equal(normalizeBaseUrl(' https://host.example/// '), 'https://host.example') }) function invalidResponseError(error) { assert.equal(error instanceof ApiError, true) assert.equal(error.status, 200) assert.equal(error.code, 'invalid_response') assert.equal(error.message, '服务器返回了无法识别的数据') return true }