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