Files
parenthese/frontend/src/api/client.test.js
T
IliesAllaliandClaude Opus 5.5 0132ecb342 Envois de la famille sans doublon : en-tête Idempotency-Key
Sur un réseau faible, une proposition, un souvenir, une photo posée sur l'arbre ou un moment pouvait être
créé deux fois : la réponse se perdait (délai dépassé), la personne renvoyait, le serveur recréait tout.

- Navigateur (api/client.js) : un envoi qui n'a pas abouti garde sa clé, et le même envoi refait (même
  adresse, même contenu) la réutilise. Une réussite libère la clé.
- Serveur (lib/idempotency.ts) : sur les quatre routes de création, une clé déjà vue par la même personne
  renvoie la réponse du premier envoi ; deux essais simultanés n'en traitent qu'un ; un échec n'est pas gardé.
  En mémoire, 24 h, 5 000 clés au plus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:14:32 +01:00

48 lines
1.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 () => '{}' }
})
afterEach(() => {
keysSent.length = 0
vi.unstubAllGlobals()
})
const send = (body = '{"a":1}') => apiRequest('/trees/t/annotation-photos', { method: 'POST', idempotent: true, body })
describe('apiRequest, envois idempotents', () => {
it('un envoi refait après une coupure garde la même clé', async () => {
vi.stubGlobal('fetch', respond('network'))
await expect(send()).rejects.toThrow()
vi.stubGlobal('fetch', respond(false))
await expect(send()).rejects.toThrow()
vi.stubGlobal('fetch', respond(true))
await send()
expect(keysSent).toHaveLength(3)
expect(new Set(keysSent).size).toBe(1)
expect(keysSent[0]).toMatch(/^[0-9a-f]{32}$/)
})
it('après une réussite, le même contenu repart avec une clé neuve', async () => {
vi.stubGlobal('fetch', respond(true))
await send('{"b":2}')
await send('{"b":2}')
expect(keysSent[0]).not.toBe(keysSent[1])
})
it('un autre contenu a sa propre clé, et un appel ordinaire n’en envoie pas', async () => {
vi.stubGlobal('fetch', respond('network'))
await expect(send('{"c":3}')).rejects.toThrow()
await expect(send('{"c":4}')).rejects.toThrow()
await expect(apiRequest('/trees/t', { method: 'POST', body: '{}' })).rejects.toThrow()
expect(keysSent[0]).not.toBe(keysSent[1])
expect(keysSent[2]).toBeUndefined()
})
})