import { describe, it, expect, vi, beforeEach } from 'vitest'; const { mockGet, mockPost, mockPatch, mockDelete } = vi.hoisted(() => ({ mockGet: vi.fn(), mockPost: vi.fn(), mockPatch: vi.fn(), mockDelete: vi.fn(), })); vi.mock('../api-client', () => ({ apiClient: { get: mockGet, post: mockPost, patch: mockPatch, delete: mockDelete }, })); import { listRecipes, getRecipe, createRecipe, updateRecipe, deleteRecipe, scaleRecipe, importRecipeFromText, importRecipeFromUrl, listRecipesByProduct, } from '../recipes'; beforeEach(() => vi.clearAllMocks()); describe('recipes service', () => { it('listRecipes with no query', async () => { mockGet.mockResolvedValue({ data: [] }); await listRecipes('hh1'); expect(mockGet).toHaveBeenCalledWith('/households/hh1/recipes'); }); it('listRecipes builds query string', async () => { mockGet.mockResolvedValue({ data: [] }); await listRecipes('hh1', { q: 'pasta', tags: 'italian', cuisine: 'Italian', maxCalories: 500, isFavorite: true, cursor: 'cur1', limit: 10, }); const url = mockGet.mock.calls[0][0] as string; expect(url).toContain('q=pasta'); expect(url).toContain('tags=italian'); expect(url).toContain('cuisine=Italian'); expect(url).toContain('maxCalories=500'); expect(url).toContain('isFavorite=true'); expect(url).toContain('cursor=cur1'); expect(url).toContain('limit=10'); }); it('getRecipe calls GET', async () => { mockGet.mockResolvedValue({ _id: 'r1' }); await getRecipe('hh1', 'r1'); expect(mockGet).toHaveBeenCalledWith('/households/hh1/recipes/r1'); }); it('createRecipe calls POST', async () => { mockPost.mockResolvedValue({ _id: 'r1' }); const data = { name: 'Pasta' }; await createRecipe('hh1', data as never); expect(mockPost).toHaveBeenCalledWith('/households/hh1/recipes', data); }); it('updateRecipe calls PATCH', async () => { mockPatch.mockResolvedValue({ _id: 'r1' }); await updateRecipe('hh1', 'r1', { name: 'Updated' } as never); expect(mockPatch).toHaveBeenCalledWith('/households/hh1/recipes/r1', { name: 'Updated' }); }); it('deleteRecipe calls DELETE', async () => { mockDelete.mockResolvedValue(undefined); await deleteRecipe('hh1', 'r1'); expect(mockDelete).toHaveBeenCalledWith('/households/hh1/recipes/r1'); }); it('scaleRecipe calls POST', async () => { mockPost.mockResolvedValue({ _id: 'r1' }); await scaleRecipe('hh1', 'r1', { targetServings: 8 }); expect(mockPost).toHaveBeenCalledWith('/households/hh1/recipes/r1/scale', { targetServings: 8, }); }); it('importRecipeFromText calls POST', async () => { mockPost.mockResolvedValue({ available: false }); await importRecipeFromText('hh1', { text: 'recipe text' } as never); expect(mockPost).toHaveBeenCalledWith('/households/hh1/recipes/import-text', { text: 'recipe text', }); }); it('importRecipeFromUrl calls POST', async () => { mockPost.mockResolvedValue({ available: false }); await importRecipeFromUrl('hh1', { url: 'http://example.com' } as never); expect(mockPost).toHaveBeenCalledWith('/households/hh1/recipes/import-url', { url: 'http://example.com', }); }); it('listRecipesByProduct with no query', async () => { mockGet.mockResolvedValue({ data: [] }); await listRecipesByProduct('hh1', 'p1'); expect(mockGet).toHaveBeenCalledWith('/households/hh1/recipes/by-product/p1'); }); it('listRecipesByProduct with query', async () => { mockGet.mockResolvedValue({ data: [] }); await listRecipesByProduct('hh1', 'p1', { cursor: 'c1', limit: 5 }); const url = mockGet.mock.calls[0][0] as string; expect(url).toContain('cursor=c1'); expect(url).toContain('limit=5'); }); });