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:
2026-04-06 13:13:37 +02:00
parent e70c66e215
commit 82cbeb91a5

View 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,
})
})
})