const mongoose = require('mongoose'); const httpStatus = require('http-status'); const httpMocks = require('node-mocks-http'); const { errorConverter, errorHandler } = require('../../../src/middlewares/error'); const ApiError = require('../../../src/utils/ApiError'); const config = require('../../../src/config/config'); const logger = require('../../../src/config/logger'); describe('Error middlewares', () => { describe('Error converter', () => { test('should return the same ApiError object it was called with', () => { const error = new ApiError(httpStatus.BAD_REQUEST, 'Any error'); const next = jest.fn(); errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next); expect(next).toHaveBeenCalledWith(error); }); test('should convert an Error to ApiError and preserve its status and message', () => { const error = new Error('Any error'); error.statusCode = httpStatus.BAD_REQUEST; const next = jest.fn(); errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next); expect(next).toHaveBeenCalledWith(expect.any(ApiError)); expect(next).toHaveBeenCalledWith( expect.objectContaining({ statusCode: error.statusCode, message: error.message, isOperational: false, }) ); }); test('should convert an Error without status to ApiError with status 500', () => { const error = new Error('Any error'); const next = jest.fn(); errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next); expect(next).toHaveBeenCalledWith(expect.any(ApiError)); expect(next).toHaveBeenCalledWith( expect.objectContaining({ statusCode: httpStatus.INTERNAL_SERVER_ERROR, message: error.message, isOperational: false, }) ); }); test('should convert an Error without message to ApiError with default message of that http status', () => { const error = new Error(); error.statusCode = httpStatus.BAD_REQUEST; const next = jest.fn(); errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next); expect(next).toHaveBeenCalledWith(expect.any(ApiError)); expect(next).toHaveBeenCalledWith( expect.objectContaining({ statusCode: error.statusCode, message: httpStatus[error.statusCode], isOperational: false, }) ); }); test('should convert a Mongoose error to ApiError with status 400 and preserve its message', () => { const error = new mongoose.Error('Any mongoose error'); const next = jest.fn(); errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next); expect(next).toHaveBeenCalledWith(expect.any(ApiError)); expect(next).toHaveBeenCalledWith( expect.objectContaining({ statusCode: httpStatus.BAD_REQUEST, message: error.message, isOperational: false, }) ); }); test('should convert any other object to ApiError with status 500 and its message', () => { const error = {}; const next = jest.fn(); errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next); expect(next).toHaveBeenCalledWith(expect.any(ApiError)); expect(next).toHaveBeenCalledWith( expect.objectContaining({ statusCode: httpStatus.INTERNAL_SERVER_ERROR, message: httpStatus[httpStatus.INTERNAL_SERVER_ERROR], isOperational: false, }) ); }); }); describe('Error handler', () => { beforeEach(() => { jest.spyOn(logger, 'error').mockImplementation(() => {}); }); test('should send proper error response and put the error message in res.locals', () => { const error = new ApiError(httpStatus.BAD_REQUEST, 'Any error'); const res = httpMocks.createResponse(); const sendSpy = jest.spyOn(res, 'send'); errorHandler(error, httpMocks.createRequest(), res); expect(sendSpy).toHaveBeenCalledWith(expect.objectContaining({ code: error.statusCode, message: error.message })); expect(res.locals.errorMessage).toBe(error.message); }); test('should put the error stack in the response if in development mode', () => { config.env = 'development'; const error = new ApiError(httpStatus.BAD_REQUEST, 'Any error'); const res = httpMocks.createResponse(); const sendSpy = jest.spyOn(res, 'send'); errorHandler(error, httpMocks.createRequest(), res); expect(sendSpy).toHaveBeenCalledWith( expect.objectContaining({ code: error.statusCode, message: error.message, stack: error.stack }) ); config.env = process.env.NODE_ENV; }); test('should send internal server error status and message if in production mode and error is not operational', () => { config.env = 'production'; const error = new ApiError(httpStatus.BAD_REQUEST, 'Any error', false); const res = httpMocks.createResponse(); const sendSpy = jest.spyOn(res, 'send'); errorHandler(error, httpMocks.createRequest(), res); expect(sendSpy).toHaveBeenCalledWith( expect.objectContaining({ code: httpStatus.INTERNAL_SERVER_ERROR, message: httpStatus[httpStatus.INTERNAL_SERVER_ERROR], }) ); expect(res.locals.errorMessage).toBe(error.message); config.env = process.env.NODE_ENV; }); test('should preserve original error status and message if in production mode and error is operational', () => { config.env = 'production'; const error = new ApiError(httpStatus.BAD_REQUEST, 'Any error'); const res = httpMocks.createResponse(); const sendSpy = jest.spyOn(res, 'send'); errorHandler(error, httpMocks.createRequest(), res); expect(sendSpy).toHaveBeenCalledWith( expect.objectContaining({ code: error.statusCode, message: error.message, }) ); config.env = process.env.NODE_ENV; }); }); });