refactor: unify error related tasks (#1154)

The current error handling logic is a mess. We have code that tries to
fetch information about errors in different places throughout the
application, and its incredibly hard to ensure certain types of error
get decent human friendly error messages.

This commit groups, improves, and tests all error related functions in
`lib/shared/errors.js`.

Here's a summary of the changes, in more detail:

- Move the `HUMAN_FRIENDLY` object to `shared/errors.js`
- Extend `HUMAN_FRIENDLY` with error descriptions
- Add `ENOMEM` to `shared/errors.js`
- Group CLI and `OSDialogService` mechanisms for getting an error title
  and an error description
- Move error serialisation routines from `robot` to `shared/errors.js`
- Create and use `createError()` and `createUserError()` utility
  functions
- Add user friendly descriptions to many errors
- Don't report user errors to TrackJS

Fixes: https://github.com/resin-io/etcher/issues/1098
Change-Type: minor
Changelog-Entry: Make errors more user friendly throughout the application.
Signed-off-by: Juan Cruz Viotti <jviotti@openmailbox.org>
This commit is contained in:
Juan Cruz Viotti 2017-03-10 13:11:45 -04:00 committed by GitHub
parent 0c7e1feb4b
commit e4a9a03239
22 changed files with 1167 additions and 473 deletions

View File

@ -26,6 +26,7 @@ const path = require('path');
const sudoPrompt = Bluebird.promisifyAll(require('sudo-prompt'));
const utils = require('./utils');
const EXIT_CODES = require('../shared/exit-codes');
const errors = require('../shared/errors');
const packageJSON = require('../../package.json');
// This script is in charge of spawning the writer process and
@ -151,7 +152,7 @@ return isElevated().then((elevated) => {
name: packageJSON.displayName
}).then((stdout, stderr) => {
if (!_.isEmpty(stderr)) {
throw new Error(stderr);
throw errors.createError(stderr);
}
}).catch({
message: 'User did not grant permission.'

View File

@ -1,126 +0,0 @@
/*
* Copyright 2016 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const _ = require('lodash');
const chalk = require('chalk');
/**
* @summary Human-friendly error messages
* @namespace HUMAN_FRIENDLY
* @public
*/
exports.HUMAN_FRIENDLY = {
/* eslint-disable new-cap */
/**
* @property {Function} ENOENT
* @memberof HUMAN_FRIENDLY
* @param {Error} error - error object
* @returns {String} message
*/
ENOENT: (error) => {
return `No such file or directory: ${error.path}`;
},
/**
* @property {Function} EPERM
* @memberof HUMAN_FRIENDLY
* @returns {String} message
*/
EPERM: _.constant('You\'re not authorized to perform this operation'),
/**
* @property {Function} EACCES
* @memberof HUMAN_FRIENDLY
* @returns {String} message
*/
EACCES: _.constant('You don\'t have access to this resource')
/* eslint-enable new-cap */
};
/**
* @summary Get default error message
* @function
* @private
*
* @param {Error} error - error
* @returns {String} error message
*
* @example
* const message = defaultMessageGetter(new Error('foo bar'));
* console.log(message);
* > 'foo bar'
*
* @example
* const message = defaultMessageGetter(new Error());
* console.log(message);
* > 'Unknown error'
*/
const defaultMessageGetter = (error) => {
return error.message || 'Unknown error';
};
/**
* @summary Get error message
* @function
* @public
*
* @param {(String|Error)} error - error
* @returns {String} error message
*
* @example
* const error = new Error('Foo bar');
* error.description = 'This is a fake error';
*
* console.log(errors.getErrorMessage(error));
* > 'Foo bar\n\nThis is a fake error'
*/
exports.getErrorMessage = (error) => {
if (_.isString(error)) {
return exports.getErrorMessage(new Error(error));
}
const message = _.attempt(() => {
const title = _.get(exports.HUMAN_FRIENDLY, error.code, defaultMessageGetter)(error);
return error.code ? `${error.code}: ${title}` : title;
});
if (error.description) {
return `${message}\n\n${error.description}`;
}
return message;
};
/**
* @summary Print an error to stderr
* @function
* @public
*
* @param {(Error|String)} error - error
*
* @example
* errors.print(new Error('Oops!'));
*/
exports.print = (error) => {
const message = exports.getErrorMessage(error);
console.error(chalk.red(message));
};

View File

@ -23,18 +23,22 @@ const visuals = require('resin-cli-visuals');
const form = require('resin-cli-form');
const drivelist = Bluebird.promisifyAll(require('drivelist'));
const writer = require('./writer');
const errors = require('./errors');
const utils = require('./utils');
const options = require('./options');
const robot = require('../shared/robot');
const messages = require('../shared/messages');
const EXIT_CODES = require('../shared/exit-codes');
const errors = require('../shared/errors');
const ARGV_IMAGE_PATH_INDEX = 0;
const imagePath = options._[ARGV_IMAGE_PATH_INDEX];
isElevated().then((elevated) => {
if (!elevated) {
throw new Error(messages.error.elevationRequired());
throw errors.createUserError(
messages.error.elevationRequired(),
'This tool requires special permissions to write to external drives'
);
}
return form.run([
@ -62,7 +66,7 @@ isElevated().then((elevated) => {
});
}).then((answers) => {
if (!answers.yes) {
throw new Error('Aborted');
throw errors.createUserError('Aborted', 'We can\'t proceed without confirmation');
}
const progressBars = {
@ -76,7 +80,7 @@ isElevated().then((elevated) => {
});
if (!selectedDrive) {
throw new Error(`Drive not found: ${answers.drive}`);
throw errors.createUserError('Drive not found', `${answers.drive} doesn't exist`);
}
return writer.writeImage(imagePath, selectedDrive, {
@ -124,7 +128,7 @@ isElevated().then((elevated) => {
return robot.printError(error);
}
errors.print(error);
utils.printError(error);
return Bluebird.resolve();
}).then(() => {
if (error.code === 'EVALIDATION') {

View File

@ -19,9 +19,10 @@
const _ = require('lodash');
const fs = require('fs');
const yargs = require('yargs');
const errors = require('./errors');
const utils = require('./utils');
const robot = require('../shared/robot');
const EXIT_CODES = require('../shared/exit-codes');
const errors = require('../shared/errors');
const packageJSON = require('../../package.json');
/**
@ -92,11 +93,13 @@ module.exports = yargs
// Error reporting
.fail((message, error) => {
const errorObject = error || errors.createUserError(message);
if (robot.isEnabled(process.env)) {
robot.printError(error || message);
robot.printError(errorObject);
} else {
yargs.showHelp();
errors.print(error || message);
utils.printError(errorObject);
}
process.exit(EXIT_CODES.GENERAL_ERROR);
@ -110,7 +113,10 @@ module.exports = yargs
.check((argv) => {
if (robot.isEnabled(process.env) && !argv.drive) {
throw new Error('Missing drive');
throw errors.createUserError(
'Missing drive',
'You need to explicitly pass a drive when enabling robot mode'
);
}
return true;

40
lib/cli/utils.js Normal file
View File

@ -0,0 +1,40 @@
/*
* Copyright 2016 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const chalk = require('chalk');
const errors = require('../shared/errors');
/**
* @summary Print an error to stderr
* @function
* @public
*
* @param {Error} error - error
*
* @example
* utils.printError(new Error('Oops!'));
*/
exports.printError = (error) => {
const title = errors.getTitle(error);
const description = errors.getDescription(error);
console.error(chalk.red(title));
if (description) {
console.error(`\n${chalk.red(description)}`);
}
};

View File

@ -21,6 +21,7 @@ const _ = require('lodash');
const redux = require('redux');
const persistState = require('redux-localstorage');
const constraints = require('../../shared/drive-constraints');
const errors = require('../../shared/errors');
/**
* @summary Application default state
@ -118,11 +119,11 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
case ACTIONS.SET_AVAILABLE_DRIVES: {
if (!action.data) {
throw new Error('Missing drives');
throw errors.createError('Missing drives');
}
if (!_.isArray(action.data) || !_.every(action.data, _.isPlainObject)) {
throw new Error(`Invalid drives: ${action.data}`);
throw errors.createError(`Invalid drives: ${action.data}`);
}
const newState = state.set('availableDrives', Immutable.fromJS(action.data));
@ -169,35 +170,35 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
case ACTIONS.SET_FLASH_STATE: {
if (!state.get('isFlashing')) {
throw new Error('Can\'t set the flashing state when not flashing');
throw errors.createError('Can\'t set the flashing state when not flashing');
}
if (!action.data.type) {
throw new Error('Missing state type');
throw errors.createError('Missing state type');
}
if (!_.isString(action.data.type)) {
throw new Error(`Invalid state type: ${action.data.type}`);
throw errors.createError(`Invalid state type: ${action.data.type}`);
}
if (_.isNil(action.data.percentage)) {
throw new Error('Missing state percentage');
throw errors.createError('Missing state percentage');
}
if (!_.isNumber(action.data.percentage)) {
throw new Error(`Invalid state percentage: ${action.data.percentage}`);
throw errors.createError(`Invalid state percentage: ${action.data.percentage}`);
}
if (_.isNil(action.data.eta)) {
throw new Error('Missing state eta');
throw errors.createError('Missing state eta');
}
if (!_.isNumber(action.data.eta)) {
throw new Error(`Invalid state eta: ${action.data.eta}`);
throw errors.createError(`Invalid state eta: ${action.data.eta}`);
}
if (_.isNil(action.data.speed)) {
throw new Error('Missing state speed');
throw errors.createError('Missing state speed');
}
return state.set('flashState', Immutable.fromJS(action.data));
@ -217,7 +218,7 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
case ACTIONS.UNSET_FLASHING_FLAG: {
if (!action.data) {
throw new Error('Missing results');
throw errors.createError('Missing results');
}
_.defaults(action.data, {
@ -225,19 +226,19 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
});
if (!_.isBoolean(action.data.cancelled)) {
throw new Error(`Invalid results cancelled: ${action.data.cancelled}`);
throw errors.createError(`Invalid results cancelled: ${action.data.cancelled}`);
}
if (action.data.cancelled && action.data.sourceChecksum) {
throw new Error('The sourceChecksum value can\'t exist if the flashing was cancelled');
throw errors.createError('The sourceChecksum value can\'t exist if the flashing was cancelled');
}
if (action.data.sourceChecksum && !_.isString(action.data.sourceChecksum)) {
throw new Error(`Invalid results sourceChecksum: ${action.data.sourceChecksum}`);
throw errors.createError(`Invalid results sourceChecksum: ${action.data.sourceChecksum}`);
}
if (action.data.errorCode && !_.isString(action.data.errorCode) && !_.isNumber(action.data.errorCode)) {
throw new Error(`Invalid results errorCode: ${action.data.errorCode}`);
throw errors.createError(`Invalid results errorCode: ${action.data.errorCode}`);
}
return state
@ -248,26 +249,26 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
case ACTIONS.SELECT_DRIVE: {
if (!action.data) {
throw new Error('Missing drive');
throw errors.createError('Missing drive');
}
if (!_.isString(action.data)) {
throw new Error(`Invalid drive: ${action.data}`);
throw errors.createError(`Invalid drive: ${action.data}`);
}
const selectedDrive = findDrive(state, action.data);
if (!selectedDrive) {
throw new Error(`The drive is not available: ${action.data}`);
throw errors.createError(`The drive is not available: ${action.data}`);
}
if (selectedDrive.get('protected')) {
throw new Error('The drive is write-protected');
throw errors.createError('The drive is write-protected');
}
const image = state.getIn([ 'selection', 'image' ]);
if (image && !constraints.isDriveLargeEnough(selectedDrive.toJS(), image.toJS())) {
throw new Error('The drive is not large enough');
throw errors.createError('The drive is not large enough');
}
return state.setIn([ 'selection', 'drive' ], Immutable.fromJS(action.data));
@ -275,31 +276,31 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
case ACTIONS.SELECT_IMAGE: {
if (!action.data.path) {
throw new Error('Missing image path');
throw errors.createError('Missing image path');
}
if (!_.isString(action.data.path)) {
throw new Error(`Invalid image path: ${action.data.path}`);
throw errors.createError(`Invalid image path: ${action.data.path}`);
}
if (!action.data.size) {
throw new Error('Missing image size');
throw errors.createError('Missing image size');
}
if (!_.isNumber(action.data.size)) {
throw new Error(`Invalid image size: ${action.data.size}`);
throw errors.createError(`Invalid image size: ${action.data.size}`);
}
if (action.data.url && !_.isString(action.data.url)) {
throw new Error(`Invalid image url: ${action.data.url}`);
throw errors.createError(`Invalid image url: ${action.data.url}`);
}
if (action.data.name && !_.isString(action.data.name)) {
throw new Error(`Invalid image name: ${action.data.name}`);
throw errors.createError(`Invalid image name: ${action.data.name}`);
}
if (action.data.logo && !_.isString(action.data.logo)) {
throw new Error(`Invalid image logo: ${action.data.logo}`);
throw errors.createError(`Invalid image logo: ${action.data.logo}`);
}
const selectedDrive = findDrive(state, state.getIn([ 'selection', 'drive' ]));
@ -331,19 +332,19 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
const value = action.data.value;
if (!key) {
throw new Error('Missing setting key');
throw errors.createError('Missing setting key');
}
if (!_.isString(key)) {
throw new Error(`Invalid setting key: ${key}`);
throw errors.createError(`Invalid setting key: ${key}`);
}
if (!DEFAULT_STATE.get('settings').has(key)) {
throw new Error(`Unsupported setting: ${key}`);
throw errors.createError(`Unsupported setting: ${key}`);
}
if (_.isObject(value)) {
throw new Error(`Invalid setting value: ${value}`);
throw errors.createError(`Invalid setting value: ${value}`);
}
return state.setIn([ 'settings', key ], value);

View File

@ -25,6 +25,7 @@ const angular = require('angular');
const username = require('username');
const isRunningInAsar = require('electron-is-running-in-asar');
const app = require('electron').remote.app;
const errors = require('../../shared/errors');
const packageJSON = require('../../../package.json');
// Force Mixpanel snippet to load Mixpanel locally
@ -139,29 +140,6 @@ analytics.service('AnalyticsService', function($log, $window, $mixpanel, Setting
this.logDebug(debugMessage);
};
/**
* @summary Check whether an error should be reported to TrackJS
* @function
* @private
*
* @description
* In order to determine whether the error should be reported, we
* check a property called `report`. For backwards compatibility, and
* to properly handle errors that we don't control, an error without
* this property is reported automatically.
*
* @param {Error} error - error
* @returns {Boolean} whether the error should be reported
*
* @example
* if (AnalyticsService.shouldReportError(new Error('foo'))) {
* console.log('We should report this error');
* }
*/
this.shouldReportError = (error) => {
return !_.has(error, [ 'report' ]) || Boolean(error.report);
};
/**
* @summary Log an exception
* @function
@ -179,7 +157,7 @@ analytics.service('AnalyticsService', function($log, $window, $mixpanel, Setting
if (_.every([
SettingsModel.get('errorReporting'),
isRunningInAsar(),
this.shouldReportError(exception)
errors.shouldReport(exception)
])) {
$window.trackJs.track(exception);
}

View File

@ -46,7 +46,7 @@ error.service('ErrorService', function(AnalyticsService, OSDialogService) {
return;
}
OSDialogService.showError(exception, exception.description);
OSDialogService.showError(exception);
AnalyticsService.logException(exception);
};

View File

@ -19,6 +19,7 @@
const _ = require('lodash');
const electron = require('electron');
const imageStream = require('../../../../image-stream');
const errors = require('../../../../shared/errors');
module.exports = function($q, SupportedFormatsModel) {
@ -146,44 +147,15 @@ module.exports = function($q, SupportedFormatsModel) {
* @function
* @public
*
* @param {(Error|String)} error - error
* @param {String} [description] - error description
* @param {Error} error - error
*
* @example
* OSDialogService.showError(new Error('Foo Bar'));
*
* @example
* OSDialogService.showError(new Error('Foo Bar'), 'A custom description');
*
* @example
* OSDialogService.showError('Foo Bar', 'An error happened!');
*/
this.showError = (error, description) => {
const errorObject = error || {};
// Try to get as most information as possible about the error
// rather than falling back to generic messages right away.
const title = _.attempt(() => {
if (_.isString(errorObject)) {
return errorObject;
}
return errorObject.message || errorObject.code || 'An error ocurred';
});
const message = description || errorObject.stack || JSON.stringify(errorObject) || '';
// Ensure the parameters are strings to prevent the following
// types of obscure errors:
//
// Error: Could not call remote function ''.
// Check that the function signature is correct.
// Underlying error:
// Error processing argument at index 0, conversion failure
//
// This can be thrown if for some reason, either `title` or `message`
// are not strings.
electron.remote.dialog.showErrorBox(title.toString(), message.toString());
this.showError = (error) => {
const title = errors.getTitle(error);
const message = errors.getDescription(error);
electron.remote.dialog.showErrorBox(title, message);
};
};

View File

@ -17,6 +17,7 @@
'use strict';
const electron = require('electron');
const errors = require('../../../../shared/errors');
module.exports = function() {
@ -48,7 +49,7 @@ module.exports = function() {
const PERCENTAGE_MAXIMUM = 100;
if (percentage > PERCENTAGE_MAXIMUM || percentage < PERCENTAGE_MINIMUM) {
throw new Error(`Invalid window progress percentage: ${percentage}`);
throw errors.createError(`Invalid window progress percentage: ${percentage}`);
}
this.currentWindow.setProgressBar(percentage / PERCENTAGE_MAXIMUM);

View File

@ -19,6 +19,7 @@
const _ = require('lodash');
const Bluebird = require('bluebird');
const messages = require('../../../../shared/messages');
const errors = require('../../../../shared/errors');
module.exports = function(
SupportedFormatsModel,
@ -65,10 +66,11 @@ module.exports = function(
*/
this.selectImage = (image) => {
if (!SupportedFormatsModel.isSupportedImage(image.path)) {
OSDialogService.showError('Invalid image', messages.error.invalidImage({
const invalidImageError = errors.createUserError('Invalid image', messages.error.invalidImage({
image
}));
OSDialogService.showError(invalidImageError);
AnalyticsService.logEvent('Invalid image', image);
return;
}

View File

@ -16,6 +16,8 @@
'use strict';
const errors = require('../../../../shared/errors');
/**
* @summary ManifestBind directive
* @function
@ -39,7 +41,7 @@ module.exports = (ManifestBindService) => {
const value = ManifestBindService.get(attributes.manifestBind);
if (!value) {
throw new Error(`ManifestBind: Unknown property \`${attributes.manifestBind}\``);
throw errors.createError(`ManifestBind: Unknown property \`${attributes.manifestBind}\``);
}
element.html(value);

View File

@ -20,6 +20,7 @@ const Bluebird = require('bluebird');
const _ = require('lodash');
const StreamZip = require('node-stream-zip');
const yauzl = Bluebird.promisifyAll(require('yauzl'));
const errors = require('../../shared/errors');
/**
* @summary Get all archive entries
@ -88,7 +89,7 @@ exports.extractFile = (archive, entries, file) => {
if (!_.find(entries, {
name: file
})) {
throw new Error(`Invalid entry: ${file}`);
throw errors.createError(`Invalid entry: ${file}`);
}
yauzl.openAsync(archive, {

View File

@ -22,6 +22,7 @@ const rindle = require('rindle');
const _ = require('lodash');
const PassThroughStream = require('stream').PassThrough;
const supportedFileTypes = require('./supported');
const errors = require('../shared/errors');
/**
* @summary Archive metadata base path
@ -118,9 +119,10 @@ const extractArchiveMetadata = (archive, basePath, options) => {
try {
return JSON.parse(manifest);
} catch (parseError) {
const error = new Error('Invalid archive manifest.json');
error.description = 'The archive manifest.json file is not valid JSON.';
throw error;
throw errors.createUserError(
'Invalid archive manifest.json',
'The archive manifest.json file is not valid JSON'
);
}
});
})
@ -176,10 +178,10 @@ exports.extractImage = (archive, hooks) => {
const VALID_NUMBER_OF_IMAGE_ENTRIES = 1;
if (imageEntries.length !== VALID_NUMBER_OF_IMAGE_ENTRIES) {
const error = new Error('Invalid archive image');
error.description = 'The archive image should contain one and only one top image file.';
error.report = false;
throw error;
throw errors.createUserError(
'Invalid archive image',
'The archive image should contain one and only one top image file'
);
}
const imageEntry = _.first(imageEntries);

View File

@ -23,6 +23,7 @@ const isStream = require('isstream');
const utils = require('./utils');
const handlers = require('./handlers');
const supportedFileTypes = require('./supported');
const errors = require('../shared/errors');
/**
* @summary Get an image stream from a file
@ -64,7 +65,7 @@ exports.getFromFilePath = (file) => {
const type = utils.getArchiveMimeType(file);
if (!_.has(handlers, type)) {
throw new Error('Invalid image');
throw errors.createUserError('Invalid image', `The ${type} format is not supported`);
}
return fs.statAsync(file).then((fileStats) => {

342
lib/shared/errors.js Normal file
View File

@ -0,0 +1,342 @@
/*
* Copyright 2016 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const _ = require('lodash');
/**
* @summary Create an error details object
* @function
* @private
*
* @param {Object} options - options
* @param {(String|Function)} options.title - error title
* @param {(String|Function)} options.description - error description
* @returns {Object} error details object
*
* @example
* const details = createErrorDetails({
* title: (error) => {
* return `An error happened, the code is ${error.code}`;
* },
* description: 'This is the error description'
* });
*/
const createErrorDetails = (options) => {
return _.pick(_.mapValues(options, (value) => {
return _.isFunction(value) ? value : _.constant(value);
}), [ 'title', 'description' ]);
};
/**
* @summary Human-friendly error messages
* @namespace HUMAN_FRIENDLY
* @public
*/
exports.HUMAN_FRIENDLY = {
/* eslint-disable new-cap */
/**
* @namespace ENOENT
* @memberof HUMAN_FRIENDLY
*/
ENOENT: createErrorDetails({
title: (error) => {
return `No such file or directory: ${error.path}`;
},
description: 'The file you\'re trying to access doesn\'t exist'
}),
/**
* @namespace EPERM
* @memberof HUMAN_FRIENDLY
*/
EPERM: createErrorDetails({
title: 'You\'re not authorized to perform this operation',
description: 'Please ensure you have necessary permissions for this task'
}),
/**
* @namespace EACCES
* @memberof HUMAN_FRIENDLY
*/
EACCES: createErrorDetails({
title: 'You don\'t have access to this resource',
description: 'Please ensure you have necessary permissions to access this resource'
}),
/**
* @namespace ENOMEM
* @memberof HUMAN_FRIENDLY
*/
ENOMEM: createErrorDetails({
title: 'Your system ran out of memory',
description: 'Please make sure your system has enough available memory for this task'
})
/* eslint-enable new-cap */
};
/**
* @summary Get user friendly property from an error
* @function
* @private
*
* @param {Error} error - error
* @param {String} property - HUMAN_FRIENDLY property
* @returns {(String|Undefined)} user friendly message
*
* @example
* const error = new Error('My error');
* error.code = 'ENOMEM';
*
* const friendlyDescription = getUserFriendlyMessageProperty(error, 'description');
*
* if (friendlyDescription) {
* console.log(friendlyDescription);
* }
*/
const getUserFriendlyMessageProperty = (error, property) => {
const code = _.get(error, [ 'code' ]);
if (_.isNil(code) || !_.isString(code)) {
return null;
}
return _.invoke(exports.HUMAN_FRIENDLY, [ code, property ], error);
};
/**
* @summary Check whether an error should be reported to TrackJS
* @function
* @public
*
* @description
* In order to determine whether the error should be reported, we
* check a property called `report`. For backwards compatibility, and
* to properly handle errors that we don't control, an error without
* this property is reported automatically.
*
* @param {Error} error - error
* @returns {Boolean} whether the error should be reported
*
* @example
* if (errors.shouldReport(new Error('foo'))) {
* console.log('We should report this error');
* }
*/
exports.shouldReport = (error) => {
return !_.has(error, [ 'report' ]) || Boolean(error.report);
};
/**
* @summary Check if a string is blank
* @function
* @private
*
* @param {String} string - string
* @returns {Boolean} whether the string is blank
*
* @example
* if (isBlank(' ')) {
* console.log('The string is blank');
* }
*/
const isBlank = _.flow([ _.trim, _.isEmpty ]);
/**
* @summary Get the title of an error
* @function
* @public
*
* @description
* Try to get as much information as possible about the error
* rather than falling back to generic messages right away.
*
* @param {Error} error - error
* @returns {String} error title
*
* @example
* const error = new Error('Foo bar');
* const title = errors.getTitle(error);
* console.log(title);
*/
exports.getTitle = (error) => {
if (!_.isError(error) && !_.isPlainObject(error) && !_.isNil(error)) {
return _.toString(error);
}
const codeTitle = getUserFriendlyMessageProperty(error, 'title');
if (!_.isNil(codeTitle)) {
return codeTitle;
}
const message = _.get(error, [ 'message' ]);
if (!isBlank(message)) {
return message;
}
const code = _.get(error, [ 'code' ]);
if (!_.isNil(code) && !isBlank(code)) {
return `Error code: ${code}`;
}
return 'An error ocurred';
};
/**
* @summary Get the description of an error
* @function
* @public
*
* @param {Error} error - error
* @returns {String} error description
*
* @example
* const error = new Error('Foo bar');
* const description = errors.getDescription(error);
* console.log(description);
*/
exports.getDescription = (error) => {
if (!_.isError(error) && !_.isPlainObject(error)) {
return '';
}
if (!isBlank(error.description)) {
return error.description;
}
const codeDescription = getUserFriendlyMessageProperty(error, 'description');
if (!_.isNil(codeDescription)) {
return codeDescription;
}
if (error.stack) {
return error.stack;
}
if (_.isEmpty(error)) {
return '';
}
const INDENTATION_SPACES = 2;
return JSON.stringify(error, null, INDENTATION_SPACES);
};
/**
* @summary Create an error
* @function
* @public
*
* @param {String} title - error title
* @param {String} [description] - error description
* @param {Object} [options] - options
* @param {Boolean} [options.report] - report error
* @returns {Error} error
*
* @example
* const error = errors.createError('Foo', 'Bar');
* throw error;
*/
exports.createError = (title, description, options = {}) => {
if (isBlank(title)) {
throw new Error(`Invalid error title: ${title}`);
}
const error = new Error(title);
error.description = description;
if (!_.isNil(options.report) && !options.report) {
error.report = false;
}
return error;
};
/**
* @summary Create a user error
* @function
* @public
*
* @description
* User errors represent invalid states that the user
* caused, that are not errors on the application itself.
* Therefore, user errors don't get reported to analytics
* and error reporting services.
*
* @param {String} title - error title
* @param {String} [description] - error description
* @returns {Error} user error
*
* @example
* const error = errors.createUserError('Foo', 'Bar');
* throw error;
*/
exports.createUserError = (title, description) => {
return exports.createError(title, description, {
report: false
});
};
/**
* @summary Convert an Error object to a JSON object
* @function
* @public
*
* @param {Error} error - error object
* @returns {Object} json error
*
* @example
* const error = errors.toJSON(new Error('foo'))
*
* console.log(error.message);
* > 'foo'
*/
exports.toJSON = (error) => {
// Handle string error objects to be on the safe side
const isErrorLike = _.isError(error) || _.isPlainObject(error);
const errorObject = isErrorLike ? error : new Error(error);
return {
message: errorObject.message,
description: errorObject.description,
stack: errorObject.stack,
report: errorObject.report,
code: errorObject.code
};
};
/**
* @summary Convert a JSON object to an Error object
* @function
* @public
*
* @param {Error} json - json object
* @returns {Object} error object
*
* @example
* const error = errors.fromJSON(errors.toJSON(new Error('foo')));
*
* console.log(error.message);
* > 'foo'
*/
exports.fromJSON = (json) => {
return _.assign(new Error(json.message), json);
};

View File

@ -110,7 +110,7 @@ functions: `.printError()` and `.recomposeErrorMessage()`.
Here's an example of these functions in action:
```javascript
const error = new Error('This is an error');
const error = errors.createError('This is an error', 'My description');
robot.printError(error);
```

View File

@ -17,6 +17,7 @@
'use strict';
const _ = require('lodash');
const errors = require('../errors');
/**
* @summary Check whether we should emit parseable output
@ -55,7 +56,7 @@ exports.isEnabled = (environment) => {
*/
exports.buildMessage = (title, data = {}) => {
if (!_.isPlainObject(data)) {
throw new Error(`Invalid data: ${data}`);
throw errors.createError(`Invalid data: ${data}`);
}
return JSON.stringify({
@ -88,15 +89,11 @@ exports.parseMessage = (string) => {
try {
output = JSON.parse(string);
} catch (error) {
error.message = 'Invalid message';
error.description = `${string}, ${error.message}`;
throw error;
throw errors.createError('Invalid message', `${string}, ${error.message}`);
}
if (!output.command || !output.data) {
const error = new Error('Invalid message');
error.description = `No command or data: ${string}`;
throw error;
throw errors.createError('Invalid message', `No command or data: ${string}`);
}
return output;
@ -107,7 +104,7 @@ exports.parseMessage = (string) => {
* @function
* @private
*
* @param {(String|Error)} error - error
* @param {Error} error - error
* @returns {String} parseable error message
*
* @example
@ -119,19 +116,9 @@ exports.parseMessage = (string) => {
*
* console.log(error.data.message);
* > 'foo'
*
* error.data.stacktrace === error.stack;
* > true
*/
exports.buildErrorMessage = (error) => {
const errorObject = _.isString(error) ? new Error(error) : error;
return exports.buildMessage('error', {
message: errorObject.message,
description: errorObject.description,
stacktrace: errorObject.stack,
code: errorObject.code
});
return exports.buildMessage('error', errors.toJSON(error));
};
/**
@ -153,10 +140,7 @@ exports.buildErrorMessage = (error) => {
* > 'foo'
*/
exports.recomposeErrorMessage = (message) => {
const error = new Error(message.data.message);
_.assign(error, _.omit(message.data, 'stacktrace'));
error.stack = message.data.stacktrace;
return error;
return errors.fromJSON(message.data);
};
/**
@ -208,7 +192,7 @@ exports.getData = (message) => {
* @function
* @public
*
* @param {(Error|String)} error - error
* @param {Error} error - error
*
* @example
* robot.printError(new Error('This is an error'));

View File

@ -1,132 +0,0 @@
/*
* Copyright 2016 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const m = require('mochainon');
const _ = require('lodash');
const errors = require('../../lib/cli/errors');
describe('CLI: Errors', function() {
describe('.HUMAN_FRIENDLY', function() {
it('should be a plain object', function() {
m.chai.expect(_.isPlainObject(errors.HUMAN_FRIENDLY)).to.be.true;
});
it('should contain function properties', function() {
m.chai.expect(_.every(_.map(errors.HUMAN_FRIENDLY, _.isFunction))).to.be.true;
});
});
describe('.getErrorMessage()', function() {
describe('given errors without code properties', function() {
it('should understand a string error', function() {
const error = 'foo bar';
m.chai.expect(errors.getErrorMessage(error)).to.equal('foo bar');
});
it('should return a generic error message if there is none', function() {
const error = new Error();
m.chai.expect(errors.getErrorMessage(error)).to.equal('Unknown error');
});
it('should return a generic error message if error is an empty string', function() {
const error = '';
m.chai.expect(errors.getErrorMessage(error)).to.equal('Unknown error');
});
it('should return the error message', function() {
const error = new Error('foo bar');
m.chai.expect(errors.getErrorMessage(error)).to.equal('foo bar');
});
it('should make use of a description if there is one', function() {
const error = new Error('foo bar');
error.description = 'This is a description';
m.chai.expect(errors.getErrorMessage(error)).to.equal(_.join([
'foo bar',
'',
'This is a description'
], '\n'));
});
});
describe('given errors with code properties', function() {
it('should provide a friendly message for ENOENT', function() {
const error = new Error('foo bar');
error.code = 'ENOENT';
error.path = 'foo.bar';
const message = 'ENOENT: No such file or directory: foo.bar';
m.chai.expect(errors.getErrorMessage(error)).to.equal(message);
});
it('should provide a friendly message for EPERM', function() {
const error = new Error('foo bar');
error.code = 'EPERM';
const message = 'EPERM: You\'re not authorized to perform this operation';
m.chai.expect(errors.getErrorMessage(error)).to.equal(message);
});
it('should provide a friendly message for EACCES', function() {
const error = new Error('foo bar');
error.code = 'EACCES';
const message = 'EACCES: You don\'t have access to this resource';
m.chai.expect(errors.getErrorMessage(error)).to.equal(message);
});
it('should make use of a description if there is one', function() {
const error = new Error('foo bar');
error.code = 'EPERM';
error.description = 'This is the EPERM description';
const message = _.join([
'EPERM: You\'re not authorized to perform this operation',
'',
'This is the EPERM description'
], '\n');
m.chai.expect(errors.getErrorMessage(error)).to.equal(message);
});
describe('given the code is not recognised', function() {
it('should make use of the error message', function() {
const error = new Error('foo bar');
error.code = 'EFOO';
const message = 'EFOO: foo bar';
m.chai.expect(errors.getErrorMessage(error)).to.equal(message);
});
it('should return a generic error message if no there is no message', function() {
const error = new Error();
error.code = 'EFOO';
m.chai.expect(errors.getErrorMessage(error)).to.equal('EFOO: Unknown error');
});
});
});
});
});

View File

@ -1,73 +0,0 @@
'use strict';
const m = require('mochainon');
const angular = require('angular');
require('angular-mocks');
describe('Browser: Analytics', function() {
beforeEach(angular.mock.module(
require('../../../lib/gui/modules/analytics')
));
describe('AnalyticsService', function() {
let AnalyticsService;
beforeEach(angular.mock.inject(function(_AnalyticsService_) {
AnalyticsService = _AnalyticsService_;
}));
describe('.shouldReportError()', function() {
it('should return true for a basic error', function() {
const error = new Error('foo');
m.chai.expect(AnalyticsService.shouldReportError(error)).to.be.true;
});
it('should return true for an error with a report true property', function() {
const error = new Error('foo');
error.report = true;
m.chai.expect(AnalyticsService.shouldReportError(error)).to.be.true;
});
it('should return false for an error with a report false property', function() {
const error = new Error('foo');
error.report = false;
m.chai.expect(AnalyticsService.shouldReportError(error)).to.be.false;
});
it('should return false for an error with a report undefined property', function() {
const error = new Error('foo');
error.report = undefined;
m.chai.expect(AnalyticsService.shouldReportError(error)).to.be.false;
});
it('should return false for an error with a report null property', function() {
const error = new Error('foo');
error.report = null;
m.chai.expect(AnalyticsService.shouldReportError(error)).to.be.false;
});
it('should return false for an error with a report 0 property', function() {
const error = new Error('foo');
error.report = 0;
m.chai.expect(AnalyticsService.shouldReportError(error)).to.be.false;
});
it('should return true for an error with a report 1 property', function() {
const error = new Error('foo');
error.report = 1;
m.chai.expect(AnalyticsService.shouldReportError(error)).to.be.true;
});
it('should cast the report property to boolean', function() {
const error = new Error('foo');
error.report = '';
m.chai.expect(AnalyticsService.shouldReportError(error)).to.be.false;
});
});
});
});

688
tests/shared/errors.spec.js Normal file
View File

@ -0,0 +1,688 @@
/*
* Copyright 2016 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const m = require('mochainon');
const _ = require('lodash');
const errors = require('../../lib/shared/errors');
describe('Shared: Errors', function() {
describe('.HUMAN_FRIENDLY', function() {
it('should be a plain object', function() {
m.chai.expect(_.isPlainObject(errors.HUMAN_FRIENDLY)).to.be.true;
});
it('should contain title and description function properties', function() {
m.chai.expect(_.every(_.map(errors.HUMAN_FRIENDLY, (error) => {
return _.isFunction(error.title) && _.isFunction(error.description);
}))).to.be.true;
});
});
describe('.shouldReport()', function() {
it('should return true for a string error', function() {
const error = 'foo';
m.chai.expect(errors.shouldReport(error)).to.be.true;
});
it('should return true for a number 0 error', function() {
const error = 0;
m.chai.expect(errors.shouldReport(error)).to.be.true;
});
it('should return true for a number 1 error', function() {
const error = 1;
m.chai.expect(errors.shouldReport(error)).to.be.true;
});
it('should return true for a number -1 error', function() {
const error = -1;
m.chai.expect(errors.shouldReport(error)).to.be.true;
});
it('should return true for an array error', function() {
const error = [ 1, 2, 3 ];
m.chai.expect(errors.shouldReport(error)).to.be.true;
});
it('should return true for an undefined error', function() {
const error = undefined;
m.chai.expect(errors.shouldReport(error)).to.be.true;
});
it('should return true for a null error', function() {
const error = null;
m.chai.expect(errors.shouldReport(error)).to.be.true;
});
it('should return true for an empty object error', function() {
const error = {};
m.chai.expect(errors.shouldReport(error)).to.be.true;
});
it('should return true for a basic error', function() {
const error = new Error('foo');
m.chai.expect(errors.shouldReport(error)).to.be.true;
});
it('should return true for an error with a report true property', function() {
const error = new Error('foo');
error.report = true;
m.chai.expect(errors.shouldReport(error)).to.be.true;
});
it('should return false for an error with a report false property', function() {
const error = new Error('foo');
error.report = false;
m.chai.expect(errors.shouldReport(error)).to.be.false;
});
it('should return false for an error with a report undefined property', function() {
const error = new Error('foo');
error.report = undefined;
m.chai.expect(errors.shouldReport(error)).to.be.false;
});
it('should return false for an error with a report null property', function() {
const error = new Error('foo');
error.report = null;
m.chai.expect(errors.shouldReport(error)).to.be.false;
});
it('should return false for an error with a report 0 property', function() {
const error = new Error('foo');
error.report = 0;
m.chai.expect(errors.shouldReport(error)).to.be.false;
});
it('should return true for an error with a report 1 property', function() {
const error = new Error('foo');
error.report = 1;
m.chai.expect(errors.shouldReport(error)).to.be.true;
});
it('should cast the report property to boolean', function() {
const error = new Error('foo');
error.report = '';
m.chai.expect(errors.shouldReport(error)).to.be.false;
});
});
describe('.getTitle()', function() {
it('should accept a string', function() {
const error = 'This is an error';
m.chai.expect(errors.getTitle(error)).to.equal('This is an error');
});
it('should accept a number 0', function() {
const error = 0;
m.chai.expect(errors.getTitle(error)).to.equal('0');
});
it('should accept a number 1', function() {
const error = 1;
m.chai.expect(errors.getTitle(error)).to.equal('1');
});
it('should accept a number -1', function() {
const error = -1;
m.chai.expect(errors.getTitle(error)).to.equal('-1');
});
it('should accept an array', function() {
const error = [ 0, 1, 2 ];
m.chai.expect(errors.getTitle(error)).to.equal('0,1,2');
});
it('should return a generic error message if the error is an empty object', function() {
const error = {};
m.chai.expect(errors.getTitle(error)).to.equal('An error ocurred');
});
it('should return a generic error message if the error is undefined', function() {
const error = undefined;
m.chai.expect(errors.getTitle(error)).to.equal('An error ocurred');
});
it('should return a generic error message if the error is null', function() {
const error = null;
m.chai.expect(errors.getTitle(error)).to.equal('An error ocurred');
});
it('should return the error message', function() {
const error = new Error('This is an error');
m.chai.expect(errors.getTitle(error)).to.equal('This is an error');
});
it('should return the error code if there is no message', function() {
const error = new Error();
error.code = 'MYERROR';
m.chai.expect(errors.getTitle(error)).to.equal('Error code: MYERROR');
});
it('should prioritise the message over the code', function() {
const error = new Error('Foo bar');
error.code = 'MYERROR';
m.chai.expect(errors.getTitle(error)).to.equal('Foo bar');
});
it('should prioritise the code over the message if the message is an empty string', function() {
const error = new Error('');
error.code = 'MYERROR';
m.chai.expect(errors.getTitle(error)).to.equal('Error code: MYERROR');
});
it('should prioritise the code over the message if the message is a blank string', function() {
const error = new Error(' ');
error.code = 'MYERROR';
m.chai.expect(errors.getTitle(error)).to.equal('Error code: MYERROR');
});
it('should understand an error-like object with a code', function() {
const error = {
code: 'MYERROR'
};
m.chai.expect(errors.getTitle(error)).to.equal('Error code: MYERROR');
});
it('should understand an error-like object with a message', function() {
const error = {
message: 'Hello world'
};
m.chai.expect(errors.getTitle(error)).to.equal('Hello world');
});
it('should understand an error-like object with a message and a code', function() {
const error = {
message: 'Hello world',
code: 'MYERROR'
};
m.chai.expect(errors.getTitle(error)).to.equal('Hello world');
});
it('should display an error code 0', function() {
const error = new Error();
error.code = 0;
m.chai.expect(errors.getTitle(error)).to.equal('Error code: 0');
});
it('should display an error code 1', function() {
const error = new Error();
error.code = 1;
m.chai.expect(errors.getTitle(error)).to.equal('Error code: 1');
});
it('should display an error code -1', function() {
const error = new Error();
error.code = -1;
m.chai.expect(errors.getTitle(error)).to.equal('Error code: -1');
});
it('should not display an empty string error code', function() {
const error = new Error();
error.code = '';
m.chai.expect(errors.getTitle(error)).to.equal('An error ocurred');
});
it('should not display a blank string error code', function() {
const error = new Error();
error.code = ' ';
m.chai.expect(errors.getTitle(error)).to.equal('An error ocurred');
});
it('should return a generic error message if no information was found', function() {
const error = new Error();
m.chai.expect(errors.getTitle(error)).to.equal('An error ocurred');
});
it('should return a generic error message if no code and the message is empty', function() {
const error = new Error('');
m.chai.expect(errors.getTitle(error)).to.equal('An error ocurred');
});
it('should return a generic error message if no code and the message is blank', function() {
const error = new Error(' ');
m.chai.expect(errors.getTitle(error)).to.equal('An error ocurred');
});
it('should rephrase an ENOENT error', function() {
const error = new Error('ENOENT error');
error.path = '/foo/bar';
error.code = 'ENOENT';
m.chai.expect(errors.getTitle(error)).to.equal('No such file or directory: /foo/bar');
});
it('should rephrase an EPERM error', function() {
const error = new Error('EPERM error');
error.code = 'EPERM';
m.chai.expect(errors.getTitle(error)).to.equal('You\'re not authorized to perform this operation');
});
it('should rephrase an EACCES error', function() {
const error = new Error('EACCES error');
error.code = 'EACCES';
m.chai.expect(errors.getTitle(error)).to.equal('You don\'t have access to this resource');
});
it('should rephrase an ENOMEM error', function() {
const error = new Error('ENOMEM error');
error.code = 'ENOMEM';
m.chai.expect(errors.getTitle(error)).to.equal('Your system ran out of memory');
});
});
describe('.getDescription()', function() {
it('should return an empty string if the error is a string', function() {
const error = 'My error';
m.chai.expect(errors.getDescription(error)).to.equal('');
});
it('should return an empty string if the error is a number', function() {
const error = 0;
m.chai.expect(errors.getDescription(error)).to.equal('');
});
it('should return an empty string if the error is an array', function() {
const error = [ 1, 2, 3 ];
m.chai.expect(errors.getDescription(error)).to.equal('');
});
it('should return an empty string if the error is undefined', function() {
const error = undefined;
m.chai.expect(errors.getDescription(error)).to.equal('');
});
it('should return an empty string if the error is null', function() {
const error = null;
m.chai.expect(errors.getDescription(error)).to.equal('');
});
it('should return an empty string if the error is an empty object', function() {
const error = {};
m.chai.expect(errors.getDescription(error)).to.equal('');
});
it('should understand an error-like object with a description', function() {
const error = {
description: 'My description'
};
m.chai.expect(errors.getDescription(error)).to.equal('My description');
});
it('should understand an error-like object with a stack', function() {
const error = {
stack: 'My stack'
};
m.chai.expect(errors.getDescription(error)).to.equal('My stack');
});
it('should understand an error-like object with a description and a stack', function() {
const error = {
description: 'My description',
stack: 'My stack'
};
m.chai.expect(errors.getDescription(error)).to.equal('My description');
});
it('should stringify and beautify an object without any known property', function() {
const error = {
name: 'John Doe',
job: 'Developer'
};
m.chai.expect(errors.getDescription(error)).to.equal([
'{',
' "name": "John Doe",',
' "job": "Developer"',
'}'
].join('\n'));
});
it('should return the stack for a basic error', function() {
const error = new Error('Foo');
m.chai.expect(errors.getDescription(error)).to.equal(error.stack);
});
it('should prefer a description property to a stack', function() {
const error = new Error('Foo');
error.description = 'My description';
m.chai.expect(errors.getDescription(error)).to.equal('My description');
});
it('should return the stack if the description is an empty string', function() {
const error = new Error('Foo');
error.description = '';
m.chai.expect(errors.getDescription(error)).to.equal(error.stack);
});
it('should return the stack if the description is a blank string', function() {
const error = new Error('Foo');
error.description = ' ';
m.chai.expect(errors.getDescription(error)).to.equal(error.stack);
});
it('should get a generic description for ENOENT', function() {
const error = new Error('Foo');
error.code = 'ENOENT';
m.chai.expect(errors.getDescription(error)).to.equal('The file you\'re trying to access doesn\'t exist');
});
it('should get a generic description for EPERM', function() {
const error = new Error('Foo');
error.code = 'EPERM';
m.chai.expect(errors.getDescription(error)).to.equal('Please ensure you have necessary permissions for this task');
});
it('should get a generic description for EACCES', function() {
const error = new Error('Foo');
error.code = 'EACCES';
const message = 'Please ensure you have necessary permissions to access this resource';
m.chai.expect(errors.getDescription(error)).to.equal(message);
});
it('should get a generic description for ENOMEM', function() {
const error = new Error('Foo');
error.code = 'ENOMEM';
const message = 'Please make sure your system has enough available memory for this task';
m.chai.expect(errors.getDescription(error)).to.equal(message);
});
it('should prefer a description property than a code description', function() {
const error = new Error('Foo');
error.code = 'ENOMEM';
error.description = 'Memory error';
m.chai.expect(errors.getDescription(error)).to.equal('Memory error');
});
});
describe('.createError()', function() {
it('should report the resulting error by default', function() {
const error = errors.createError('Foo', 'Something happened');
m.chai.expect(errors.shouldReport(error)).to.be.true;
});
it('should not report the error if report is false', function() {
const error = errors.createError('Foo', 'Something happened', {
report: false
});
m.chai.expect(errors.shouldReport(error)).to.be.false;
});
it('should not report the error if report evaluates to false', function() {
const error = errors.createError('Foo', 'Something happened', {
report: 0
});
m.chai.expect(errors.shouldReport(error)).to.be.false;
});
it('should be an instance of Error', function() {
const error = errors.createError('Foo', 'Something happened');
m.chai.expect(error).to.be.an.instanceof(Error);
});
it('should correctly add both a message and a description', function() {
const error = errors.createError('Foo', 'Something happened');
m.chai.expect(errors.getTitle(error)).to.equal('Foo');
m.chai.expect(errors.getDescription(error)).to.equal('Something happened');
});
it('should correctly add only a message', function() {
const error = errors.createError('Foo');
m.chai.expect(errors.getTitle(error)).to.equal('Foo');
m.chai.expect(errors.getDescription(error)).to.equal(error.stack);
});
it('should ignore an empty description', function() {
const error = errors.createError('Foo', '');
m.chai.expect(errors.getDescription(error)).to.equal(error.stack);
});
it('should ignore a blank description', function() {
const error = errors.createError('Foo', ' ');
m.chai.expect(errors.getDescription(error)).to.equal(error.stack);
});
it('should throw if no message', function() {
m.chai.expect(() => {
errors.createError();
}).to.throw('Invalid error title: undefined');
});
it('should throw if message is empty', function() {
m.chai.expect(() => {
errors.createError('');
}).to.throw('Invalid error title: ');
});
it('should throw if message is blank', function() {
m.chai.expect(() => {
errors.createError(' ');
}).to.throw('Invalid error title: ');
});
});
describe('.createUserError()', function() {
it('should not report the resulting error', function() {
const error = errors.createUserError('Foo', 'Something happened');
m.chai.expect(errors.shouldReport(error)).to.be.false;
});
it('should be an instance of Error', function() {
const error = errors.createUserError('Foo', 'Something happened');
m.chai.expect(error).to.be.an.instanceof(Error);
});
it('should correctly add both a message and a description', function() {
const error = errors.createUserError('Foo', 'Something happened');
m.chai.expect(errors.getTitle(error)).to.equal('Foo');
m.chai.expect(errors.getDescription(error)).to.equal('Something happened');
});
it('should correctly add only a message', function() {
const error = errors.createUserError('Foo');
m.chai.expect(errors.getTitle(error)).to.equal('Foo');
m.chai.expect(errors.getDescription(error)).to.equal(error.stack);
});
it('should ignore an empty description', function() {
const error = errors.createUserError('Foo', '');
m.chai.expect(errors.getDescription(error)).to.equal(error.stack);
});
it('should ignore a blank description', function() {
const error = errors.createUserError('Foo', ' ');
m.chai.expect(errors.getDescription(error)).to.equal(error.stack);
});
it('should throw if no message', function() {
m.chai.expect(() => {
errors.createUserError();
}).to.throw('Invalid error title: undefined');
});
it('should throw if message is empty', function() {
m.chai.expect(() => {
errors.createUserError('');
}).to.throw('Invalid error title: ');
});
it('should throw if message is blank', function() {
m.chai.expect(() => {
errors.createUserError(' ');
}).to.throw('Invalid error title: ');
});
});
describe('.toJSON()', function() {
it('should convert a simple error', function() {
const error = new Error('My error');
m.chai.expect(errors.toJSON(error)).to.deep.equal({
code: undefined,
description: undefined,
message: 'My error',
stack: error.stack,
report: undefined
});
});
it('should convert an error with a description', function() {
const error = new Error('My error');
error.description = 'My description';
m.chai.expect(errors.toJSON(error)).to.deep.equal({
code: undefined,
description: 'My description',
message: 'My error',
stack: error.stack,
report: undefined
});
});
it('should convert an error with a code', function() {
const error = new Error('My error');
error.code = 'ENOENT';
m.chai.expect(errors.toJSON(error)).to.deep.equal({
code: 'ENOENT',
description: undefined,
message: 'My error',
stack: error.stack,
report: undefined
});
});
it('should convert an error with a description and a code', function() {
const error = new Error('My error');
error.description = 'My description';
error.code = 'ENOENT';
m.chai.expect(errors.toJSON(error)).to.deep.equal({
code: 'ENOENT',
description: 'My description',
message: 'My error',
stack: error.stack,
report: undefined
});
});
it('should convert an error with a report value', function() {
const error = new Error('My error');
error.report = true;
m.chai.expect(errors.toJSON(error)).to.deep.equal({
code: undefined,
description: undefined,
message: 'My error',
stack: error.stack,
report: true
});
});
it('should convert an error without a message', function() {
const error = new Error();
m.chai.expect(errors.toJSON(error)).to.deep.equal({
code: undefined,
description: undefined,
message: '',
stack: error.stack,
report: undefined
});
});
});
describe('.fromJSON()', function() {
it('should return an Error object', function() {
const error = new Error('My error');
const result = errors.fromJSON(errors.toJSON(error));
m.chai.expect(result).to.be.an.instanceof(Error);
});
it('should convert a simple JSON error', function() {
const error = new Error('My error');
const result = errors.fromJSON(errors.toJSON(error));
m.chai.expect(result.message).to.equal(error.message);
m.chai.expect(result.description).to.equal(error.description);
m.chai.expect(result.code).to.equal(error.code);
m.chai.expect(result.stack).to.equal(error.stack);
m.chai.expect(result.report).to.equal(error.report);
});
it('should convert a JSON error with a description', function() {
const error = new Error('My error');
error.description = 'My description';
const result = errors.fromJSON(errors.toJSON(error));
m.chai.expect(result.message).to.equal(error.message);
m.chai.expect(result.description).to.equal(error.description);
m.chai.expect(result.code).to.equal(error.code);
m.chai.expect(result.stack).to.equal(error.stack);
m.chai.expect(result.report).to.equal(error.report);
});
it('should convert a JSON error with a code', function() {
const error = new Error('My error');
error.code = 'ENOENT';
const result = errors.fromJSON(errors.toJSON(error));
m.chai.expect(result.message).to.equal(error.message);
m.chai.expect(result.description).to.equal(error.description);
m.chai.expect(result.code).to.equal(error.code);
m.chai.expect(result.stack).to.equal(error.stack);
m.chai.expect(result.report).to.equal(error.report);
});
it('should convert a JSON error with a report value', function() {
const error = new Error('My error');
error.report = false;
const result = errors.fromJSON(errors.toJSON(error));
m.chai.expect(result.message).to.equal(error.message);
m.chai.expect(result.description).to.equal(error.description);
m.chai.expect(result.code).to.equal(error.code);
m.chai.expect(result.stack).to.equal(error.stack);
m.chai.expect(result.report).to.equal(error.report);
});
});
});

View File

@ -101,7 +101,7 @@ describe('Shared: Robot', function() {
command: 'error',
data: {
message: 'foo',
stacktrace: error.stack
stack: error.stack
}
});
});
@ -116,7 +116,7 @@ describe('Shared: Robot', function() {
data: {
message: 'foo',
description: 'error description',
stacktrace: error.stack
stack: error.stack
}
});
});
@ -131,7 +131,7 @@ describe('Shared: Robot', function() {
data: {
message: 'foo',
code: 'MYERROR',
stacktrace: error.stack
stack: error.stack
}
});
});
@ -139,8 +139,8 @@ describe('Shared: Robot', function() {
it('should handle a string error', function() {
const message = JSON.parse(robot.buildErrorMessage('foo'));
m.chai.expect(message.data.message).to.equal('foo');
m.chai.expect(message.data.stacktrace).to.be.a.string;
m.chai.expect(_.isEmpty(message.data.stacktrace)).to.be.false;
m.chai.expect(message.data.stack).to.be.a.string;
m.chai.expect(_.isEmpty(message.data.stack)).to.be.false;
});
});