All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 11m55s
239 lines
9.9 KiB
TypeScript
239 lines
9.9 KiB
TypeScript
// 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');
|
|
});
|
|
});
|
|
});
|