linting done now fix unit tests
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 8m19s

This commit is contained in:
2025-12-14 21:48:53 -08:00
parent 43ca7f9df2
commit 69e2287870
12 changed files with 106 additions and 57 deletions

View File

@@ -5,7 +5,7 @@ import type { Pool } from 'pg';
import { RecipeRepository } from './recipe.db';
// Un-mock the module we are testing to ensure we use the real implementation.
vi.unmock('./recipe.db');
vi.unmock('./recipe.db'); // This line is correct.
const mockQuery = mockPoolInstance.query;
import type { Recipe, FavoriteRecipe, RecipeComment } from '../../types';
@@ -20,6 +20,7 @@ vi.mock('../logger.server', () => ({
},
}));
import { logger as mockLogger } from '../logger.server';
import { ForeignKeyConstraintError, NotFoundError, UniqueConstraintError } from './errors.db';
describe('Recipe DB Service', () => {
let recipeRepo: RecipeRepository;
@@ -119,8 +120,8 @@ describe('Recipe DB Service', () => {
describe('addFavoriteRecipe', () => {
it('should execute an INSERT query and return the new favorite', async () => {
const mockFavorite: FavoriteRecipe = { user_id: 'user-123', recipe_id: 1, created_at: new Date().toISOString() };
mockQuery.mockResolvedValue({ rows: [mockFavorite] });
const mockFavorite: FavoriteRecipe = { user_id: 'user-123', recipe_id: 1, created_at: new Date().toISOString() }; // This line is correct.
mockQuery.mockResolvedValue({ rows: [mockFavorite], rowCount: 1 });
const result = await recipeRepo.addFavoriteRecipe('user-123', 1, mockLogger);
@@ -136,11 +137,12 @@ describe('Recipe DB Service', () => {
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: 'user-123', recipeId: 999 }, 'Database error in addFavoriteRecipe');
});
it('should return undefined if the favorite already exists (ON CONFLICT)', async () => {
it('should throw UniqueConstraintError if the favorite already exists (ON CONFLICT)', async () => {
// When ON CONFLICT DO NOTHING happens, the RETURNING clause does not execute, so rows is empty.
mockQuery.mockResolvedValue({ rows: [] });
const result = await recipeRepo.addFavoriteRecipe('user-123', 1, mockLogger);
expect(result).toBeUndefined();
// The implementation now throws an error when rowCount is 0.
mockQuery.mockResolvedValue({ rows: [], rowCount: 0 });
await expect(recipeRepo.addFavoriteRecipe('user-123', 1, mockLogger))
.rejects.toThrow(UniqueConstraintError);
});
it('should throw a generic error if the database query fails', async () => {

View File

@@ -1,7 +1,7 @@
// src/services/db/recipe.db.ts
import type { Pool, PoolClient } from 'pg';
import { getPool } from './connection.db';
import { ForeignKeyConstraintError, NotFoundError } from './errors.db';
import { ForeignKeyConstraintError, NotFoundError, UniqueConstraintError } from './errors.db';
import type { Logger } from 'pino';
import type { Recipe, FavoriteRecipe, RecipeComment } from '../../types';
@@ -80,11 +80,16 @@ export class RecipeRepository {
* @returns A promise that resolves to the created favorite record.
*/
async addFavoriteRecipe(userId: string, recipeId: number, logger: Logger): Promise<FavoriteRecipe> {
// The ON CONFLICT DO NOTHING clause prevents duplicates but also causes the query to return
// zero rows if the favorite already exists. We need to handle this case.
try {
const res = await this.db.query<FavoriteRecipe>(
'INSERT INTO public.favorite_recipes (user_id, recipe_id) VALUES ($1, $2) ON CONFLICT (user_id, recipe_id) DO NOTHING RETURNING *',
[userId, recipeId]
);
if (res.rowCount === 0) {
throw new UniqueConstraintError('This recipe is already in the user\'s favorites.');
}
return res.rows[0];
} catch (error) {
if (error instanceof Error && 'code' in error && error.code === '23503') {

View File

@@ -195,13 +195,13 @@ describe('User DB Service', () => {
describe('findUserById', () => {
it('should query for a user by their ID', async () => {
mockPoolInstance.query.mockResolvedValue({ rows: [{ user_id: '123' }] });
mockPoolInstance.query.mockResolvedValue({ rows: [{ user_id: '123' }], rowCount: 1 });
await userRepo.findUserById('123', mockLogger);
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('FROM public.users WHERE user_id = $1'), ['123']);
});
it('should throw NotFoundError if user is not found', async () => {
mockPoolInstance.query.mockResolvedValue({ rows: [] });
mockPoolInstance.query.mockResolvedValue({ rows: [], rowCount: 0 });
await expect(userRepo.findUserById('not-found-id', mockLogger)).rejects.toThrow('User with ID not-found-id not found.');
});
@@ -215,11 +215,17 @@ describe('User DB Service', () => {
describe('findUserWithPasswordHashById', () => {
it('should query for a user and their password hash by ID', async () => {
mockPoolInstance.query.mockResolvedValue({ rows: [{ user_id: '123', password_hash: 'hash' }] });
mockPoolInstance.query.mockResolvedValue({ rows: [{ user_id: '123', password_hash: 'hash' }], rowCount: 1 });
await userRepo.findUserWithPasswordHashById('123', mockLogger);
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('SELECT user_id, email, password_hash'), ['123']);
});
it('should throw NotFoundError if user is not found', async () => {
mockPoolInstance.query.mockResolvedValue({ rows: [], rowCount: 0 });
await expect(userRepo.findUserWithPasswordHashById('not-found-id', mockLogger)).rejects.toThrow(NotFoundError);
await expect(userRepo.findUserWithPasswordHashById('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 () => {
const dbError = new Error('DB Connection Error');
mockPoolInstance.query.mockRejectedValue(dbError);
@@ -241,11 +247,6 @@ describe('User DB Service', () => {
await expect(userRepo.findUserProfileById('not-found-id', mockLogger)).rejects.toThrow('Profile not found for this user.');
});
it('should return undefined if user is not found', async () => {
mockPoolInstance.query.mockResolvedValue({ rows: [] });
const result = await userRepo.findUserWithPasswordHashById('not-found-id', mockLogger);
expect(result).toBeUndefined();
});
it('should throw a generic error if the database query fails', async () => {
const dbError = new Error('DB Connection Error');
mockPoolInstance.query.mockRejectedValue(dbError);

View File

@@ -172,12 +172,15 @@ export class UserRepository {
* @returns A promise that resolves to the user object (id, email, password_hash) or undefined if not found.
*/
// prettier-ignore
async findUserWithPasswordHashById(userId: string, logger: Logger): Promise<{ user_id: string; email: string; password_hash: string | null } | undefined> {
async findUserWithPasswordHashById(userId: string, logger: Logger): Promise<{ user_id: string; email: string; password_hash: string | null }> {
try {
const res = await this.db.query<{ user_id: string; email: string; password_hash: string | null }>(
'SELECT user_id, email, password_hash FROM public.users WHERE user_id = $1',
[userId]
);
if (res.rowCount === 0) {
throw new NotFoundError(`User with ID ${userId} not found.`);
}
return res.rows[0];
} catch (error) {
logger.error({ err: error, userId }, 'Database error in findUserWithPasswordHashById');