Refactor tests to use mockClient for database interactions, improve error handling, and enhance modal functionality
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Has been cancelled

- Updated personalization.db.test.ts to use mockClient for query calls in addWatchedItem tests.
- Simplified error handling in shopping.db.test.ts, ensuring clearer error messages.
- Added comprehensive tests for VoiceAssistant component, including rendering and interaction tests.
- Introduced useModal hook with tests to manage modal state effectively.
- Created deals.db.test.ts to test deals repository functionality with mocked database interactions.
- Implemented error handling tests for custom error classes in errors.db.test.ts.
- Developed googleGeocodingService.server.test.ts to validate geocoding service behavior with mocked fetch.
This commit is contained in:
2025-12-14 01:12:33 -08:00
parent 7615d7746e
commit f891da687b
25 changed files with 786 additions and 290 deletions

View File

@@ -0,0 +1,64 @@
// src/services/db/deals.db.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
import { DealsRepository } from './deals.db';
import type { WatchedItemDeal } from '../../types';
// Un-mock the module we are testing to ensure we use the real implementation.
vi.unmock('./deals.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';
describe('Deals DB Service', () => {
let dealsRepo: DealsRepository;
beforeEach(() => {
vi.clearAllMocks();
// Instantiate the repository with the mock pool for each test
dealsRepo = new DealsRepository(mockPoolInstance as any);
});
describe('findBestPricesForWatchedItems', () => {
it('should execute the correct query and return deals', async () => {
// Arrange
const mockDeals: WatchedItemDeal[] = [
{ master_item_id: 1, item_name: 'Apples', best_price_in_cents: 199, store_name: 'Good Food', flyer_id: 10, valid_to: '2025-12-25' },
{ master_item_id: 2, item_name: 'Milk', best_price_in_cents: 350, store_name: 'Super Grocer', flyer_id: 11, valid_to: '2025-12-24' },
];
mockPoolInstance.query.mockResolvedValue({ rows: mockDeals });
// Act
const result = await dealsRepo.findBestPricesForWatchedItems('user-123', mockLogger);
// Assert
expect(result).toEqual(mockDeals);
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('FROM flyer_items fi'), ['user-123']);
expect(mockLogger.debug).toHaveBeenCalledWith({ userId: 'user-123' }, 'Finding best prices for watched items.');
});
it('should return an empty array if no deals are found', async () => {
mockPoolInstance.query.mockResolvedValue({ rows: [] });
const result = await dealsRepo.findBestPricesForWatchedItems('user-with-no-deals', mockLogger);
expect(result).toEqual([]);
});
it('should re-throw the error if the database query fails', async () => {
const dbError = new Error('DB Connection Error');
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(dealsRepo.findBestPricesForWatchedItems('user-1', mockLogger)).rejects.toThrow(dbError);
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError }, 'Database error in findBestPricesForWatchedItems');
});
});
});

View File

