Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions lib/helpers/AppError.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ class AppError extends Error {
constructor(message, { details, status, code } = {}) {
super(message);
// Set HTTP status code
// eslint-disable-next-line no-unused-vars
status = status || 500;
this.status = status || 500;

// Set API error code
this.code = code || AppErrorCodes.serverError;
Expand Down
9 changes: 4 additions & 5 deletions lib/helpers/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,9 @@ const getMessageFromErrors = (err) => {
};

const cleanMessage = (message) => {
let output = '';
if (message[message.length - 1] !== '.') output = `${message}.`;
else output = message;
return output;
if (!message || typeof message !== 'string' || !message.trim()) return 'Something went wrong.';
if (message[message.length - 1] !== '.') return `${message}.`;
return message;
};

/**
Expand All @@ -88,7 +87,7 @@ const getMessage = (err) => {
if (err.code) output = getMessageFromCode(err);
else if (err.errors) output = getMessageFromErrors(err);
else if (err.message) output = err.message;
else output = `error while retrieving the error :o : ${JSON.stringify(err)}`;
else output = 'Something went wrong.';
return cleanMessage(output);
};

Expand Down
73 changes: 67 additions & 6 deletions lib/helpers/responses.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,82 @@ const success = (res, message) => (data) => {
return result;
};

/**
* @desc Validate HTTP status code boundaries
* @param {number} status - Candidate HTTP status code
* @return {boolean} true when status is a valid HTTP code
*/
const isValidHttpStatus = (status) => Number.isInteger(status) && status >= 100 && status <= 599;

/**
* @desc Resolve HTTP status code from explicit value or error object fallback
* @param {number} status - Explicit status code
* @param {Object} err - Error object potentially containing status values
* @return {number} normalized HTTP status code
*/
const getHttpStatus = (status, err) => {
if (isValidHttpStatus(status)) return status;
if (isValidHttpStatus(err?.status)) return err.status;
if (isValidHttpStatus(err?.statusCode)) return err.statusCode;
if (isValidHttpStatus(err?.code)) return err.code;
return 500;
};

/**
* @desc Resolve safe client description text for error payload
* @param {string} description - Explicit description override
* @param {Object} err - Error object
* @return {string} error description
*/
const getDescription = (description, err) => {
if (description) return description;
if (err?.description) return err.description;
if (typeof err?.details === 'string') return err.details;
Comment thread
PierreBrisorgueil marked this conversation as resolved.
if (err?.details?.message) return err.details.message;
return '';
};

/**
* @desc Resolve stable domain error code from an error object
* @param {Object} err - Error object
* @return {string} domain error code
*/
const getErrorCode = (err) => {
if (typeof err?.code === 'string' && err.code) return err.code;
return 'SERVER_ERROR';
};

/**
* @desc JSON stringify helper resilient to circular payloads
* @param {Object} value - Value to stringify
* @return {string} safe JSON string
*/
const safeStringify = (value) => {
try {
return JSON.stringify(value);
} catch (_err) {
return JSON.stringify({ message: 'Unserializable error object' });
}
};

/**
* @desc Function res error
* @param {Object} res - Express response object
* @param {String} success message
* @return {Object} type, message and error
Comment thread
PierreBrisorgueil marked this conversation as resolved.
Outdated
*/
const error = (res, code, message, description) => (error) => {
const error = (res, code, message, description) => (error = {}) => {
const status = getHttpStatus(code, error);
const result = {
type: 'error',
message: message || error.message,
code: code || error.code,
description: description || error.description || error.details || '',
error: JSON.stringify(error),
message: message || error.message || 'Something went wrong.',
Comment thread
PierreBrisorgueil marked this conversation as resolved.
Outdated
code: status,
status,
errorCode: getErrorCode(error),
description: getDescription(description, error),
};
res.status(code || error.code).json(result);
if (process.env.NODE_ENV !== 'production') result.error = safeStringify(error);
res.status(status).json(result);
return result;
};

Expand Down
51 changes: 50 additions & 1 deletion modules/core/tests/core.unit.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import logger from '../../../lib/services/logger.js';
import mongooseService from '../../../lib/services/mongoose.js';
import expressService from '../../../lib/services/express.js';
import errors from '../../../lib/helpers/errors.js';
import responses from '../../../lib/helpers/responses.js';
import AppError from '../../../lib/helpers/AppError.js';
import policy from '../../../lib/middlewares/policy.js';

/**
Expand Down Expand Up @@ -216,12 +218,59 @@ describe('Core unit tests:', () => {
expect(fromMessage).toBe('error1.');

const fromEmpty = errors.getMessage({});
expect(fromEmpty).toBe('error while retrieving the error :o : {}.');
expect(fromEmpty).toBe('Something went wrong.');
} catch (err) {
console.log(err);
expect(err).toBeFalsy();
}
});

it('should sanitize unknown errors message', () => {
const fromUnknown = errors.getMessage({ random: 'value' });
expect(fromUnknown).toBe('Something went wrong.');
});
});

describe('Responses', () => {
it('should return explicit status and domain code in error response', () => {
const mockRes = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
};
const err = new AppError('Schema validation error', {
status: 422,
code: 'VALIDATION_ERROR',
details: { message: 'First name is required.' },
});

const result = responses.error(mockRes)(err);

expect(mockRes.status).toHaveBeenCalledWith(422);
expect(result.type).toBe('error');
expect(result.status).toBe(422);
expect(result.code).toBe(422);
expect(result.errorCode).toBe('VALIDATION_ERROR');
expect(result.description).toBe('First name is required.');
});

it('should not expose raw error payload in production mode', () => {
const originalNodeEnv = process.env.NODE_ENV;
try {
process.env.NODE_ENV = 'production';

const mockRes = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
};
const result = responses.error(mockRes, 422, 'Schema validation error', 'Invalid payload')({
details: { internal: 'secret' },
});

expect(result.error).toBeUndefined();
} finally {
process.env.NODE_ENV = originalNodeEnv;
}
});
});

describe('Config helpers', () => {
Expand Down