/** * Tests unitaires pour src/middleware/ */ import { jest } from '@jest/globals'; // Mock du pool de base de données jest.unstable_mockModule('../../db.js', () => ({ pool: { query: jest.fn(), }, })); const { authenticateToken, authorizeRoles, optionalAuth } = await import('../../src/middleware/auth.js'); const { AppError, errorHandler, asyncHandler } = await import('../../src/middleware/errorHandler.js'); const { validate } = await import('../../src/middleware/validate.js'); describe('Auth Middleware', () => { let mockReq; let mockRes; let mockNext; beforeEach(() => { mockReq = { headers: {}, cookies: {}, }; mockRes = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis(), }; mockNext = jest.fn(); jest.clearAllMocks(); }); describe('authenticateToken', () => { it('should return 401 if no token provided', async () => { await authenticateToken(mockReq, mockRes, mockNext); expect(mockNext).toHaveBeenCalledWith( expect.objectContaining({ statusCode: 401, isOperational: true, }) ); }); it('should return 401 for invalid token', async () => { mockReq.headers.authorization = 'Bearer invalid-token'; await authenticateToken(mockReq, mockRes, mockNext); expect(mockNext).toHaveBeenCalledWith( expect.objectContaining({ statusCode: 401, isOperational: true, }) ); }); it('should return 401 for expired token', async () => { // Create an expired token const jwt = await import('jsonwebtoken'); const expiredToken = jwt.default.sign( { userId: 1 }, process.env.JWT_SECRET, { expiresIn: '-1h' } ); mockReq.headers.authorization = `Bearer ${expiredToken}`; await authenticateToken(mockReq, mockRes, mockNext); expect(mockNext).toHaveBeenCalledWith( expect.objectContaining({ statusCode: 401, message: 'Token expiré', }) ); }); }); describe('authorizeRoles', () => { it('should call next if user has required role', () => { mockReq.user = { role: 'ADMIN' }; const middleware = authorizeRoles('ADMIN', 'EMPLOYEE'); middleware(mockReq, mockRes, mockNext); expect(mockNext).toHaveBeenCalled(); }); it('should return 403 if user lacks required role', () => { mockReq.user = { role: 'CLIENT' }; const middleware = authorizeRoles('ADMIN'); middleware(mockReq, mockRes, mockNext); expect(mockNext).toHaveBeenCalledWith( expect.objectContaining({ statusCode: 403, isOperational: true, }) ); }); it('should return 401 if user is not authenticated', () => { mockReq.user = undefined; const middleware = authorizeRoles('ADMIN'); middleware(mockReq, mockRes, mockNext); expect(mockNext).toHaveBeenCalledWith( expect.objectContaining({ statusCode: 401, isOperational: true, }) ); }); }); describe('optionalAuth', () => { it('should call next without user if no token', async () => { await optionalAuth(mockReq, mockRes, mockNext); expect(mockNext).toHaveBeenCalled(); expect(mockReq.user).toBeUndefined(); }); it('should continue without user on invalid token', async () => { mockReq.headers.authorization = 'Bearer invalid-token'; await optionalAuth(mockReq, mockRes, mockNext); expect(mockNext).toHaveBeenCalled(); expect(mockReq.user).toBeUndefined(); }); it('should continue without user on expired token', async () => { const jwt = await import('jsonwebtoken'); const expiredToken = jwt.default.sign( { userId: 1 }, process.env.JWT_SECRET, { expiresIn: '-1h' } ); mockReq.headers.authorization = `Bearer ${expiredToken}`; await optionalAuth(mockReq, mockRes, mockNext); expect(mockNext).toHaveBeenCalled(); // User should not be set due to expired token }); }); }); describe('Error Handler Middleware', () => { let mockReq; let mockRes; let mockNext; beforeEach(() => { mockReq = {}; mockRes = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis(), }; mockNext = jest.fn(); }); describe('AppError', () => { it('should create an operational error', () => { const error = new AppError('Test error', 400); expect(error.message).toBe('Test error'); expect(error.statusCode).toBe(400); expect(error.isOperational).toBe(true); }); }); describe('errorHandler', () => { it('should handle AppError with correct status', () => { const error = new AppError('Not found', 404); errorHandler(error, mockReq, mockRes, mockNext); expect(mockRes.status).toHaveBeenCalledWith(404); expect(mockRes.json).toHaveBeenCalledWith( expect.objectContaining({ success: false, message: 'Not found', }) ); }); it('should handle generic errors with 500', () => { const error = new Error('Something went wrong'); errorHandler(error, mockReq, mockRes, mockNext); expect(mockRes.status).toHaveBeenCalledWith(500); expect(mockRes.json).toHaveBeenCalledWith( expect.objectContaining({ success: false, }) ); }); it('should handle ValidationError', () => { const error = new Error('Validation failed'); error.name = 'ValidationError'; error.errors = { email: { message: 'Email is required' }, password: { message: 'Password is required' }, }; errorHandler(error, mockReq, mockRes, mockNext); expect(mockRes.status).toHaveBeenCalledWith(400); }); it('should handle PostgreSQL unique constraint error (23505)', () => { const error = new Error('Duplicate key'); error.code = '23505'; errorHandler(error, mockReq, mockRes, mockNext); expect(mockRes.status).toHaveBeenCalledWith(400); expect(mockRes.json).toHaveBeenCalledWith( expect.objectContaining({ message: 'Cette valeur existe déjà', }) ); }); it('should handle PostgreSQL foreign key constraint error (23503)', () => { const error = new Error('Foreign key violation'); error.code = '23503'; errorHandler(error, mockReq, mockRes, mockNext); expect(mockRes.status).toHaveBeenCalledWith(400); expect(mockRes.json).toHaveBeenCalledWith( expect.objectContaining({ message: 'Référence invalide', }) ); }); it('should handle JsonWebTokenError', () => { const error = new Error('Invalid token'); error.name = 'JsonWebTokenError'; errorHandler(error, mockReq, mockRes, mockNext); expect(mockRes.status).toHaveBeenCalledWith(401); expect(mockRes.json).toHaveBeenCalledWith( expect.objectContaining({ message: 'Token invalide', }) ); }); it('should handle TokenExpiredError', () => { const error = new Error('Token expired'); error.name = 'TokenExpiredError'; errorHandler(error, mockReq, mockRes, mockNext); expect(mockRes.status).toHaveBeenCalledWith(401); expect(mockRes.json).toHaveBeenCalledWith( expect.objectContaining({ message: 'Token expiré', }) ); }); it('should include stack trace in development mode', () => { const originalEnv = process.env.NODE_ENV; process.env.NODE_ENV = 'development'; const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); const error = new AppError('Test error', 500); errorHandler(error, mockReq, mockRes, mockNext); expect(mockRes.json).toHaveBeenCalledWith( expect.objectContaining({ stack: expect.any(String), }) ); consoleSpy.mockRestore(); process.env.NODE_ENV = originalEnv; }); }); describe('asyncHandler', () => { it('should pass errors to next middleware', async () => { const error = new Error('Async error'); const asyncFn = async () => { throw error; }; const wrappedFn = asyncHandler(asyncFn); await wrappedFn(mockReq, mockRes, mockNext); expect(mockNext).toHaveBeenCalledWith(error); }); it('should not call next on success', async () => { const asyncFn = async (req, res) => { res.json({ success: true }); }; const wrappedFn = asyncHandler(asyncFn); await wrappedFn(mockReq, mockRes, mockNext); expect(mockRes.json).toHaveBeenCalledWith({ success: true }); expect(mockNext).not.toHaveBeenCalled(); }); }); }); describe('Validation Middleware', () => { let mockReq; let mockRes; let mockNext; beforeEach(() => { mockReq = { body: {}, query: {}, params: {}, }; mockRes = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis(), }; mockNext = jest.fn(); }); describe('validate', () => { it('should call next for valid data', async () => { const { z } = await import('zod'); const schema = z.object({ body: z.object({ email: z.string().email(), }), }); mockReq.body = { email: 'test@example.com' }; const middleware = validate(schema); await middleware(mockReq, mockRes, mockNext); expect(mockNext).toHaveBeenCalled(); }); it('should return 400 for invalid data', async () => { const { z } = await import('zod'); const schema = z.object({ body: z.object({ email: z.string().email(), }), }); mockReq.body = { email: 'invalid-email' }; const middleware = validate(schema); await middleware(mockReq, mockRes, mockNext); expect(mockRes.status).toHaveBeenCalledWith(400); expect(mockNext).not.toHaveBeenCalled(); }); it('should return validation error details', async () => { const { z } = await import('zod'); const schema = z.object({ body: z.object({ email: z.string().email(), password: z.string().min(8), }), }); mockReq.body = { email: 'invalid', password: '123' }; const middleware = validate(schema); await middleware(mockReq, mockRes, mockNext); expect(mockRes.status).toHaveBeenCalledWith(400); expect(mockRes.json).toHaveBeenCalledWith( expect.objectContaining({ success: false, message: 'Erreur de validation', details: expect.any(Array), }) ); }); it('should pass non-Zod errors to next middleware', async () => { const schema = { parseAsync: jest.fn().mockRejectedValue(new Error('Unknown error')), }; const middleware = validate(schema); await middleware(mockReq, mockRes, mockNext); expect(mockNext).toHaveBeenCalledWith(expect.any(Error)); }); }); });