diff --git a/src/domain/card-schemas.test.ts b/src/domain/card-schemas.test.ts new file mode 100644 index 0000000..7be3590 --- /dev/null +++ b/src/domain/card-schemas.test.ts @@ -0,0 +1,39 @@ +// @vitest-environment node +import { describe, expect, it } from 'vitest' +import { createCardSchema, updateCardSchema } from './card-schemas' + +describe('createCardSchema', () => { + it('accepte une entrée valide et normalise les champs', () => { + const result = createCardSchema.parse({ + question: ' Question ? ', + response: 'Réponse', + theme: ' javascript ', + tags: ['scope'], + }) + expect(result.question).toBe('Question ?') + expect(result.theme).toBe('javascript') + }) + + it('accepte une entrée sans tags', () => { + expect(createCardSchema.parse({ question: 'Q', response: 'R', theme: 'git' }).tags).toBeUndefined() + }) + + it('rejette une question vide', () => { + expect(() => createCardSchema.parse({ question: ' ', response: 'R', theme: 'git' })).toThrow() + }) + + it('rejette un thème manquant', () => { + expect(() => createCardSchema.parse({ question: 'Q', response: 'R' })).toThrow() + }) +}) + +describe('updateCardSchema', () => { + it('accepte une mise à jour partielle', () => { + const result = updateCardSchema.parse({ response: 'Nouvelle réponse' }) + expect(result).toEqual({ response: 'Nouvelle réponse' }) + }) + + it('rejette un champ vide lors d une mise à jour', () => { + expect(() => updateCardSchema.parse({ theme: '' })).toThrow() + }) +}) \ No newline at end of file diff --git a/src/services/card-service.test.ts b/src/services/card-service.test.ts new file mode 100644 index 0000000..0e1062b --- /dev/null +++ b/src/services/card-service.test.ts @@ -0,0 +1,115 @@ +// @vitest-environment node +import { describe, expect, it, vi } from 'vitest' +import type { Card } from '@/domain/card' +import type { CardRepository } from '@/domain/card-repository' +import { NotFoundError } from '@/domain/errors' +import { CardService } from './card-service' + +const CARD: Card = { + id: '10000000-0000-4000-8000-000000000001', + question: 'Qu est-ce qu une closure ?', + response: 'Une fonction qui conserve la portée de sa définition.', + theme: 'javascript', + tags: ['scope'], + createdAt: new Date('2026-01-01T08:00:00Z'), + updatedAt: new Date('2026-01-01T08:00:00Z'), +} + +type RepoMock = Record> + +function setup() { + const repository: RepoMock = { + findAll: vi.fn(), + findById: vi.fn(), + create: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + findRandom: vi.fn(), + findRandomByTheme: vi.fn(), + findThemes: vi.fn(), + } + const service = new CardService(repository as unknown as CardRepository) + return { service, repository } +} + +describe('CardService', () => { + it('list délègue au repository et renvoie toutes les cartes', async () => { + const { service, repository } = setup() + repository.findAll.mockResolvedValueOnce([CARD]) + await expect(service.list()).resolves.toEqual([CARD]) + }) + + it('get renvoie la carte quand elle existe', async () => { + const { service, repository } = setup() + repository.findById.mockResolvedValueOnce(CARD) + await expect(service.get(CARD.id)).resolves.toEqual(CARD) + }) + + it('get lève NotFoundError si la carte est inconnue', async () => { + const { service, repository } = setup() + repository.findById.mockResolvedValueOnce(null) + await expect(service.get('inconnu')).rejects.toThrow(NotFoundError) + }) + + it('create délègue l entrée au repository', async () => { + const { service, repository } = setup() + const input = { question: 'Q ?', response: 'R', theme: 'git' } + repository.create.mockResolvedValueOnce(CARD) + await expect(service.create(input)).resolves.toEqual(CARD) + expect(repository.create).toHaveBeenCalledWith(input) + }) + + it('update renvoie la carte mise à jour', async () => { + const { service, repository } = setup() + repository.update.mockResolvedValueOnce({ ...CARD, question: 'Nouvelle' }) + const card = await service.update(CARD.id, { question: 'Nouvelle' }) + expect(card.question).toBe('Nouvelle') + }) + + it('update lève NotFoundError si la carte est inconnue', async () => { + const { service, repository } = setup() + repository.update.mockResolvedValueOnce(null) + await expect(service.update('inconnu', { question: 'X' })).rejects.toThrow(NotFoundError) + }) + + it('remove délègue la suppression', async () => { + const { service, repository } = setup() + repository.delete.mockResolvedValueOnce(true) + await expect(service.remove(CARD.id)).resolves.toBeUndefined() + expect(repository.delete).toHaveBeenCalledWith(CARD.id) + }) + + it('remove lève NotFoundError si aucune carte ne correspond', async () => { + const { service, repository } = setup() + repository.delete.mockResolvedValueOnce(false) + await expect(service.remove('inconnu')).rejects.toThrow(NotFoundError) + }) + + it('random sans thème utilise findRandom', async () => { + const { service, repository } = setup() + repository.findRandom.mockResolvedValueOnce(CARD) + await expect(service.random()).resolves.toEqual(CARD) + expect(repository.findRandomByTheme).not.toHaveBeenCalled() + }) + + it('random avec thème utilise findRandomByTheme', async () => { + const { service, repository } = setup() + repository.findRandomByTheme.mockResolvedValueOnce(CARD) + await expect(service.random('javascript')).resolves.toEqual(CARD) + expect(repository.findRandomByTheme).toHaveBeenCalledWith('javascript') + }) + + it('random lève NotFoundError si aucune carte', async () => { + const { service, repository } = setup() + repository.findRandom.mockResolvedValueOnce(null) + await expect(service.random()).rejects.toThrow(NotFoundError) + repository.findRandomByTheme.mockResolvedValueOnce(null) + await expect(service.random('react')).rejects.toThrow('Aucune carte pour le thème react') + }) + + it('themes renvoie la liste des thèmes', async () => { + const { service, repository } = setup() + repository.findThemes.mockResolvedValueOnce(['docker', 'git']) + await expect(service.themes()).resolves.toEqual(['docker', 'git']) + }) +}) \ No newline at end of file