233 lines
9.6 KiB
TypeScript
233 lines
9.6 KiB
TypeScript
// src/routes/recipe.routes.test.ts
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import supertest from 'supertest';
|
|
import { createMockRecipe, createMockRecipeComment } from '../tests/utils/mockFactories';
|
|
import { NotFoundError } from '../services/db/errors.db';
|
|
import { createTestApp } from '../tests/utils/createTestApp';
|
|
|
|
// 1. Mock the Service Layer directly.
|
|
vi.mock('../services/db/index.db', () => ({
|
|
recipeRepo: {
|
|
getRecipesBySalePercentage: vi.fn(),
|
|
getRecipesByMinSaleIngredients: vi.fn(),
|
|
getRecipeById: vi.fn(),
|
|
findRecipesByIngredientAndTag: vi.fn(),
|
|
getRecipeComments: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
// Import the router and mocked DB AFTER all mocks are defined.
|
|
import recipeRouter from './recipe.routes';
|
|
import * as db from '../services/db/index.db';
|
|
import { mockLogger } from '../tests/utils/mockLogger';
|
|
|
|
// Mock the logger to keep test output clean
|
|
vi.mock('../services/logger.server', async () => ({
|
|
// Use async import to avoid hoisting issues with mockLogger
|
|
logger: (await import('../tests/utils/mockLogger')).mockLogger,
|
|
}));
|
|
|
|
// Import the mocked db module to control its functions in tests
|
|
const expectLogger = expect.objectContaining({
|
|
info: expect.any(Function),
|
|
error: expect.any(Function),
|
|
});
|
|
|
|
describe('Recipe Routes (/api/recipes)', () => {
|
|
const app = createTestApp({ router: recipeRouter, basePath: '/api/recipes' });
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('GET /by-sale-percentage', () => {
|
|
it('should return recipes based on sale percentage', async () => {
|
|
const mockRecipes = [createMockRecipe({ recipe_id: 1, name: 'Pasta' })];
|
|
vi.mocked(db.recipeRepo.getRecipesBySalePercentage).mockResolvedValue(mockRecipes);
|
|
|
|
const response = await supertest(app).get('/api/recipes/by-sale-percentage?minPercentage=75');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual(mockRecipes);
|
|
expect(db.recipeRepo.getRecipesBySalePercentage).toHaveBeenCalledWith(75, expectLogger);
|
|
});
|
|
|
|
it('should use the default minPercentage of 50 when none is provided', async () => {
|
|
vi.mocked(db.recipeRepo.getRecipesBySalePercentage).mockResolvedValue([]);
|
|
await supertest(app).get('/api/recipes/by-sale-percentage');
|
|
expect(db.recipeRepo.getRecipesBySalePercentage).toHaveBeenCalledWith(50, expectLogger);
|
|
});
|
|
|
|
it('should return 500 if the database call fails', async () => {
|
|
const dbError = new Error('DB Error');
|
|
vi.mocked(db.recipeRepo.getRecipesBySalePercentage).mockRejectedValue(dbError);
|
|
const response = await supertest(app).get('/api/recipes/by-sale-percentage');
|
|
expect(response.status).toBe(500);
|
|
expect(response.body.message).toBe('DB Error');
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ error: dbError },
|
|
'Error fetching recipes in /api/recipes/by-sale-percentage:',
|
|
);
|
|
});
|
|
|
|
it('should return 400 for an invalid minPercentage', async () => {
|
|
const response = await supertest(app).get(
|
|
'/api/recipes/by-sale-percentage?minPercentage=101',
|
|
);
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.errors[0].message).toContain('Too big');
|
|
});
|
|
});
|
|
|
|
describe('GET /by-sale-ingredients', () => {
|
|
it('should return recipes with default minIngredients', async () => {
|
|
vi.mocked(db.recipeRepo.getRecipesByMinSaleIngredients).mockResolvedValue([]);
|
|
const response = await supertest(app).get('/api/recipes/by-sale-ingredients');
|
|
expect(response.status).toBe(200);
|
|
expect(db.recipeRepo.getRecipesByMinSaleIngredients).toHaveBeenCalledWith(3, expectLogger);
|
|
});
|
|
|
|
it('should use provided minIngredients query parameter', async () => {
|
|
vi.mocked(db.recipeRepo.getRecipesByMinSaleIngredients).mockResolvedValue([]);
|
|
await supertest(app).get('/api/recipes/by-sale-ingredients?minIngredients=5');
|
|
expect(db.recipeRepo.getRecipesByMinSaleIngredients).toHaveBeenCalledWith(5, expectLogger);
|
|
});
|
|
|
|
it('should return 500 if the database call fails', async () => {
|
|
const dbError = new Error('DB Error');
|
|
vi.mocked(db.recipeRepo.getRecipesByMinSaleIngredients).mockRejectedValue(dbError);
|
|
const response = await supertest(app).get('/api/recipes/by-sale-ingredients');
|
|
expect(response.status).toBe(500);
|
|
expect(response.body.message).toBe('DB Error');
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ error: dbError },
|
|
'Error fetching recipes in /api/recipes/by-sale-ingredients:',
|
|
);
|
|
});
|
|
|
|
it('should return 400 for an invalid minIngredients', async () => {
|
|
const response = await supertest(app).get(
|
|
'/api/recipes/by-sale-ingredients?minIngredients=abc',
|
|
);
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.errors[0].message).toContain('received NaN');
|
|
});
|
|
});
|
|
|
|
describe('GET /by-ingredient-and-tag', () => {
|
|
it('should return recipes for a given ingredient and tag', async () => {
|
|
const mockRecipes = [createMockRecipe({ recipe_id: 2, name: 'Chicken Tacos' })];
|
|
vi.mocked(db.recipeRepo.findRecipesByIngredientAndTag).mockResolvedValue(mockRecipes);
|
|
|
|
const response = await supertest(app).get(
|
|
'/api/recipes/by-ingredient-and-tag?ingredient=chicken&tag=quick',
|
|
);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual(mockRecipes);
|
|
});
|
|
|
|
it('should return 500 if the database call fails', async () => {
|
|
const dbError = new Error('DB Error');
|
|
vi.mocked(db.recipeRepo.findRecipesByIngredientAndTag).mockRejectedValue(dbError);
|
|
const response = await supertest(app).get(
|
|
'/api/recipes/by-ingredient-and-tag?ingredient=chicken&tag=quick',
|
|
);
|
|
expect(response.status).toBe(500);
|
|
expect(response.body.message).toBe('DB Error');
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ error: dbError },
|
|
'Error fetching recipes in /api/recipes/by-ingredient-and-tag:',
|
|
);
|
|
});
|
|
|
|
it('should return 400 if required query parameters are missing', async () => {
|
|
const response = await supertest(app).get(
|
|
'/api/recipes/by-ingredient-and-tag?ingredient=chicken',
|
|
);
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.errors[0].message).toBe('Query parameter "tag" is required.');
|
|
});
|
|
});
|
|
|
|
describe('GET /:recipeId/comments', () => {
|
|
it('should return comments for a specific recipe', async () => {
|
|
const mockComments = [createMockRecipeComment({ recipe_id: 1, content: 'Great recipe!' })];
|
|
vi.mocked(db.recipeRepo.getRecipeComments).mockResolvedValue(mockComments);
|
|
|
|
const response = await supertest(app).get('/api/recipes/1/comments');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual(mockComments);
|
|
expect(db.recipeRepo.getRecipeComments).toHaveBeenCalledWith(1, expectLogger);
|
|
});
|
|
|
|
it('should return an empty array if recipe has no comments', async () => {
|
|
vi.mocked(db.recipeRepo.getRecipeComments).mockResolvedValue([]);
|
|
const response = await supertest(app).get('/api/recipes/2/comments');
|
|
expect(response.body).toEqual([]);
|
|
});
|
|
|
|
it('should return 500 if the database call fails', async () => {
|
|
const dbError = new Error('DB Error');
|
|
vi.mocked(db.recipeRepo.getRecipeComments).mockRejectedValue(dbError);
|
|
const response = await supertest(app).get('/api/recipes/1/comments');
|
|
expect(response.status).toBe(500);
|
|
expect(response.body.message).toBe('DB Error');
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ error: dbError },
|
|
`Error fetching comments for recipe ID 1:`,
|
|
);
|
|
});
|
|
|
|
it('should return 400 for an invalid recipeId', async () => {
|
|
const response = await supertest(app).get('/api/recipes/abc/comments');
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.errors[0].message).toContain('received NaN');
|
|
});
|
|
});
|
|
|
|
describe('GET /:recipeId', () => {
|
|
it('should return a single recipe on success', async () => {
|
|
const mockRecipe = createMockRecipe({ recipe_id: 456, name: 'Specific Recipe' });
|
|
vi.mocked(db.recipeRepo.getRecipeById).mockResolvedValue(mockRecipe);
|
|
|
|
const response = await supertest(app).get('/api/recipes/456');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual(mockRecipe);
|
|
expect(db.recipeRepo.getRecipeById).toHaveBeenCalledWith(456, expectLogger);
|
|
});
|
|
|
|
it('should return 404 if the recipe is not found', async () => {
|
|
const notFoundError = new NotFoundError('Recipe not found');
|
|
vi.mocked(db.recipeRepo.getRecipeById).mockRejectedValue(notFoundError);
|
|
const response = await supertest(app).get('/api/recipes/999');
|
|
expect(response.status).toBe(404);
|
|
expect(response.body.message).toContain('not found');
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ error: notFoundError },
|
|
`Error fetching recipe ID 999:`,
|
|
);
|
|
});
|
|
|
|
it('should return 500 if the database call fails', async () => {
|
|
const dbError = new Error('DB Error');
|
|
vi.mocked(db.recipeRepo.getRecipeById).mockRejectedValue(dbError);
|
|
const response = await supertest(app).get('/api/recipes/456');
|
|
expect(response.status).toBe(500);
|
|
expect(response.body.message).toBe('DB Error');
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ error: dbError },
|
|
`Error fetching recipe ID 456:`,
|
|
);
|
|
});
|
|
|
|
it('should return 400 for an invalid recipeId', async () => {
|
|
const response = await supertest(app).get('/api/recipes/abc');
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.errors[0].message).toContain('received NaN');
|
|
});
|
|
});
|
|
});
|