test(constructeur-links) : add sync algorithm data integrity tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
237
frontend/tests/composables/useConstructeurLinks.test.ts
Normal file
237
frontend/tests/composables/useConstructeurLinks.test.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
import { useConstructeurLinks } from '~/composables/useConstructeurLinks'
|
||||
import {
|
||||
mockLinkSKF,
|
||||
mockLinkFAG,
|
||||
mockConstructeurSKF,
|
||||
mockConstructeurFAG,
|
||||
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(),
|
||||
}),
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// fetchLinks
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('fetchLinks', () => {
|
||||
it('returns parsed links with all properties for composant', async () => {
|
||||
const apiLinks = [
|
||||
{
|
||||
id: mockLinkSKF.linkId,
|
||||
constructeur: mockConstructeurSKF,
|
||||
supplierReference: mockLinkSKF.supplierReference,
|
||||
},
|
||||
{
|
||||
id: mockLinkFAG.linkId,
|
||||
constructeur: mockConstructeurFAG,
|
||||
supplierReference: mockLinkFAG.supplierReference,
|
||||
},
|
||||
]
|
||||
mockGet.mockResolvedValue({ success: true, data: wrapCollection(apiLinks) })
|
||||
|
||||
const { fetchLinks } = useConstructeurLinks()
|
||||
const result = await fetchLinks('composant', 'comp-001')
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0]).toEqual({
|
||||
linkId: mockLinkSKF.linkId,
|
||||
constructeurId: mockConstructeurSKF.id,
|
||||
constructeur: mockConstructeurSKF,
|
||||
supplierReference: mockLinkSKF.supplierReference,
|
||||
})
|
||||
expect(result[1]).toEqual({
|
||||
linkId: mockLinkFAG.linkId,
|
||||
constructeurId: mockConstructeurFAG.id,
|
||||
constructeur: mockConstructeurFAG,
|
||||
supplierReference: mockLinkFAG.supplierReference,
|
||||
})
|
||||
})
|
||||
|
||||
it('returns supplierReference as null when absent from API', async () => {
|
||||
const apiLinks = [
|
||||
{
|
||||
id: 'link-no-ref',
|
||||
constructeur: mockConstructeurSKF,
|
||||
// no supplierReference key
|
||||
},
|
||||
]
|
||||
mockGet.mockResolvedValue({ success: true, data: wrapCollection(apiLinks) })
|
||||
|
||||
const { fetchLinks } = useConstructeurLinks()
|
||||
const result = await fetchLinks('composant', 'comp-001')
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]!.supplierReference).toBeNull()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['machine', '/machine_constructeur_links?machine=/api/machines/m-001', 'm-001'],
|
||||
['product', '/product_constructeur_links?product=/api/products/p-001', 'p-001'],
|
||||
['piece', '/piece_constructeur_links?piece=/api/pieces/pc-001', 'pc-001'],
|
||||
['composant', '/composant_constructeur_links?composant=/api/composants/c-001', 'c-001'],
|
||||
] as const)('uses correct endpoint for %s', async (entityType, expectedUrl, entityId) => {
|
||||
mockGet.mockResolvedValue({ success: true, data: wrapCollection([]) })
|
||||
|
||||
const { fetchLinks } = useConstructeurLinks()
|
||||
await fetchLinks(entityType, entityId)
|
||||
|
||||
expect(mockGet).toHaveBeenCalledWith(expectedUrl)
|
||||
})
|
||||
|
||||
it('returns empty array on API failure', async () => {
|
||||
mockGet.mockResolvedValue({ success: false, data: null })
|
||||
|
||||
const { fetchLinks } = useConstructeurLinks()
|
||||
const result = await fetchLinks('composant', 'comp-001')
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// syncLinks — 3-way diff
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('syncLinks', () => {
|
||||
it('creates new links via POST', async () => {
|
||||
mockPost.mockResolvedValue({ success: true })
|
||||
|
||||
const { syncLinks } = useConstructeurLinks()
|
||||
await syncLinks('composant', 'comp-001', [], [mockLinkSKF])
|
||||
|
||||
expect(mockPost).toHaveBeenCalledWith('/composant_constructeur_links', {
|
||||
composant: '/api/composants/comp-001',
|
||||
constructeur: `/api/constructeurs/${mockConstructeurSKF.id}`,
|
||||
supplierReference: mockLinkSKF.supplierReference,
|
||||
})
|
||||
expect(mockDel).not.toHaveBeenCalled()
|
||||
expect(mockPatch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('deletes removed links via DELETE', async () => {
|
||||
mockDel.mockResolvedValue({ success: true })
|
||||
|
||||
const { syncLinks } = useConstructeurLinks()
|
||||
await syncLinks('composant', 'comp-001', [mockLinkSKF], [])
|
||||
|
||||
expect(mockDel).toHaveBeenCalledWith(`/composant_constructeur_links/${mockLinkSKF.linkId}`)
|
||||
expect(mockPost).not.toHaveBeenCalled()
|
||||
expect(mockPatch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('patches when supplierReference changes (value to new value)', async () => {
|
||||
mockPatch.mockResolvedValue({ success: true })
|
||||
|
||||
const updatedLink = { ...mockLinkSKF, supplierReference: 'NEW-REF-999' }
|
||||
const { syncLinks } = useConstructeurLinks()
|
||||
await syncLinks('composant', 'comp-001', [mockLinkSKF], [updatedLink])
|
||||
|
||||
expect(mockPatch).toHaveBeenCalledWith(`/composant_constructeur_links/${mockLinkSKF.linkId}`, {
|
||||
supplierReference: 'NEW-REF-999',
|
||||
})
|
||||
expect(mockPost).not.toHaveBeenCalled()
|
||||
expect(mockDel).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('patches when supplierReference changes from value to null', async () => {
|
||||
mockPatch.mockResolvedValue({ success: true })
|
||||
|
||||
const updatedLink = { ...mockLinkSKF, supplierReference: null }
|
||||
const { syncLinks } = useConstructeurLinks()
|
||||
await syncLinks('composant', 'comp-001', [mockLinkSKF], [updatedLink])
|
||||
|
||||
expect(mockPatch).toHaveBeenCalledWith(`/composant_constructeur_links/${mockLinkSKF.linkId}`, {
|
||||
supplierReference: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('does nothing when links are identical (no API calls)', async () => {
|
||||
const { syncLinks } = useConstructeurLinks()
|
||||
await syncLinks('composant', 'comp-001', [mockLinkSKF], [mockLinkSKF])
|
||||
|
||||
expect(mockPost).not.toHaveBeenCalled()
|
||||
expect(mockDel).not.toHaveBeenCalled()
|
||||
expect(mockPatch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('handles add + delete in same operation', async () => {
|
||||
mockPost.mockResolvedValue({ success: true })
|
||||
mockDel.mockResolvedValue({ success: true })
|
||||
|
||||
const { syncLinks } = useConstructeurLinks()
|
||||
await syncLinks('composant', 'comp-001', [mockLinkSKF], [mockLinkFAG])
|
||||
|
||||
expect(mockDel).toHaveBeenCalledWith(`/composant_constructeur_links/${mockLinkSKF.linkId}`)
|
||||
expect(mockPost).toHaveBeenCalledWith('/composant_constructeur_links', {
|
||||
composant: '/api/composants/comp-001',
|
||||
constructeur: `/api/constructeurs/${mockConstructeurFAG.id}`,
|
||||
supplierReference: mockLinkFAG.supplierReference,
|
||||
})
|
||||
expect(mockPatch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('handles empty original and empty form (no-op)', async () => {
|
||||
const { syncLinks } = useConstructeurLinks()
|
||||
await syncLinks('composant', 'comp-001', [], [])
|
||||
|
||||
expect(mockPost).not.toHaveBeenCalled()
|
||||
expect(mockDel).not.toHaveBeenCalled()
|
||||
expect(mockPatch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('sends supplierReference as null when empty string', async () => {
|
||||
mockPost.mockResolvedValue({ success: true })
|
||||
|
||||
const linkWithEmpty = { ...mockLinkFAG, supplierReference: '' }
|
||||
const { syncLinks } = useConstructeurLinks()
|
||||
await syncLinks('composant', 'comp-001', [], [linkWithEmpty])
|
||||
|
||||
expect(mockPost).toHaveBeenCalledWith('/composant_constructeur_links', {
|
||||
composant: '/api/composants/comp-001',
|
||||
constructeur: `/api/constructeurs/${mockConstructeurFAG.id}`,
|
||||
supplierReference: null,
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
['machine', '/machine_constructeur_links', 'machine', '/api/machines/m-001', 'm-001'],
|
||||
['product', '/product_constructeur_links', 'product', '/api/products/p-001', 'p-001'],
|
||||
['piece', '/piece_constructeur_links', 'piece', '/api/pieces/pc-001', 'pc-001'],
|
||||
['composant', '/composant_constructeur_links', 'composant', '/api/composants/c-001', 'c-001'],
|
||||
] as const)('uses correct endpoint and entity IRI for %s', async (entityType, endpoint, key, entityIri, entityId) => {
|
||||
mockPost.mockResolvedValue({ success: true })
|
||||
mockDel.mockResolvedValue({ success: true })
|
||||
|
||||
const { syncLinks } = useConstructeurLinks()
|
||||
await syncLinks(entityType, entityId, [mockLinkSKF], [mockLinkFAG])
|
||||
|
||||
expect(mockDel).toHaveBeenCalledWith(`${endpoint}/${mockLinkSKF.linkId}`)
|
||||
expect(mockPost).toHaveBeenCalledWith(endpoint, {
|
||||
[key]: entityIri,
|
||||
constructeur: `/api/constructeurs/${mockConstructeurFAG.id}`,
|
||||
supplierReference: mockLinkFAG.supplierReference,
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user