// Customers domain use-case hooks: create/update must invalidate the list // whole domain key family (customersKeys.all — lists AND cached search // autocompletes; code-review finding, Phase 4) and toast. The domain api module is // mocked — no axios traffic. import { describe, it, expect, vi, beforeEach } from 'vitest'; import { renderHook, waitFor } from '@testing-library/react'; import { toast } from 'react-toastify'; import { createQueryHarness } from './hookTestUtils'; import { customersKeys, useCreateCustomer, useUpdateCustomer, } from '../customers/use-cases'; import { customersApi } from '../customers/api'; import type { Customer } from '../customers/types'; vi.mock('../customers/api', () => ({ customersApi: { search: vi.fn(), list: vi.fn(), create: vi.fn(), update: vi.fn(), }, })); vi.mock('react-toastify', () => ({ toast: { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() }, })); const customer = { id: 'c-1', name: 'Acme Corp' } as unknown as Customer; beforeEach(() => { vi.clearAllMocks(); }); describe('customersKeys', () => { it('list keys nest under lists() so one invalidation hits every page/filter', () => { expect(customersKeys.all).toEqual(['customers']); expect(customersKeys.lists()).toEqual(['customers', 'list']); expect(customersKeys.list({ page: 3 })).toEqual(['customers', 'list', { page: 3 }]); expect(customersKeys.search('acme')).toEqual(['customers', 'search', 'acme']); }); }); describe('useCreateCustomer', () => { it('invalidates the whole customers domain (lists + search caches) and toasts on success', async () => { vi.mocked(customersApi.create).mockResolvedValue(customer); const { queryClient, wrapper } = createQueryHarness(); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); const { result } = renderHook(() => useCreateCustomer(), { wrapper }); result.current.mutate({ name: 'Acme Corp' } as never); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: customersKeys.all }); expect(toast.success).toHaveBeenCalledWith('Customer created'); }); it('toasts the failure and skips invalidation', async () => { vi.mocked(customersApi.create).mockRejectedValue(new Error('duplicate name')); const { queryClient, wrapper } = createQueryHarness(); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); const { result } = renderHook(() => useCreateCustomer(), { wrapper }); result.current.mutate({ name: 'Acme Corp' } as never); await waitFor(() => expect(result.current.isError).toBe(true)); expect(toast.error).toHaveBeenCalledWith('Failed to create customer: duplicate name'); expect(invalidateSpy).not.toHaveBeenCalled(); }); }); describe('useUpdateCustomer', () => { it('passes id + payload through and invalidates the whole customers domain on success', async () => { vi.mocked(customersApi.update).mockResolvedValue(customer); const { queryClient, wrapper } = createQueryHarness(); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); const { result } = renderHook(() => useUpdateCustomer(), { wrapper }); const data = { name: 'Acme Corp (Renamed)' } as never; result.current.mutate({ id: 'c-1', data }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(customersApi.update).toHaveBeenCalledWith('c-1', data); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: customersKeys.all }); expect(toast.success).toHaveBeenCalledWith('Customer updated'); }); it('toasts the failure on update error', async () => { vi.mocked(customersApi.update).mockRejectedValue(new Error('not found')); const { wrapper } = createQueryHarness(); const { result } = renderHook(() => useUpdateCustomer(), { wrapper }); result.current.mutate({ id: 'c-404', data: { name: 'x' } as never }); await waitFor(() => expect(result.current.isError).toBe(true)); expect(toast.error).toHaveBeenCalledWith('Failed to update customer: not found'); }); });