From 82cbeb91a5962ec01c24ff5bda50200cae7118ce Mon Sep 17 00:00:00 2001 From: r-dev Date: Mon, 6 Apr 2026 13:13:37 +0200 Subject: [PATCH] test(constructeur-links) : add sync algorithm data integrity tests Co-Authored-By: Claude Opus 4.6 (1M context) --- .../composables/useConstructeurLinks.test.ts | 237 ++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 frontend/tests/composables/useConstructeurLinks.test.ts diff --git a/frontend/tests/composables/useConstructeurLinks.test.ts b/frontend/tests/composables/useConstructeurLinks.test.ts new file mode 100644 index 0000000..a4ddd51 --- /dev/null +++ b/frontend/tests/composables/useConstructeurLinks.test.ts @@ -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, + }) + }) +})