test(repo): couvrir les implémentations mysql et mongo de CardRepository
This commit is contained in:
1 parent
4140942ae3
commit
2ddb687286
4 files changed
+325
-2
No files matched your search
@@ -0,0 +1,32 @@
|
|||||||
|
// @vitest-environment node
|
||||||
|
import { afterEach, describe, expect, it } from 'vitest'
|
||||||
|
import { MongoCardRepository } from '../repositories/mongo-card-repository'
|
||||||
|
import { MysqlCardRepository } from '../repositories/mysql-card-repository'
|
||||||
|
import { getCardRepository } from './card-repository-factory'
|
||||||
|
|
||||||
|
describe('getCardRepository', () => {
|
||||||
|
const original = process.env.DB_IMPL
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (original === undefined) {
|
||||||
|
delete process.env.DB_IMPL
|
||||||
|
} else {
|
||||||
|
process.env.DB_IMPL = original
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('instancie MysqlCardRepository par défaut', () => {
|
||||||
|
delete process.env.DB_IMPL
|
||||||
|
expect(getCardRepository()).toBeInstanceOf(MysqlCardRepository)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('instancie MysqlCardRepository si DB_IMPL=mysql', () => {
|
||||||
|
process.env.DB_IMPL = 'mysql'
|
||||||
|
expect(getCardRepository()).toBeInstanceOf(MysqlCardRepository)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('instancie MongoCardRepository si DB_IMPL=mongo', () => {
|
||||||
|
process.env.DB_IMPL = 'mongo'
|
||||||
|
expect(getCardRepository()).toBeInstanceOf(MongoCardRepository)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
// @vitest-environment node
|
||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import type { Collection } from 'mongodb'
|
||||||
|
import { MongoCardRepository } 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'),
|
||||||
|
}
|
||||||
|
|
||||||
|
function cursorOf(docs: unknown[]) {
|
||||||
|
return { toArray: vi.fn().mockResolvedValue(docs) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
const collection = {
|
||||||
|
find: vi.fn(),
|
||||||
|
findOne: vi.fn(),
|
||||||
|
insertOne: vi.fn(),
|
||||||
|
updateOne: vi.fn(),
|
||||||
|
deleteOne: vi.fn(),
|
||||||
|
aggregate: vi.fn(),
|
||||||
|
distinct: vi.fn(),
|
||||||
|
} as unknown as Collection<any>
|
||||||
|
const repo = new MongoCardRepository(collection)
|
||||||
|
return { repo, collection }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('MongoCardRepository', () => {
|
||||||
|
it('findAll renvoie toutes les cartes mappées', async () => {
|
||||||
|
const { repo, collection } = setup()
|
||||||
|
collection.find.mockReturnValue(cursorOf([DOC]) as any)
|
||||||
|
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([]) as any)
|
||||||
|
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: '' } as any)
|
||||||
|
const card = await repo.create({
|
||||||
|
question: 'Que retourne typeof null ?',
|
||||||
|
response: 'object',
|
||||||
|
theme: 'javascript',
|
||||||
|
})
|
||||||
|
const [doc] = collection.insertOne.mock.calls[0]
|
||||||
|
expect(doc.c_id).toBe(card.id)
|
||||||
|
expect(doc.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 } as any)
|
||||||
|
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]
|
||||||
|
expect(filter).toEqual({ c_id: DOC.c_id })
|
||||||
|
expect(set.$set.response).toBe('Nouvelle')
|
||||||
|
expect(set.$set.updatedAt).toBeInstanceOf(Date)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('update renvoie null si aucune carte ne correspond', async () => {
|
||||||
|
const { repo, collection } = setup()
|
||||||
|
collection.updateOne.mockResolvedValueOnce({ matchedCount: 0 } as any)
|
||||||
|
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 } as any)
|
||||||
|
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 } as any)
|
||||||
|
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]) as any)
|
||||||
|
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([]) as any)
|
||||||
|
await expect(repo.findRandom()).resolves.toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('findRandomByTheme filtre par thème', async () => {
|
||||||
|
const { repo, collection } = setup()
|
||||||
|
collection.aggregate.mockReturnValueOnce(cursorOf([DOC]) as any)
|
||||||
|
const card = await repo.findRandomByTheme('javascript')
|
||||||
|
expect(card?.id).toBe(DOC.c_id)
|
||||||
|
const stages = collection.aggregate.mock.calls[0][0]
|
||||||
|
expect(stages[0]).toEqual({ $match: { theme: 'javascript' } })
|
||||||
|
collection.aggregate.mockReturnValueOnce(cursorOf([]) as any)
|
||||||
|
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'])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
// @vitest-environment node
|
||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import type { Pool } from 'mysql2/promise'
|
||||||
|
import type { Card } from '../domain/card'
|
||||||
|
import { MysqlCardRepository } from './mysql-card-repository'
|
||||||
|
|
||||||
|
const ROW = {
|
||||||
|
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'),
|
||||||
|
}
|
||||||
|
|
||||||
|
const EXPECTED_CARD: Card = {
|
||||||
|
id: ROW.id,
|
||||||
|
question: ROW.question,
|
||||||
|
response: ROW.response,
|
||||||
|
theme: ROW.theme,
|
||||||
|
tags: ['scope'],
|
||||||
|
createdAt: ROW.createdAt,
|
||||||
|
updatedAt: ROW.updatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
const query = vi.fn()
|
||||||
|
const pool = { query } as unknown as Pool
|
||||||
|
return { repo: new MysqlCardRepository(pool), query }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('MysqlCardRepository', () => {
|
||||||
|
it('findAll renvoie toutes les cartes mappées', async () => {
|
||||||
|
const { repo, query } = setup()
|
||||||
|
query.mockResolvedValueOnce([
|
||||||
|
[ROW, { ...ROW, id: '10000000-0000-4000-8000-000000000002', tags: null }],
|
||||||
|
])
|
||||||
|
const cards = await repo.findAll()
|
||||||
|
expect(cards).toHaveLength(2)
|
||||||
|
expect(cards[0]).toEqual(EXPECTED_CARD)
|
||||||
|
expect(cards[1].tags).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('findAll renvoie un tableau vide si aucune carte', async () => {
|
||||||
|
const { repo, query } = setup()
|
||||||
|
query.mockResolvedValueOnce([[]])
|
||||||
|
await expect(repo.findAll()).resolves.toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('findById renvoie la carte correspondante', async () => {
|
||||||
|
const { repo, query } = setup()
|
||||||
|
query.mockResolvedValueOnce([[ROW]])
|
||||||
|
await expect(repo.findById(ROW.id)).resolves.toEqual(EXPECTED_CARD)
|
||||||
|
expect(query.mock.calls[0][1]).toEqual([ROW.id])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('findById renvoie null si la carte est inconnue', async () => {
|
||||||
|
const { repo, query } = setup()
|
||||||
|
query.mockResolvedValueOnce([[]])
|
||||||
|
await expect(repo.findById('inconnu')).resolves.toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create insère une carte et renvoie l entité complète', async () => {
|
||||||
|
const { repo, query } = setup()
|
||||||
|
query.mockResolvedValueOnce([{ affectedRows: 1 }])
|
||||||
|
const card = await repo.create({
|
||||||
|
question: 'Que retourne typeof null ?',
|
||||||
|
response: 'object',
|
||||||
|
theme: 'javascript',
|
||||||
|
tags: ['types'],
|
||||||
|
})
|
||||||
|
expect(card.id).toHaveLength(36)
|
||||||
|
expect(card.tags).toEqual(['types'])
|
||||||
|
expect(card.createdAt).toBeInstanceOf(Date)
|
||||||
|
const [params] = query.mock.calls[0][1] as unknown[]
|
||||||
|
expect(params).toBe(card.id)
|
||||||
|
expect(query.mock.calls[0][0]).toContain('INSERT INTO cards')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create utilise un tableau de tags vide si non fourni', async () => {
|
||||||
|
const { repo, query } = setup()
|
||||||
|
query.mockResolvedValueOnce([{ affectedRows: 1 }])
|
||||||
|
const card = await repo.create({ question: 'Q ?', response: 'R', theme: 'git' })
|
||||||
|
expect(card.tags).toEqual([])
|
||||||
|
expect(query.mock.calls[0][1]).toContain('[]')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('update modifie les champs fournis et renvoie la carte', async () => {
|
||||||
|
const { repo, query } = setup()
|
||||||
|
const updated = { ...ROW, response: 'Nouvelle réponse' }
|
||||||
|
query.mockResolvedValueOnce([{ affectedRows: 1 }]).mockResolvedValueOnce([[updated]])
|
||||||
|
const card = await repo.update(ROW.id, { response: 'Nouvelle réponse' })
|
||||||
|
expect(card?.response).toBe('Nouvelle réponse')
|
||||||
|
const [updateCall] = query.mock.calls
|
||||||
|
expect(updateCall[0]).toContain('UPDATE cards SET')
|
||||||
|
expect(updateCall[1]).toContain('Nouvelle réponse')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('update sérialise les tags en JSON', async () => {
|
||||||
|
const { repo, query } = setup()
|
||||||
|
query.mockResolvedValueOnce([{ affectedRows: 1 }]).mockResolvedValueOnce([[ROW]])
|
||||||
|
await repo.update(ROW.id, { tags: ['a', 'b'] })
|
||||||
|
expect(query.mock.calls[0][1]).toContain('["a","b"]')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('update sans champs ne lance pas d UPDATE', async () => {
|
||||||
|
const { repo, query } = setup()
|
||||||
|
query.mockResolvedValueOnce([[ROW]])
|
||||||
|
await expect(repo.update(ROW.id, {})).resolves.toEqual(EXPECTED_CARD)
|
||||||
|
expect(query).toHaveBeenCalledTimes(1)
|
||||||
|
expect(query.mock.calls[0][0]).toContain('SELECT')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('update renvoie null si la carte est inconnue', async () => {
|
||||||
|
const { repo, query } = setup()
|
||||||
|
query.mockResolvedValueOnce([{ affectedRows: 1 }]).mockResolvedValueOnce([[]])
|
||||||
|
await expect(repo.update('inconnu', { question: 'X' })).resolves.toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('delete renvoie true si une ligne est supprimée', async () => {
|
||||||
|
const { repo, query } = setup()
|
||||||
|
query.mockResolvedValueOnce([{ affectedRows: 1 }])
|
||||||
|
await expect(repo.delete(ROW.id)).resolves.toBe(true)
|
||||||
|
expect(query.mock.calls[0][1]).toEqual([ROW.id])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('delete renvoie false si aucune ligne ne correspond', async () => {
|
||||||
|
const { repo, query } = setup()
|
||||||
|
query.mockResolvedValueOnce([{ affectedRows: 0 }])
|
||||||
|
await expect(repo.delete('inconnu')).resolves.toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('findRandom renvoie une carte au hasard ou null', async () => {
|
||||||
|
const { repo, query } = setup()
|
||||||
|
query.mockResolvedValueOnce([[ROW]])
|
||||||
|
await expect(repo.findRandom()).resolves.toEqual(EXPECTED_CARD)
|
||||||
|
query.mockResolvedValueOnce([[]])
|
||||||
|
await expect(repo.findRandom()).resolves.toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('findRandomByTheme filtre par thème', async () => {
|
||||||
|
const { repo, query } = setup()
|
||||||
|
query.mockResolvedValueOnce([[ROW]])
|
||||||
|
const card = await repo.findRandomByTheme('javascript')
|
||||||
|
expect(card).toEqual(EXPECTED_CARD)
|
||||||
|
const [sql, params] = query.mock.calls[0]
|
||||||
|
expect(sql).toContain('WHERE theme = ?')
|
||||||
|
expect(params).toEqual(['javascript'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('findThemes renvoie les thèmes distincts', async () => {
|
||||||
|
const { repo, query } = setup()
|
||||||
|
query.mockResolvedValueOnce([[{ theme: 'docker' }, { theme: 'javascript' }]])
|
||||||
|
await expect(repo.findThemes()).resolves.toEqual(['docker', 'javascript'])
|
||||||
|
})
|
||||||
|
})
|
||||||
+4
-2
@@ -4,7 +4,8 @@ import { afterEach } from 'vitest'
|
|||||||
|
|
||||||
afterEach(() => cleanup())
|
afterEach(() => cleanup())
|
||||||
|
|
||||||
Object.defineProperty(window, 'matchMedia', {
|
if (typeof window !== 'undefined') {
|
||||||
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
writable: true,
|
writable: true,
|
||||||
value: (query: string) => ({
|
value: (query: string) => ({
|
||||||
matches: false,
|
matches: false,
|
||||||
@@ -16,4 +17,5 @@ Object.defineProperty(window, 'matchMedia', {
|
|||||||
removeEventListener: () => {},
|
removeEventListener: () => {},
|
||||||
dispatchEvent: () => false,
|
dispatchEvent: () => false,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
}
|
||||||
Reference in new issue
Block a user