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') }) })