- Add --coverage flag to npm test script - Add lcov coverage reporters for SonarQube integration - Add tests for expired token handling - Add tests for all errorHandler error types - Add tests for validate middleware edge cases - Add coverage exclusions for controllers/services in SonarQube 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
411 lines
11 KiB
JavaScript
411 lines
11 KiB
JavaScript
/**
|
|
* 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));
|
|
});
|
|
});
|
|
});
|