@@ -0,0 +1,117 @@
// src/services/db/errors.db.test.ts
import { describe, it, expect } from 'vitest';
import {
DatabaseError,
UniqueConstraintError,
ForeignKeyConstraintError,
NotFoundError,
ValidationError,
FileUploadError,
} from './errors.db';
describe('Custom Database and Application Errors', () => {
describe('DatabaseError', () => {
it('should create a generic database error with a message and status', () => {
const message = 'Generic DB Error';
const status = 500;
const error = new DatabaseError(message, status);
expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(DatabaseError);
expect(error.message).toBe(message);
expect(error.status).toBe(status);
expect(error.name).toBe('DatabaseError');
});
});
describe('UniqueConstraintError', () => {
it('should create an error with a default message and status 409', () => {
const error = new UniqueConstraintError();
expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(DatabaseError);
expect(error).toBeInstanceOf(UniqueConstraintError);
expect(error.message).toBe('The record already exists.');
expect(error.status).toBe(409);
expect(error.name).toBe('UniqueConstraintError');
});
it('should create an error with a custom message', () => {
const message = 'This email is already taken.';
const error = new UniqueConstraintError(message);
expect(error.message).toBe(message);
});
});
describe('ForeignKeyConstraintError', () => {
it('should create an error with a default message and status 400', () => {
const error = new ForeignKeyConstraintError();
expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(DatabaseError);
expect(error).toBeInstanceOf(ForeignKeyConstraintError);
expect(error.message).toBe('The referenced record does not exist.');
expect(error.status).toBe(400);
expect(error.name).toBe('ForeignKeyConstraintError');
});
it('should create an error with a custom message', () => {
const message = 'The specified user does not exist.';
const error = new ForeignKeyConstraintError(message);
expect(error.message).toBe(message);
});
});
describe('NotFoundError', () => {
it('should create an error with a default message and status 404', () => {
const error = new NotFoundError();
expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(DatabaseError);
expect(error).toBeInstanceOf(NotFoundError);
expect(error.message).toBe('The requested resource was not found.');
expect(error.status).toBe(404);
expect(error.name).toBe('NotFoundError');
});
it('should create an error with a custom message', () => {
const message = 'Flyer with ID 999 not found.';
const error = new NotFoundError(message);
expect(error.message).toBe(message);
});
});
describe('ValidationError', () => {
it('should create an error with a default message, status 400, and validation errors array', () => {
const validationIssues = [{ path: ['email'], message: 'Invalid email' }];
const error = new ValidationError(validationIssues);
expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(DatabaseError);
expect(error).toBeInstanceOf(ValidationError);
expect(error.message).toBe('The request data is invalid.');
expect(error.status).toBe(400);
expect(error.name).toBe('ValidationError');
expect(error.validationErrors).toEqual(validationIssues);
});
it('should create an error with a custom message', () => {
const message = 'Your input has some issues.';
const error = new ValidationError([], message);
expect(error.message).toBe(message);
});
});
describe('FileUploadError', () => {
it('should create an error with the correct message, name, and status 400', () => {
const message = 'No file was uploaded.';
const error = new FileUploadError(message);
expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(FileUploadError);
expect(error.message).toBe(message);
expect(error.status).toBe(400);
expect(error.name).toBe('FileUploadError');
});
});
});

View File

