import assert from 'node:assert/strict' import { afterEach, mock, test } from 'node:test' import { apiRequest, ApiError, AUTH_EXPIRED_EVENT } from '../src/lib/api.ts' import { listClouds, uploadCloud, deleteClouds } from '../src/features/clouds/api.ts' import { collectPages } from '../src/lib/pagination.ts' afterEach(() => mock.restoreAll()) const cloud = { id: 'cloud-1', owner: { id: 'owner-1', username: '观云者' }, cloud_type: { id: 1, name: '积云', genus: null, icon_id: null, rarity: 2, description: '' }, latitude: 0, longitude: 0, description: '', captured_at: null, uploaded_at: '2026-09-30T00:00:00.000Z', updated_at: '2026-09-30T00:00:00.000Z', received_like_count: 3, review_status: 'approved', is_hidden: false, } test('cloud API encodes search once and maps the server page into view models', async () => { mock.method(globalThis, 'fetch', async (url, options) => { const parsed = new URL(url) assert.equal(parsed.pathname, '/clouds') assert.equal(parsed.searchParams.get('value'), '@云 & +') assert.equal(parsed.searchParams.has('filter'), false) assert.equal(options.credentials, 'include') return Response.json({ items: [cloud], page: 1, page_size: 1, has_more: false }) }) const page = await listClouds({ page: 1, page_size: 1, value: '@云 & +' }) assert.equal(page.has_more, false) assert.equal(page.items[0].status, 'approved') assert.equal(page.items[0].username, '观云者') assert.equal(page.items[0].cloud_type_id, 1) assert.match(page.items[0].image_url, /\/images\/cloud-1\/original$/) }) test('JSON, typed arrays and multipart preserve their distinct encodings', async () => { const calls = [] mock.method(globalThis, 'fetch', async (url, options) => { calls.push({ url, ...options }) return Response.json({ message: '成功', cloud_id: 'new' }, { status: 201 }) }) await apiRequest('/example', { method: 'PATCH', body: { is_hidden: false } }) assert.equal(calls[0].headers.get('content-type'), 'application/json') assert.equal(calls[0].body, '{"is_hidden":false}') const bytes = new Uint8Array([1, 2]) await apiRequest('/example', { method: 'POST', body: bytes }) assert.equal(calls[1].body, bytes) const file = new File(['image'], 'cloud.png', { type: 'image/png' }) await uploadCloud({ image: file, cloud_type_id: 1, latitude: 0, longitude: 0, is_hidden: false, }) assert.equal(calls[2].headers.has('content-type'), false) assert.equal(calls[2].body.get('is_hidden'), 'false') assert.equal(calls[2].body.get('cloud_type_id'), '1') assert.equal(calls[2].body.has('type_id'), false) assert.equal(calls[2].body.get('image').name, file.name) }) test('errors retain status and validation issues; invalid success responses fail early', async () => { const payload = { message: '用户名已被占用', issues: [{ path: 'username', message: '重复' }] } mock.method(globalThis, 'fetch', async () => Response.json(payload, { status: 409 })) await assert.rejects(apiRequest('/profiles/me'), error => { assert.ok(error instanceof ApiError) assert.equal(error.status, 409) assert.equal(error.message, payload.message) assert.deepEqual(error.detail, payload) return true }) globalThis.fetch.mock.mockImplementation(async () => new Response('gateway')) await assert.rejects(apiRequest('/clouds'), /无效的 JSON/) globalThis.fetch.mock.mockImplementation( async () => new Response('{oops', { headers: { 'content-type': 'application/json' } }), ) await assert.rejects(apiRequest('/clouds'), /无效的 JSON/) globalThis.fetch.mock.mockImplementation(async () => new Response(null, { status: 204 })) assert.equal(await apiRequest('/clouds'), undefined) }) test('401 expires required sessions only; transport never retries a mutation', async () => { const events = new EventTarget() let expired = 0 events.addEventListener(AUTH_EXPIRED_EVENT, () => expired++) globalThis.window = events try { const fetch = mock.method(globalThis, 'fetch', async () => Response.json({ message: '失效' }, { status: 401 }), ) await assert.rejects(apiRequest('/clouds', { method: 'POST', body: {} })) assert.equal(expired, 1) await assert.rejects(apiRequest('/clouds', { auth: false })) assert.equal(expired, 1) assert.equal(fetch.mock.callCount(), 2) } finally { delete globalThis.window } }) test('pagination follows explicit has_more even on a full final page', async () => { const pages = [] const result = await collectPages(async page => { pages.push(page) return { items: [page], page, page_size: 1, has_more: page < 2 } }) assert.deepEqual(result, [1, 2]) assert.deepEqual(pages, [1, 2]) }) test('batch deletion reports completed batches when a later request fails', async () => { const completed = [] let calls = 0 mock.method(globalThis, 'fetch', async (_url, options) => { const ids = JSON.parse(options.body).cloud_ids assert.ok(ids.length <= 100) assert.ok(ids.every(id => typeof id === 'string')) return ++calls === 1 ? Response.json({ message: '完成' }) : Response.json({ message: '失败' }, { status: 500 }) }) const ids = Array.from({ length: 101 }, (_, index) => String(index)) await assert.rejects( deleteClouds(ids, batch => completed.push(...batch)), /失败/, ) assert.deepEqual(completed, ids.slice(0, 100)) })