// @vitest-environment node import { describe, expect, it, vi } from 'vitest' import type { Collection } from 'mongodb' import { MongoCardRepository, type CardDocument } from './mongo-card-repository' const DOC = { c_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-02T09:00:00Z'), } interface CollectionMock { find: ReturnType findOne: ReturnType insertOne: ReturnType updateOne: ReturnType deleteOne: ReturnType aggregate: ReturnType distinct: ReturnType } function cursorOf(docs: unknown[]) { return { toArray: vi.fn().mockResolvedValue(docs) } } function setup() { const collection: CollectionMock = { find: vi.fn(), findOne: vi.fn(), insertOne: vi.fn(), updateOne: vi.fn(), deleteOne: vi.fn(), aggregate: vi.fn(), distinct: vi.fn(), } const repo = new MongoCardRepository(collection as unknown as Collection) return { repo, collection } } describe('MongoCardRepository', () => { it('findAll renvoie toutes les cartes mappées', async () => { const { repo, collection } = setup() collection.find.mockReturnValue(cursorOf([DOC])) const cards = await repo.findAll() expect(cards).toHaveLength(1) expect(cards[0].id).toBe(DOC.c_id) expect(cards[0].tags).toEqual(['scope']) }) it('findAll renvoie un tableau vide si aucune carte', async () => { const { repo, collection } = setup() collection.find.mockReturnValue(cursorOf([])) await expect(repo.findAll()).resolves.toEqual([]) }) it('findById renvoie la carte ou null', async () => { const { repo, collection } = setup() collection.findOne.mockResolvedValueOnce(DOC) const card = await repo.findById(DOC.c_id) expect(card?.id).toBe(DOC.c_id) expect(collection.findOne).toHaveBeenCalledWith({ c_id: DOC.c_id }, expect.anything()) collection.findOne.mockResolvedValueOnce(null) await expect(repo.findById('inconnu')).resolves.toBeNull() }) it('create insère un document avec c_id et renvoie l entité', async () => { const { repo, collection } = setup() collection.insertOne.mockResolvedValueOnce({ insertedId: '' }) const card = await repo.create({ question: 'Que retourne typeof null ?', response: 'object', theme: 'javascript', }) const [doc] = collection.insertOne.mock.calls[0] as unknown[] const inserted = doc as Record expect(inserted.c_id).toBe(card.id) expect(inserted.tags).toEqual([]) expect(card.tags).toEqual([]) expect(card.createdAt).toBeInstanceOf(Date) }) it('update modifie les champs fournis et renvoie la carte', async () => { const { repo, collection } = setup() collection.updateOne.mockResolvedValueOnce({ matchedCount: 1 }) collection.findOne.mockResolvedValueOnce({ ...DOC, response: 'Nouvelle' }) const card = await repo.update(DOC.c_id, { response: 'Nouvelle' }) expect(card?.response).toBe('Nouvelle') const [filter, set] = collection.updateOne.mock.calls[0] as unknown[] const setDoc = set as Record expect(filter).toEqual({ c_id: DOC.c_id }) expect(setDoc.$set.response).toBe('Nouvelle') expect(setDoc.$set.updatedAt).toBeInstanceOf(Date) }) it('update renvoie null si aucune carte ne correspond', async () => { const { repo, collection } = setup() collection.updateOne.mockResolvedValueOnce({ matchedCount: 0 }) await expect(repo.update('inconnu', { question: 'X' })).resolves.toBeNull() }) it('delete renvoie true si un document est supprimé', async () => { const { repo, collection } = setup() collection.deleteOne.mockResolvedValueOnce({ deletedCount: 1 }) await expect(repo.delete(DOC.c_id)).resolves.toBe(true) expect(collection.deleteOne).toHaveBeenCalledWith({ c_id: DOC.c_id }) }) it('delete renvoie false si aucun document ne correspond', async () => { const { repo, collection } = setup() collection.deleteOne.mockResolvedValueOnce({ deletedCount: 0 }) await expect(repo.delete('inconnu')).resolves.toBe(false) }) it('findRandom renvoie une carte au hasard ou null', async () => { const { repo, collection } = setup() collection.aggregate.mockReturnValueOnce(cursorOf([DOC])) const card = await repo.findRandom() expect(card?.id).toBe(DOC.c_id) expect(collection.aggregate.mock.calls[0][0]).toContainEqual({ $sample: { size: 1 } }) collection.aggregate.mockReturnValueOnce(cursorOf([])) await expect(repo.findRandom()).resolves.toBeNull() }) it('findRandomByTheme filtre par thème', async () => { const { repo, collection } = setup() collection.aggregate.mockReturnValueOnce(cursorOf([DOC])) const card = await repo.findRandomByTheme('javascript') expect(card?.id).toBe(DOC.c_id) const stages = collection.aggregate.mock.calls[0][0] as unknown[] expect(stages[0]).toEqual({ $match: { theme: 'javascript' } }) collection.aggregate.mockReturnValueOnce(cursorOf([])) await expect(repo.findRandomByTheme('react')).resolves.toBeNull() }) it('findThemes renvoie les thèmes distincts triés', async () => { const { repo, collection } = setup() collection.distinct.mockResolvedValueOnce(['react', 'docker']) await expect(repo.findThemes()).resolves.toEqual(['docker', 'react']) }) })