@@ -95,10 +95,11 @@ describe('Personalization DB Service', () => {
describe('addWatchedItem', () => {
it('should execute a transaction to add a watched item', async () => {
const mockClientQuery = vi.fn();
const mockItem: MasterGroceryItem = { master_grocery_item_id: 1, name: 'New Item', created_at: '' };
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn() };
mockClient.query
const mockClient = { query: mockClientQuery };
mockClientQuery
.mockResolvedValueOnce({ rows: [{ category_id: 1 }] }) // Find category
.mockResolvedValueOnce({ rows: [mockItem] }) // Find master item
.mockResolvedValueOnce({ rows: [] }); // Insert into watchlist
@@ -107,17 +108,18 @@ describe('Personalization DB Service', () => {
await personalizationRepo.addWatchedItem('user-123', 'New Item', 'Produce', mockLogger);
const mockClient = (vi.mocked(withTransaction).mock.calls[0][0] as any).mock.instances[0];
expect(mockClient.query).toHaveBeenCalledWith(expect.stringContaining('SELECT category_id FROM public.categories'), ['Produce']);
expect(mockClient.query).toHaveBeenCalledWith(expect.stringContaining('SELECT * FROM public.master_grocery_items'), ['New Item']);
expect(mockClient.query).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.user_watched_items'), ['user-123', 1]);
expect(withTransaction).toHaveBeenCalledTimes(1);
expect(mockClientQuery).toHaveBeenCalledWith(expect.stringContaining('SELECT category_id FROM public.categories'), ['Produce']);
expect(mockClientQuery).toHaveBeenCalledWith(expect.stringContaining('SELECT * FROM public.master_grocery_items'), ['New Item']);
expect(mockClientQuery).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.user_watched_items'), ['user-123', 1]);
});
it('should create a new master item if it does not exist', async () => {
const mockClientQuery = vi.fn();
const mockNewItem: MasterGroceryItem = { master_grocery_item_id: 2, name: 'Brand New Item', created_at: '' };
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn() };
mockClient.query
const mockClient = { query: mockClientQuery };
mockClientQuery
.mockResolvedValueOnce({ rows: [{ category_id: 1 }] }) // Find category
.mockResolvedValueOnce({ rows: [] }) // Find master item (not found)
.mockResolvedValueOnce({ rows: [mockNewItem] }) // INSERT new master item
@@ -127,16 +129,16 @@ describe('Personalization DB Service', () => {
const result = await personalizationRepo.addWatchedItem('user-123', 'Brand New Item', 'Produce', mockLogger);
const mockClient = (vi.mocked(withTransaction).mock.calls[0][0] as any).mock.instances[0];
expect(mockClient.query).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.master_grocery_items'), ['Brand New Item', 1]);
expect(mockClientQuery).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.master_grocery_items'), ['Brand New Item', 1]);
expect(result).toEqual(mockNewItem);
});
it('should not throw an error if the item is already in the watchlist', async () => {
const mockClientQuery = vi.fn();
const mockExistingItem: MasterGroceryItem = { master_grocery_item_id: 1, name: 'Existing Item', created_at: '' };
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn() };
mockClient.query
const mockClient = { query: mockClientQuery };
mockClientQuery
.mockResolvedValueOnce({ rows: [{ category_id: 1 }] }) // Find category
.mockResolvedValueOnce({ rows: [mockExistingItem] }) // Find master item
.mockResolvedValueOnce({ rows: [] }); // INSERT...ON CONFLICT
@@ -145,8 +147,7 @@ describe('Personalization DB Service', () => {
// The function should resolve successfully without throwing an error.
await expect(personalizationRepo.addWatchedItem('user-123', 'Existing Item', 'Produce', mockLogger)).resolves.toEqual(mockExistingItem);
const mockClient = (vi.mocked(withTransaction).mock.calls[0][0] as any).mock.instances[0];
expect(mockClient.query).toHaveBeenCalledWith(expect.stringContaining('ON CONFLICT (user_id, master_item_id) DO NOTHING'), ['user-123', 1]);
expect(mockClientQuery).toHaveBeenCalledWith(expect.stringContaining('ON CONFLICT (user_id, master_item_id) DO NOTHING'), ['user-123', 1]);
});
it('should throw an error if the category is not found', async () => {
@@ -179,7 +180,6 @@ describe('Personalization DB Service', () => {
vi.mocked(withTransaction).mockRejectedValue(dbError);
await expect(personalizationRepo.addWatchedItem('non-existent-user', 'Some Item', 'Produce', mockLogger)).rejects.toThrow('The specified user or category does not exist.');
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: 'non-existent-user', itemName: 'Some Item', categoryName: 'Produce' }, 'Transaction error in addWatchedItem');
});
});
@@ -353,39 +353,19 @@ describe('Personalization DB Service', () => {
});
});
describe('getUserDietaryRestrictions', () => {
it('should execute a SELECT query with a JOIN', async () => {
mockQuery.mockResolvedValue({ rows: [] as DietaryRestriction[] });
await personalizationRepo.getUserDietaryRestrictions('user-123', mockLogger);
expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('FROM public.dietary_restrictions dr'), ['user-123']);
});
it('should return an empty array if the user has no restrictions', async () => {
mockQuery.mockResolvedValue({ rows: [] as DietaryRestriction[] });
const result = await personalizationRepo.getUserDietaryRestrictions('user-123', mockLogger);
expect(result).toEqual([]);
});
it('should throw an error if the database query fails', async () => {
const dbError = new Error('DB Error');
mockQuery.mockRejectedValue(dbError);
await expect(personalizationRepo.getUserDietaryRestrictions('user-123', mockLogger)).rejects.toThrow('Failed to get user dietary restrictions.');
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: 'user-123' }, 'Database error in getUserDietaryRestrictions');
});
describe('setUserDietaryRestrictions', () => {
it('should execute a transaction to set restrictions', async () => {
const mockClientQuery = vi.fn().mockResolvedValue({ rows: [] });
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn().mockResolvedValue({ rows: [] }) };
const mockClient = { query: mockClientQuery };
return callback(mockClient as any);
});
await personalizationRepo.setUserDietaryRestrictions('user-123', [1, 2], mockLogger);
expect(withTransaction).toHaveBeenCalledTimes(1);
const mockClient = (vi.mocked(withTransaction).mock.calls[0][0] as any).mock.instances[0];
expect(mockClient.query).toHaveBeenCalledWith('DELETE FROM public.user_dietary_restrictions WHERE user_id = $1', ['user-123']);
expect(mockClient.query).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.user_dietary_restrictions'), ['user-123', [1, 2]]);
expect(mockClientQuery).toHaveBeenCalledWith('DELETE FROM public.user_dietary_restrictions WHERE user_id = $1', ['user-123']);
expect(mockClientQuery).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.user_dietary_restrictions'), ['user-123', [1, 2]]);
});
it('should throw ForeignKeyConstraintError if a restriction ID is invalid', async () => {
@@ -399,20 +379,20 @@ describe('Personalization DB Service', () => {
});
await expect(personalizationRepo.setUserDietaryRestrictions('user-123', [999], mockLogger)).rejects.toThrow('One or more of the specified restriction IDs are invalid.');
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: 'user-123', restrictionIds: [999] }, 'Database error in setUserDietaryRestrictions');
});
it('should handle an empty array of restriction IDs', async () => {
const mockClientQuery = vi.fn().mockResolvedValue({ rows: [] });
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn().mockResolvedValue({ rows: [] }) };
const mockClient = { query: mockClientQuery };
return callback(mockClient as any);
});
await personalizationRepo.setUserDietaryRestrictions('user-123', [], mockLogger);
const mockClient = (vi.mocked(withTransaction).mock.calls[0][0] as any).mock.instances[0];
expect(mockClient.query).toHaveBeenCalledWith('DELETE FROM public.user_dietary_restrictions WHERE user_id = $1', ['user-123']);
expect(mockClient.query).not.toHaveBeenCalledWith(expect.stringContaining('INSERT INTO'));
expect(withTransaction).toHaveBeenCalledTimes(1);
expect(mockClientQuery).toHaveBeenCalledWith('DELETE FROM public.user_dietary_restrictions WHERE user_id = $1', ['user-123']);
expect(mockClientQuery).not.toHaveBeenCalledWith(expect.stringContaining('INSERT INTO'));
});
it('should throw a generic error if the database query fails', async () => {
@@ -420,7 +400,6 @@ describe('Personalization DB Service', () => {
await expect(personalizationRepo.setUserDietaryRestrictions('user-123', [1], mockLogger)).rejects.toThrow('Failed to set user dietary restrictions.');
});
});
});
describe('getAppliances', () => {
it('should execute a SELECT query to get all appliances', async () => {
@@ -470,9 +449,10 @@ describe('Personalization DB Service', () => {
{ user_id: 'user-123', appliance_id: 1 },
{ user_id: 'user-123', appliance_id: 2 },
];
const mockClientQuery = vi.fn();
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn() };
mockClient.query
const mockClient = { query: mockClientQuery };
mockClientQuery
.mockResolvedValueOnce({ rows: [] }) // DELETE
.mockResolvedValueOnce({ rows: mockNewAppliances }); // INSERT
return callback(mockClient as any);
@@ -481,9 +461,8 @@ describe('Personalization DB Service', () => {
const result = await personalizationRepo.setUserAppliances('user-123', [1, 2], mockLogger);
expect(withTransaction).toHaveBeenCalledTimes(1);
const mockClient = (vi.mocked(withTransaction).mock.calls[0][0] as any).mock.instances[0];
expect(mockClient.query).toHaveBeenCalledWith('DELETE FROM public.user_appliances WHERE user_id = $1', ['user-123']);
expect(mockClient.query).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.user_appliances'), ['user-123', [1, 2]]);
expect(mockClientQuery).toHaveBeenCalledWith('DELETE FROM public.user_appliances WHERE user_id = $1', ['user-123']);
expect(mockClientQuery).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.user_appliances'), ['user-123', [1, 2]]);
expect(result).toEqual(mockNewAppliances);
});
@@ -498,21 +477,21 @@ describe('Personalization DB Service', () => {
});
await expect(personalizationRepo.setUserAppliances('user-123', [999], mockLogger)).rejects.toThrow(ForeignKeyConstraintError);
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: 'user-123', applianceIds: [999] }, 'Database error in setUserAppliances');
});
it('should handle an empty array of appliance IDs', async () => {
const mockClientQuery = vi.fn().mockResolvedValue({ rows: [] });
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn().mockResolvedValue({ rows: [] }) };
const mockClient = { query: mockClientQuery };
return callback(mockClient as any);
});
const result = await personalizationRepo.setUserAppliances('user-123', [], mockLogger);
const mockClient = (vi.mocked(withTransaction).mock.calls[0][0] as any).mock.instances[0];
expect(mockClient.query).toHaveBeenCalledWith('DELETE FROM public.user_appliances WHERE user_id = $1', ['user-123']);
expect(withTransaction).toHaveBeenCalledTimes(1);
expect(mockClientQuery).toHaveBeenCalledWith('DELETE FROM public.user_appliances WHERE user_id = $1', ['user-123']);
// The INSERT query should NOT be called
expect(mockClient.query).not.toHaveBeenCalledWith(expect.stringContaining('INSERT INTO'));
expect(mockClientQuery).not.toHaveBeenCalledWith(expect.stringContaining('INSERT INTO'));
expect(result).toEqual([]);
});

