71 lines
3.7 KiB
TypeScript
71 lines
3.7 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { ApiError, requestBlob, requestJson, uploadJson } from './http'
|
|
import { downloadFullBackup, preflightBackup, restoreBackup } from './backups'
|
|
|
|
afterEach(() => vi.restoreAllMocks())
|
|
|
|
function response(body: BodyInit | null, init: ResponseInit = {}) {
|
|
return new Response(body, { status: 200, ...init })
|
|
}
|
|
|
|
describe('typed HTTP client', () => {
|
|
it('sends credentials, CSRF, JSON and AbortSignal consistently', async () => {
|
|
document.cookie = 'dodo_csrf=csrf-token'
|
|
const signal = new AbortController().signal
|
|
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(response('{"ok":true}', { headers: { 'content-type': 'application/json' } }))
|
|
|
|
await requestJson<{ ok: boolean }>('/example', { method: 'POST', body: { value: 1 }, signal })
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith('/api/v1/example', expect.objectContaining({ credentials: 'include', signal, method: 'POST', body: '{"value":1}' }))
|
|
const headers = new Headers(fetchMock.mock.calls[0][1]?.headers)
|
|
expect(headers.get('content-type')).toBe('application/json')
|
|
expect(headers.get('x-csrf-token')).toBe('csrf-token')
|
|
})
|
|
|
|
it('normalizes FastAPI validation details without losing the status or code', async () => {
|
|
vi.spyOn(globalThis, 'fetch').mockResolvedValue(response(JSON.stringify({ detail: [{ loc: ['body', 'name'], msg: '必填', type: 'missing' }], code: 'invalid_backup' }), { status: 422, headers: { 'content-type': 'application/json' } }))
|
|
|
|
const error = await requestJson('/bad').catch((reason) => reason)
|
|
|
|
expect(error).toBeInstanceOf(ApiError)
|
|
const apiError = error as ApiError
|
|
expect(apiError).toMatchObject({ status: 422, code: 'invalid_backup' })
|
|
expect(apiError.message).toContain('name')
|
|
expect(apiError.message).toContain('必填')
|
|
})
|
|
|
|
it('keeps blob and multipart requests typed without forcing JSON content type', async () => {
|
|
const fetchMock = vi.spyOn(globalThis, 'fetch')
|
|
.mockResolvedValueOnce(response('zip-data', { headers: { 'content-type': 'application/zip' } }))
|
|
.mockResolvedValueOnce(response('{"valid":true}', { headers: { 'content-type': 'application/json' } }))
|
|
const blob = await requestBlob('/backup/export.zip')
|
|
expect(blob.size).toBe(8)
|
|
expect(blob.type).toBe('application/zip')
|
|
await uploadJson('/backup/preflight?mode=merge', new File(['zip'], 'backup.zip'))
|
|
const uploadHeaders = new Headers(fetchMock.mock.calls[1][1]?.headers)
|
|
expect(uploadHeaders.has('content-type')).toBe(false)
|
|
expect(fetchMock.mock.calls[1][1]?.body).toBeInstanceOf(FormData)
|
|
})
|
|
})
|
|
|
|
describe('backup API', () => {
|
|
it('uses the versioned ZIP export, preflight and restore endpoints', async () => {
|
|
const fetchMock = vi.spyOn(globalThis, 'fetch')
|
|
.mockResolvedValueOnce(response('zip-data', { headers: { 'content-type': 'application/zip' } }))
|
|
.mockResolvedValueOnce(response(JSON.stringify({ valid: true, preflight_token: 'token', backup_id: 'backup', archive_sha256: 'sha', entities: { tasks: 2, attachments: 1 } }), { headers: { 'content-type': 'application/json' } }))
|
|
.mockResolvedValueOnce(response(JSON.stringify({ restored: 3, mode: 'replace' }), { headers: { 'content-type': 'application/json' } }))
|
|
|
|
await downloadFullBackup()
|
|
const preview = await preflightBackup(new File(['zip'], 'dodo.zip'), 'replace')
|
|
await restoreBackup(preview.preflight_token!, 'replace')
|
|
|
|
expect(fetchMock.mock.calls.map(([url]) => url)).toEqual([
|
|
'/api/v1/backup/export.zip',
|
|
'/api/v1/backup/preflight?mode=replace',
|
|
'/api/v1/backup/restore',
|
|
])
|
|
expect(preview.entities.tasks).toBe(2)
|
|
expect(fetchMock.mock.calls[2][1]?.body).toBe(JSON.stringify({ preflight_token: 'token', mode: 'replace' }))
|
|
})
|
|
})
|