mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-08 15:42:42 +00:00
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>
48 lines
1.8 KiB
JavaScript
48 lines
1.8 KiB
JavaScript
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()
|
||
})
|
||
})
|