Files
flashcards/src/lib/api-client.test.ts
T
devcodetools 67569e2d16 feat(tags): gestion des tags dans le formulaire carte (max 3, suggestions, normalisation)
- schéma zod : tags normalisés (trim + minuscules + dédupe), 3 max par carte
- couche générique : findTags (MySQL + Mongo), service tags()
- API GET /api/tags + cardsApi.tags() pour les suggestions
- formulaire : saisie + ajout (Entrée/bouton), suggestions des tags existants,
  chips supprimables, compteur n/3, blocage au-delà de 3
- tests : schémas, repositories, service, route, api-client, formulaire
2026-09-22 10:18:13 +02:00

115 lines
4.0 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ApiClientError, cardsApi } from './api-client'
const CARD = {
id: '10000000-0000-4000-8000-000000000001',
question: 'Q ?',
response: 'R',
theme: 'git',
tags: ['a'],
createdAt: '2026-01-01T08:00:00.000Z',
updatedAt: '2026-01-01T08:00:00.000Z',
}
const jsonResponse = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } })
const fetchMock = vi.fn()
beforeEach(() => {
global.fetch = fetchMock as unknown as typeof fetch
fetchMock.mockReset()
})
function expectRequest(method: string, path: string) {
expect(fetchMock).toHaveBeenCalledWith(path, expect.objectContaining({ method }))
}
describe('cardsApi', () => {
it('list() appelle GET /api/cards', async () => {
fetchMock.mockResolvedValue(jsonResponse([CARD]))
await expect(cardsApi.list()).resolves.toEqual([CARD])
expectRequest('GET', '/api/cards')
})
it('get() appelle GET /api/cards/:id', async () => {
fetchMock.mockResolvedValue(jsonResponse(CARD))
await expect(cardsApi.get(CARD.id)).resolves.toEqual(CARD)
expectRequest('GET', `/api/cards/${CARD.id}`)
})
it('create() envoie POST avec un corps JSON', async () => {
fetchMock.mockResolvedValue(jsonResponse(CARD, 201))
await expect(
cardsApi.create({ question: 'Q ?', response: 'R', theme: 'git' } as never),
).resolves.toEqual(CARD)
const [, init] = fetchMock.mock.calls[0]
expect(init.method).toBe('POST')
expect(JSON.parse(init.body)).toEqual({ question: 'Q ?', response: 'R', theme: 'git' })
})
it('update() envoie PUT /api/cards/:id', async () => {
fetchMock.mockResolvedValue(jsonResponse(CARD))
await expect(cardsApi.update(CARD.id, { question: 'Q ?' } as never)).resolves.toEqual(CARD)
expectRequest('PUT', `/api/cards/${CARD.id}`)
})
it('remove() envoie DELETE et ignore le corps', async () => {
fetchMock.mockResolvedValue(new Response(null, { status: 204 }))
await expect(cardsApi.remove(CARD.id)).resolves.toBeUndefined()
expectRequest('DELETE', `/api/cards/${CARD.id}`)
})
it('random() sans thème et avec thème (URL-encodé)', async () => {
fetchMock.mockResolvedValue(jsonResponse(CARD))
await cardsApi.random()
expectRequest('GET', '/api/cards/random')
await cardsApi.random('git ops')
expectRequest('GET', '/api/cards/random?theme=git%20ops')
})
it('themes() appelle GET /api/themes', async () => {
fetchMock.mockResolvedValue(jsonResponse(['git', 'docker']))
await expect(cardsApi.themes()).resolves.toEqual(['git', 'docker'])
expectRequest('GET', '/api/themes')
})
it('tags() appelle GET /api/tags', async () => {
fetchMock.mockResolvedValue(jsonResponse(['scope', 'fonction']))
await expect(cardsApi.tags()).resolves.toEqual(['scope', 'fonction'])
expectRequest('GET', '/api/tags')
})
it('lève ApiClientError avec le message de l API sur une erreur 404', async () => {
fetchMock.mockResolvedValue(
jsonResponse({ error: 'NotFoundError', message: 'Carte inconnue' }, 404),
)
await expect(cardsApi.get('nope')).rejects.toMatchObject({
name: 'NotFoundError',
message: 'Carte inconnue',
status: 404,
})
})
it('lève ApiClientError sur une réponse invalide', async () => {
fetchMock.mockResolvedValue(
jsonResponse({ error: 'ValidationError', message: 'Payload invalide' }, 400),
)
await expect(cardsApi.create({} as never)).rejects.toBeInstanceOf(ApiClientError)
})
it('lève ApiClientError NetworkError quand fetch échoue', async () => {
fetchMock.mockRejectedValue(new TypeError('network'))
await expect(cardsApi.list()).rejects.toMatchObject({ name: 'NetworkError', status: 0 })
})
})
describe('ApiClientError', () => {
it('porte status, name et message', () => {
const err = new ApiClientError(404, 'NotFoundError', 'inconnu')
expect(err.status).toBe(404)
expect(err.name).toBe('NotFoundError')
expect(err.message).toBe('inconnu')
})
})