Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 1m17s
237 lines
8.4 KiB
TypeScript
237 lines
8.4 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import supertest from 'supertest';
|
|
import type { Request, Response, NextFunction } from 'express';
|
|
import { createTestApp } from '../tests/utils/createTestApp';
|
|
import { createMockUserProfile } from '../tests/utils/mockFactories';
|
|
import type { UserReaction } from '../types';
|
|
|
|
// 1. Mock the Service Layer directly.
|
|
vi.mock('../services/db/index.db', () => ({
|
|
reactionRepo: {
|
|
getReactions: vi.fn(),
|
|
getReactionSummary: vi.fn(),
|
|
toggleReaction: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
// Mock the logger to keep test output clean
|
|
vi.mock('../services/logger.server', async () => ({
|
|
logger: (await import('../tests/utils/mockLogger')).mockLogger,
|
|
}));
|
|
|
|
// Mock Passport middleware
|
|
vi.mock('../config/passport', () => ({
|
|
default: {
|
|
authenticate: vi.fn(() => (req: Request, res: Response, next: NextFunction) => {
|
|
// If we are testing the unauthenticated state (no user injected), simulate 401.
|
|
if (!req.user) {
|
|
return res.status(401).json({ message: 'Unauthorized' });
|
|
}
|
|
next();
|
|
}),
|
|
},
|
|
}));
|
|
|
|
// Import the router and mocked DB AFTER all mocks are defined.
|
|
import reactionsRouter from './reactions.routes';
|
|
import { reactionRepo } from '../services/db/index.db';
|
|
import { mockLogger } from '../tests/utils/mockLogger';
|
|
|
|
const expectLogger = expect.objectContaining({
|
|
info: expect.any(Function),
|
|
error: expect.any(Function),
|
|
});
|
|
|
|
describe('Reaction Routes (/api/reactions)', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('GET /', () => {
|
|
const app = createTestApp({ router: reactionsRouter, basePath: '/api/reactions' });
|
|
|
|
it('should return a list of reactions', async () => {
|
|
const mockReactions = [
|
|
{ reaction_id: 1, reaction_type: 'like', entity_id: '123' },
|
|
] as unknown as UserReaction[];
|
|
vi.mocked(reactionRepo.getReactions).mockResolvedValue(mockReactions);
|
|
|
|
const response = await supertest(app).get('/api/reactions');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data).toEqual(mockReactions);
|
|
expect(reactionRepo.getReactions).toHaveBeenCalledWith({}, expectLogger);
|
|
});
|
|
|
|
it('should filter by query parameters', async () => {
|
|
const mockReactions = [
|
|
{ reaction_id: 1, reaction_type: 'like' },
|
|
] as unknown as UserReaction[];
|
|
vi.mocked(reactionRepo.getReactions).mockResolvedValue(mockReactions);
|
|
|
|
const validUuid = '123e4567-e89b-12d3-a456-426614174000';
|
|
const query = { userId: validUuid, entityType: 'recipe', entityId: '1' };
|
|
|
|
const response = await supertest(app).get('/api/reactions').query(query);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(reactionRepo.getReactions).toHaveBeenCalledWith(
|
|
expect.objectContaining(query),
|
|
expectLogger,
|
|
);
|
|
});
|
|
|
|
it('should return 500 on database error', async () => {
|
|
const error = new Error('DB Error');
|
|
vi.mocked(reactionRepo.getReactions).mockRejectedValue(error);
|
|
|
|
const response = await supertest(app).get('/api/reactions');
|
|
|
|
expect(response.status).toBe(500);
|
|
expect(mockLogger.error).toHaveBeenCalledWith({ error }, 'Error fetching user reactions');
|
|
});
|
|
});
|
|
|
|
describe('GET /summary', () => {
|
|
const app = createTestApp({ router: reactionsRouter, basePath: '/api/reactions' });
|
|
|
|
it('should return reaction summary for an entity', async () => {
|
|
const mockSummary = [
|
|
{ reaction_type: 'like', count: 10 },
|
|
{ reaction_type: 'love', count: 5 },
|
|
];
|
|
vi.mocked(reactionRepo.getReactionSummary).mockResolvedValue(mockSummary);
|
|
|
|
const response = await supertest(app)
|
|
.get('/api/reactions/summary')
|
|
.query({ entityType: 'recipe', entityId: '123' });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data).toEqual(mockSummary);
|
|
expect(reactionRepo.getReactionSummary).toHaveBeenCalledWith('recipe', '123', expectLogger);
|
|
});
|
|
|
|
it('should return 400 if required parameters are missing', async () => {
|
|
const response = await supertest(app).get('/api/reactions/summary');
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error.details[0].message).toContain('required');
|
|
});
|
|
|
|
it('should return 500 on database error', async () => {
|
|
const error = new Error('DB Error');
|
|
vi.mocked(reactionRepo.getReactionSummary).mockRejectedValue(error);
|
|
|
|
const response = await supertest(app)
|
|
.get('/api/reactions/summary')
|
|
.query({ entityType: 'recipe', entityId: '123' });
|
|
|
|
expect(response.status).toBe(500);
|
|
expect(mockLogger.error).toHaveBeenCalledWith({ error }, 'Error fetching reaction summary');
|
|
});
|
|
});
|
|
|
|
describe('POST /toggle', () => {
|
|
const mockUser = createMockUserProfile({ user: { user_id: 'user-123' } });
|
|
const app = createTestApp({
|
|
router: reactionsRouter,
|
|
basePath: '/api/reactions',
|
|
authenticatedUser: mockUser,
|
|
});
|
|
|
|
const validBody = {
|
|
entity_type: 'recipe',
|
|
entity_id: '123',
|
|
reaction_type: 'like',
|
|
};
|
|
|
|
it('should return 201 when a reaction is added', async () => {
|
|
const mockResult = {
|
|
...validBody,
|
|
reaction_id: 1,
|
|
user_id: 'user-123',
|
|
} as unknown as UserReaction;
|
|
vi.mocked(reactionRepo.toggleReaction).mockResolvedValue(mockResult);
|
|
|
|
const response = await supertest(app).post('/api/reactions/toggle').send(validBody);
|
|
|
|
expect(response.status).toBe(201);
|
|
expect(response.body.data).toEqual({ message: 'Reaction added.', reaction: mockResult });
|
|
expect(reactionRepo.toggleReaction).toHaveBeenCalledWith(
|
|
{ user_id: 'user-123', ...validBody },
|
|
expectLogger,
|
|
);
|
|
});
|
|
|
|
it('should return 200 when a reaction is removed', async () => {
|
|
// Returning null/false from toggleReaction implies the reaction was removed
|
|
vi.mocked(reactionRepo.toggleReaction).mockResolvedValue(null);
|
|
|
|
const response = await supertest(app).post('/api/reactions/toggle').send(validBody);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data).toEqual({ message: 'Reaction removed.' });
|
|
});
|
|
|
|
it('should return 400 if body is invalid', async () => {
|
|
const response = await supertest(app)
|
|
.post('/api/reactions/toggle')
|
|
.send({ entity_type: 'recipe' }); // Missing other required fields
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error.details).toBeDefined();
|
|
});
|
|
|
|
it('should return 401 if not authenticated', async () => {
|
|
const unauthApp = createTestApp({ router: reactionsRouter, basePath: '/api/reactions' });
|
|
const response = await supertest(unauthApp).post('/api/reactions/toggle').send(validBody);
|
|
|
|
expect(response.status).toBe(401);
|
|
});
|
|
|
|
it('should return 500 on database error', async () => {
|
|
const error = new Error('DB Error');
|
|
vi.mocked(reactionRepo.toggleReaction).mockRejectedValue(error);
|
|
|
|
const response = await supertest(app).post('/api/reactions/toggle').send(validBody);
|
|
|
|
expect(response.status).toBe(500);
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ error, body: validBody },
|
|
'Error toggling user reaction',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Rate Limiting', () => {
|
|
it('should apply publicReadLimiter to GET /', async () => {
|
|
const app = createTestApp({ router: reactionsRouter, basePath: '/api/reactions' });
|
|
vi.mocked(reactionRepo.getReactions).mockResolvedValue([]);
|
|
const response = await supertest(app)
|
|
.get('/api/reactions')
|
|
.set('X-Test-Rate-Limit-Enable', 'true');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.headers).toHaveProperty('ratelimit-limit');
|
|
});
|
|
|
|
it('should apply userUpdateLimiter to POST /toggle', async () => {
|
|
const mockUser = createMockUserProfile({ user: { user_id: 'user-123' } });
|
|
const app = createTestApp({
|
|
router: reactionsRouter,
|
|
basePath: '/api/reactions',
|
|
authenticatedUser: mockUser,
|
|
});
|
|
vi.mocked(reactionRepo.toggleReaction).mockResolvedValue(null);
|
|
|
|
const response = await supertest(app)
|
|
.post('/api/reactions/toggle')
|
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
|
.send({ entity_type: 'recipe', entity_id: '1', reaction_type: 'like' });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.headers).toHaveProperty('ratelimit-limit');
|
|
expect(parseInt(response.headers['ratelimit-limit'])).toBe(150);
|
|
});
|
|
});
|
|
});
|