mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-21 22:47:48 +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 { getHelp } from './utils/messages';
|
||||
|
||||
// Support stubbing of module functions.
|
||||
let ErrorsModule: any;
|
||||
|
||||
export class ExpectedError extends TypedError {}
|
||||
|
||||
export class NotLoggedInError extends ExpectedError {}
|
||||
@ -135,11 +138,17 @@ const EXPECTED_ERROR_REGEXES = [
|
||||
/^BalenaAmbiguousApplication:/, // balena-sdk
|
||||
/^BalenaApplicationNotFound:/, // balena-sdk
|
||||
/^BalenaDeviceNotFound:/, // balena-sdk
|
||||
/^BalenaExpiredToken:/, // balena-sdk
|
||||
/^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
|
||||
];
|
||||
|
||||
// Support unit testing of handleError
|
||||
async function getSentry() {
|
||||
return await import('@sentry/node');
|
||||
}
|
||||
|
||||
export async function handleError(error: any) {
|
||||
// Set appropriate exitCode
|
||||
process.exitCode =
|
||||
@ -149,20 +158,15 @@ export async function handleError(error: any) {
|
||||
|
||||
// Handle non-Error objects (probably strings)
|
||||
if (!(error instanceof Error)) {
|
||||
printErrorMessage(String(error));
|
||||
ErrorsModule.printErrorMessage(String(error));
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare message
|
||||
const message = [interpret(error)];
|
||||
|
||||
if (error.stack) {
|
||||
if (process.env.DEBUG) {
|
||||
message.push('\n' + error.stack);
|
||||
} else {
|
||||
// Include first line of stacktrace
|
||||
message.push('\n' + error.stack.split(`\n`)[0]);
|
||||
}
|
||||
if (error.stack && process.env.DEBUG) {
|
||||
message.push('\n' + error.stack);
|
||||
}
|
||||
|
||||
// Expected?
|
||||
@ -172,14 +176,20 @@ export async function handleError(error: any) {
|
||||
|
||||
// Output/report error
|
||||
if (isExpectedError) {
|
||||
printExpectedErrorMessage(message.join('\n'));
|
||||
ErrorsModule.printExpectedErrorMessage(message.join('\n'));
|
||||
} else {
|
||||
printErrorMessage(message.join('\n'));
|
||||
ErrorsModule.printErrorMessage(message.join('\n'));
|
||||
|
||||
// Report "unexpected" errors via Sentry.io
|
||||
const Sentry = await import('@sentry/node');
|
||||
const Sentry = await ErrorsModule.getSentry();
|
||||
Sentry.captureException(error);
|
||||
await Sentry.close(1000);
|
||||
try {
|
||||
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.
|
||||
// The exit error code was set above through `process.exitCode`.
|
||||
process.exit();
|
||||
@ -223,3 +233,14 @@ export function exitWithExpectedError(message: string | Error): never {
|
||||
printErrorMessage(message);
|
||||
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",
|
||||
"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: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",
|
||||
"ci": "npm run test && npm run catch-uncommitted",
|
||||
"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