// src/services/db/shopping.db.test.ts import { describe, it, expect, vi, beforeEach } from 'vitest'; import { mockPoolInstance } from '../../tests/setup/tests-setup-unit'; import type { Pool, PoolClient } from 'pg'; import { withTransaction } from './connection.db'; import { createMockShoppingList, createMockShoppingListItem, } from '../../tests/utils/mockFactories'; // Un-mock the module we are testing to ensure we use the real implementation. vi.unmock('./shopping.db'); import { ShoppingRepository } from './shopping.db'; import { ForeignKeyConstraintError, UniqueConstraintError } from './errors.db'; // Mock the logger to prevent console output during tests vi.mock('../logger.server', () => ({ logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), }, })); import { logger as mockLogger } from '../logger.server'; // Mock the withTransaction helper vi.mock('./connection.db', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, withTransaction: vi.fn() }; }); describe('Shopping DB Service', () => { let shoppingRepo: ShoppingRepository; beforeEach(() => { vi.clearAllMocks(); // Instantiate the repository with the mock pool for each test shoppingRepo = new ShoppingRepository(mockPoolInstance as unknown as Pool); }); describe('getShoppingLists', () => { it('should execute the correct query and return shopping lists', async () => { const mockLists = [createMockShoppingList({ user_id: 'user-1' })]; mockPoolInstance.query.mockResolvedValue({ rows: mockLists }); const result = await shoppingRepo.getShoppingLists('user-1', mockLogger); expect(mockPoolInstance.query).toHaveBeenCalledWith( expect.stringContaining('FROM public.shopping_lists sl'), ['user-1'], ); expect(result).toEqual(mockLists); }); it('should return an empty array if a user has no shopping lists', async () => { mockPoolInstance.query.mockResolvedValue({ rows: [] }); const result = await shoppingRepo.getShoppingLists('user-with-no-lists', mockLogger); expect(mockPoolInstance.query).toHaveBeenCalledWith( expect.stringContaining('FROM public.shopping_lists sl'), ['user-with-no-lists'], ); expect(result).toEqual([]); }); it('should throw an error if the database query fails', async () => { const dbError = new Error('DB Connection Error'); mockPoolInstance.query.mockRejectedValue(dbError); await expect(shoppingRepo.getShoppingLists('user-1', mockLogger)).rejects.toThrow( 'Failed to retrieve shopping lists.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, userId: 'user-1' }, 'Database error in getShoppingLists', ); }); }); describe('getShoppingListById', () => { it('should execute the correct query and return a single shopping list', async () => { const mockList = createMockShoppingList({ shopping_list_id: 1, user_id: 'user-1' }); mockPoolInstance.query.mockResolvedValue({ rows: [mockList] }); const result = await shoppingRepo.getShoppingListById(1, 'user-1', mockLogger); expect(mockPoolInstance.query).toHaveBeenCalledWith( expect.stringContaining('WHERE sl.shopping_list_id = $1 AND sl.user_id = $2'), [1, 'user-1'], ); expect(result).toEqual(mockList); }); it('should throw NotFoundError if the shopping list is not found or not owned by the user', async () => { mockPoolInstance.query.mockResolvedValue({ rows: [], rowCount: 0 }); await expect(shoppingRepo.getShoppingListById(999, 'user-1', mockLogger)).rejects.toThrow( 'Shopping list not found or you do not have permission to view it.', ); }); it('should throw an error if the database query fails', async () => { const dbError = new Error('DB Connection Error'); mockPoolInstance.query.mockRejectedValue(dbError); await expect(shoppingRepo.getShoppingListById(1, 'user-1', mockLogger)).rejects.toThrow( 'Failed to retrieve shopping list.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, listId: 1, userId: 'user-1' }, 'Database error in getShoppingListById', ); }); }); describe('createShoppingList', () => { it('should insert a new shopping list and return it', async () => { const mockList = createMockShoppingList({ name: 'New List' }); mockPoolInstance.query.mockResolvedValue({ rows: [mockList] }); const result = await shoppingRepo.createShoppingList('user-1', 'New List', mockLogger); expect(mockPoolInstance.query).toHaveBeenCalledWith( expect.stringContaining('INSERT INTO public.shopping_lists'), ['user-1', 'New List'], ); expect(result).toEqual(mockList); }); it('should throw ForeignKeyConstraintError if user does not exist', async () => { const dbError = new Error( 'insert or update on table "shopping_lists" violates foreign key constraint "shopping_lists_user_id_fkey"', ); (dbError as Error & { code: string }).code = '23503'; mockPoolInstance.query.mockRejectedValue(dbError); await expect( shoppingRepo.createShoppingList('non-existent-user', 'Wont work', mockLogger), ).rejects.toThrow(ForeignKeyConstraintError); }); it('should throw a generic error if the database query fails for other reasons', async () => { const dbError = new Error('DB Connection Error'); mockPoolInstance.query.mockRejectedValue(dbError); await expect( shoppingRepo.createShoppingList('user-1', 'New List', mockLogger), ).rejects.toThrow('Failed to create shopping list.'); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, userId: 'user-1', name: 'New List' }, 'Database error in createShoppingList', ); }); }); describe('deleteShoppingList', () => { it('should delete a shopping list if rowCount is 1', async () => { mockPoolInstance.query.mockResolvedValue({ rowCount: 1, rows: [], command: 'DELETE' }); await expect( shoppingRepo.deleteShoppingList(1, 'user-1', mockLogger), ).resolves.toBeUndefined(); expect(mockPoolInstance.query).toHaveBeenCalledWith( 'DELETE FROM public.shopping_lists WHERE shopping_list_id = $1 AND user_id = $2', [1, 'user-1'], ); }); it('should throw an error if no rows are deleted (list not found or wrong user)', async () => { mockPoolInstance.query.mockResolvedValue({ rowCount: 0, rows: [], command: 'DELETE' }); await expect(shoppingRepo.deleteShoppingList(999, 'user-1', mockLogger)).rejects.toThrow( 'Shopping list not found or user does not have permission to delete.', ); }); it('should throw a generic error if the database query fails', async () => { const dbError = new Error('DB Connection Error'); mockPoolInstance.query.mockRejectedValue(dbError); await expect(shoppingRepo.deleteShoppingList(1, 'user-1', mockLogger)).rejects.toThrow( 'Failed to delete shopping list.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, listId: 1, userId: 'user-1' }, 'Database error in deleteShoppingList', ); }); }); describe('addShoppingListItem', () => { it('should add a custom item to a shopping list', async () => { const mockItem = createMockShoppingListItem({ custom_item_name: 'Custom Item' }); mockPoolInstance.query.mockResolvedValue({ rows: [mockItem] }); const result = await shoppingRepo.addShoppingListItem( 1, 'user-1', { customItemName: 'Custom Item' }, mockLogger, ); expect(mockPoolInstance.query).toHaveBeenCalledWith( expect.stringContaining('INSERT INTO public.shopping_list_items'), [1, null, 'Custom Item', 'user-1'], ); expect(result).toEqual(mockItem); }); it('should add a master item to a shopping list', async () => { const mockItem = createMockShoppingListItem({ master_item_id: 123 }); mockPoolInstance.query.mockResolvedValue({ rows: [mockItem] }); const result = await shoppingRepo.addShoppingListItem(1, 'user-1', { masterItemId: 123 }, mockLogger); expect(mockPoolInstance.query).toHaveBeenCalledWith( expect.stringContaining('INSERT INTO public.shopping_list_items'), [1, 123, null, 'user-1'], ); expect(result).toEqual(mockItem); }); it('should add an item with both masterItemId and customItemName', async () => { const mockItem = createMockShoppingListItem({ master_item_id: 123, custom_item_name: 'Organic Apples', }); mockPoolInstance.query.mockResolvedValue({ rows: [mockItem] }); const result = await shoppingRepo.addShoppingListItem( 1, 'user-1', { masterItemId: 123, customItemName: 'Organic Apples' }, mockLogger, ); expect(mockPoolInstance.query).toHaveBeenCalledWith( expect.stringContaining('INSERT INTO public.shopping_list_items'), [1, 123, 'Organic Apples', 'user-1'], ); expect(result).toEqual(mockItem); }); it('should throw an error if both masterItemId and customItemName are missing', async () => { await expect(shoppingRepo.addShoppingListItem(1, 'user-1', {}, mockLogger)).rejects.toThrow( 'Either masterItemId or customItemName must be provided.', ); }); it('should throw ForeignKeyConstraintError if list or master item does not exist', async () => { const dbError = new Error('violates foreign key constraint'); (dbError as Error & { code: string }).code = '23503'; mockPoolInstance.query.mockRejectedValue(dbError); await expect(shoppingRepo.addShoppingListItem(999, 'user-1', { masterItemId: 999 }, mockLogger)).rejects.toThrow( 'Referenced list or item does not exist.', ); }); it('should throw a generic error if the database query fails', async () => { const dbError = new Error('DB Connection Error'); mockPoolInstance.query.mockRejectedValue(dbError); await expect( shoppingRepo.addShoppingListItem(1, 'user-1', { customItemName: 'Test' }, mockLogger), ).rejects.toThrow('Failed to add item to shopping list.'); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, listId: 1, userId: 'user-1', item: { customItemName: 'Test' } }, 'Database error in addShoppingListItem', ); }); }); describe('updateShoppingListItem', () => { it('should update an item and return the updated record', async () => { const mockItem = createMockShoppingListItem({ shopping_list_item_id: 1, is_purchased: true }); mockPoolInstance.query.mockResolvedValue({ rows: [mockItem], rowCount: 1 }); const result = await shoppingRepo.updateShoppingListItem( 1, 'user-1', { is_purchased: true }, mockLogger, ); expect(mockPoolInstance.query).toHaveBeenCalledWith( expect.stringContaining('UPDATE public.shopping_list_items sli'), [true, 1, 'user-1'], ); expect(result).toEqual(mockItem); }); it('should update multiple fields at once', async () => { const updates = { is_purchased: true, quantity: 5, notes: 'Get the green ones' }; const mockItem = createMockShoppingListItem({ shopping_list_item_id: 1, ...updates }); mockPoolInstance.query.mockResolvedValue({ rows: [mockItem], rowCount: 1 }); const result = await shoppingRepo.updateShoppingListItem(1, 'user-1', updates, mockLogger); expect(mockPoolInstance.query).toHaveBeenCalledWith( expect.stringContaining('UPDATE public.shopping_list_items sli'), [updates.quantity, updates.is_purchased, updates.notes, 1, 'user-1'], ); expect(result).toEqual(mockItem); }); it('should throw an error if the item to update is not found', async () => { mockPoolInstance.query.mockResolvedValue({ rowCount: 0, rows: [], command: 'UPDATE' }); await expect( shoppingRepo.updateShoppingListItem(999, 'user-1', { quantity: 5 }, mockLogger), ).rejects.toThrow('Shopping list item not found.'); }); it('should throw an error if no valid fields are provided to update', async () => { // The function should throw before even querying the database. await expect(shoppingRepo.updateShoppingListItem(1, 'user-1', {}, mockLogger)).rejects.toThrow( 'No valid fields to update.', ); }); it('should throw a generic error if the database query fails', async () => { const dbError = new Error('DB Connection Error'); mockPoolInstance.query.mockRejectedValue(dbError); await expect( shoppingRepo.updateShoppingListItem(1, 'user-1', { is_purchased: true }, mockLogger), ).rejects.toThrow('Failed to update shopping list item.'); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, itemId: 1, userId: 'user-1', updates: { is_purchased: true } }, 'Database error in updateShoppingListItem', ); }); }); describe('updateShoppingListItem - Ownership Check', () => { it('should not update an item if the user does not own the shopping list', async () => { mockPoolInstance.query.mockResolvedValue({ rowCount: 0 }); await expect( shoppingRepo.updateShoppingListItem(1, 'wrong-user', { is_purchased: true }, mockLogger), ).rejects.toThrow('Shopping list item not found.'); }); }); describe('removeShoppingListItem', () => { it('should delete an item if rowCount is 1', async () => { mockPoolInstance.query.mockResolvedValue({ rowCount: 1, rows: [], command: 'DELETE' }); await expect(shoppingRepo.removeShoppingListItem(1, 'user-1', mockLogger)).resolves.toBeUndefined(); expect(mockPoolInstance.query).toHaveBeenCalledWith( expect.stringContaining('DELETE FROM public.shopping_list_items sli'), [1, 'user-1'], ); }); it('should throw an error if no rows are deleted (item not found)', async () => { mockPoolInstance.query.mockResolvedValue({ rowCount: 0, rows: [], command: 'DELETE' }); await expect(shoppingRepo.removeShoppingListItem(999, 'user-1', mockLogger)).rejects.toThrow( 'Shopping list item not found or user does not have permission.', ); }); it('should throw a generic error if the database query fails', async () => { const dbError = new Error('DB Connection Error'); mockPoolInstance.query.mockRejectedValue(dbError); await expect(shoppingRepo.removeShoppingListItem(1, 'user-1', mockLogger)).rejects.toThrow( 'Failed to remove item from shopping list.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, itemId: 1, userId: 'user-1' }, 'Database error in removeShoppingListItem', ); }); }); describe('removeShoppingListItem - Ownership Check', () => { it('should not remove an item if the user does not own the shopping list', async () => { mockPoolInstance.query.mockResolvedValue({ rowCount: 0 }); await expect(shoppingRepo.removeShoppingListItem(1, 'wrong-user', mockLogger)).rejects.toThrow( 'Shopping list item not found or user does not have permission.', ); }); }); describe('completeShoppingList', () => { it('should call the complete_shopping_list database function', async () => { mockPoolInstance.query.mockResolvedValue({ rows: [{ complete_shopping_list: 1 }] }); const result = await shoppingRepo.completeShoppingList(1, 'user-123', mockLogger, 5000); expect(result).toBe(1); expect(mockPoolInstance.query).toHaveBeenCalledWith( 'SELECT public.complete_shopping_list($1, $2, $3)', [1, 'user-123', 5000], ); }); it('should throw ForeignKeyConstraintError if the shopping list does not exist', async () => { const dbError = new Error('violates foreign key constraint'); (dbError as Error & { code: string }).code = '23503'; mockPoolInstance.query.mockRejectedValue(dbError); await expect(shoppingRepo.completeShoppingList(999, 'user-123', mockLogger)).rejects.toThrow( 'The specified shopping list does not exist.', ); }); it('should throw a generic error if the database query fails', async () => { const dbError = new Error('DB Function Error'); mockPoolInstance.query.mockRejectedValue(dbError); await expect(shoppingRepo.completeShoppingList(1, 'user-123', mockLogger)).rejects.toThrow( 'Failed to complete shopping list.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, shoppingListId: 1, userId: 'user-123' }, 'Database error in completeShoppingList', ); }); }); describe('generateShoppingListForMenuPlan', () => { it('should call the correct database function and return items', async () => { const mockItems = [{ master_item_id: 1, item_name: 'Apples', quantity_needed: 2 }]; mockPoolInstance.query.mockResolvedValue({ rows: mockItems }); const result = await shoppingRepo.generateShoppingListForMenuPlan(1, 'user-1', mockLogger); expect(mockPoolInstance.query).toHaveBeenCalledWith( 'SELECT * FROM public.generate_shopping_list_for_menu_plan($1, $2)', [1, 'user-1'], ); expect(result).toEqual(mockItems); }); it('should return an empty array if the menu plan generates no items', async () => { mockPoolInstance.query.mockResolvedValue({ rows: [] }); const result = await shoppingRepo.generateShoppingListForMenuPlan(1, 'user-1', mockLogger); expect(result).toEqual([]); }); it('should throw an error if the database query fails', async () => { const dbError = new Error('DB Error'); mockPoolInstance.query.mockRejectedValue(dbError); await expect( shoppingRepo.generateShoppingListForMenuPlan(1, 'user-1', mockLogger), ).rejects.toThrow('Failed to generate shopping list for menu plan.'); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, menuPlanId: 1, userId: 'user-1' }, 'Database error in generateShoppingListForMenuPlan', ); }); }); describe('addMenuPlanToShoppingList', () => { it('should call the correct database function and return added items', async () => { const mockItems = [{ master_item_id: 1, item_name: 'Apples', quantity_needed: 2 }]; mockPoolInstance.query.mockResolvedValue({ rows: mockItems }); const result = await shoppingRepo.addMenuPlanToShoppingList(1, 10, 'user-1', mockLogger); expect(mockPoolInstance.query).toHaveBeenCalledWith( 'SELECT * FROM public.add_menu_plan_to_shopping_list($1, $2, $3)', [1, 10, 'user-1'], ); expect(result).toEqual(mockItems); }); it('should return an empty array if no items are added from the menu plan', async () => { mockPoolInstance.query.mockResolvedValue({ rows: [] }); const result = await shoppingRepo.addMenuPlanToShoppingList(1, 10, 'user-1', mockLogger); expect(result).toEqual([]); }); it('should throw an error if the database query fails', async () => { const dbError = new Error('DB Error'); mockPoolInstance.query.mockRejectedValue(dbError); await expect( shoppingRepo.addMenuPlanToShoppingList(1, 10, 'user-1', mockLogger), ).rejects.toThrow('Failed to add menu plan to shopping list.'); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, menuPlanId: 1, shoppingListId: 10, userId: 'user-1' }, 'Database error in addMenuPlanToShoppingList', ); }); }); describe('getPantryLocations', () => { it('should return a list of pantry locations for a user', async () => { const mockLocations = [{ pantry_location_id: 1, name: 'Fridge', user_id: 'user-1' }]; mockPoolInstance.query.mockResolvedValue({ rows: mockLocations }); const result = await shoppingRepo.getPantryLocations('user-1', mockLogger); expect(mockPoolInstance.query).toHaveBeenCalledWith( 'SELECT * FROM public.pantry_locations WHERE user_id = $1 ORDER BY name', ['user-1'], ); expect(result).toEqual(mockLocations); }); it('should return an empty array if user has no pantry locations', async () => { mockPoolInstance.query.mockResolvedValue({ rows: [] }); const result = await shoppingRepo.getPantryLocations('user-1', mockLogger); expect(result).toEqual([]); }); it('should throw an error if the database query fails', async () => { const dbError = new Error('DB Error'); mockPoolInstance.query.mockRejectedValue(dbError); await expect(shoppingRepo.getPantryLocations('user-1', mockLogger)).rejects.toThrow( 'Failed to get pantry locations.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, userId: 'user-1' }, 'Database error in getPantryLocations', ); }); }); describe('createPantryLocation', () => { it('should insert a new pantry location and return it', async () => { const mockLocation = { pantry_location_id: 1, name: 'Freezer', user_id: 'user-1' }; mockPoolInstance.query.mockResolvedValue({ rows: [mockLocation] }); const result = await shoppingRepo.createPantryLocation('user-1', 'Freezer', mockLogger); expect(mockPoolInstance.query).toHaveBeenCalledWith( 'INSERT INTO public.pantry_locations (user_id, name) VALUES ($1, $2) RETURNING *', ['user-1', 'Freezer'], ); expect(result).toEqual(mockLocation); }); it('should throw UniqueConstraintError on duplicate name', async () => { const dbError = new Error('duplicate key value violates unique constraint'); (dbError as Error & { code: string }).code = '23505'; mockPoolInstance.query.mockRejectedValue(dbError); await expect( shoppingRepo.createPantryLocation('user-1', 'Fridge', mockLogger), ).rejects.toThrow(UniqueConstraintError); }); it('should throw ForeignKeyConstraintError if user does not exist', async () => { const dbError = new Error('violates foreign key constraint'); (dbError as Error & { code: string }).code = '23503'; mockPoolInstance.query.mockRejectedValue(dbError); await expect( shoppingRepo.createPantryLocation('non-existent-user', 'Pantry', mockLogger), ).rejects.toThrow(ForeignKeyConstraintError); }); it('should throw a generic error if the database query fails', async () => { const dbError = new Error('DB Error'); mockPoolInstance.query.mockRejectedValue(dbError); await expect( shoppingRepo.createPantryLocation('user-1', 'Pantry', mockLogger), ).rejects.toThrow('Failed to create pantry location.'); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, userId: 'user-1', name: 'Pantry' }, 'Database error in createPantryLocation', ); }); }); describe('getShoppingTripHistory', () => { it('should return a list of shopping trips for a user', async () => { const mockTrips = [{ shopping_trip_id: 1, user_id: 'user-1', items: [] }]; mockPoolInstance.query.mockResolvedValue({ rows: mockTrips }); const result = await shoppingRepo.getShoppingTripHistory('user-1', mockLogger); expect(mockPoolInstance.query).toHaveBeenCalledWith( expect.stringContaining('FROM public.shopping_trips st'), ['user-1'], ); expect(result).toEqual(mockTrips); }); it('should return an empty array if a user has no shopping trips', async () => { mockPoolInstance.query.mockResolvedValue({ rows: [] }); const result = await shoppingRepo.getShoppingTripHistory('user-no-trips', mockLogger); expect(result).toEqual([]); }); it('should throw an error if the database query fails', async () => { const dbError = new Error('DB Error'); mockPoolInstance.query.mockRejectedValue(dbError); await expect(shoppingRepo.getShoppingTripHistory('user-1', mockLogger)).rejects.toThrow( 'Failed to retrieve shopping trip history.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, userId: 'user-1' }, 'Database error in getShoppingTripHistory', ); }); }); describe('createReceipt', () => { it('should insert a new receipt and return it', async () => { const mockReceipt = { receipt_id: 1, user_id: 'user-1', receipt_image_url: 'url', status: 'pending', }; mockPoolInstance.query.mockResolvedValue({ rows: [mockReceipt] }); const result = await shoppingRepo.createReceipt('user-1', 'url', mockLogger); expect(mockPoolInstance.query).toHaveBeenCalledWith( expect.stringContaining('INSERT INTO public.receipts'), ['user-1', 'url'], ); expect(result).toEqual(mockReceipt); }); it('should throw ForeignKeyConstraintError if user does not exist', async () => { const dbError = new Error('violates foreign key constraint'); (dbError as Error & { code: string }).code = '23503'; mockPoolInstance.query.mockRejectedValue(dbError); await expect( shoppingRepo.createReceipt('non-existent-user', 'url', mockLogger), ).rejects.toThrow('User not found'); }); it('should throw a generic error if the database query fails', async () => { const dbError = new Error('DB Error'); mockPoolInstance.query.mockRejectedValue(dbError); await expect(shoppingRepo.createReceipt('user-1', 'url', mockLogger)).rejects.toThrow( 'Failed to create receipt record.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, userId: 'user-1', receiptImageUrl: 'url' }, 'Database error in createReceipt', ); }); }); describe('findReceiptOwner', () => { it('should return the user_id of the receipt owner', async () => { const mockOwner = { user_id: 'owner-123' }; mockPoolInstance.query.mockResolvedValue({ rows: [mockOwner] }); const result = await shoppingRepo.findReceiptOwner(1, mockLogger); expect(mockPoolInstance.query).toHaveBeenCalledWith( 'SELECT user_id FROM public.receipts WHERE receipt_id = $1', [1], ); expect(result).toEqual(mockOwner); }); it('should return undefined if the receipt is not found', async () => { mockPoolInstance.query.mockResolvedValue({ rows: [] }); const result = await shoppingRepo.findReceiptOwner(999, mockLogger); expect(result).toBeUndefined(); }); it('should throw a generic error if the database query fails', async () => { const dbError = new Error('DB Error'); mockPoolInstance.query.mockRejectedValue(dbError); await expect(shoppingRepo.findReceiptOwner(1, mockLogger)).rejects.toThrow( 'Failed to retrieve receipt owner from database.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, receiptId: 1 }, 'Database error in findReceiptOwner', ); }); }); describe('processReceiptItems', () => { it('should call the process_receipt_items database function with correct parameters', async () => { const mockClientQuery = vi.fn().mockResolvedValue({ rows: [] }); vi.mocked(withTransaction).mockImplementation(async (callback) => { const mockClient = { query: mockClientQuery }; return callback(mockClient as unknown as PoolClient); }); const items = [{ raw_item_description: 'Milk', price_paid_cents: 399 }]; await shoppingRepo.processReceiptItems(1, items, mockLogger); const expectedItemsWithQuantity = [ { raw_item_description: 'Milk', price_paid_cents: 399, quantity: 1 }, ]; expect(withTransaction).toHaveBeenCalledTimes(1); expect(mockClientQuery).toHaveBeenCalledWith( 'SELECT public.process_receipt_items($1, $2, $3)', [1, JSON.stringify(expectedItemsWithQuantity), JSON.stringify(expectedItemsWithQuantity)], ); }); it('should update receipt status to "failed" on error', async () => { const dbError = new Error('Function error'); vi.mocked(withTransaction).mockImplementation(async (callback) => { const mockClient = { query: vi.fn().mockRejectedValue(dbError) }; // The callback will throw, and withTransaction will catch and re-throw await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(dbError); throw dbError; }); const items = [{ raw_item_description: 'Milk', price_paid_cents: 399 }]; await expect(shoppingRepo.processReceiptItems(1, items, mockLogger)).rejects.toThrow( 'Failed to process and save receipt items.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, receiptId: 1 }, 'Database transaction error in processReceiptItems', ); // Verify that the status was updated to 'failed' in the catch block expect(mockPoolInstance.query).toHaveBeenCalledWith( "UPDATE public.receipts SET status = 'failed' WHERE receipt_id = $1", [1], ); }); }); describe('findDealsForReceipt', () => { it('should call the find_deals_for_receipt_items database function', async () => { const mockDeals = [ { receipt_item_id: 1, master_item_id: 10, item_name: 'Milk', price_paid_cents: 399, current_best_price_in_cents: 350, potential_savings_cents: 49, deal_store_name: 'Grocer', flyer_id: 101, }, ]; mockPoolInstance.query.mockResolvedValue({ rows: mockDeals }); const result = await shoppingRepo.findDealsForReceipt(1, mockLogger); expect(mockPoolInstance.query).toHaveBeenCalledWith( 'SELECT * FROM public.find_deals_for_receipt_items($1)', [1], ); expect(result).toEqual(mockDeals); }); it('should return an empty array if no deals are found', async () => { mockPoolInstance.query.mockResolvedValue({ rows: [] }); const result = await shoppingRepo.findDealsForReceipt(1, mockLogger); expect(result).toEqual([]); }); it('should throw an error if the database query fails', async () => { const dbError = new Error('DB Error'); mockPoolInstance.query.mockRejectedValue(dbError); await expect(shoppingRepo.findDealsForReceipt(1, mockLogger)).rejects.toThrow( 'Failed to find deals for receipt.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: dbError, receiptId: 1 }, 'Database error in findDealsForReceipt', ); }); }); describe('processReceiptItems error handling', () => { it('should log an error if updating receipt status to "failed" also fails', async () => { const transactionError = new Error('Transaction failed'); const updateStatusError = new Error('Failed to update status'); // Mock withTransaction to throw an error vi.mocked(withTransaction).mockImplementation(async () => { throw transactionError; }); // Mock the subsequent update query to also throw an error mockPoolInstance.query.mockRejectedValueOnce(updateStatusError); const items = [{ raw_item_description: 'Milk', price_paid_cents: 399 }]; await expect(shoppingRepo.processReceiptItems(1, items, mockLogger)).rejects.toThrow( 'Failed to process and save receipt items.', ); expect(mockLogger.error).toHaveBeenCalledWith( { err: transactionError, receiptId: 1 }, 'Database transaction error in processReceiptItems', ); expect(mockLogger.error).toHaveBeenCalledWith( { updateError: updateStatusError, receiptId: 1 }, 'Failed to update receipt status to "failed" after transaction rollback.', ); }); }); });