Tests du client : ceux du délai maximal remis, ceux de l'idempotence à la suite

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
IliesAllaliandClaude Opus 5.5 committed 2026-10-27 15:15:37 +01:00
1 parent 0132ecb342
commit b978b96864
1 file changed
+49 -11
+49 -11
View File
@@ -1,23 +1,55 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { apiRequest } from './client.js'
// Clé d'idempotence : gardée tant que l'envoi n'a pas abouti, libérée par une réussite
const keysSent = []
const respond = (ok) => vi.fn(async (_url, init) => {
keysSent.push(init.headers['Idempotency-Key'])
if (ok === 'network') throw new TypeError('Failed to fetch')
return { ok, status: ok ? 201 : 503, text: async () => '{}' }
})
import { ApiError, apiRequest } from './client'
afterEach(() => {
keysSent.length = 0
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
const send = (body = '{"a":1}') => apiRequest('/trees/t/annotation-photos', { method: 'POST', idempotent: true, body })
describe('apiRequest', () => {
it('une requête qui ne répond pas finit en erreur lisible', async () => {
const timeout = vi.spyOn(AbortSignal, 'timeout')
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new DOMException('trop long', 'TimeoutError')))
await expect(apiRequest('/trees')).rejects.toMatchObject({
name: 'ApiError', message: 'Le serveur ne répond pas. Vérifiez la connexion et réessayez.', status: 0, payload: { error: 'request_timeout' },
})
expect(timeout).toHaveBeenCalledWith(30_000)
})
it('un envoi de fichier a plus de temps, et un délai demandé passe devant', async () => {
const timeout = vi.spyOn(AbortSignal, 'timeout')
vi.stubGlobal('fetch', vi.fn().mockImplementation(async () => new Response('{"ok":true}', { status: 200 })))
await apiRequest('/upload', { method: 'POST', body: 'x'.repeat(200_000) })
expect(timeout).toHaveBeenLastCalledWith(5 * 60_000)
await apiRequest('/import', { method: 'POST', body: '{}', timeoutMs: 1234 })
expect(timeout).toHaveBeenLastCalledWith(1234)
expect(fetch.mock.calls[1][1]).not.toHaveProperty('timeoutMs')
})
it('une autre panne réseau remonte telle quelle', async () => {
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new TypeError('Failed to fetch')))
const error = await apiRequest('/trees').catch((e) => e)
expect(error).toBeInstanceOf(TypeError)
expect(error).not.toBeInstanceOf(ApiError)
})
})
// Clé d'idempotence : gardée tant que l'envoi n'a pas abouti, libérée par une réussite
describe('apiRequest, envois idempotents', () => {
it('un envoi refait après une coupure garde la même clé', async () => {
const keysSent = []
const respond = (ok) => vi.fn(async (_url, init) => {
keysSent.push(init.headers['Idempotency-Key'])
if (ok === 'network') throw new TypeError('Failed to fetch')
return new Response('{}', { status: ok ? 201 : 503 })
})
const send = (body = '{"a":1}') => apiRequest('/trees/t/annotation-photos', { method: 'POST', idempotent: true, body })
afterEach(() => {
keysSent.length = 0
})
it('un envoi refait après une coupure ou une erreur garde la même clé', async () => {
vi.stubGlobal('fetch', respond('network'))
await expect(send()).rejects.toThrow()
vi.stubGlobal('fetch', respond(false))
@@ -44,4 +76,10 @@ describe('apiRequest, envois idempotents', () => {
expect(keysSent[0]).not.toBe(keysSent[1])
expect(keysSent[2]).toBeUndefined()
})
it('l’option idempotent ne part pas dans fetch', async () => {
vi.stubGlobal('fetch', respond(true))
await send('{"d":5}')
expect(fetch.mock.calls[0][1]).not.toHaveProperty('idempotent')
})
})