diff --git a/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts b/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts index 75dcda5cb3..1da112d69c 100644 --- a/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts +++ b/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts @@ -43,6 +43,7 @@ import { Bools, Logger, LoggerProvider, + ExceptionHelper, Servers, } from "@hyperledger/cactus-common"; @@ -241,16 +242,16 @@ export class ApiServer { return { addressInfoCockpit, addressInfoApi, addressInfoGrpc }; } catch (ex) { - const errorMessage = `Failed to start ApiServer: ${ex.stack}`; - this.log.error(errorMessage); + const context = "Failed to start ApiServer"; + this.log.exception(ex, context); this.log.error(`Attempting shutdown...`); try { await this.shutdown(); this.log.info(`Server shut down after crash OK`); } catch (ex) { - this.log.error(ApiServer.E_POST_CRASH_SHUTDOWN, ex); + this.log.exception(ex, ApiServer.E_POST_CRASH_SHUTDOWN); } - throw new Error(errorMessage); + throw ExceptionHelper.newRuntimeError(ex, context); } } @@ -296,11 +297,11 @@ export class ApiServer { await this.getPluginImportsCount(), ); return this.pluginRegistry; - } catch (e) { + } catch (ex) { this.pluginRegistry = new PluginRegistry({ plugins: [] }); - const errorMessage = `Failed init PluginRegistry: ${e.stack}`; - this.log.error(errorMessage); - throw new Error(errorMessage); + const context = "Failed to init PluginRegistry"; + this.log.exception(ex, context); + throw ExceptionHelper.newRuntimeError(ex, context); } } @@ -357,15 +358,10 @@ export class ApiServer { await plugin.onPluginInit(); return plugin; - } catch (error) { - const errorMessage = `${fnTag} failed instantiating plugin '${packageName}' with the instanceId '${options.instanceId}'`; - this.log.error(errorMessage, error); - - if (error instanceof Error) { - throw new RuntimeError(errorMessage, error); - } else { - throw new RuntimeError(errorMessage, JSON.stringify(error)); - } + } catch (ex) { + const context = `${fnTag} failed instantiating plugin '${packageName}' with the instanceId '${options.instanceId}'`; + this.log.exception(ex, context); + throw ExceptionHelper.newRuntimeError(ex, context); } } @@ -387,9 +383,9 @@ export class ApiServer { await fs.mkdirp(pluginPackageDir); this.log.debug(`${pkgName} plugin package dir: %o`, pluginPackageDir); } catch (ex) { - const errorMessage = + const context = "Could not create plugin installation directory, check the file-system permissions."; - throw new RuntimeError(errorMessage, ex); + throw ExceptionHelper.newRuntimeError(ex, context); } try { lmify.setPackageManager("npm"); @@ -408,18 +404,13 @@ export class ApiServer { ]); this.log.debug("%o install result: %o", pkgName, out); if (out.exitCode !== 0) { - throw new RuntimeError("Non-zero exit code: ", JSON.stringify(out)); + throw ExceptionHelper.newRuntimeError(out, "Non-zero exit code"); } this.log.info(`Installed ${pkgName} OK`); } catch (ex) { - const errorMessage = `${fnTag} failed installing plugin '${pkgName}`; - this.log.error(errorMessage, ex); - - if (ex instanceof Error) { - throw new RuntimeError(errorMessage, ex); - } else { - throw new RuntimeError(errorMessage, JSON.stringify(ex)); - } + const context = `${fnTag} failed installing plugin '${pkgName}`; + this.log.exception(ex, context); + throw ExceptionHelper.newRuntimeError(ex, context); } } @@ -456,7 +447,7 @@ export class ApiServer { await new Promise((resolve, reject) => { this.grpcServer.tryShutdown((ex?: Error) => { if (ex) { - this.log.error("Failed to shut down gRPC server: ", ex); + this.log.exception(ex, "Failed to shut down gRPC server"); reject(ex); } else { resolve(); diff --git a/packages/cactus-common/package.json b/packages/cactus-common/package.json index 374d6d59e3..bc46db6852 100644 --- a/packages/cactus-common/package.json +++ b/packages/cactus-common/package.json @@ -52,6 +52,11 @@ "name": "Peter Somogyvari", "email": "peter.somogyvari@accenture.com", "url": "https://accenture.com" + }, + { + "name": "Michael Courtin", + "email": "michael.courtin@accenture.com", + "url": "https://accenture.com" } ], "license": "Apache-2.0", @@ -60,6 +65,7 @@ }, "homepage": "https://github.com/hyperledger/cactus#readme", "dependencies": { + "fast-safe-stringify": "2.1.1", "json-stable-stringify": "1.0.1", "key-encoder": "2.0.3", "loglevel": "1.7.1", diff --git a/packages/cactus-common/src/main/typescript/exception/exception-helper.ts b/packages/cactus-common/src/main/typescript/exception/exception-helper.ts new file mode 100644 index 0000000000..2fcba2623b --- /dev/null +++ b/packages/cactus-common/src/main/typescript/exception/exception-helper.ts @@ -0,0 +1,333 @@ +import { RuntimeError } from "run-time-error"; +import stringify from "fast-safe-stringify"; +import { Logger } from "../logging/logger"; + +export class ExceptionHelper { + /** + * Fetches the exception-content and submits it to the logger + * + * @param logger logger created thru the LoggerProvider.getOrCreate() + * @param exception as an arbitrary exception of whatever type and format it might be + * @param optionalAdditionalContext as optional on top info / context on the exception scenario + */ + public static logException( + logger: Logger, + exception: unknown, + optionalAdditionalContext = "", + ): void { + const exceptionContent = this.getExceptionContent( + exception, + optionalAdditionalContext, + ); + + // log to destination + logger.error(...exceptionContent); + } + + /** + * Creates a new RuntimeError based on the incoming exception and context to preserve the complete hierarchy of exception information + * + * @param exception as an arbitrary exception of whatever type and format it might be + * @param context providing context about the exception scenario + * @returns a new RuntimeError created from exception to preserve the complete hierarchy of exception information + */ + public static newRuntimeError( + exception: unknown, + context: string, + ): RuntimeError { + const exceptionMessageInfo = this.getExceptionMessageInfo(exception); + const exceptionStackInfo = this.getExceptionStackInfo(exception); + let content = ""; + let runtimeError: RuntimeError | undefined = undefined; + + if (exception instanceof Error) { + // scenario 1: exception is already an instance of Error / RuntimeError -> can be used directly + runtimeError = new RuntimeError(context, exception); + } else { + // scenario 2: exception is of custom type + // -> need to fetch content + if (exceptionMessageInfo.containsCompleteStringifiedException) { + // the custom exception does not contain a message property therefore + // the whole exception needed to be stringified as resulting message + // -> no need to extra append stack as it would result in partially duplicated information + content = exceptionMessageInfo.message; + } else { + content = + exceptionMessageInfo.message + " - " + exceptionStackInfo.stack; + } + + runtimeError = new RuntimeError(context, content); + } + + return runtimeError; + } + + /** + * + * @param exception as an arbitrary exception of whatever type and format it might be + * @param optionalAdditionalContext as optional on top info / context on the exception scenario + * @returns an array of string information about the exception like: + * - message, + * - stack + * - other properties (depending on the type of exception) + */ + public static getExceptionContent( + exception: unknown, + optionalAdditionalContext = "", + ): string[] { + const exceptionStackInfo = this.getExceptionStackInfo(exception); + const exceptionMessageInfo = this.getExceptionMessageInfo(exception); + const validOptionalAdditionalContext = + optionalAdditionalContext && + optionalAdditionalContext !== "" && + optionalAdditionalContext !== " "; + let content: string[] = []; + let done = false; + + // scenario 1: messageInfo.message is containing a fully stringified exception -> no need to additionally fetch for the exception stack + // and some valid additional context is provided + if ( + exceptionMessageInfo.containsCompleteStringifiedException && + validOptionalAdditionalContext + ) { + content = [optionalAdditionalContext, exceptionMessageInfo.message]; + done = true; + } + + // scenario 2: messageInfo.message is containing a fully stringified exception -> no need to additionally fetch for the exception stack + // and no valid additional context is provided + if ( + !done && + exceptionMessageInfo.containsCompleteStringifiedException && + !validOptionalAdditionalContext + ) { + content = [exceptionMessageInfo.message]; + } + + // scenario 3: messageInfo.message is not containing a fully stringified exception -> need to fetch also exception stack + // and some valid additional context is provided + if ( + !done && + !exceptionMessageInfo.containsCompleteStringifiedException && + validOptionalAdditionalContext + ) { + content = [ + optionalAdditionalContext, + exceptionMessageInfo.message, + exceptionStackInfo.stack, + ]; + } + + // scenario 4: messageInfo.message is not containing a fully stringified exception -> need to fetch also exception stack + // and no valid additional context is provided + if ( + !done && + !exceptionMessageInfo.containsCompleteStringifiedException && + !validOptionalAdditionalContext + ) { + content = [exceptionMessageInfo.message, exceptionStackInfo.stack]; + } + + return content; + } + + /** + * USE THIS FUNCTION ONLY IN CASE OF SPECIAL EXCEPTION HANDLING + * For a general exception handling / logging use the function logException() or consoleException() above + * + * @param exception as an arbitrary exception of whatever type and format it might be + * @returns the message information of the exception as a string + */ + public static getExceptionMessage(exception: unknown): string { + const exceptionMessageInfo = this.getExceptionMessageInfo(exception); + + return exceptionMessageInfo.message; + } + + /** + * USE THIS FUNCTION ONLY IN CASE OF SPECIAL EXCEPTION HANDLING + * For a general exception handling / logging use the function logException() or consoleException() above + * + * @param exception as an arbitrary exception of whatever type and format it might be + * @returns the stack information of the exception as a string + */ + public static getExceptionStack(exception: unknown): string { + const exceptionStackInfo = this.getExceptionStackInfo(exception); + + return exceptionStackInfo.stack; + } + + /** + * private helper method to obtain the message information of the exception as a message-string and + * an additional indicator if the message contains a fully stringified exception + * + * @param exception as an arbitrary exception of whatever type it might be + * @returns the message information of the exception as a message-string and an indicator if the message contains fully stringified exception + */ + private static getExceptionMessageInfo( + exception: unknown, + ): { message: string; containsCompleteStringifiedException: boolean } { + // handle unknown exception input + const defaultMessage = "NO_MESSAGE_INCLUDED_IN_EXCEPTION"; + const invalidException = "INVALID_EXCEPTION"; + const invalidMessage = "INVALID_EXCEPTION_MESSAGE"; + const customExceptionPrefix = "A CUSTOM EXCEPTION WAS THROWN: "; + let message = defaultMessage; + let exceptionHandled = false; + let containsCompleteStringifiedException = false; + + // 1st need to check that exception is not null or undefined before trying to access the wanted message information + if (exception) { + const isOfTypeString = typeof exception === "string"; + const isOfTypeObject = typeof exception === "object"; + const hasOwnPropertyMessage = Object.hasOwnProperty.call( + exception, + "message", + ); + const messageIsOfTypeString = + typeof (exception as Record).message === "string"; + + // scenario 1: exception is of type object and providing a string message property + if (isOfTypeObject && hasOwnPropertyMessage && messageIsOfTypeString) { + message = (exception as { message: string }).message; + exceptionHandled = true; + } + + // scenario 2: exception is of type object and providing a non-string message property + if ( + !exceptionHandled && + isOfTypeObject && + hasOwnPropertyMessage && + !messageIsOfTypeString + ) { + // need to stringify message information first + message = this.safeJsonStringify( + (exception as { message: unknown }).message, + invalidMessage, + ); + exceptionHandled = true; + } + + // scenario 3: handling of string type exceptions + if (!exceptionHandled && isOfTypeString) { + message = customExceptionPrefix + exception; + exceptionHandled = true; + // as this scenario handles exception of string only type + // there is no other property and it is per se completely stringified + containsCompleteStringifiedException = true; + } + + // scenario 4: handling of custom exceptions + if (!exceptionHandled && !isOfTypeString) { + // custom exception is of a different type -> need to stringify it + message = + customExceptionPrefix + + this.safeJsonStringify(exception, invalidException); + exceptionHandled = true; + containsCompleteStringifiedException = true; + } + } + return { + message, + containsCompleteStringifiedException, + }; + } + + /** + * private helper method to obtain the stack information of the exception as a stack-string and + * an additional indicator if the stack contains a fully stringified exception + * + * @param exception as an arbitrary exception of whatever type it might be + * @returns the stack information of the exception as a stack-string and an indicator if the stack contains fully stringified exception + */ + private static getExceptionStackInfo( + exception: unknown, + ): { stack: string; containsCompleteStringifiedException: boolean } { + // handle unknown exception input + const fallbackStack = "NO_STACK_INFORMATION_INCLUDED_IN_EXCEPTION"; + const invalidStack = "INVALID_STACK_INFORMATION"; + const invalidException = "INVALID_EXCEPTION"; + const customExceptionPrefix = "A CUSTOM EXCEPTION WAS THROWN: "; + let stack = fallbackStack; + let exceptionHandled = false; + let containsCompleteStringifiedException = false; + + // 1st need to check that exception is not null or undefined before trying to access the wanted stack information + // otherwise the default fallback stack info will be returned + if (exception) { + const isOfTypeObject = typeof exception === "object"; + const isInstanceOfRuntimeError = exception instanceof RuntimeError; + const hasOwnPropertyStack = Object.hasOwnProperty.call( + exception, + "stack", + ); + const stackIsOfTypeString = + typeof (exception as Record).stack === "string"; + + // scenario 1: exception is an instance of RuntimeError + if (isInstanceOfRuntimeError) { + // handling RuntimeError stack inclusive nested / cascaded stacks + stack = this.safeJsonStringify(exception); + containsCompleteStringifiedException = true; + exceptionHandled = true; + } + + // scenario 2: exception is of type object and providing a string stack property + if ( + !exceptionHandled && + isOfTypeObject && + hasOwnPropertyStack && + stackIsOfTypeString + ) { + stack = (exception as { stack: string }).stack; + exceptionHandled = true; + } + + // scenario 3: exception is of type object and providing a non-string stack property + if ( + !exceptionHandled && + isOfTypeObject && + hasOwnPropertyStack && + !stackIsOfTypeString + ) { + // need to stringify stack information first + stack = this.safeJsonStringify( + (exception as { stack: unknown }).stack, + invalidStack, + ); + containsCompleteStringifiedException = true; + exceptionHandled = true; + } + + // scenario 4: handling of custom exceptions + if (!exceptionHandled) { + // custom exception is of a different type -> need to stringify it + stack = + customExceptionPrefix + + this.safeJsonStringify(exception, invalidException); + containsCompleteStringifiedException = true; + exceptionHandled = true; + } + } + return { + stack, + containsCompleteStringifiedException, + }; + } + + private static safeJsonStringify( + input: unknown, + catchMessage = "INVALID_INPUT", + ): string { + let message = ""; + + try { + // use fast-safe-stringify to also handle gracefully circular structures + message = stringify(input); + } catch (error) { + // fast and safe stringify failed + message = catchMessage; + } + return message; + } +} diff --git a/packages/cactus-common/src/main/typescript/logging/logger.ts b/packages/cactus-common/src/main/typescript/logging/logger.ts index 9c16efdf99..bd050d7f89 100644 --- a/packages/cactus-common/src/main/typescript/logging/logger.ts +++ b/packages/cactus-common/src/main/typescript/logging/logger.ts @@ -1,5 +1,6 @@ import libLogLevel, { Logger as LogLevelLogger, LogLevelDesc } from "loglevel"; import prefix from "loglevel-plugin-prefix"; +import { ExceptionHelper } from "../exception/exception-helper"; prefix.reg(libLogLevel); @@ -21,6 +22,77 @@ export interface ILoggerOptions { level?: LogLevelDesc; } +/** + * STANDARD EXCEPTION HANDLING - EXAMPLE WITH RE-THROW: + * ==================================================== + * + * Use the this logger with the .exception(ex) and hand in whatever exception of whatever type and format + * The logger will take care of it + * After logging it use the ExceptionHelper.newRuntimeError() to re-throw for a fast-fail + * + * + * public doSomething(): void { + * try { + * executeSomething(); + * } catch (ex) { + * this.log.exception(ex); + * throw ExceptionHelper.newRuntimeError(ex, "ExecuteSomething failed"); + * } + * + * + * + * EXCEPTION HANDLING WITH CONTEXT - EXAMPLE: + * ========================================== + * + * Use the this logger with the .exception(ex, "additional context") and hand in whatever exception of whatever type and format + * The logger will take care of it + * Additionally hand in some information about the exception scenario + * After logging the exception use the ExceptionHelper.newRuntimeError() to re-throw for a fast-fail + * + * + * public doSomething(): void { + * try { + * executeSomething(); + * } catch (ex) { + * this.log.exception(ex, "Some additional information / context about the scenario"); + * throw ExceptionHelper.newRuntimeError(ex, "ExecuteSomething failed"); + * } + * + * + * + * EXCEPTION HANDLING WITH CONDITIONAL HANDLING AND RE-THROW - EXAMPLE: + * ==================================================================== + * + * In case you need to do a conditional exception-handling: + * - Use the RuntimeError to re-throw and + * provide the previous exception as cause in the new RuntimeError to retain the information and distinguish + * between an exception you can handle and recover from and one you can't + * + * public async doSomething(): Promise { + * try { + * await doSubTaskThatsAPartOfDoingSomething(); + * } catch (ex) { + * if (ex instanceof MyErrorThatICanHandleAndRecoverFrom) { + * // An exception with a fixable scenario we can recover from thru an additional handling + * // do something here to handle and fix the issue + * // where "fixing" means that the we end up recovering + * // OK instead of having to crash. Recovery means that + * // we are confident that the second sub-task is safe to proceed with + * // despite of the error that was caught here + * this.log.exception(ex, "We got an failure in 'doSubTaskThatsAPartOfDoingSomething()' but we could fix it and recover to continue"); + * } else { + * // An "unexpected exception" where we want to fail immediately + * // to avoid follow-up problems + * const context = "We got an severe failure in 'doSubTaskThatsAPartOfDoingSomething()' and need to stop directly here to avoid follow-up problems"; + * this.log.exception(ex, context); + * throw ExceptionHelper.newRuntimeError(ex, context); + * } + * } + * const result = await doSecondAndFinalSubTask(); + * return result; // 42 + * } + */ + /** * Levels: * - error: 0, @@ -46,6 +118,32 @@ export class Logger { this.backend.info("Shut down logger OK."); } + /** + * The 'exception' method is retrieving and stringify-ing the complete exception content like message + stack + custom properties (if available) in a safe and secure way + * of any arbitrary exception or whatever it might be and logging it as an error thru the usage of this logger.error() functionality. + * + * So there is no need of dealing with different exception types or checks if a stack trace etc. is available -> all is done automatically here + * Only hand in the exception and maybe some optional, additional information / context can be provided as second parameter about the scenario where the exception happened + * + * + * + * Standard Exception Handling: + * ============================ + * logger.exception(ex); + * + * + * Optional additional context: + * ============================ + * logger.exception(ex, "Some optional, additional information / context about the scenario") + * + * + * @param ex as an arbitrary exception + * @param optionalAdditionalContext as optional string information about the scenario where the exception happened + */ + public exception(ex: unknown, optionalAdditionalContext = ""): void { + ExceptionHelper.logException(this, ex, optionalAdditionalContext); + } + public error(...msg: unknown[]): void { this.backend.error(...msg); } diff --git a/packages/cactus-common/src/main/typescript/public-api.ts b/packages/cactus-common/src/main/typescript/public-api.ts index 209b91f6dc..b768cb936d 100755 --- a/packages/cactus-common/src/main/typescript/public-api.ts +++ b/packages/cactus-common/src/main/typescript/public-api.ts @@ -1,4 +1,5 @@ export { LoggerProvider } from "./logging/logger-provider"; +export { ExceptionHelper } from "./exception/exception-helper"; export { Logger, ILoggerOptions } from "./logging/logger"; export { LogLevelDesc } from "loglevel"; export { Objects } from "./objects"; diff --git a/packages/cactus-common/src/test/typescript/unit/exception/exception-helper.test.ts b/packages/cactus-common/src/test/typescript/unit/exception/exception-helper.test.ts new file mode 100644 index 0000000000..f38c795c67 --- /dev/null +++ b/packages/cactus-common/src/test/typescript/unit/exception/exception-helper.test.ts @@ -0,0 +1,362 @@ +import "jest-extended"; +import { LogLevelDesc } from "loglevel"; +import { RuntimeError } from "run-time-error"; +import { ExceptionHelper } from "../../../../main/typescript/exception/exception-helper"; +import { LoggerProvider } from "../../../../main/typescript/logging/logger-provider"; + +describe("exception-helper tests", () => { + const no_message_available = "NO_MESSAGE_INCLUDED_IN_EXCEPTION"; + const no_stack_available = "NO_STACK_INFORMATION_INCLUDED_IN_EXCEPTION"; + const custom_exception_was_thrown = "A CUSTOM EXCEPTION WAS THROWN"; + const valid_additional_information = "VERY_IMPORTANT_ADDITIONAL_INFORMATION"; + const errorMessage = "Oops"; + const errorNumber = 2468; + + describe("exception stack-tests", () => { + it("gets the stack information from a regular Error object", () => { + let expectedResult: string | undefined = ""; + let stack = no_stack_available; + + try { + const testError = new Error(errorMessage); + expectedResult = testError.stack; + throw testError; + } catch (error) { + stack = ExceptionHelper.getExceptionStack(error); + } + + // check stack + expect(stack).toBe(expectedResult); + }); + + it("gets stack information from a faked Error object which is containing stack information as string type", () => { + const expectedResult = "Faked stack"; + let stack = no_stack_available; + + const fakeErrorWithStack = { + message: + "This is a fake error object with string-type stack information", + stack: expectedResult, + }; + + try { + throw fakeErrorWithStack; + } catch (error) { + stack = ExceptionHelper.getExceptionStack(error); + } + + // check stack + expect(stack).toBe(expectedResult); + }); + + it("gets stack information from a faked Error object which is containing stack information as number type and therefore need to be stringified", () => { + const expectedResult = "123456"; + let stack = no_stack_available; + + const fakeErrorWithStack = { + message: + "This is a fake error object with number-type stack information", + stack: 123456, + }; + + try { + throw fakeErrorWithStack; + } catch (error) { + stack = ExceptionHelper.getExceptionStack(error); + } + + // check stack + expect(stack).toBe(expectedResult); + }); + + it("gets stringified exception as custom stack information as the faked Error object is not containing any specific stack information", () => { + const msg = "This is a fake error object without stack information"; + let stack = no_stack_available; + + const fakeErrorWithoutStack = { + message: msg, + }; + + try { + throw fakeErrorWithoutStack; + } catch (error) { + stack = ExceptionHelper.getExceptionStack(error); + } + + // check stack + expect(stack).toContain(msg); + expect(stack).toContain(custom_exception_was_thrown); + }); + + it("handles throwing null successfully and returns NO_STACK_INFORMATION_INCLUDED_IN_EXCEPTION string", () => { + const expectedResult = no_stack_available; + let stack = no_stack_available; + + const fakeError = null; + + try { + throw fakeError; + } catch (error) { + stack = ExceptionHelper.getExceptionStack(error); + } + + // check stack + expect(stack).toBe(expectedResult); + }); + + it("handles throwing undefined successfully and returns NO_STACK_INFORMATION_INCLUDED_IN_EXCEPTION string", () => { + const expectedResult = no_stack_available; + let stack = no_stack_available; + + const fakeError = undefined; + + try { + throw fakeError; + } catch (error) { + stack = ExceptionHelper.getExceptionStack(error); + } + + // check stack + expect(stack).toBe(expectedResult); + }); + }); + + describe("exception message-tests", () => { + it("gets the exception message from a regular Error object", () => { + const expectedResult = errorMessage; + let message = no_message_available; + + try { + const testError = new Error(errorMessage); + throw testError; + } catch (error) { + message = ExceptionHelper.getExceptionMessage(error); + } + + // check message + expect(message).toBe(expectedResult); + }); + + it("gets the exception message from a faked Error object which is containing message as string type", () => { + const expectedResult = errorMessage; + let message = no_message_available; + + const fakeErrorWithMessage = { + message: errorMessage, + stack: expectedResult, + }; + + try { + throw fakeErrorWithMessage; + } catch (error) { + message = ExceptionHelper.getExceptionMessage(error); + } + + // check message + expect(message).toBe(expectedResult); + }); + + it("gets exception message from a faked Error object which is containing message information as number type and therefore need to be stringified", () => { + const expectedResult = "123456"; + let message = no_message_available; + + const fakeErrorWithNumberMessage = { + message: 123456, + }; + + try { + throw fakeErrorWithNumberMessage; + } catch (error) { + message = ExceptionHelper.getExceptionMessage(error); + } + + // check message + expect(message).toBe(expectedResult); + }); + + it("gets no exception message information as the faked Error object is not containing any message information and therefore tries to stringify the whole exception", () => { + const msg = "This is a fake error object without message information"; + const expectedResultPart2 = msg; + let message = no_message_available; + + const fakeErrorWithoutMessage = { + stack: "This is a fake error object without message information", + }; + + try { + throw fakeErrorWithoutMessage; + } catch (error) { + message = ExceptionHelper.getExceptionMessage(error); + } + + // check message + expect(message).toContain(custom_exception_was_thrown); + expect(message).toContain(expectedResultPart2); + }); + + it("handles throwing null successfully and returning NO_MESSAGE_INCLUDED_IN_EXCEPTION string", () => { + const expectedResult = no_message_available; + let message = no_message_available; + + const fakeError = null; + + try { + throw fakeError; + } catch (error) { + message = ExceptionHelper.getExceptionMessage(error); + } + + // check message + expect(message).toBe(expectedResult); + }); + + it("handles throwing undefined successfully and returning NO_MESSAGE_INCLUDED_IN_EXCEPTION string", () => { + const expectedResult = no_message_available; + let message = no_message_available; + + const fakeError = undefined; + + try { + throw fakeError; + } catch (error) { + message = ExceptionHelper.getExceptionMessage(error); + } + + // check message + expect(message).toBe(expectedResult); + }); + }); + + describe("handling of custom exceptions", () => { + it("handles a thrown string", () => { + let message = no_message_available; + let stack = no_stack_available; + + try { + throw errorMessage; + } catch (error) { + message = ExceptionHelper.getExceptionMessage(error); + stack = ExceptionHelper.getExceptionStack(error); + } + + // check message + stack + expect(message).toContain(errorMessage); + expect(message).toContain(custom_exception_was_thrown); + expect(stack).toContain(errorMessage); + expect(stack).toContain(custom_exception_was_thrown); + }); + + it("handles a thrown number", () => { + const expectedMessage = `${errorNumber}`; + let message = no_message_available; + let stack = no_stack_available; + + try { + throw errorNumber; + } catch (error) { + message = ExceptionHelper.getExceptionMessage(error); + stack = ExceptionHelper.getExceptionStack(error); + } + + // check message + stack + expect(message).toContain(expectedMessage); + expect(message).toContain(custom_exception_was_thrown); + expect(stack).toContain(expectedMessage); + expect(stack).toContain(custom_exception_was_thrown); + }); + + it("handles an arbitrary exception", () => { + let message = no_message_available; + let stack = no_stack_available; + const arbitraryException = { error: errorMessage }; + + try { + throw arbitraryException; + } catch (error) { + message = ExceptionHelper.getExceptionMessage(error); + stack = ExceptionHelper.getExceptionStack(error); + } + + // check message + stack + expect(message).toContain(errorMessage); + expect(message).toContain(custom_exception_was_thrown); + expect(stack).toContain(errorMessage); + expect(stack).toContain(custom_exception_was_thrown); + }); + + it("handles nested exceptions", () => { + const expectedErrorMessage = "RTE3"; + const expectedStackPart = "RTE1"; + let message = no_message_available; + let stack = no_stack_available; + const rtE1 = new RuntimeError("RTE1"); + const rtE2 = new RuntimeError("RTE2", rtE1); + const rtE3 = new RuntimeError("RTE3", rtE2); + + try { + throw rtE3; + } catch (error) { + message = ExceptionHelper.getExceptionMessage(error); + stack = ExceptionHelper.getExceptionStack(error); + } + + // check message + stack + expect(message).toBe(expectedErrorMessage); + expect(stack).toContain(expectedStackPart); + }); + }); + + describe("logging of exceptions", () => { + it("logs the exception to logger", () => { + let message = no_message_available; + let stack = no_stack_available; + const testError = new Error(errorMessage); + const testLogLevel: LogLevelDesc = "error"; + + try { + throw testError; + } catch (error) { + message = ExceptionHelper.getExceptionMessage(error); + stack = ExceptionHelper.getExceptionStack(error); + } + + const log = LoggerProvider.getOrCreate({ + label: "logHelper-UT", + level: testLogLevel, + }); + + log.exception(testError, valid_additional_information); + + // check message + stack + expect(message).toContain(errorMessage); + expect(stack).toContain(errorMessage); + }); + }); + + describe("RuntimeError", () => { + it("creates new RuntimeError from error", () => { + const context = "It happened ..."; + const error = new Error(errorMessage); + + const result = ExceptionHelper.newRuntimeError(error, context); + + expect(result instanceof RuntimeError).toBe(true); + expect(result.message).toBe(context); + expect(result.cause?.toString()).toContain(errorMessage); + }); + + it("creates new RuntimeError from custom exception", () => { + const context = "It happened ..."; + const arbitraryException = { error: errorMessage }; + + const result = ExceptionHelper.newRuntimeError( + arbitraryException, + context, + ); + + expect(result instanceof RuntimeError).toBe(true); + expect(result.message).toBe(context); + expect(result.cause?.toString()).toContain(errorMessage); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 7d6b71b757..0c449ffd4f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10575,10 +10575,10 @@ eyes@0.1.x: resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" integrity sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A= -fabric-ca-client@1.4.19: - version "1.4.19" - resolved "https://registry.yarnpkg.com/fabric-ca-client/-/fabric-ca-client-1.4.19.tgz#d2e13480d3d6139f41ae30c6ab908a6b9c8f2420" - integrity sha512-YX74Kme1jr21s8YlTAHaCOKYnX6GWBqtpsCUPTsoxoQcBkHcfxSOKA+wgUoj26T0+SJLmcgcYrGrBdeg/Nawpw== +fabric-ca-client@1.4.18: + version "1.4.18" + resolved "https://registry.yarnpkg.com/fabric-ca-client/-/fabric-ca-client-1.4.18.tgz#ee9efe0fd4809c6052af19d3a834d6953dbab95a" + integrity sha512-OklEHXkY93Z4voT044ZY0le9iqPLHYMvU44oDd7bZxFkgAixEUPwWOGr9VjfDyeQdZlU9I3IMgtiK5V4SK7XeA== dependencies: grpc "1.24.11" jsrsasign "^10.4.1" @@ -10606,10 +10606,10 @@ fabric-ca-client@2.3.0-snapshot.62: url "^0.11.0" winston "^2.4.5" -fabric-client@1.4.19: - version "1.4.19" - resolved "https://registry.yarnpkg.com/fabric-client/-/fabric-client-1.4.19.tgz#5387de0026981c597a5d7a5f4372ae1715304982" - integrity sha512-D2Kk+MzNLjbG1+vlhI1tn7nFjs/HHqF4a900Mi4vQ7ECD0TQ7uvrEngrWfBImRd8TYwMDnthZc2UJpwjoUQeTQ== +fabric-client@^1.4.0: + version "1.4.18" + resolved "https://registry.yarnpkg.com/fabric-client/-/fabric-client-1.4.18.tgz#d124bd57be4763a93478bfdf9231169834cdaf61" + integrity sha512-gs3BkGbl+KAeu0H0s108eTXVcOiwJJaZpRcGqZt3DoiC5rwHipVERx/OH+bmZxoL7o8aaUEw8JkA+SmDqCh5BA== dependencies: "@types/bytebuffer" "^5.0.34" bn.js "^4.11.3" @@ -19537,7 +19537,7 @@ rfdc@^1.3.0: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== -rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3: +rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==