All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m38s
245 lines
8.1 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|
|
});
|