Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 45s
754 lines
30 KiB
TypeScript
754 lines
30 KiB
TypeScript
// src/services/db/personalization.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 { PersonalizationRepository } from './personalization.db';
|
|
import type { MasterGroceryItem, UserAppliance, DietaryRestriction, Appliance } from '../../types';
|
|
import { createMockMasterGroceryItem, createMockUserAppliance } from '../../tests/utils/mockFactories';
|
|
|
|
// Un-mock the module we are testing to ensure we use the real implementation.
|
|
vi.unmock('./personalization.db');
|
|
|
|
const mockQuery = mockPoolInstance.query;
|
|
|
|
// Mock the withTransaction helper
|
|
vi.mock('./connection.db', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('./connection.db')>();
|
|
return { ...actual, withTransaction: vi.fn() };
|
|
});
|
|
|
|
import { ForeignKeyConstraintError } from './errors.db';
|
|
// Mock the logger to prevent console output during tests. This is a server-side DB test.
|
|
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('Personalization DB Service', () => {
|
|
let personalizationRepo: PersonalizationRepository;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
// Reset the withTransaction mock before each test
|
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
|
const mockClient = { query: vi.fn() };
|
|
return callback(mockClient as unknown as PoolClient);
|
|
});
|
|
// Instantiate the repository with the mock pool for each test
|
|
personalizationRepo = new PersonalizationRepository(mockPoolInstance as unknown as Pool);
|
|
});
|
|
|
|
describe('getAllMasterItems', () => {
|
|
it('should execute the correct query and return master items', async () => {
|
|
const mockItems: MasterGroceryItem[] = [
|
|
createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Apples' }),
|
|
];
|
|
mockQuery.mockResolvedValue({ rows: mockItems });
|
|
|
|
const result = await personalizationRepo.getAllMasterItems(mockLogger);
|
|
|
|
const expectedQuery = `
|
|
SELECT
|
|
mgi.*,
|
|
c.name as category_name
|
|
FROM public.master_grocery_items mgi
|
|
LEFT JOIN public.categories c ON mgi.category_id = c.category_id
|
|
ORDER BY mgi.name ASC`;
|
|
|
|
// The query string in the implementation has a lot of whitespace from the template literal.
|
|
// This updated expectation matches the new query exactly.
|
|
expect(mockQuery).toHaveBeenCalledWith(expectedQuery);
|
|
expect(result).toEqual(mockItems);
|
|
});
|
|
|
|
it('should return an empty array if no master items exist', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [] });
|
|
const result = await personalizationRepo.getAllMasterItems(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.getAllMasterItems(mockLogger)).rejects.toThrow(
|
|
'Failed to retrieve master grocery items.',
|
|
);
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ err: dbError },
|
|
'Database error in getAllMasterItems',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('getWatchedItems', () => {
|
|
it('should execute the correct query and return watched items', async () => {
|
|
const mockItems: MasterGroceryItem[] = [
|
|
createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Apples' }),
|
|
];
|
|
mockQuery.mockResolvedValue({ rows: mockItems });
|
|
|
|
const result = await personalizationRepo.getWatchedItems('user-123', mockLogger);
|
|
|
|
expect(mockQuery).toHaveBeenCalledWith(
|
|
expect.stringContaining('FROM public.master_grocery_items mgi'),
|
|
['user-123'],
|
|
);
|
|
expect(result).toEqual(mockItems);
|
|
});
|
|
|
|
it('should return an empty array if the user has no watched items', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [] });
|
|
const result = await personalizationRepo.getWatchedItems('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.getWatchedItems('user-123', mockLogger)).rejects.toThrow(
|
|
'Failed to retrieve watched items.',
|
|
);
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ err: dbError, userId: 'user-123' },
|
|
'Database error in getWatchedItems',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('addWatchedItem', () => {
|
|
it('should execute a transaction to add a watched item', async () => {
|
|
const mockClientQuery = vi.fn();
|
|
const mockItem = createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'New Item' });
|
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
|
const mockClient = { query: mockClientQuery };
|
|
mockClientQuery
|
|
.mockResolvedValueOnce({ rows: [{ category_id: 1 }] }) // Find category
|
|
.mockResolvedValueOnce({ rows: [mockItem] }) // Find master item
|
|
.mockResolvedValueOnce({ rows: [] }); // Insert into watchlist
|
|
return callback(mockClient as unknown as PoolClient);
|
|
});
|
|
|
|
await personalizationRepo.addWatchedItem('user-123', 'New Item', 'Produce', mockLogger);
|
|
|
|
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 = createMockMasterGroceryItem({
|
|
master_grocery_item_id: 2,
|
|
name: 'Brand New Item',
|
|
});
|
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
|
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
|
|
.mockResolvedValueOnce({ rows: [] }); // Insert into watchlist
|
|
return callback(mockClient as unknown as PoolClient);
|
|
});
|
|
|
|
const result = await personalizationRepo.addWatchedItem(
|
|
'user-123',
|
|
'Brand New Item',
|
|
'Produce',
|
|
mockLogger,
|
|
);
|
|
|
|
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 (ON CONFLICT DO NOTHING)', async () => {
|
|
const mockClientQuery = vi.fn();
|
|
const mockExistingItem = createMockMasterGroceryItem({
|
|
master_grocery_item_id: 1,
|
|
name: 'Existing Item',
|
|
});
|
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
|
const mockClient = { query: mockClientQuery };
|
|
mockClientQuery
|
|
.mockResolvedValueOnce({ rows: [{ category_id: 1 }] }) // Find category
|
|
.mockResolvedValueOnce({ rows: [mockExistingItem] }) // Find master item
|
|
.mockResolvedValueOnce({ rows: [], rowCount: 0 }); // INSERT...ON CONFLICT DO NOTHING
|
|
return callback(mockClient as unknown as PoolClient);
|
|
});
|
|
|
|
// The function should resolve successfully without throwing an error.
|
|
await expect(
|
|
personalizationRepo.addWatchedItem('user-123', 'Existing Item', 'Produce', mockLogger),
|
|
).resolves.toEqual(mockExistingItem);
|
|
expect(mockClientQuery).toHaveBeenCalledWith(
|
|
expect.stringContaining('INSERT INTO public.user_watched_items'),
|
|
['user-123', 1],
|
|
);
|
|
});
|
|
|
|
it('should throw an error if the category is not found', async () => {
|
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
|
const mockClient = { query: vi.fn().mockResolvedValue({ rows: [] }) };
|
|
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(
|
|
"Category 'Fake Category' not found.",
|
|
);
|
|
throw new Error("Category 'Fake Category' not found.");
|
|
});
|
|
|
|
await expect(
|
|
personalizationRepo.addWatchedItem('user-123', 'Some Item', 'Fake Category', mockLogger),
|
|
).rejects.toThrow('Failed to add item to watchlist.');
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{
|
|
err: expect.any(Error),
|
|
userId: 'user-123',
|
|
itemName: 'Some Item',
|
|
categoryName: 'Fake Category',
|
|
},
|
|
'Transaction error in addWatchedItem',
|
|
);
|
|
});
|
|
|
|
it('should throw a generic error on failure', async () => {
|
|
const dbError = new Error('DB Error');
|
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
|
const mockClient = { query: vi.fn() };
|
|
mockClient.query
|
|
.mockResolvedValueOnce({ rows: [{ category_id: 1 }] })
|
|
.mockRejectedValueOnce(dbError);
|
|
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(dbError);
|
|
throw dbError;
|
|
});
|
|
|
|
await expect(
|
|
personalizationRepo.addWatchedItem('user-123', 'Failing Item', 'Produce', mockLogger),
|
|
).rejects.toThrow('Failed to add item to watchlist.');
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ err: dbError, userId: 'user-123', itemName: 'Failing Item', categoryName: 'Produce' },
|
|
'Transaction error in addWatchedItem',
|
|
);
|
|
});
|
|
|
|
it('should throw ForeignKeyConstraintError on invalid user or category', async () => {
|
|
const dbError = new Error('violates foreign key constraint');
|
|
(dbError as Error & { code: string }).code = '23503';
|
|
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.');
|
|
});
|
|
});
|
|
|
|
describe('getDietaryRestrictions', () => {
|
|
it('should throw an error if the database query fails', async () => {
|
|
const dbError = new Error('DB Error');
|
|
mockQuery.mockRejectedValue(dbError);
|
|
await expect(personalizationRepo.getDietaryRestrictions(mockLogger)).rejects.toThrow(
|
|
'Failed to get dietary restrictions.',
|
|
);
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ err: dbError },
|
|
'Database error in getDietaryRestrictions',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('removeWatchedItem', () => {
|
|
it('should execute a DELETE query', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [] });
|
|
await personalizationRepo.removeWatchedItem('user-123', 1, mockLogger);
|
|
expect(mockQuery).toHaveBeenCalledWith(
|
|
'DELETE FROM public.user_watched_items WHERE user_id = $1 AND master_item_id = $2',
|
|
['user-123', 1],
|
|
);
|
|
});
|
|
|
|
it('should complete without error if the item to remove is not in the watchlist', async () => {
|
|
// Simulate the DB returning 0 rows affected
|
|
mockQuery.mockResolvedValue({ rowCount: 0 });
|
|
await expect(
|
|
personalizationRepo.removeWatchedItem('user-123', 999, mockLogger),
|
|
).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('should throw an error if the database query fails', async () => {
|
|
const dbError = new Error('DB Error');
|
|
mockQuery.mockRejectedValue(dbError);
|
|
await expect(
|
|
personalizationRepo.removeWatchedItem('user-123', 1, mockLogger),
|
|
).rejects.toThrow('Failed to remove item from watchlist.');
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ err: dbError, userId: 'user-123', masterItemId: 1 },
|
|
'Database error in removeWatchedItem',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('findRecipesFromPantry', () => {
|
|
it('should call the correct database function', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [] });
|
|
await personalizationRepo.findRecipesFromPantry('user-123', mockLogger);
|
|
expect(mockQuery).toHaveBeenCalledWith('SELECT * FROM public.find_recipes_from_pantry($1)', [
|
|
'user-123',
|
|
]);
|
|
});
|
|
|
|
it('should return an empty array if no recipes are found', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [] });
|
|
const result = await personalizationRepo.findRecipesFromPantry('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.findRecipesFromPantry('user-123', mockLogger),
|
|
).rejects.toThrow('Failed to find recipes from pantry.');
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ err: dbError, userId: 'user-123' },
|
|
'Database error in findRecipesFromPantry',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('recommendRecipesForUser', () => {
|
|
it('should call the correct database function', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [] });
|
|
await personalizationRepo.recommendRecipesForUser('user-123', 5, mockLogger);
|
|
expect(mockQuery).toHaveBeenCalledWith(
|
|
'SELECT * FROM public.recommend_recipes_for_user($1, $2)',
|
|
['user-123', 5],
|
|
);
|
|
});
|
|
|
|
it('should return an empty array if no recipes are recommended', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [] });
|
|
const result = await personalizationRepo.recommendRecipesForUser('user-123', 5, 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.recommendRecipesForUser('user-123', 5, mockLogger),
|
|
).rejects.toThrow('Failed to recommend recipes.');
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ err: dbError, userId: 'user-123', limit: 5 },
|
|
'Database error in recommendRecipesForUser',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('getBestSalePricesForUser', () => {
|
|
it('should call the correct database function', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [] });
|
|
await personalizationRepo.getBestSalePricesForUser('user-123', mockLogger);
|
|
expect(mockQuery).toHaveBeenCalledWith(
|
|
'SELECT * FROM public.get_best_sale_prices_for_user($1)',
|
|
['user-123'],
|
|
);
|
|
});
|
|
|
|
it('should return an empty array if no deals are found for the user', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [] });
|
|
const result = await personalizationRepo.getBestSalePricesForUser('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.getBestSalePricesForUser('user-123', mockLogger),
|
|
).rejects.toThrow('Failed to get best sale prices.');
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ err: dbError, userId: 'user-123' },
|
|
'Database error in getBestSalePricesForUser',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('getBestSalePricesForAllUsers', () => {
|
|
it('should call the correct database function', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [] });
|
|
await personalizationRepo.getBestSalePricesForAllUsers(mockLogger);
|
|
expect(mockQuery).toHaveBeenCalledWith(
|
|
'SELECT * FROM public.get_best_sale_prices_for_all_users()',
|
|
);
|
|
});
|
|
|
|
it('should return an empty array if no deals are found for any user', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [] });
|
|
const result = await personalizationRepo.getBestSalePricesForAllUsers(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.getBestSalePricesForAllUsers(mockLogger)).rejects.toThrow(
|
|
'Failed to get best sale prices for all users.',
|
|
);
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ err: dbError },
|
|
'Database error in getBestSalePricesForAllUsers',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('suggestPantryItemConversions', () => {
|
|
it('should call the correct database function', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [] });
|
|
await personalizationRepo.suggestPantryItemConversions(1, mockLogger);
|
|
expect(mockQuery).toHaveBeenCalledWith(
|
|
'SELECT * FROM public.suggest_pantry_item_conversions($1)',
|
|
[1],
|
|
);
|
|
});
|
|
|
|
it('should return an empty array if no conversions are suggested', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [] });
|
|
const result = await personalizationRepo.suggestPantryItemConversions(1, 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.suggestPantryItemConversions(1, mockLogger)).rejects.toThrow(
|
|
'Failed to suggest pantry item conversions.',
|
|
);
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ err: dbError, pantryItemId: 1 },
|
|
'Database error in suggestPantryItemConversions',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('getUserDietaryRestrictions', () => {
|
|
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',
|
|
);
|
|
});
|
|
|
|
it('should return an empty array if user has no restrictions', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [] });
|
|
const result = await personalizationRepo.getUserDietaryRestrictions('user-123', mockLogger);
|
|
expect(result).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('findPantryItemOwner', () => {
|
|
it('should execute a SELECT query to find the owner', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [{ user_id: 'user-123' }] });
|
|
const result = await personalizationRepo.findPantryItemOwner(1, mockLogger);
|
|
expect(mockQuery).toHaveBeenCalledWith(
|
|
'SELECT user_id FROM public.pantry_items WHERE pantry_item_id = $1',
|
|
[1],
|
|
);
|
|
expect(result?.user_id).toBe('user-123');
|
|
});
|
|
|
|
it('should return undefined if the pantry item is not found', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [] });
|
|
const result = await personalizationRepo.findPantryItemOwner(999, mockLogger);
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it('should throw an error if the database query fails', async () => {
|
|
const dbError = new Error('DB Error');
|
|
mockQuery.mockRejectedValue(dbError);
|
|
await expect(personalizationRepo.findPantryItemOwner(1, mockLogger)).rejects.toThrow(
|
|
'Failed to retrieve pantry item owner from database.',
|
|
);
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ err: dbError, pantryItemId: 1 },
|
|
'Database error in findPantryItemOwner',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('getDietaryRestrictions', () => {
|
|
it('should execute a SELECT query to get all restrictions', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [] as DietaryRestriction[] });
|
|
await personalizationRepo.getDietaryRestrictions(mockLogger);
|
|
expect(mockQuery).toHaveBeenCalledWith(
|
|
'SELECT * FROM public.dietary_restrictions ORDER BY type, name',
|
|
);
|
|
});
|
|
|
|
it('should return an empty array if no restrictions exist', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [] });
|
|
const result = await personalizationRepo.getDietaryRestrictions(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.getDietaryRestrictions(mockLogger)).rejects.toThrow(
|
|
'Failed to get dietary restrictions.',
|
|
);
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ err: dbError },
|
|
'Database error in getDietaryRestrictions',
|
|
);
|
|
});
|
|
});
|
|
|
|
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: mockClientQuery };
|
|
return callback(mockClient as unknown as PoolClient);
|
|
});
|
|
|
|
await personalizationRepo.setUserDietaryRestrictions('user-123', [1, 2], mockLogger);
|
|
|
|
expect(withTransaction).toHaveBeenCalledTimes(1);
|
|
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 () => {
|
|
const dbError = new Error('violates foreign key constraint');
|
|
(dbError as Error & { code: string }).code = '23503';
|
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
|
const mockClient = { query: vi.fn() };
|
|
mockClient.query.mockResolvedValueOnce({ rows: [] }).mockRejectedValueOnce(dbError); // DELETE ok, INSERT fail
|
|
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(dbError);
|
|
throw dbError;
|
|
});
|
|
|
|
await expect(
|
|
personalizationRepo.setUserDietaryRestrictions('user-123', [999], mockLogger),
|
|
).rejects.toThrow('One or more of the specified restriction IDs are invalid.');
|
|
});
|
|
|
|
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: mockClientQuery };
|
|
return callback(mockClient as unknown as PoolClient);
|
|
});
|
|
|
|
await personalizationRepo.setUserDietaryRestrictions('user-123', [], mockLogger);
|
|
|
|
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 () => {
|
|
vi.mocked(withTransaction).mockRejectedValue(new Error('DB Error'));
|
|
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 () => {
|
|
mockQuery.mockResolvedValue({ rows: [] as Appliance[] });
|
|
await personalizationRepo.getAppliances(mockLogger);
|
|
expect(mockQuery).toHaveBeenCalledWith('SELECT * FROM public.appliances ORDER BY name');
|
|
});
|
|
|
|
it('should return an empty array if no appliances exist', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [] });
|
|
const result = await personalizationRepo.getAppliances(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.getAppliances(mockLogger)).rejects.toThrow(
|
|
'Failed to get appliances.',
|
|
);
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ err: dbError },
|
|
'Database error in getAppliances',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('getUserAppliances', () => {
|
|
it('should execute a SELECT query with a JOIN', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [] as Appliance[] });
|
|
await personalizationRepo.getUserAppliances('user-123', mockLogger);
|
|
expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('FROM public.appliances a'), [
|
|
'user-123',
|
|
]);
|
|
});
|
|
|
|
it('should return an empty array if the user has no appliances', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [] as Appliance[] });
|
|
const result = await personalizationRepo.getUserAppliances('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.getUserAppliances('user-123', mockLogger)).rejects.toThrow(
|
|
'Failed to get user appliances.',
|
|
);
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ err: dbError, userId: 'user-123' },
|
|
'Database error in getUserAppliances',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('setUserAppliances', () => {
|
|
it('should execute a transaction to set appliances', async () => {
|
|
const mockNewAppliances: UserAppliance[] = [
|
|
createMockUserAppliance({ user_id: 'user-123', appliance_id: 1 }),
|
|
createMockUserAppliance({ user_id: 'user-123', appliance_id: 2 }),
|
|
];
|
|
const mockClientQuery = vi.fn();
|
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
|
const mockClient = { query: mockClientQuery };
|
|
mockClientQuery
|
|
.mockResolvedValueOnce({ rows: [] }) // DELETE
|
|
.mockResolvedValueOnce({ rows: mockNewAppliances }); // INSERT
|
|
return callback(mockClient as unknown as PoolClient);
|
|
});
|
|
|
|
const result = await personalizationRepo.setUserAppliances('user-123', [1, 2], mockLogger);
|
|
|
|
expect(withTransaction).toHaveBeenCalledTimes(1);
|
|
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);
|
|
});
|
|
|
|
it('should throw ForeignKeyConstraintError if an appliance ID is invalid', async () => {
|
|
const dbError = new Error('violates foreign key constraint');
|
|
(dbError as Error & { code: string }).code = '23503';
|
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
|
const mockClient = { query: vi.fn() };
|
|
mockClient.query.mockResolvedValueOnce({ rows: [] }).mockRejectedValueOnce(dbError); // DELETE ok, INSERT fail
|
|
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(dbError);
|
|
throw dbError;
|
|
});
|
|
|
|
await expect(
|
|
personalizationRepo.setUserAppliances('user-123', [999], mockLogger),
|
|
).rejects.toThrow(ForeignKeyConstraintError);
|
|
});
|
|
|
|
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: mockClientQuery };
|
|
return callback(mockClient as unknown as PoolClient);
|
|
});
|
|
|
|
const result = await personalizationRepo.setUserAppliances('user-123', [], mockLogger);
|
|
|
|
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(mockClientQuery).not.toHaveBeenCalledWith(expect.stringContaining('INSERT INTO'));
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('should rollback transaction on generic error', async () => {
|
|
const dbError = new Error('DB Error');
|
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
|
const mockClient = { query: vi.fn().mockRejectedValue(dbError) };
|
|
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(dbError);
|
|
throw dbError;
|
|
});
|
|
|
|
await expect(
|
|
personalizationRepo.setUserAppliances('user-123', [1], mockLogger),
|
|
).rejects.toThrow('Failed to set user appliances.');
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ err: dbError, userId: 'user-123', applianceIds: [1] },
|
|
'Database error in setUserAppliances',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('getRecipesForUserDiets', () => {
|
|
it('should call the correct database function', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [] });
|
|
await personalizationRepo.getRecipesForUserDiets('user-123', mockLogger);
|
|
expect(mockQuery).toHaveBeenCalledWith(
|
|
'SELECT * FROM public.get_recipes_for_user_diets($1)',
|
|
['user-123'],
|
|
);
|
|
});
|
|
|
|
it('should return an empty array if no recipes match the diet', async () => {
|
|
mockQuery.mockResolvedValue({ rows: [] });
|
|
const result = await personalizationRepo.getRecipesForUserDiets('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.getRecipesForUserDiets('user-123', mockLogger),
|
|
).rejects.toThrow('Failed to get recipes compatible with user diet.');
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ err: dbError, userId: 'user-123' },
|
|
'Database error in getRecipesForUserDiets',
|
|
);
|
|
});
|
|
});
|
|
});
|