test(crud) : add CRUD cache data integrity tests for products, composants, pieces
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
157
frontend/tests/composables/useComposants.test.ts
Normal file
157
frontend/tests/composables/useComposants.test.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
import { useComposants } from '~/composables/useComposants'
|
||||
import { mockComponentFromApi, wrapCollection } from '../fixtures/mockData'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const mockGet = vi.fn()
|
||||
const mockPost = vi.fn()
|
||||
const mockPatch = vi.fn()
|
||||
const mockDel = vi.fn()
|
||||
|
||||
vi.mock('~/composables/useApi', () => ({
|
||||
useApi: () => ({
|
||||
get: mockGet,
|
||||
post: mockPost,
|
||||
patch: mockPatch,
|
||||
put: vi.fn(),
|
||||
delete: mockDel,
|
||||
postFormData: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('~/composables/useToast', () => ({
|
||||
useToast: () => ({
|
||||
showSuccess: vi.fn(),
|
||||
showError: vi.fn(),
|
||||
showInfo: vi.fn(),
|
||||
showToast: vi.fn(),
|
||||
toasts: { value: [] },
|
||||
clearAll: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('~/composables/useConstructeurs', () => ({
|
||||
useConstructeurs: () => ({
|
||||
ensureConstructeurs: vi.fn().mockResolvedValue([]),
|
||||
}),
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
const { clearComposantsCache } = useComposants()
|
||||
clearComposantsCache()
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// createComposant
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('createComposant', () => {
|
||||
it('sends all fields in creation payload', async () => {
|
||||
const created = { ...mockComponentFromApi, id: 'comp-new' }
|
||||
mockPost.mockResolvedValue({ success: true, data: created })
|
||||
|
||||
const { createComposant } = useComposants()
|
||||
const result = await createComposant({
|
||||
name: 'Moteur principal',
|
||||
reference: 'COMP-MOT-001',
|
||||
description: 'Un moteur',
|
||||
typeComposantId: 'tc-moteur',
|
||||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
// normalizeRelationIds converts typeComposantId to typeComposant IRI
|
||||
expect(mockPost).toHaveBeenCalledWith('/composants', expect.objectContaining({
|
||||
name: 'Moteur principal',
|
||||
reference: 'COMP-MOT-001',
|
||||
description: 'Un moteur',
|
||||
typeComposant: '/api/model_types/tc-moteur',
|
||||
}))
|
||||
// typeComposantId should be removed by normalizeRelationIds
|
||||
const payload = mockPost.mock.calls[0]![1]
|
||||
expect(payload).not.toHaveProperty('typeComposantId')
|
||||
})
|
||||
|
||||
it('adds created composant to cache', async () => {
|
||||
const created = { ...mockComponentFromApi, id: 'comp-new', name: 'Nouveau composant' }
|
||||
mockPost.mockResolvedValue({ success: true, data: created })
|
||||
|
||||
const { createComposant, composants, total } = useComposants()
|
||||
|
||||
expect(composants.value).toHaveLength(0)
|
||||
expect(total.value).toBe(0)
|
||||
|
||||
await createComposant({ name: 'Nouveau composant' })
|
||||
|
||||
expect(composants.value).toHaveLength(1)
|
||||
expect(composants.value[0]!.id).toBe('comp-new')
|
||||
expect(total.value).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// updateComposant
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('updateComposant', () => {
|
||||
it('patches and updates cache', async () => {
|
||||
// Seed the cache with one composant
|
||||
const original = { ...mockComponentFromApi, id: 'comp-001', name: 'Ancien nom' }
|
||||
mockPost.mockResolvedValue({ success: true, data: original })
|
||||
const { createComposant, updateComposant, composants } = useComposants()
|
||||
await createComposant({ name: 'Ancien nom' })
|
||||
expect(composants.value).toHaveLength(1)
|
||||
|
||||
// Now update
|
||||
const updated = { ...original, name: 'Nouveau nom' }
|
||||
mockPatch.mockResolvedValue({ success: true, data: updated })
|
||||
|
||||
const result = await updateComposant('comp-001', { name: 'Nouveau nom' })
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(mockPatch).toHaveBeenCalledWith('/composants/comp-001', expect.objectContaining({
|
||||
name: 'Nouveau nom',
|
||||
}))
|
||||
expect(composants.value[0]!.name).toBe('Nouveau nom')
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// deleteComposant
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('deleteComposant', () => {
|
||||
it('removes composant from cache on success', async () => {
|
||||
// Seed cache
|
||||
const item = { ...mockComponentFromApi, id: 'comp-del', name: 'A supprimer' }
|
||||
mockPost.mockResolvedValue({ success: true, data: item })
|
||||
const { createComposant, deleteComposant, composants, total } = useComposants()
|
||||
await createComposant({ name: 'A supprimer' })
|
||||
expect(composants.value).toHaveLength(1)
|
||||
expect(total.value).toBe(1)
|
||||
|
||||
mockDel.mockResolvedValue({ success: true })
|
||||
const result = await deleteComposant('comp-del')
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(composants.value).toHaveLength(0)
|
||||
expect(total.value).toBe(0)
|
||||
})
|
||||
|
||||
it('keeps composant in cache on failure', async () => {
|
||||
// Seed cache
|
||||
const item = { ...mockComponentFromApi, id: 'comp-keep', name: 'Garder' }
|
||||
mockPost.mockResolvedValue({ success: true, data: item })
|
||||
const { createComposant, deleteComposant, composants, total } = useComposants()
|
||||
await createComposant({ name: 'Garder' })
|
||||
expect(composants.value).toHaveLength(1)
|
||||
|
||||
mockDel.mockResolvedValue({ success: false, error: 'Server error' })
|
||||
const result = await deleteComposant('comp-keep')
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(composants.value).toHaveLength(1)
|
||||
expect(composants.value[0]!.id).toBe('comp-keep')
|
||||
})
|
||||
})
|
||||
166
frontend/tests/composables/usePieces.test.ts
Normal file
166
frontend/tests/composables/usePieces.test.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
import { usePieces } from '~/composables/usePieces'
|
||||
import { mockPieceFromApi, wrapCollection } from '../fixtures/mockData'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const mockGet = vi.fn()
|
||||
const mockPost = vi.fn()
|
||||
const mockPatch = vi.fn()
|
||||
const mockDel = vi.fn()
|
||||
|
||||
vi.mock('~/composables/useApi', () => ({
|
||||
useApi: () => ({
|
||||
get: mockGet,
|
||||
post: mockPost,
|
||||
patch: mockPatch,
|
||||
put: vi.fn(),
|
||||
delete: mockDel,
|
||||
postFormData: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('~/composables/useToast', () => ({
|
||||
useToast: () => ({
|
||||
showSuccess: vi.fn(),
|
||||
showError: vi.fn(),
|
||||
showInfo: vi.fn(),
|
||||
showToast: vi.fn(),
|
||||
toasts: { value: [] },
|
||||
clearAll: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('~/composables/useConstructeurs', () => ({
|
||||
useConstructeurs: () => ({
|
||||
ensureConstructeurs: vi.fn().mockResolvedValue([]),
|
||||
}),
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
const { clearPiecesCache } = usePieces()
|
||||
clearPiecesCache()
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// createPiece
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('createPiece', () => {
|
||||
it('sends all fields including prix in POST payload', async () => {
|
||||
const created = { ...mockPieceFromApi, id: 'piece-new' }
|
||||
mockPost.mockResolvedValue({ success: true, data: created })
|
||||
|
||||
const { createPiece } = usePieces()
|
||||
await createPiece({
|
||||
name: 'Roulement 6205',
|
||||
reference: 'ROUL-6205',
|
||||
prix: 25.50,
|
||||
typePieceId: 'tp-bearing-001',
|
||||
} as any)
|
||||
|
||||
expect(mockPost).toHaveBeenCalledWith('/pieces', expect.objectContaining({
|
||||
name: 'Roulement 6205',
|
||||
reference: 'ROUL-6205',
|
||||
prix: 25.50,
|
||||
typePiece: '/api/model_types/tp-bearing-001',
|
||||
}))
|
||||
})
|
||||
|
||||
it('strips constructeur fields from payload', async () => {
|
||||
const created = { ...mockPieceFromApi, id: 'piece-new' }
|
||||
mockPost.mockResolvedValue({ success: true, data: created })
|
||||
|
||||
const { createPiece } = usePieces()
|
||||
await createPiece({
|
||||
name: 'Test Piece',
|
||||
constructeurIds: ['cstr-skf-001'],
|
||||
constructeurs: [{ id: 'cstr-skf-001', name: 'SKF' }] as any,
|
||||
})
|
||||
|
||||
const payload = mockPost.mock.calls[0]![1]
|
||||
expect(payload).not.toHaveProperty('constructeurIds')
|
||||
expect(payload).not.toHaveProperty('constructeurs')
|
||||
expect(payload).not.toHaveProperty('constructeurId')
|
||||
expect(payload).not.toHaveProperty('constructeur')
|
||||
})
|
||||
|
||||
it('adds created piece to cache (pieces array and total)', async () => {
|
||||
const created = { ...mockPieceFromApi, id: 'piece-new' }
|
||||
mockPost.mockResolvedValue({ success: true, data: created })
|
||||
|
||||
const { createPiece, pieces, total } = usePieces()
|
||||
const result = await createPiece({ name: 'New Piece' })
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(pieces.value).toHaveLength(1)
|
||||
expect(pieces.value[0]!.id).toBe('piece-new')
|
||||
expect(total.value).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// updatePiece
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('updatePiece', () => {
|
||||
it('patches with supplied fields and updates cache', async () => {
|
||||
// Seed cache first
|
||||
const original = { ...mockPieceFromApi }
|
||||
mockPost.mockResolvedValue({ success: true, data: original })
|
||||
const { createPiece, updatePiece, pieces } = usePieces()
|
||||
await createPiece({ name: 'Roulement 6205' })
|
||||
|
||||
const updated = { ...mockPieceFromApi, name: 'Updated Name', reference: 'ROUL-NEW' }
|
||||
mockPatch.mockResolvedValue({ success: true, data: updated })
|
||||
|
||||
const result = await updatePiece(mockPieceFromApi.id, {
|
||||
name: 'Updated Name',
|
||||
reference: 'ROUL-NEW',
|
||||
})
|
||||
|
||||
expect(mockPatch).toHaveBeenCalledWith(`/pieces/${mockPieceFromApi.id}`, expect.objectContaining({
|
||||
name: 'Updated Name',
|
||||
reference: 'ROUL-NEW',
|
||||
}))
|
||||
expect(result.success).toBe(true)
|
||||
expect(pieces.value.find(p => p.id === mockPieceFromApi.id)?.name).toBe('Updated Name')
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// deletePiece
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('deletePiece', () => {
|
||||
it('removes piece from cache on success', async () => {
|
||||
// Seed cache
|
||||
mockPost.mockResolvedValue({ success: true, data: { ...mockPieceFromApi } })
|
||||
const { createPiece, deletePiece, pieces, total } = usePieces()
|
||||
await createPiece({ name: 'To Delete' })
|
||||
expect(pieces.value).toHaveLength(1)
|
||||
|
||||
mockDel.mockResolvedValue({ success: true })
|
||||
const result = await deletePiece(mockPieceFromApi.id)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(pieces.value).toHaveLength(0)
|
||||
expect(total.value).toBe(0)
|
||||
})
|
||||
|
||||
it('does not remove on failure', async () => {
|
||||
// Seed cache
|
||||
mockPost.mockResolvedValue({ success: true, data: { ...mockPieceFromApi } })
|
||||
const { createPiece, deletePiece, pieces, total } = usePieces()
|
||||
await createPiece({ name: 'Should Stay' })
|
||||
expect(pieces.value).toHaveLength(1)
|
||||
|
||||
mockDel.mockResolvedValue({ success: false, error: 'Server error' })
|
||||
const result = await deletePiece(mockPieceFromApi.id)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(pieces.value).toHaveLength(1)
|
||||
expect(total.value).toBe(1)
|
||||
})
|
||||
})
|
||||
209
frontend/tests/composables/useProducts.test.ts
Normal file
209
frontend/tests/composables/useProducts.test.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
import { useProducts } from '~/composables/useProducts'
|
||||
import { mockProductFromApi, mockConstructeurSKF, wrapCollection } from '../fixtures/mockData'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const mockGet = vi.fn()
|
||||
const mockPost = vi.fn()
|
||||
const mockPatch = vi.fn()
|
||||
const mockDel = vi.fn()
|
||||
|
||||
vi.mock('~/composables/useApi', () => ({
|
||||
useApi: () => ({
|
||||
get: mockGet,
|
||||
post: mockPost,
|
||||
patch: mockPatch,
|
||||
put: vi.fn(),
|
||||
delete: mockDel,
|
||||
postFormData: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('~/composables/useToast', () => ({
|
||||
useToast: () => ({
|
||||
showSuccess: vi.fn(),
|
||||
showError: vi.fn(),
|
||||
showInfo: vi.fn(),
|
||||
showToast: vi.fn(),
|
||||
toasts: { value: [] },
|
||||
clearAll: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('~/composables/useConstructeurs', () => ({
|
||||
useConstructeurs: () => ({
|
||||
ensureConstructeurs: vi.fn().mockResolvedValue([]),
|
||||
}),
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
const { clearProductsCache } = useProducts()
|
||||
clearProductsCache()
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// createProduct
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('createProduct', () => {
|
||||
it('sends all fields including supplierPrice in POST payload', async () => {
|
||||
const created = { ...mockProductFromApi, id: 'prod-new' }
|
||||
mockPost.mockResolvedValue({ success: true, data: created })
|
||||
|
||||
const { createProduct } = useProducts()
|
||||
await createProduct({
|
||||
name: 'Graisse LGMT2',
|
||||
reference: 'LUB-LGMT2',
|
||||
supplierPrice: 45.90,
|
||||
typeProductId: 'tprod-grease-001',
|
||||
})
|
||||
|
||||
expect(mockPost).toHaveBeenCalledWith('/products', expect.objectContaining({
|
||||
name: 'Graisse LGMT2',
|
||||
reference: 'LUB-LGMT2',
|
||||
supplierPrice: 45.90,
|
||||
typeProduct: '/api/model_types/tprod-grease-001',
|
||||
}))
|
||||
})
|
||||
|
||||
it('strips constructeur fields from payload', async () => {
|
||||
const created = { ...mockProductFromApi, id: 'prod-new' }
|
||||
mockPost.mockResolvedValue({ success: true, data: created })
|
||||
|
||||
const { createProduct } = useProducts()
|
||||
await createProduct({
|
||||
name: 'Test Product',
|
||||
constructeurIds: ['cstr-skf-001'],
|
||||
constructeurs: [mockConstructeurSKF] as any,
|
||||
})
|
||||
|
||||
const payload = mockPost.mock.calls[0]![1]
|
||||
expect(payload).not.toHaveProperty('constructeurIds')
|
||||
expect(payload).not.toHaveProperty('constructeurs')
|
||||
expect(payload).not.toHaveProperty('constructeurId')
|
||||
expect(payload).not.toHaveProperty('constructeur')
|
||||
})
|
||||
|
||||
it('adds created product to cache (products array and total)', async () => {
|
||||
const created = { ...mockProductFromApi, id: 'prod-new' }
|
||||
mockPost.mockResolvedValue({ success: true, data: created })
|
||||
|
||||
const { createProduct, products, total } = useProducts()
|
||||
const result = await createProduct({ name: 'New Product' })
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(products.value).toHaveLength(1)
|
||||
expect(products.value[0]!.id).toBe('prod-new')
|
||||
expect(total.value).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// updateProduct
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('updateProduct', () => {
|
||||
it('patches with supplied fields and updates cache', async () => {
|
||||
// Seed cache first
|
||||
const original = { ...mockProductFromApi }
|
||||
mockPost.mockResolvedValue({ success: true, data: original })
|
||||
const { createProduct, updateProduct, products } = useProducts()
|
||||
await createProduct({ name: 'Graisse LGMT2' })
|
||||
|
||||
const updated = { ...mockProductFromApi, name: 'Updated Name', supplierPrice: 99.99 }
|
||||
mockPatch.mockResolvedValue({ success: true, data: updated })
|
||||
|
||||
const result = await updateProduct(mockProductFromApi.id, {
|
||||
name: 'Updated Name',
|
||||
supplierPrice: 99.99,
|
||||
})
|
||||
|
||||
expect(mockPatch).toHaveBeenCalledWith(`/products/${mockProductFromApi.id}`, expect.objectContaining({
|
||||
name: 'Updated Name',
|
||||
supplierPrice: 99.99,
|
||||
}))
|
||||
expect(result.success).toBe(true)
|
||||
expect(products.value.find(p => p.id === mockProductFromApi.id)?.name).toBe('Updated Name')
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// deleteProduct
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('deleteProduct', () => {
|
||||
it('removes product from cache on success', async () => {
|
||||
// Seed cache
|
||||
mockPost.mockResolvedValue({ success: true, data: { ...mockProductFromApi } })
|
||||
const { createProduct, deleteProduct, products, total } = useProducts()
|
||||
await createProduct({ name: 'To Delete' })
|
||||
expect(products.value).toHaveLength(1)
|
||||
|
||||
mockDel.mockResolvedValue({ success: true })
|
||||
const result = await deleteProduct(mockProductFromApi.id)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(products.value).toHaveLength(0)
|
||||
expect(total.value).toBe(0)
|
||||
})
|
||||
|
||||
it('does not remove on failure', async () => {
|
||||
// Seed cache
|
||||
mockPost.mockResolvedValue({ success: true, data: { ...mockProductFromApi } })
|
||||
const { createProduct, deleteProduct, products, total } = useProducts()
|
||||
await createProduct({ name: 'Should Stay' })
|
||||
expect(products.value).toHaveLength(1)
|
||||
|
||||
mockDel.mockResolvedValue({ success: false, error: 'Server error' })
|
||||
const result = await deleteProduct(mockProductFromApi.id)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(products.value).toHaveLength(1)
|
||||
expect(total.value).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getProduct
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('getProduct', () => {
|
||||
it('returns cached product if available with constructeurs (no extra API call)', async () => {
|
||||
// Seed cache with a product that has resolved constructeurs
|
||||
const productWithConstructeurs = {
|
||||
...mockProductFromApi,
|
||||
constructeurs: [mockConstructeurSKF],
|
||||
}
|
||||
mockPost.mockResolvedValue({ success: true, data: productWithConstructeurs })
|
||||
const { createProduct, getProduct } = useProducts()
|
||||
await createProduct({ name: 'Cached' })
|
||||
|
||||
mockGet.mockClear()
|
||||
const result = await getProduct(mockProductFromApi.id)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.data?.id).toBe(mockProductFromApi.id)
|
||||
expect(mockGet).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('fetches from API with force: true', async () => {
|
||||
// Seed cache with a product that has resolved constructeurs
|
||||
const productWithConstructeurs = {
|
||||
...mockProductFromApi,
|
||||
constructeurs: [mockConstructeurSKF],
|
||||
}
|
||||
mockPost.mockResolvedValue({ success: true, data: productWithConstructeurs })
|
||||
const { createProduct, getProduct } = useProducts()
|
||||
await createProduct({ name: 'Cached' })
|
||||
|
||||
const freshData = { ...mockProductFromApi, name: 'Fresh from API' }
|
||||
mockGet.mockResolvedValue({ success: true, data: freshData })
|
||||
|
||||
const result = await getProduct(mockProductFromApi.id, { force: true })
|
||||
|
||||
expect(mockGet).toHaveBeenCalledWith(`/products/${mockProductFromApi.id}`)
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.data?.name).toBe('Fresh from API')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user