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
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:
64
src/services/db/deals.db.test.ts
Normal file
64
src/services/db/deals.db.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
117
src/services/db/errors.db.test.ts
Normal file
117
src/services/db/errors.db.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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([]);
|
||||
});
|
||||
|
||||
|
||||
@@ -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)]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user