Files
flyer-crawler.projectium.com/src/tests/integration/reactions.integration.test.ts
Torben Sorensen cd46f1d4c2
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m38s
integration test fixes
2026-01-18 16:23:34 -08:00

245 lines
8.1 KiB
TypeScript

// src/tests/integration/reactions.integration.test.ts
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import supertest from 'supertest';
import { createAndLoginUser } from '../utils/testHelpers';
import { cleanupDb } from '../utils/cleanup';
import { getPool } from '../../services/db/connection.db';
/**
* @vitest-environment node
*
* Integration tests for the Reactions API routes.
* These routes were previously unmounted and are now available at /api/reactions.
*/
describe('Reactions API Routes Integration Tests', () => {
let request: ReturnType<typeof supertest>;
let authToken: string;
let testRecipeId: number;
const createdUserIds: string[] = [];
const createdReactionIds: number[] = [];
beforeAll(async () => {
vi.stubEnv('FRONTEND_URL', 'https://example.com');
const app = (await import('../../../server')).default;
request = supertest(app);
// Create a user for the tests
const { user, token } = await createAndLoginUser({
email: `reactions-user-${Date.now()}@example.com`,
fullName: 'Reactions Test User',
request,
});
authToken = token;
createdUserIds.push(user.user.user_id);
// Get an existing recipe ID from the seed data to use for reactions
const recipeResult = await getPool().query(`SELECT recipe_id FROM public.recipes LIMIT 1`);
if (recipeResult.rows.length > 0) {
testRecipeId = recipeResult.rows[0].recipe_id;
} else {
// Create a minimal recipe if none exist
const newRecipe = await getPool().query(
`INSERT INTO public.recipes (title, description, instructions, prep_time_minutes, cook_time_minutes, servings)
VALUES ('Test Recipe for Reactions', 'A test recipe', 'Test instructions', 10, 20, 4)
RETURNING recipe_id`,
);
testRecipeId = newRecipe.rows[0].recipe_id;
}
});
afterAll(async () => {
vi.unstubAllEnvs();
// Clean up reactions created during tests
if (createdReactionIds.length > 0) {
await getPool().query(
'DELETE FROM public.user_reactions WHERE reaction_id = ANY($1::int[])',
[createdReactionIds],
);
}
await cleanupDb({
userIds: createdUserIds,
});
});
describe('GET /api/reactions', () => {
it('should return reactions (public endpoint)', async () => {
const response = await request.get('/api/reactions');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toBeInstanceOf(Array);
});
it('should filter reactions by entityType', async () => {
const response = await request.get('/api/reactions').query({ entityType: 'recipe' });
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toBeInstanceOf(Array);
});
it('should filter reactions by entityId', async () => {
const response = await request
.get('/api/reactions')
.query({ entityType: 'recipe', entityId: String(testRecipeId) });
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toBeInstanceOf(Array);
});
});
describe('GET /api/reactions/summary', () => {
it('should return reaction summary for an entity', async () => {
const response = await request
.get('/api/reactions/summary')
.query({ entityType: 'recipe', entityId: String(testRecipeId) });
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
// Summary should have reaction counts
expect(response.body.data).toBeDefined();
});
it('should return 400 when entityType is missing', async () => {
const response = await request
.get('/api/reactions/summary')
.query({ entityId: String(testRecipeId) });
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
});
it('should return 400 when entityId is missing', async () => {
const response = await request.get('/api/reactions/summary').query({ entityType: 'recipe' });
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
});
});
describe('POST /api/reactions/toggle', () => {
it('should require authentication', async () => {
const response = await request.post('/api/reactions/toggle').send({
entity_type: 'recipe',
entity_id: String(testRecipeId),
reaction_type: 'like',
});
expect(response.status).toBe(401);
});
it('should add a reaction when none exists', async () => {
const response = await request
.post('/api/reactions/toggle')
.set('Authorization', `Bearer ${authToken}`)
.send({
entity_type: 'recipe',
entity_id: String(testRecipeId),
reaction_type: 'like',
});
expect(response.status).toBe(201);
expect(response.body.success).toBe(true);
expect(response.body.data.message).toBe('Reaction added.');
expect(response.body.data.reaction).toBeDefined();
// Track for cleanup
if (response.body.data.reaction?.reaction_id) {
createdReactionIds.push(response.body.data.reaction.reaction_id);
}
});
it('should remove the reaction when toggled again', async () => {
// First add the reaction
const addResponse = await request
.post('/api/reactions/toggle')
.set('Authorization', `Bearer ${authToken}`)
.send({
entity_type: 'recipe',
entity_id: String(testRecipeId),
reaction_type: 'love', // Use different type to not conflict
});
expect(addResponse.status).toBe(201);
if (addResponse.body.data.reaction?.reaction_id) {
createdReactionIds.push(addResponse.body.data.reaction.reaction_id);
}
// Then toggle it off
const removeResponse = await request
.post('/api/reactions/toggle')
.set('Authorization', `Bearer ${authToken}`)
.send({
entity_type: 'recipe',
entity_id: String(testRecipeId),
reaction_type: 'love',
});
expect(removeResponse.status).toBe(200);
expect(removeResponse.body.success).toBe(true);
expect(removeResponse.body.data.message).toBe('Reaction removed.');
});
it('should return 400 for missing entity_type', async () => {
const response = await request
.post('/api/reactions/toggle')
.set('Authorization', `Bearer ${authToken}`)
.send({
entity_id: String(testRecipeId),
reaction_type: 'like',
});
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
});
it('should return 400 for missing entity_id', async () => {
const response = await request
.post('/api/reactions/toggle')
.set('Authorization', `Bearer ${authToken}`)
.send({
entity_type: 'recipe',
reaction_type: 'like',
});
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
});
it('should return 400 for missing reaction_type', async () => {
const response = await request
.post('/api/reactions/toggle')
.set('Authorization', `Bearer ${authToken}`)
.send({
entity_type: 'recipe',
entity_id: String(testRecipeId),
});
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
});
it('should accept entity_id as string (required format)', async () => {
// entity_id must be a string per the Zod schema
const response = await request
.post('/api/reactions/toggle')
.set('Authorization', `Bearer ${authToken}`)
.send({
entity_type: 'recipe',
entity_id: String(testRecipeId),
reaction_type: 'helpful',
});
// Should succeed (201 for add, 200 for remove)
expect([200, 201]).toContain(response.status);
expect(response.body.success).toBe(true);
if (response.body.data.reaction?.reaction_id) {
createdReactionIds.push(response.body.data.reaction.reaction_id);
}
});
});
});