Files
2026-09-30 12:11:32 +08:00

135 lines
5.6 KiB
JavaScript

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('<html>gateway</html>'))
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))
})