DB refactor for easier testsing
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 5m16s
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 5m16s
App.ts refactor into hooks unit tests
This commit is contained in:
@@ -1,18 +1,7 @@
|
||||
// src/services/db/recipe.db.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { mockPoolInstance } from '../../tests/setup/mock-db';
|
||||
import {
|
||||
getRecipesBySalePercentage,
|
||||
getRecipesByMinSaleIngredients,
|
||||
findRecipesByIngredientAndTag,
|
||||
getUserFavoriteRecipes,
|
||||
addFavoriteRecipe,
|
||||
removeFavoriteRecipe,
|
||||
getRecipeById,
|
||||
getRecipeComments,
|
||||
addRecipeComment,
|
||||
forkRecipe,
|
||||
} from './recipe.db';
|
||||
import { RecipeRepository } from './recipe.db';
|
||||
|
||||
// Un-mock the module we are testing to ensure we use the real implementation.
|
||||
vi.unmock('./recipe.db');
|
||||
@@ -20,6 +9,7 @@ vi.unmock('./recipe.db');
|
||||
const mockQuery = mockPoolInstance.query;
|
||||
import type { Recipe, FavoriteRecipe, RecipeComment } from '../../types';
|
||||
|
||||
import { ForeignKeyConstraintError } from './errors.db';
|
||||
// Mock the logger to prevent console output during tests
|
||||
vi.mock('../logger', () => ({
|
||||
logger: {
|
||||
@@ -31,63 +21,76 @@ vi.mock('../logger', () => ({
|
||||
}));
|
||||
|
||||
describe('Recipe DB Service', () => {
|
||||
let recipeRepo: RecipeRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Instantiate the repository with the mock pool for each test
|
||||
recipeRepo = new RecipeRepository(mockPoolInstance as any);
|
||||
});
|
||||
|
||||
describe('getRecipesBySalePercentage', () => {
|
||||
it('should call the correct database function', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await getRecipesBySalePercentage(50);
|
||||
await recipeRepo.getRecipesBySalePercentage(50);
|
||||
expect(mockQuery).toHaveBeenCalledWith('SELECT * FROM public.get_recipes_by_sale_percentage($1)', [50]);
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
const dbError = new Error('DB Connection Error');
|
||||
mockQuery.mockRejectedValue(dbError);
|
||||
await expect(getRecipesBySalePercentage(50)).rejects.toThrow('Failed to get recipes by sale percentage.');
|
||||
await expect(recipeRepo.getRecipesBySalePercentage(50)).rejects.toThrow('Failed to get recipes by sale percentage.');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('getRecipesByMinSaleIngredients', () => {
|
||||
it('should call the correct database function', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await getRecipesByMinSaleIngredients(3);
|
||||
await recipeRepo.getRecipesByMinSaleIngredients(3);
|
||||
expect(mockQuery).toHaveBeenCalledWith('SELECT * FROM public.get_recipes_by_min_sale_ingredients($1)', [3]);
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
const dbError = new Error('DB Connection Error');
|
||||
mockQuery.mockRejectedValue(dbError);
|
||||
await expect(getRecipesByMinSaleIngredients(3)).rejects.toThrow('Failed to get recipes by minimum sale ingredients.');
|
||||
await expect(recipeRepo.getRecipesByMinSaleIngredients(3)).rejects.toThrow('Failed to get recipes by minimum sale ingredients.');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('findRecipesByIngredientAndTag', () => {
|
||||
it('should call the correct database function', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await findRecipesByIngredientAndTag('chicken', 'quick');
|
||||
await recipeRepo.findRecipesByIngredientAndTag('chicken', 'quick');
|
||||
expect(mockQuery).toHaveBeenCalledWith('SELECT * FROM public.find_recipes_by_ingredient_and_tag($1, $2)', ['chicken', 'quick']);
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
const dbError = new Error('DB Connection Error');
|
||||
mockQuery.mockRejectedValue(dbError);
|
||||
await expect(findRecipesByIngredientAndTag('chicken', 'quick')).rejects.toThrow('Failed to find recipes by ingredient and tag.');
|
||||
await expect(recipeRepo.findRecipesByIngredientAndTag('chicken', 'quick')).rejects.toThrow('Failed to find recipes by ingredient and tag.');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('getUserFavoriteRecipes', () => {
|
||||
it('should call the correct database function', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await getUserFavoriteRecipes('user-123');
|
||||
await recipeRepo.getUserFavoriteRecipes('user-123');
|
||||
expect(mockQuery).toHaveBeenCalledWith('SELECT * FROM public.get_user_favorite_recipes($1)', ['user-123']);
|
||||
});
|
||||
|
||||
it('should return an empty array if user has no favorites', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
const result = await recipeRepo.getUserFavoriteRecipes('user-123');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
const dbError = new Error('DB Connection Error');
|
||||
mockQuery.mockRejectedValue(dbError);
|
||||
await expect(getUserFavoriteRecipes('user-123')).rejects.toThrow('Failed to get favorite recipes.');
|
||||
await expect(recipeRepo.getUserFavoriteRecipes('user-123')).rejects.toThrow('Failed to get favorite recipes.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,7 +99,7 @@ describe('Recipe DB Service', () => {
|
||||
const mockFavorite: FavoriteRecipe = { user_id: 'user-123', recipe_id: 1, created_at: new Date().toISOString() };
|
||||
mockQuery.mockResolvedValue({ rows: [mockFavorite] });
|
||||
|
||||
const result = await addFavoriteRecipe('user-123', 1);
|
||||
const result = await recipeRepo.addFavoriteRecipe('user-123', 1);
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.favorite_recipes'), ['user-123', 1]);
|
||||
expect(result).toEqual(mockFavorite);
|
||||
@@ -106,40 +109,40 @@ describe('Recipe DB Service', () => {
|
||||
const dbError = new Error('violates foreign key constraint');
|
||||
(dbError as any).code = '23503';
|
||||
mockQuery.mockRejectedValue(dbError);
|
||||
await expect(addFavoriteRecipe('user-123', 999)).rejects.toThrow('The specified user or recipe does not exist.');
|
||||
await expect(recipeRepo.addFavoriteRecipe('user-123', 999)).rejects.toThrow('The specified user or recipe does not exist.');
|
||||
});
|
||||
|
||||
it('should return undefined 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 addFavoriteRecipe('user-123', 1);
|
||||
const result = await recipeRepo.addFavoriteRecipe('user-123', 1);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
const dbError = new Error('DB Connection Error');
|
||||
mockQuery.mockRejectedValue(dbError);
|
||||
await expect(addFavoriteRecipe('user-123', 1)).rejects.toThrow('Failed to add favorite recipe.');
|
||||
await expect(recipeRepo.addFavoriteRecipe('user-123', 1)).rejects.toThrow('Failed to add favorite recipe.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeFavoriteRecipe', () => {
|
||||
it('should execute a DELETE query', async () => {
|
||||
mockQuery.mockResolvedValue({ rowCount: 1 });
|
||||
await removeFavoriteRecipe('user-123', 1);
|
||||
await recipeRepo.removeFavoriteRecipe('user-123', 1);
|
||||
expect(mockQuery).toHaveBeenCalledWith('DELETE FROM public.favorite_recipes WHERE user_id = $1 AND recipe_id = $2', ['user-123', 1]);
|
||||
});
|
||||
|
||||
it('should throw an error if the favorite recipe is not found', async () => {
|
||||
// Simulate the DB returning 0 rows affected
|
||||
mockQuery.mockResolvedValue({ rowCount: 0 });
|
||||
await expect(removeFavoriteRecipe('user-123', 999)).rejects.toThrow('Favorite recipe not found for this user.');
|
||||
await expect(recipeRepo.removeFavoriteRecipe('user-123', 999)).rejects.toThrow('Favorite recipe not found for this user.');
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
const dbError = new Error('DB Connection Error');
|
||||
mockQuery.mockRejectedValue(dbError);
|
||||
await expect(removeFavoriteRecipe('user-123', 1)).rejects.toThrow('Failed to remove favorite recipe.');
|
||||
await expect(recipeRepo.removeFavoriteRecipe('user-123', 1)).rejects.toThrow('Failed to remove favorite recipe.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -147,29 +150,41 @@ describe('Recipe DB Service', () => {
|
||||
it('should execute a SELECT query and return the recipe', async () => {
|
||||
const mockRecipe: Recipe = { recipe_id: 1, name: 'Test Recipe', avg_rating: 0, rating_count: 0, fork_count: 0, status: 'public', created_at: '' };
|
||||
mockQuery.mockResolvedValue({ rows: [mockRecipe] });
|
||||
const result = await getRecipeById(1);
|
||||
const result = await recipeRepo.getRecipeById(1);
|
||||
expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('FROM public.recipes r'), [1]);
|
||||
expect(result).toEqual(mockRecipe);
|
||||
});
|
||||
|
||||
it('should return undefined if recipe is not found', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
const result = await recipeRepo.getRecipeById(999);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
const dbError = new Error('DB Connection Error');
|
||||
mockQuery.mockRejectedValue(dbError);
|
||||
await expect(getRecipeById(1)).rejects.toThrow('Failed to retrieve recipe.');
|
||||
await expect(recipeRepo.getRecipeById(1)).rejects.toThrow('Failed to retrieve recipe.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRecipeComments', () => {
|
||||
it('should execute a SELECT query with a JOIN', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
await getRecipeComments(1);
|
||||
await recipeRepo.getRecipeComments(1);
|
||||
expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('FROM public.recipe_comments rc'), [1]);
|
||||
});
|
||||
|
||||
it('should return an empty array if recipe has no comments', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
const result = await recipeRepo.getRecipeComments(1);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
const dbError = new Error('DB Connection Error');
|
||||
mockQuery.mockRejectedValue(dbError);
|
||||
await expect(getRecipeComments(1)).rejects.toThrow('Failed to get recipe comments.');
|
||||
await expect(recipeRepo.getRecipeComments(1)).rejects.toThrow('Failed to get recipe comments.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -178,7 +193,7 @@ describe('Recipe DB Service', () => {
|
||||
const mockComment: RecipeComment = { recipe_comment_id: 1, recipe_id: 1, user_id: 'user-123', content: 'Great!', status: 'visible', created_at: new Date().toISOString() };
|
||||
mockQuery.mockResolvedValue({ rows: [mockComment] });
|
||||
|
||||
const result = await addRecipeComment(1, 'user-123', 'Great!');
|
||||
const result = await recipeRepo.addRecipeComment(1, 'user-123', 'Great!');
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.recipe_comments'), [1, 'user-123', 'Great!', undefined]);
|
||||
expect(result).toEqual(mockComment);
|
||||
@@ -188,13 +203,13 @@ describe('Recipe DB Service', () => {
|
||||
const dbError = new Error('violates foreign key constraint');
|
||||
(dbError as any).code = '23503';
|
||||
mockQuery.mockRejectedValue(dbError);
|
||||
await expect(addRecipeComment(999, 'user-123', 'Fail')).rejects.toThrow('The specified recipe, user, or parent comment does not exist.');
|
||||
await expect(recipeRepo.addRecipeComment(999, 'user-123', 'Fail')).rejects.toThrow('The specified recipe, user, or parent comment does not exist.');
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
const dbError = new Error('DB Connection Error');
|
||||
mockQuery.mockRejectedValue(dbError);
|
||||
await expect(addRecipeComment(1, 'user-123', 'Fail')).rejects.toThrow('Failed to add recipe comment.');
|
||||
await expect(recipeRepo.addRecipeComment(1, 'user-123', 'Fail')).rejects.toThrow('Failed to add recipe comment.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -203,15 +218,23 @@ describe('Recipe DB Service', () => {
|
||||
const mockRecipe: Recipe = { recipe_id: 2, name: 'Forked Recipe', avg_rating: 0, rating_count: 0, fork_count: 0, status: 'private', created_at: new Date().toISOString() };
|
||||
mockQuery.mockResolvedValue({ rows: [mockRecipe] });
|
||||
|
||||
const result = await forkRecipe('user-123', 1);
|
||||
const result = await recipeRepo.forkRecipe('user-123', 1);
|
||||
expect(mockQuery).toHaveBeenCalledWith('SELECT * FROM public.fork_recipe($1, $2)', ['user-123', 1]);
|
||||
expect(result).toEqual(mockRecipe);
|
||||
});
|
||||
|
||||
it('should re-throw the specific error message from the database function', async () => {
|
||||
const dbError = new Error('Recipe is not public and cannot be forked.');
|
||||
(dbError as any).code = 'P0001'; // raise_exception
|
||||
mockQuery.mockRejectedValue(dbError);
|
||||
|
||||
await expect(recipeRepo.forkRecipe('user-123', 1)).rejects.toThrow('Recipe is not public and cannot be forked.');
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
const dbError = new Error('DB Connection Error');
|
||||
mockQuery.mockRejectedValue(dbError);
|
||||
await expect(forkRecipe('user-123', 1)).rejects.toThrow('Failed to fork recipe.');
|
||||
await expect(recipeRepo.forkRecipe('user-123', 1)).rejects.toThrow('Failed to fork recipe.');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user