mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-01-03 04:26:39 +00:00
Merge pull request #1827 from balena-io/errors-tests
Add unit tests for errors module
This commit is contained in:
commit
df440f0580
@ -21,6 +21,9 @@ import { TypedError } from 'typed-error';
|
|||||||
import { getChalk } from './utils/lazy';
|
import { getChalk } from './utils/lazy';
|
||||||
import { getHelp } from './utils/messages';
|
import { getHelp } from './utils/messages';
|
||||||
|
|
||||||
|
// Support stubbing of module functions.
|
||||||
|
let ErrorsModule: any;
|
||||||
|
|
||||||
export class ExpectedError extends TypedError {}
|
export class ExpectedError extends TypedError {}
|
||||||
|
|
||||||
export class NotLoggedInError extends ExpectedError {}
|
export class NotLoggedInError extends ExpectedError {}
|
||||||
@ -135,11 +138,17 @@ const EXPECTED_ERROR_REGEXES = [
|
|||||||
/^BalenaAmbiguousApplication:/, // balena-sdk
|
/^BalenaAmbiguousApplication:/, // balena-sdk
|
||||||
/^BalenaApplicationNotFound:/, // balena-sdk
|
/^BalenaApplicationNotFound:/, // balena-sdk
|
||||||
/^BalenaDeviceNotFound:/, // balena-sdk
|
/^BalenaDeviceNotFound:/, // balena-sdk
|
||||||
|
/^BalenaExpiredToken:/, // balena-sdk
|
||||||
/^Missing \w+$/, // Capitano, oclif parser: RequiredArgsError, RequiredFlagError
|
/^Missing \w+$/, // Capitano, oclif parser: RequiredArgsError, RequiredFlagError
|
||||||
/^Unexpected arguments?:/, // oclif parser: UnexpectedArgsError
|
/^Unexpected argument/, // oclif parser: UnexpectedArgsError
|
||||||
/to be one of/, // oclif parser: FlagInvalidOptionError, ArgInvalidOptionError
|
/to be one of/, // oclif parser: FlagInvalidOptionError, ArgInvalidOptionError
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Support unit testing of handleError
|
||||||
|
async function getSentry() {
|
||||||
|
return await import('@sentry/node');
|
||||||
|
}
|
||||||
|
|
||||||
export async function handleError(error: any) {
|
export async function handleError(error: any) {
|
||||||
// Set appropriate exitCode
|
// Set appropriate exitCode
|
||||||
process.exitCode =
|
process.exitCode =
|
||||||
@ -149,20 +158,15 @@ export async function handleError(error: any) {
|
|||||||
|
|
||||||
// Handle non-Error objects (probably strings)
|
// Handle non-Error objects (probably strings)
|
||||||
if (!(error instanceof Error)) {
|
if (!(error instanceof Error)) {
|
||||||
printErrorMessage(String(error));
|
ErrorsModule.printErrorMessage(String(error));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare message
|
// Prepare message
|
||||||
const message = [interpret(error)];
|
const message = [interpret(error)];
|
||||||
|
|
||||||
if (error.stack) {
|
if (error.stack && process.env.DEBUG) {
|
||||||
if (process.env.DEBUG) {
|
|
||||||
message.push('\n' + error.stack);
|
message.push('\n' + error.stack);
|
||||||
} else {
|
|
||||||
// Include first line of stacktrace
|
|
||||||
message.push('\n' + error.stack.split(`\n`)[0]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expected?
|
// Expected?
|
||||||
@ -172,14 +176,20 @@ export async function handleError(error: any) {
|
|||||||
|
|
||||||
// Output/report error
|
// Output/report error
|
||||||
if (isExpectedError) {
|
if (isExpectedError) {
|
||||||
printExpectedErrorMessage(message.join('\n'));
|
ErrorsModule.printExpectedErrorMessage(message.join('\n'));
|
||||||
} else {
|
} else {
|
||||||
printErrorMessage(message.join('\n'));
|
ErrorsModule.printErrorMessage(message.join('\n'));
|
||||||
|
|
||||||
// Report "unexpected" errors via Sentry.io
|
// Report "unexpected" errors via Sentry.io
|
||||||
const Sentry = await import('@sentry/node');
|
const Sentry = await ErrorsModule.getSentry();
|
||||||
Sentry.captureException(error);
|
Sentry.captureException(error);
|
||||||
|
try {
|
||||||
await Sentry.close(1000);
|
await Sentry.close(1000);
|
||||||
|
} catch (e) {
|
||||||
|
if (process.env.DEBUG) {
|
||||||
|
console.error('Timeout reporting error to sentry.io');
|
||||||
|
}
|
||||||
|
}
|
||||||
// Unhandled/unexpected error: ensure that the process terminates.
|
// Unhandled/unexpected error: ensure that the process terminates.
|
||||||
// The exit error code was set above through `process.exitCode`.
|
// The exit error code was set above through `process.exitCode`.
|
||||||
process.exit();
|
process.exit();
|
||||||
@ -223,3 +233,14 @@ export function exitWithExpectedError(message: string | Error): never {
|
|||||||
printErrorMessage(message);
|
printErrorMessage(message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Support stubbing of module functions.
|
||||||
|
export default ErrorsModule = {
|
||||||
|
ExpectedError,
|
||||||
|
NotLoggedInError,
|
||||||
|
getSentry,
|
||||||
|
handleError,
|
||||||
|
printErrorMessage,
|
||||||
|
printExpectedErrorMessage,
|
||||||
|
exitWithExpectedError,
|
||||||
|
};
|
||||||
|
@ -55,6 +55,7 @@
|
|||||||
"pretest": "npm run build",
|
"pretest": "npm run build",
|
||||||
"test": "mocha --timeout 6000 -r ts-node/register/transpile-only \"tests/**/*.spec.ts\"",
|
"test": "mocha --timeout 6000 -r ts-node/register/transpile-only \"tests/**/*.spec.ts\"",
|
||||||
"test:fast": "npm run build:fast && mocha --timeout 6000 -r ts-node/register/transpile-only \"tests/**/*.spec.ts\"",
|
"test:fast": "npm run build:fast && mocha --timeout 6000 -r ts-node/register/transpile-only \"tests/**/*.spec.ts\"",
|
||||||
|
"test:only": "npm run build:fast && mocha --timeout 6000 -r ts-node/register/transpile-only \"tests/**/${npm_config_test}.spec.ts\"",
|
||||||
"catch-uncommitted": "ts-node --transpile-only automation/run.ts catch-uncommitted",
|
"catch-uncommitted": "ts-node --transpile-only automation/run.ts catch-uncommitted",
|
||||||
"ci": "npm run test && npm run catch-uncommitted",
|
"ci": "npm run test && npm run catch-uncommitted",
|
||||||
"watch": "gulp watch",
|
"watch": "gulp watch",
|
||||||
|
196
tests/errors.spec.ts
Normal file
196
tests/errors.spec.ts
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2020 Balena Ltd.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import * as sinon from 'sinon';
|
||||||
|
import ErrorsModule from '../build/errors';
|
||||||
|
import { getHelp } from '../build/utils/messages';
|
||||||
|
|
||||||
|
function red(s: string) {
|
||||||
|
if (process.env.CI) {
|
||||||
|
// If CI, don't color.
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return `\u001b[31m${s}\u001b[39m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('handleError() function', () => {
|
||||||
|
const sandbox = sinon.createSandbox();
|
||||||
|
let printErrorMessage: any;
|
||||||
|
let printExpectedErrorMessage: any;
|
||||||
|
let captureException: any;
|
||||||
|
let processExit: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
printErrorMessage = sandbox.stub(ErrorsModule, 'printErrorMessage');
|
||||||
|
printExpectedErrorMessage = sandbox.stub(
|
||||||
|
ErrorsModule,
|
||||||
|
'printExpectedErrorMessage',
|
||||||
|
);
|
||||||
|
captureException = sinon.stub();
|
||||||
|
// @ts-ignore
|
||||||
|
sandbox.stub(ErrorsModule, 'getSentry').resolves({ captureException });
|
||||||
|
processExit = sandbox.stub(process, 'exit');
|
||||||
|
|
||||||
|
// Force debug mode off (currently set to true in CI env)
|
||||||
|
sandbox.stub(process, 'env').value({ DEBUG: false });
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call printErrorMessage and exit when passed a string', async () => {
|
||||||
|
const errorString = 'a string';
|
||||||
|
|
||||||
|
await ErrorsModule.handleError(errorString);
|
||||||
|
|
||||||
|
expect(printErrorMessage.calledOnce).to.be.true;
|
||||||
|
expect(printErrorMessage.getCall(0).args[0]).to.equal(errorString);
|
||||||
|
|
||||||
|
expect(printExpectedErrorMessage.notCalled).to.be.true;
|
||||||
|
expect(captureException.notCalled).to.be.true;
|
||||||
|
expect(processExit.notCalled).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should process ExpectedErrors as expected', async () => {
|
||||||
|
const errorMessage = 'an expected error';
|
||||||
|
const error = new ErrorsModule.ExpectedError(errorMessage);
|
||||||
|
|
||||||
|
await ErrorsModule.handleError(error);
|
||||||
|
|
||||||
|
expect(printExpectedErrorMessage.calledOnce).to.be.true;
|
||||||
|
expect(printExpectedErrorMessage.getCall(0).args[0]).to.equal(errorMessage);
|
||||||
|
|
||||||
|
expect(printErrorMessage.notCalled).to.be.true;
|
||||||
|
expect(captureException.notCalled).to.be.true;
|
||||||
|
expect(processExit.notCalled).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should process subclasses of ExpectedErrors as expected', async () => {
|
||||||
|
const errorMessage = 'an expected error';
|
||||||
|
const error = new ErrorsModule.NotLoggedInError(errorMessage);
|
||||||
|
|
||||||
|
await ErrorsModule.handleError(error);
|
||||||
|
|
||||||
|
expect(printExpectedErrorMessage.calledOnce).to.be.true;
|
||||||
|
expect(printExpectedErrorMessage.getCall(0).args[0]).to.equal(errorMessage);
|
||||||
|
|
||||||
|
expect(printErrorMessage.notCalled).to.be.true;
|
||||||
|
expect(captureException.notCalled).to.be.true;
|
||||||
|
expect(processExit.notCalled).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should process unexpected errors correctly (no debug)', async () => {
|
||||||
|
const errorMessage = 'an unexpected error';
|
||||||
|
await ErrorsModule.handleError(new Error(errorMessage));
|
||||||
|
|
||||||
|
expect(printErrorMessage.calledOnce).to.be.true;
|
||||||
|
expect(printErrorMessage.getCall(0).args[0]).to.equal(errorMessage);
|
||||||
|
expect(captureException.calledOnce).to.be.true;
|
||||||
|
expect(processExit.calledOnce).to.be.true;
|
||||||
|
|
||||||
|
expect(printExpectedErrorMessage.notCalled);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should process unexpected errors correctly (debug)', async () => {
|
||||||
|
sandbox.stub(process, 'env').value({ DEBUG: true });
|
||||||
|
|
||||||
|
const errorMessage = 'an unexpected error';
|
||||||
|
const error = new Error(errorMessage);
|
||||||
|
await ErrorsModule.handleError(error);
|
||||||
|
|
||||||
|
const expectedMessage = errorMessage + '\n\n' + error.stack;
|
||||||
|
|
||||||
|
expect(printErrorMessage.calledOnce).to.be.true;
|
||||||
|
expect(printErrorMessage.getCall(0).args[0]).to.equal(expectedMessage);
|
||||||
|
expect(captureException.calledOnce).to.be.true;
|
||||||
|
expect(processExit.calledOnce).to.be.true;
|
||||||
|
|
||||||
|
expect(printExpectedErrorMessage.notCalled);
|
||||||
|
});
|
||||||
|
|
||||||
|
const messagesToMatch = [
|
||||||
|
'BalenaAmbiguousApplication:',
|
||||||
|
'BalenaApplicationNotFound:',
|
||||||
|
'BalenaDeviceNotFound:',
|
||||||
|
'BalenaExpiredToken:',
|
||||||
|
'Missing argument',
|
||||||
|
'Missing arguments',
|
||||||
|
'Unexpected argument',
|
||||||
|
'Unexpected arguments',
|
||||||
|
'to be one of',
|
||||||
|
];
|
||||||
|
|
||||||
|
messagesToMatch.forEach(message => {
|
||||||
|
it(`should match as expected: "${message}"`, async () => {
|
||||||
|
await ErrorsModule.handleError(new Error(message));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
printExpectedErrorMessage.calledOnce,
|
||||||
|
`Pattern not expected: ${message}`,
|
||||||
|
).to.be.true;
|
||||||
|
|
||||||
|
expect(printErrorMessage.notCalled).to.be.true;
|
||||||
|
expect(captureException.notCalled).to.be.true;
|
||||||
|
expect(processExit.notCalled).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('printErrorMessage() function', () => {
|
||||||
|
it('should correctly output message', () => {
|
||||||
|
const consoleError = sinon.spy(console, 'error');
|
||||||
|
|
||||||
|
const errorMessageLines = [
|
||||||
|
'first line should be red',
|
||||||
|
'second line should not be red',
|
||||||
|
'third line should not be red',
|
||||||
|
];
|
||||||
|
|
||||||
|
const inputMessage = errorMessageLines.join('\n');
|
||||||
|
const expectedOutputMessages = [
|
||||||
|
red(errorMessageLines[0]),
|
||||||
|
errorMessageLines[1],
|
||||||
|
errorMessageLines[2],
|
||||||
|
];
|
||||||
|
|
||||||
|
ErrorsModule.printErrorMessage(inputMessage);
|
||||||
|
|
||||||
|
expect(consoleError.callCount).to.equal(4);
|
||||||
|
expect(consoleError.getCall(0).args[0]).to.equal(expectedOutputMessages[0]);
|
||||||
|
expect(consoleError.getCall(1).args[0]).to.equal(expectedOutputMessages[1]);
|
||||||
|
expect(consoleError.getCall(2).args[0]).to.equal(expectedOutputMessages[2]);
|
||||||
|
expect(consoleError.getCall(3).args[0]).to.equal(`\n${getHelp}\n`);
|
||||||
|
|
||||||
|
consoleError.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('printExpectedErrorMessage() function', () => {
|
||||||
|
it('should correctly output message', () => {
|
||||||
|
const consoleError = sinon.spy(console, 'error');
|
||||||
|
|
||||||
|
const errorMessage = ['first line', 'second line'].join('\n');
|
||||||
|
|
||||||
|
ErrorsModule.printExpectedErrorMessage(errorMessage);
|
||||||
|
|
||||||
|
expect(consoleError.calledOnce).to.be.true;
|
||||||
|
expect(consoleError.getCall(0).args[0]).to.equal(errorMessage + '\n');
|
||||||
|
|
||||||
|
consoleError.restore();
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user