// src/tests/integration/recipe.integration.test.ts import { describe, it, expect, beforeAll, afterAll, vi, afterEach } from 'vitest'; import supertest from 'supertest'; import { createAndLoginUser } from '../utils/testHelpers'; import { cleanupDb } from '../utils/cleanup'; import type { UserProfile, Recipe } from '../../types'; import { getPool } from '../../services/db/connection.db'; import { aiService } from '../../services/aiService.server'; /** * @vitest-environment node */ describe('Recipe API Routes Integration Tests', () => { let request: ReturnType; let testUser: UserProfile; let authToken: string; let testRecipe: Recipe; const createdUserIds: string[] = []; const createdRecipeIds: number[] = []; beforeAll(async () => { vi.stubEnv('FRONTEND_URL', 'https://example.com'); const app = (await import('../../../server')).default; request = supertest(app); // Create a user to own the recipe and perform authenticated actions const { user, token } = await createAndLoginUser({ email: `recipe-user-${Date.now()}@example.com`, fullName: 'Recipe Test User', request, }); testUser = user; authToken = token; createdUserIds.push(user.user.user_id); // Mock the AI service method using spyOn to preserve other exports like DuplicateFlyerError vi.spyOn(aiService, 'generateRecipeSuggestion').mockResolvedValue('Default Mock Suggestion'); // Create a recipe owned by the test user const recipeRes = await getPool().query( `INSERT INTO public.recipes (name, instructions, user_id, status, description) VALUES ('Integration Test Recipe', '1. Do this. 2. Do that.', $1, 'public', 'A test recipe description.') RETURNING *`, [testUser.user.user_id], ); testRecipe = recipeRes.rows[0]; createdRecipeIds.push(testRecipe.recipe_id); }); afterEach(() => { vi.clearAllMocks(); // Reset the mock to its default state for the next test vi.mocked(aiService.generateRecipeSuggestion).mockResolvedValue('Default Mock Suggestion'); }); afterAll(async () => { vi.unstubAllEnvs(); // Clean up all created resources await cleanupDb({ userIds: createdUserIds, recipeIds: createdRecipeIds, }); }); describe('GET /api/recipes/:recipeId', () => { it('should fetch a single public recipe by its ID', async () => { const response = await request.get(`/api/recipes/${testRecipe.recipe_id}`); expect(response.status).toBe(200); expect(response.body).toBeDefined(); expect(response.body.recipe_id).toBe(testRecipe.recipe_id); expect(response.body.name).toBe('Integration Test Recipe'); }); it('should return 404 for a non-existent recipe ID', async () => { const response = await request.get('/api/recipes/999999'); expect(response.status).toBe(404); }); }); it('should allow an authenticated user to create a new recipe', async () => { const newRecipeData = { name: 'My New Awesome Recipe', instructions: '1. Be awesome. 2. Make recipe.', description: 'A recipe created during an integration test.', }; const response = await request .post('/api/users/recipes') .set('Authorization', `Bearer ${authToken}`) .send(newRecipeData); // Assert the response from the POST request expect(response.status).toBe(201); const createdRecipe: Recipe = response.body; expect(createdRecipe).toBeDefined(); expect(createdRecipe.recipe_id).toBeTypeOf('number'); expect(createdRecipe.name).toBe(newRecipeData.name); expect(createdRecipe.user_id).toBe(testUser.user.user_id); // Add the new recipe ID to the cleanup array to ensure it's deleted after tests createdRecipeIds.push(createdRecipe.recipe_id); // Verify the recipe can be fetched from the public endpoint const verifyResponse = await request.get(`/api/recipes/${createdRecipe.recipe_id}`); expect(verifyResponse.status).toBe(200); expect(verifyResponse.body.name).toBe(newRecipeData.name); }); it('should allow an authenticated user to update their own recipe', async () => { const recipeUpdates = { name: 'Updated Integration Test Recipe', instructions: '1. Do the new thing. 2. Do the other new thing.', }; const response = await request .put(`/api/users/recipes/${testRecipe.recipe_id}`) // Authenticated recipe update endpoint .set('Authorization', `Bearer ${authToken}`) .send(recipeUpdates); // Assert the response from the PUT request expect(response.status).toBe(200); const updatedRecipe: Recipe = response.body; expect(updatedRecipe.name).toBe(recipeUpdates.name); expect(updatedRecipe.instructions).toBe(recipeUpdates.instructions); // Verify the changes were persisted by fetching the recipe again const verifyResponse = await request.get(`/api/recipes/${testRecipe.recipe_id}`); expect(verifyResponse.status).toBe(200); expect(verifyResponse.body.name).toBe(recipeUpdates.name); }); it.todo("should prevent a user from updating another user's recipe"); it.todo('should allow an authenticated user to delete their own recipe'); it.todo("should prevent a user from deleting another user's recipe"); it.todo('should allow an authenticated user to post a comment on a recipe'); it.todo('should allow an authenticated user to fork a recipe'); describe('POST /api/recipes/suggest', () => { it('should return a recipe suggestion based on ingredients', async () => { const ingredients = ['chicken', 'rice', 'broccoli']; const mockSuggestion = 'Chicken and Broccoli Stir-fry with Rice'; vi.mocked(aiService.generateRecipeSuggestion).mockResolvedValue(mockSuggestion); const response = await request .post('/api/recipes/suggest') .set('Authorization', `Bearer ${authToken}`) .send({ ingredients }); expect(response.status).toBe(200); expect(response.body).toEqual({ suggestion: mockSuggestion }); expect(aiService.generateRecipeSuggestion).toHaveBeenCalledWith( ingredients, expect.anything(), ); }); }); });