From b978b9686442d79940590b0dff4adf49fc6bec6a Mon Sep 17 00:00:00 2001 From: IliesAllali Date: Tue, 27 Oct 2026 15:15:37 +0100 Subject: [PATCH] =?UTF-8?q?Tests=20du=20client=20:=20ceux=20du=20d=C3=A9la?= =?UTF-8?q?i=20maximal=20remis,=20ceux=20de=20l'idempotence=20=C3=A0=20la?= =?UTF-8?q?=20suite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- frontend/src/api/client.test.js | 60 +++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/frontend/src/api/client.test.js b/frontend/src/api/client.test.js index e4a12b7..ccb7f9d 100644 --- a/frontend/src/api/client.test.js +++ b/frontend/src/api/client.test.js @@ -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') + }) })