// src/tests/integration/public.routes.integration.test.ts import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import supertest from 'supertest'; import app from '../../../server'; import type { Flyer, FlyerItem, Recipe, RecipeComment, DietaryRestriction, Appliance, UserProfile, } from '../../types'; import { getPool } from '../../services/db/connection.db'; import { cleanupDb } from '../utils/cleanup'; import { createAndLoginUser } from '../utils/testHelpers'; /** * @vitest-environment node */ const request = supertest(app); describe('Public API Routes Integration Tests', () => { // Shared state for tests let testUser: UserProfile; let testRecipe: Recipe; let testFlyer: Flyer; let testStoreId: number; beforeAll(async () => { const pool = getPool(); // Create a user to own the recipe const userEmail = `public-routes-user-${Date.now()}@example.com`; // Use the helper to create a user and ensure a full UserProfile is returned, // which also handles activity logging correctly. const { user: createdUser } = await createAndLoginUser({ email: userEmail, password: 'a-Very-Strong-Password-123!', fullName: 'Public Routes Test User', request, }); testUser = createdUser; // DEBUG: Verify user existence in DB console.log(`[DEBUG] createAndLoginUser returned user ID: ${testUser.user.user_id}`); const userCheck = await pool.query('SELECT user_id FROM public.users WHERE user_id = $1', [testUser.user.user_id]); console.log(`[DEBUG] DB check for user found ${userCheck.rowCount ?? 0} rows.`); if (!userCheck.rowCount) { console.error(`[DEBUG] CRITICAL: User ${testUser.user.user_id} does not exist in public.users table! Attempting to wait...`); // Wait loop to ensure user persistence if there's a race condition for (let i = 0; i < 5; i++) { await new Promise((resolve) => setTimeout(resolve, 500)); const retryCheck = await pool.query('SELECT user_id FROM public.users WHERE user_id = $1', [testUser.user.user_id]); if (retryCheck.rowCount && retryCheck.rowCount > 0) { console.log(`[DEBUG] User found after retry ${i + 1}`); break; } } } // Final check before proceeding to avoid FK error const finalCheck = await pool.query('SELECT user_id FROM public.users WHERE user_id = $1', [testUser.user.user_id]); if (!finalCheck.rowCount) { throw new Error(`User ${testUser.user.user_id} failed to persist in DB. Cannot continue test.`); } // Create a recipe const recipeRes = await pool.query( `INSERT INTO public.recipes (name, instructions, user_id, status) VALUES ('Public Test Recipe', 'Instructions here', $1, 'public') RETURNING *`, [testUser.user.user_id], ); testRecipe = recipeRes.rows[0]; // Create a store and flyer const storeRes = await pool.query( `INSERT INTO public.stores (name) VALUES ('Public Routes Test Store') RETURNING store_id`, ); testStoreId = storeRes.rows[0].store_id; const flyerRes = await pool.query( `INSERT INTO public.flyers (store_id, file_name, image_url, item_count, checksum) VALUES ($1, 'public-routes-test.jpg', 'http://test.com/public-routes.jpg', 1, $2) RETURNING *`, [testStoreId, `checksum-public-routes-${Date.now()}`], ); testFlyer = flyerRes.rows[0]; // Add an item to the flyer await pool.query( `INSERT INTO public.flyer_items (flyer_id, item, price_display, quantity) VALUES ($1, 'Test Item', '$0.00', 'each')`, [testFlyer.flyer_id], ); }); afterAll(async () => { await cleanupDb({ userIds: testUser ? [testUser.user.user_id] : [], recipeIds: testRecipe ? [testRecipe.recipe_id] : [], flyerIds: testFlyer ? [testFlyer.flyer_id] : [], storeIds: testStoreId ? [testStoreId] : [], }); }); describe('Health Check Endpoints', () => { it('GET /api/health/ping should return "pong"', async () => { const response = await request.get('/api/health/ping'); expect(response.status).toBe(200); expect(response.text).toBe('pong'); }); it('GET /api/health/db-schema should return success', async () => { const response = await request.get('/api/health/db-schema'); expect(response.status).toBe(200); expect(response.body.success).toBe(true); }); it('GET /api/health/storage should return success', async () => { const response = await request.get('/api/health/storage'); expect(response.status).toBe(200); expect(response.body.success).toBe(true); }); it('GET /api/health/db-pool should return success', async () => { const response = await request.get('/api/health/db-pool'); expect(response.status).toBe(200); expect(response.body.success).toBe(true); }); it('GET /api/health/time should return the server time', async () => { const response = await request.get('/api/health/time'); expect(response.status).toBe(200); expect(response.body).toHaveProperty('currentTime'); expect(response.body).toHaveProperty('year'); expect(response.body).toHaveProperty('week'); }); }); describe('Public Data Endpoints', () => { it('GET /api/flyers should return a list of flyers', async () => { const response = await request.get('/api/flyers'); const flyers: Flyer[] = response.body; expect(flyers.length).toBeGreaterThan(0); const foundFlyer = flyers.find((f) => f.flyer_id === testFlyer.flyer_id); expect(foundFlyer).toBeDefined(); expect(foundFlyer).toHaveProperty('store'); }); it('GET /api/flyers/:id/items should return items for a specific flyer', async () => { const response = await request.get(`/api/flyers/${testFlyer.flyer_id}/items`); const items: FlyerItem[] = response.body; expect(response.status).toBe(200); expect(items).toBeInstanceOf(Array); expect(items.length).toBe(1); expect(items[0].flyer_id).toBe(testFlyer.flyer_id); }); it('POST /api/flyers/items/batch-fetch should return items for multiple flyers', async () => { const flyerIds = [testFlyer.flyer_id]; const response = await request.post('/api/flyers/items/batch-fetch').send({ flyerIds }); const items: FlyerItem[] = response.body; expect(response.status).toBe(200); expect(items).toBeInstanceOf(Array); expect(items.length).toBeGreaterThan(0); }); it('POST /api/flyers/items/batch-count should return a count for multiple flyers', async () => { const flyerIds = [testFlyer.flyer_id]; const response = await request.post('/api/flyers/items/batch-count').send({ flyerIds }); expect(response.status).toBe(200); expect(response.body.count).toBeTypeOf('number'); expect(response.body.count).toBeGreaterThan(0); }); it('GET /api/personalization/master-items should return a list of master grocery items', async () => { const response = await request.get('/api/personalization/master-items'); const masterItems = response.body; expect(response.status).toBe(200); expect(masterItems).toBeInstanceOf(Array); expect(masterItems.length).toBeGreaterThan(0); // This relies on seed data for master items. expect(masterItems[0]).toHaveProperty('master_grocery_item_id'); }); it('GET /api/recipes/by-sale-percentage should return recipes', async () => { const response = await request.get('/api/recipes/by-sale-percentage?minPercentage=10'); const recipes: Recipe[] = response.body; expect(response.status).toBe(200); expect(recipes).toBeInstanceOf(Array); }); it('GET /api/recipes/by-ingredient-and-tag should return recipes', async () => { // This test is now less brittle. It might return our created recipe or others. const response = await request.get( '/api/recipes/by-ingredient-and-tag?ingredient=Test&tag=Public', ); const recipes: Recipe[] = response.body; expect(response.status).toBe(200); expect(recipes).toBeInstanceOf(Array); }); it('GET /api/recipes/:recipeId/comments should return comments for a recipe', async () => { // Add a comment to our test recipe first await getPool().query( `INSERT INTO public.recipe_comments (recipe_id, user_id, content) VALUES ($1, $2, 'Test comment')`, [testRecipe.recipe_id, testUser.user.user_id], ); const response = await request.get(`/api/recipes/${testRecipe.recipe_id}/comments`); const comments: RecipeComment[] = response.body; expect(response.status).toBe(200); expect(comments).toBeInstanceOf(Array); expect(comments.length).toBe(1); expect(comments[0].content).toBe('Test comment'); }); it('GET /api/stats/most-frequent-sales should return frequent items', async () => { const response = await request.get('/api/stats/most-frequent-sales?days=365&limit=5'); const items = response.body; expect(response.status).toBe(200); expect(items).toBeInstanceOf(Array); }); it('GET /api/personalization/dietary-restrictions should return a list of restrictions', async () => { // This test relies on static seed data for a lookup table, which is acceptable. const response = await request.get('/api/personalization/dietary-restrictions'); const restrictions: DietaryRestriction[] = response.body; expect(response.status).toBe(200); expect(restrictions).toBeInstanceOf(Array); expect(restrictions.length).toBeGreaterThan(0); expect(restrictions[0]).toHaveProperty('dietary_restriction_id'); }); it('GET /api/personalization/appliances should return a list of appliances', async () => { const response = await request.get('/api/personalization/appliances'); const appliances: Appliance[] = response.body; expect(response.status).toBe(200); expect(appliances).toBeInstanceOf(Array); expect(appliances.length).toBeGreaterThan(0); expect(appliances[0]).toHaveProperty('appliance_id'); }); }); });