diff --git a/src/application/Parser/Executable/Validation/ExecutableErrorContextMessage.ts b/src/application/Parser/Executable/Validation/ExecutableErrorContextMessage.ts index 70234e6b..3165066c 100644 --- a/src/application/Parser/Executable/Validation/ExecutableErrorContextMessage.ts +++ b/src/application/Parser/Executable/Validation/ExecutableErrorContextMessage.ts @@ -18,18 +18,26 @@ export const createExecutableContextErrorMessage: ExecutableContextErrorMessageC message += `${ExecutableType[context.type]}: `; } message += errorMessage; - message += `\n${getErrorContextDetails(context)}`; + message += `\n\n${getErrorContextDetails(context)}`; return message; }; function getErrorContextDetails(context: ExecutableErrorContext): string { - let output = `Self: ${printExecutable(context.self)}`; + let output = `Executable: ${formatExecutable(context.self)}`; if (context.parentCategory) { - output += `\nParent: ${printExecutable(context.parentCategory)}`; + output += `\n\nParent category: ${formatExecutable(context.parentCategory)}`; } return output; } -function printExecutable(executable: ExecutableData): string { - return JSON.stringify(executable, undefined, 2); +function formatExecutable(executable: ExecutableData): string { + if (!executable) { + return 'Executable data is missing.'; + } + const maxLength = 1000; + let output = JSON.stringify(executable, undefined, 2); + if (output.length > maxLength) { + output = `${output.substring(0, maxLength)}\n... [Rest of the executable trimmed]`; + } + return output; } diff --git a/tests/unit/application/Parser/Executable/Validation/ExecutableErrorContextMessage.spec.ts b/tests/unit/application/Parser/Executable/Validation/ExecutableErrorContextMessage.spec.ts new file mode 100644 index 00000000..966abe56 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Validation/ExecutableErrorContextMessage.spec.ts @@ -0,0 +1,194 @@ +import { describe, it, expect } from 'vitest'; +import { createExecutableErrorContextStub } from '@tests/unit/shared/Stubs/ExecutableErrorContextStub'; +import { createExecutableContextErrorMessage } from '@/application/Parser/Executable/Validation/ExecutableErrorContextMessage'; +import type { ExecutableErrorContext } from '@/application/Parser/Executable/Validation/ExecutableErrorContext'; +import { ExecutableType } from '@/application/Parser/Executable/Validation/ExecutableType'; +import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub'; + +describe('ExecutableErrorContextMessage', () => { + describe('createExecutableContextErrorMessage', () => { + it('includes the specified error message', () => { + // arrange + const expectedErrorMessage = 'expected error message'; + const context = new TestContext() + .withErrorMessage(expectedErrorMessage); + // act + const actualMessage = context.createExecutableContextErrorMessage(); + // assert + expect(actualMessage).to.include(expectedErrorMessage); + }); + it('includes the type of executable', () => { + // arrange + const executableType = ExecutableType.Category; + const expectedType = ExecutableType[executableType]; + const errorContext: ExecutableErrorContext = { + type: executableType, + self: new CategoryDataStub(), + }; + const context = new TestContext() + .withErrorContext(errorContext); + // act + const actualMessage = context.createExecutableContextErrorMessage(); + // assert + expect(actualMessage).to.include(expectedType); + }); + it('includes details of the self executable', () => { + // arrange + const expectedName = 'expected name'; + const selfExecutable = new CategoryDataStub() + .withName(expectedName); + const errorContext: ExecutableErrorContext = { + type: ExecutableType.Category, + self: selfExecutable, + }; + const context = new TestContext() + .withErrorContext(errorContext); + // act + const actualMessage = context.createExecutableContextErrorMessage(); + // assert + expect(actualMessage).to.include(expectedName); + }); + it('includes details of the parent category', () => { + // arrange + const expectedName = 'expected parent name'; + const parentCategoryData = new CategoryDataStub() + .withName(expectedName); + const errorContext: ExecutableErrorContext = { + type: ExecutableType.Category, + self: new CategoryDataStub(), + parentCategory: parentCategoryData, + }; + const context = new TestContext() + .withErrorContext(errorContext); + // act + const actualMessage = context.createExecutableContextErrorMessage(); + // assert + expect(actualMessage).to.include(expectedName); + }); + it('constructs the complete message format correctly', () => { + // arrange + const errorMessage = 'expected error message'; + const expectedName = 'expected name'; + const expectedFormat = new RegExp(`^${escapeRegExp(errorMessage)}\\s+Executable:\\s+{\\s+"name":\\s+"${escapeRegExp(expectedName)}"\\s+}`); + const errorContext: ExecutableErrorContext = { + self: { + name: expectedName, + } as unknown as ExecutableErrorContext['self'], + }; + const context = new TestContext() + .withErrorContext(errorContext) + .withErrorMessage(errorMessage); + // act + const actualMessage = context.createExecutableContextErrorMessage(); + // assert + expect(actualMessage).to.match(expectedFormat); + }); + describe('output trimming', () => { + const totalLongTextDataCharacters = 5000; + const expectedTrimmedText = '[Rest of the executable trimmed]'; + const longName = 'a'.repeat(totalLongTextDataCharacters); + const testScenarios: readonly { + readonly description: string; + readonly errorContext: ExecutableErrorContext; + } [] = [ + { + description: 'long text from parent category data', + errorContext: { + type: ExecutableType.Category, + self: new CategoryDataStub(), + parentCategory: new CategoryDataStub().withName(longName), + }, + }, + { + description: 'long text from self executable data', + errorContext: { + type: ExecutableType.Category, + self: new CategoryDataStub().withName(longName), + }, + }, + ]; + testScenarios.forEach(({ + description, errorContext, + }) => { + it(description, () => { + const context = new TestContext() + .withErrorContext(errorContext); + // act + const actualMessage = context.createExecutableContextErrorMessage(); + // assert + expect(actualMessage).to.include(expectedTrimmedText); + expect(actualMessage).to.have.length.lessThan(totalLongTextDataCharacters); + }); + }); + }); + describe('missing data handling', () => { + it('generates a message when the executable type is undefined', () => { + // arrange + const errorContext: ExecutableErrorContext = { + type: undefined, + self: new CategoryDataStub(), + }; + const context = new TestContext() + .withErrorContext(errorContext); + // act + const actualMessage = context.createExecutableContextErrorMessage(); + // assert + expect(actualMessage).to.have.length.greaterThan(0); + }); + it('generates a message when executable data is missing', () => { + // arrange + const errorContext: ExecutableErrorContext = { + type: undefined, + self: undefined as unknown as ExecutableErrorContext['self'], + }; + const context = new TestContext() + .withErrorContext(errorContext); + // act + const actualMessage = context.createExecutableContextErrorMessage(); + // assert + expect(actualMessage).to.have.length.greaterThan(0); + }); + it('generates a message when parent category is missing', () => { + // arrange + const errorContext: ExecutableErrorContext = { + type: undefined, + self: new CategoryDataStub(), + parentCategory: undefined, + }; + const context = new TestContext() + .withErrorContext(errorContext); + // act + const actualMessage = context.createExecutableContextErrorMessage(); + // assert + expect(actualMessage).to.have.length.greaterThan(0); + }); + }); + }); +}); + +class TestContext { + private errorMessage = `[${TestContext.name}] error message`; + + private errorContext: ExecutableErrorContext = createExecutableErrorContextStub(); + + public withErrorMessage(errorMessage: string): this { + this.errorMessage = errorMessage; + return this; + } + + public withErrorContext(context: ExecutableErrorContext): this { + this.errorContext = context; + return this; + } + + public createExecutableContextErrorMessage() { + return createExecutableContextErrorMessage( + this.errorMessage, + this.errorContext, + ); + } +} + +function escapeRegExp(string: string): string { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');// $& means the whole matched string +}