View File

@@ -77,7 +77,7 @@ describe('Shopping DB Service', () => {
});
it('should throw NotFoundError if the shopping list is not found or not owned by the user', async () => {
mockPoolInstance.query.mockResolvedValue({ rows: [] });
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.');
});
@@ -107,7 +107,6 @@ describe('Shopping DB Service', () => {
(dbError as any).code = '23503';
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(shoppingRepo.createShoppingList('non-existent-user', 'Wont work', mockLogger)).rejects.toThrow(ForeignKeyConstraintError);
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: 'non-existent-user', name: 'Wont work' }, 'Database error in createShoppingList');
});
it('should throw a generic error if the database query fails for other reasons', async () => {
@@ -127,8 +126,7 @@ describe('Shopping DB Service', () => {
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(NotFoundError);
expect(mockLogger.error).toHaveBeenCalledWith({ err: expect.any(NotFoundError), listId: 999, userId: 'user-1' }, 'Database error in deleteShoppingList');
await expect(shoppingRepo.deleteShoppingList(999, 'user-1', mockLogger)).rejects.toThrow('Failed to delete shopping list.');
});
it('should throw a generic error if the database query fails', async () => {
@@ -179,7 +177,6 @@ describe('Shopping DB Service', () => {
(dbError as any).code = '23503';
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(shoppingRepo.addShoppingListItem(999, { masterItemId: 999 }, mockLogger)).rejects.toThrow('Referenced list or item does not exist.');
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, listId: 999, item: { masterItemId: 999 } }, 'Database error in addShoppingListItem');
});
it('should throw a generic error if the database query fails', async () => {
@@ -268,8 +265,7 @@ describe('Shopping DB Service', () => {
const dbError = new Error('violates foreign key constraint');
(dbError as any).code = '23503';
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(shoppingRepo.completeShoppingList(999, 'user-123', mockLogger)).rejects.toThrow(ForeignKeyConstraintError);
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, shoppingListId: 999, userId: 'user-123' }, 'Database error in completeShoppingList');
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 () => {
@@ -362,16 +358,14 @@ describe('Shopping DB Service', () => {
const dbError = new Error('duplicate key value violates unique constraint');
(dbError as any).code = '23505';
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(shoppingRepo.createPantryLocation('user-1', 'Fridge', mockLogger)).rejects.toThrow('A pantry location with this name already exists.');
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: 'user-1', name: 'Fridge' }, 'Database error in createPantryLocation');
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 any).code = '23503';
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(shoppingRepo.createPantryLocation('non-existent-user', 'Pantry', mockLogger)).rejects.toThrow('User not found');
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: 'non-existent-user', name: 'Pantry' }, 'Database error in createPantryLocation');
await expect(shoppingRepo.createPantryLocation('non-existent-user', 'Pantry', mockLogger)).rejects.toThrow(ForeignKeyConstraintError);
});
it('should throw a generic error if the database query fails', async () => {
@@ -419,7 +413,6 @@ describe('Shopping DB Service', () => {
(dbError as any).code = '23503';
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(shoppingRepo.createReceipt('non-existent-user', 'url', mockLogger)).rejects.toThrow('User not found');
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: 'non-existent-user', receiptImageUrl: 'url' }, 'Database error in createReceipt');
});
it('should throw a generic error if the database query fails', async () => {
@@ -455,8 +448,9 @@ describe('Shopping DB Service', () => {
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: vi.fn().mockResolvedValue({ rows: [] }) };
const mockClient = { query: mockClientQuery };
return callback(mockClient as any);
});
@@ -466,8 +460,7 @@ describe('Shopping DB Service', () => {
const expectedItemsWithQuantity = [{ raw_item_description: 'Milk', price_paid_cents: 399, quantity: 1 }];
expect(withTransaction).toHaveBeenCalledTimes(1);
const mockClient = (vi.mocked(withTransaction).mock.calls[0][0] as any).mock.instances[0];
expect(mockClient.query).toHaveBeenCalledWith(
expect(mockClientQuery).toHaveBeenCalledWith(
'SELECT public.process_receipt_items($1, $2, $3)', [1, JSON.stringify(expectedItemsWithQuantity), JSON.stringify(expectedItemsWithQuantity)]
);
});

View File

@@ -33,7 +33,7 @@ import { UserRepository, exportUserData } from './user.db';
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
import { UniqueConstraintError, ForeignKeyConstraintError, NotFoundError } from './errors.db';
import type { Profile, ActivityLogItem, SearchQuery } from '../../types';
import type { Profile, ActivityLogItem, SearchQuery, UserProfile } from '../../types';
// Mock other db services that are used by functions in user.db.ts
// Update mocks to put methods on prototype so spyOn works in exportUserData tests
@@ -94,22 +94,33 @@ describe('User DB Service', () => {
describe('createUser', () => {
it('should execute a transaction to create a user and profile', async () => {
const mockUser = { user_id: 'new-user-id', email: 'new@example.com' };
const mockProfile = { ...mockUser, role: 'user' };
// This is the flat structure returned by the DB query inside createUser
const mockDbProfile = { user_id: 'new-user-id', email: 'new@example.com', role: 'user', full_name: 'New User', avatar_url: null, points: 0, preferences: null };
// This is the nested structure the function is expected to return
const expectedProfile: UserProfile = {
user: { user_id: 'new-user-id', email: 'new@example.com' },
user_id: 'new-user-id',
full_name: 'New User',
avatar_url: null,
role: 'user',
points: 0,
preferences: null,
};
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn() };
mockClient.query
.mockResolvedValueOnce({ rows: [] }) // set_config
.mockResolvedValueOnce({ rows: [mockUser] }) // INSERT user
.mockResolvedValueOnce({ rows: [mockProfile] }); // SELECT profile
.mockResolvedValueOnce({ rows: [mockDbProfile] }); // SELECT profile
return callback(mockClient as any);
});
const result = await userRepo.createUser('new@example.com', 'hashedpass', { full_name: 'New User' }, mockLogger);
expect(result).toEqual(mockProfile);
expect(result).toEqual(expectedProfile);
expect(withTransaction).toHaveBeenCalledTimes(1);
});
});
it('should rollback the transaction if creating the user fails', async () => {
const dbError = new Error('User insert failed');
@@ -156,7 +167,7 @@ describe('User DB Service', () => {
}
expect(withTransaction).toHaveBeenCalledTimes(1);
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, email: 'exists@example.com' }, 'Error during createUser transaction');
expect(mockLogger.warn).toHaveBeenCalledWith(`Attempted to create a user with an existing email: exists@example.com`);
});
});
@@ -194,7 +205,7 @@ describe('User DB Service', () => {
it('should throw NotFoundError if user is not found', async () => {
mockPoolInstance.query.mockResolvedValue({ rows: [] });
await expect(userRepo.findUserById('not-found-id', mockLogger)).rejects.toThrow(NotFoundError);
await expect(userRepo.findUserById('not-found-id', mockLogger)).rejects.toThrow('User with ID not-found-id not found.');
});
it('should throw a generic error if the database query fails', async () => {