Add object property validation in parser #369

This commit introduces stricter type validation across the application
to reject objects with unexpected properties, enhancing the robustness
and predictability of data handling.

Changes include:

- Implement a common utility to validate object types.
- Refactor across various parsers and data handlers to utilize the new
  validations.
- Update error messages for better clarity and troubleshooting.
This commit is contained in:
undergroundwires
2024-06-13 22:26:57 +02:00
parent c138f74460
commit 6ecfa9b954
43 changed files with 1215 additions and 466 deletions

View File

@@ -5,10 +5,9 @@ import { type ScriptParser } from '@/application/Parser/Executable/Script/Script
import { type DocsParser } from '@/application/Parser/Executable/DocumentationParser';
import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub';
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
import { getAbsentCollectionTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
import { ExecutableType } from '@/application/Parser/Executable/Validation/ExecutableType';
import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub';
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import { ErrorWrapperStub } from '@tests/unit/shared/Stubs/ErrorWrapperStub';
import type { ExecutableValidatorFactory } from '@/application/Parser/Executable/Validation/ExecutableValidator';
import { ExecutableValidatorStub, createExecutableValidatorFactoryStub } from '@tests/unit/shared/Stubs/ExecutableValidatorStub';
@@ -20,8 +19,9 @@ import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
import { ScriptParserStub } from '@tests/unit/shared/Stubs/ScriptParserStub';
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
import { indentText } from '@tests/shared/Text';
import { itThrowsContextualError } from '../ContextualErrorTester';
import { itValidatesName, itValidatesDefinedData, itAsserts } from './Validation/ExecutableValidationTester';
import type { NonEmptyCollectionAssertion, ObjectAssertion } from '@/application/Parser/Common/TypeValidator';
import { itThrowsContextualError } from '../Common/ContextualErrorTester';
import { itValidatesName, itValidatesType, itAsserts } from './Validation/ExecutableValidationTester';
import { generateDataValidationTestScenarios } from './Validation/DataValidationTestScenarioGenerator';
describe('CategoryParser', () => {
@@ -49,14 +49,18 @@ describe('CategoryParser', () => {
};
});
});
describe('validates for defined data', () => {
describe('validates for unknown object', () => {
// arrange
const category = new CategoryDataStub();
const expectedContext: CategoryErrorContext = {
type: ExecutableType.Category,
self: category,
};
itValidatesDefinedData(
const expectedAssertion: ObjectAssertion<unknown> = {
value: category,
valueName: 'Executable',
};
itValidatesType(
(validatorFactory) => {
// act
new TestBuilder()
@@ -65,58 +69,65 @@ describe('CategoryParser', () => {
.parseCategory();
// assert
return {
expectedDataToValidate: category,
assertValidation: (validator) => validator.assertObject(expectedAssertion),
expectedErrorContext: expectedContext,
};
},
);
});
describe('validates that category has some children', () => {
const categoryName = 'test';
const testScenarios = generateDataValidationTestScenarios<CategoryData>({
expectFail: getAbsentCollectionTestCases<ExecutableData>().map(({
valueName, absentValue: absentCollectionValue,
}) => ({
description: `with \`${valueName}\` value as children`,
data: new CategoryDataStub()
.withName(categoryName)
.withChildren(absentCollectionValue as unknown as ExecutableData[]),
})),
expectPass: [{
description: 'has single children',
data: new CategoryDataStub()
.withName(categoryName)
.withChildren([createScriptDataWithCode()]),
}],
});
testScenarios.forEach(({
description, expectedPass, data: categoryData,
}) => {
describe(description, () => {
itAsserts({
expectedConditionResult: expectedPass,
test: (validatorFactory) => {
const expectedMessage = `"${categoryName}" has no children.`;
const expectedContext: CategoryErrorContext = {
type: ExecutableType.Category,
self: categoryData,
};
// act
try {
new TestBuilder()
.withData(categoryData)
.withValidatorFactory(validatorFactory)
.parseCategory();
} catch { /* It may throw due to assertions not being evaluated */ }
// assert
return {
expectedErrorMessage: expectedMessage,
expectedErrorContext: expectedContext,
};
},
});
});
});
describe('validates for category', () => {
// arrange
const category = new CategoryDataStub();
const expectedContext: CategoryErrorContext = {
type: ExecutableType.Category,
self: category,
};
const expectedAssertion: ObjectAssertion<CategoryData> = {
value: category,
valueName: category.category,
allowedProperties: ['docs', 'children', 'category'],
};
itValidatesType(
(validatorFactory) => {
// act
new TestBuilder()
.withData(category)
.withValidatorFactory(validatorFactory)
.parseCategory();
// assert
return {
assertValidation: (validator) => validator.assertObject(expectedAssertion),
expectedErrorContext: expectedContext,
};
},
);
});
describe('validates children for non-empty collection', () => {
// arrange
const category = new CategoryDataStub()
.withChildren([createScriptDataWithCode()]);
const expectedContext: CategoryErrorContext = {
type: ExecutableType.Category,
self: category,
};
const expectedAssertion: NonEmptyCollectionAssertion = {
value: category.children,
valueName: category.category,
};
itValidatesType(
(validatorFactory) => {
// act
new TestBuilder()
.withData(category)
.withValidatorFactory(validatorFactory)
.parseCategory();
// assert
return {
assertValidation: (validator) => validator.assertObject(expectedAssertion),
expectedErrorContext: expectedContext,
};
},
);
});
describe('validates that a child is a category or a script', () => {
// arrange
@@ -171,7 +182,7 @@ describe('CategoryParser', () => {
});
});
describe('validates children recursively', () => {
describe('validates (1th-level) child data', () => {
describe('validates (1th-level) child type', () => {
// arrange
const expectedName = 'child category';
const child = new CategoryDataStub()
@@ -183,7 +194,11 @@ describe('CategoryParser', () => {
self: child,
parentCategory: parent,
};
itValidatesDefinedData(
const expectedAssertion: ObjectAssertion<unknown> = {
value: child,
valueName: 'Executable',
};
itValidatesType(
(validatorFactory) => {
// act
new TestBuilder()
@@ -192,7 +207,7 @@ describe('CategoryParser', () => {
.parseCategory();
// assert
return {
expectedDataToValidate: child,
assertValidation: (validator) => validator.assertObject(expectedAssertion),
expectedErrorContext: expectedContext,
};
},

View File

@@ -18,7 +18,7 @@ describe('DocumentationParser', () => {
});
describe('throws when type is unexpected', () => {
// arrange
const expectedTypeError = 'docs field (documentation) must be an array of strings';
const expectedTypeError = 'docs field (documentation) must be a single string or an array of strings.';
const wrongTypedValue = 22 as never;
const testCases: ReadonlyArray<{
readonly name: string;

View File

@@ -8,7 +8,7 @@ import {
import { ExpressionPosition } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPosition';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester';
import { ExpressionStub } from '@tests/unit/shared/Stubs/ExpressionStub';
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
import type { IExpression } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/IExpression';

View File

@@ -9,8 +9,8 @@ import { SingleCallCompilerStub } from '@tests/unit/shared/Stubs/SingleCallCompi
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
import type { CompiledCode } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester';
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
describe('NestedFunctionCallCompiler', () => {

View File

@@ -11,8 +11,8 @@ import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/Fun
import { createSharedFunctionStubWithCode } from '@tests/unit/shared/Stubs/SharedFunctionStub';
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester';
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
describe('NestedFunctionArgumentCompiler', () => {

View File

@@ -13,10 +13,10 @@ import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines';
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import { FunctionParameterStub } from '@tests/unit/shared/Stubs/FunctionParameterStub';
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester';
import type { FunctionParameterCollectionFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory';
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
import { expectCallsFunctionBody, expectCodeFunctionBody } from './ExpectFunctionBodyType';

View File

@@ -16,12 +16,12 @@ import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub';
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
import type { ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
import { createScriptCodeFactoryStub } from '@tests/unit/shared/Stubs/ScriptCodeFactoryStub';
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester';
describe('ScriptCompiler', () => {
describe('canCompile', () => {

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import type { ScriptData } from '@/application/collections/';
import type { ScriptData, CallScriptData, CodeScriptData } from '@/application/collections/';
import { parseScript, type ScriptFactory } from '@/application/Parser/Executable/Script/ScriptParser';
import { type DocsParser } from '@/application/Parser/Executable/DocumentationParser';
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
@@ -8,14 +8,13 @@ import { ScriptCompilerStub } from '@tests/unit/shared/Stubs/ScriptCompilerStub'
import { createScriptDataWithCall, createScriptDataWithCode, createScriptDataWithoutCallOrCodes } from '@tests/unit/shared/Stubs/ScriptDataStub';
import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub';
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub';
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
import type { IEnumParser } from '@/application/Common/Enum';
import type { EnumParser } from '@/application/Common/Enum';
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines';
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import { ErrorWrapperStub } from '@tests/unit/shared/Stubs/ErrorWrapperStub';
import type { ExecutableValidatorFactory } from '@/application/Parser/Executable/Validation/ExecutableValidator';
import { ExecutableValidatorStub, createExecutableValidatorFactoryStub } from '@tests/unit/shared/Stubs/ExecutableValidatorStub';
@@ -26,9 +25,11 @@ import { createScriptCodeFactoryStub } from '@tests/unit/shared/Stubs/ScriptCode
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
import { createScriptFactorySpy } from '@tests/unit/shared/Stubs/ScriptFactoryStub';
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester';
import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub';
import type { CategoryCollectionSpecificUtilities } from '@/application/Parser/Executable/CategoryCollectionSpecificUtilities';
import { itAsserts, itValidatesDefinedData, itValidatesName } from '../Validation/ExecutableValidationTester';
import type { ObjectAssertion } from '@/application/Parser/Common/TypeValidator';
import { itAsserts, itValidatesType, itValidatesName } from '../Validation/ExecutableValidationTester';
import { generateDataValidationTestScenarios } from '../Validation/DataValidationTestScenarioGenerator';
describe('ScriptParser', () => {
@@ -290,7 +291,14 @@ describe('ScriptParser', () => {
type: ExecutableType.Script,
self: expectedScript,
};
itValidatesDefinedData(
const expectedAssertion: ObjectAssertion<CallScriptData & CodeScriptData> = {
value: expectedScript,
valueName: expectedScript.name,
allowedProperties: [
'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
],
};
itValidatesType(
(validatorFactory) => {
// act
new TestContext()
@@ -301,6 +309,7 @@ describe('ScriptParser', () => {
return {
expectedDataToValidate: expectedScript,
expectedErrorContext: expectedContext,
assertValidation: (validator) => validator.assertObject(expectedAssertion),
};
},
);
@@ -430,7 +439,7 @@ class TestContext {
private collectionUtilities
: CategoryCollectionSpecificUtilities = new CategoryCollectionSpecificUtilitiesStub();
private levelParser: IEnumParser<RecommendationLevel> = new EnumParserStub<RecommendationLevel>()
private levelParser: EnumParser<RecommendationLevel> = new EnumParserStub<RecommendationLevel>()
.setupDefaultValue(RecommendationLevel.Standard);
private scriptFactory: ScriptFactory = createScriptFactorySpy().scriptFactorySpy;
@@ -464,7 +473,7 @@ class TestContext {
return this;
}
public withParser(parser: IEnumParser<RecommendationLevel>): this {
public withParser(parser: EnumParser<RecommendationLevel>): this {
this.levelParser = parser;
return this;
}

View File

@@ -3,10 +3,11 @@ import type { ExecutableValidator, ExecutableValidatorFactory } from '@/applicat
import type { ExecutableErrorContext } from '@/application/Parser/Executable/Validation/ExecutableErrorContext';
import { ExecutableValidatorStub } from '@tests/unit/shared/Stubs/ExecutableValidatorStub';
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
import type { ExecutableData } from '@/application/collections/';
import type { FunctionKeys } from '@/TypeHelpers';
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
import { indentText } from '@tests/shared/Text';
import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub';
import { expectDeepIncludes } from '@tests/shared/Assertions/ExpectDeepIncludes';
type ValidationTestFunction<TExpectation> = (
factory: ExecutableValidatorFactory,
@@ -52,39 +53,41 @@ export function itValidatesName(
});
}
interface ValidDataExpectation {
readonly expectedDataToValidate: ExecutableData;
interface TypeAssertionExpectation {
readonly expectedErrorContext: ExecutableErrorContext;
readonly assertValidation: (validator: TypeValidatorStub) => void;
}
export function itValidatesDefinedData(
test: ValidationTestFunction<ValidDataExpectation>,
export function itValidatesType(
test: ValidationTestFunction<TypeAssertionExpectation>,
) {
it('validates data', () => {
it('validates type', () => {
// arrange
const validator = new ExecutableValidatorStub();
const factoryStub: ExecutableValidatorFactory = () => validator;
// act
test(factoryStub);
// assert
const call = validator.callHistory.find((c) => c.methodName === 'assertDefined');
const call = validator.callHistory.find((c) => c.methodName === 'assertType');
expectExists(call);
});
it('validates data with correct data', () => {
it('validates type using specified validator', () => {
// arrange
const typeValidator = new TypeValidatorStub();
const validator = new ExecutableValidatorStub();
const factoryStub: ExecutableValidatorFactory = () => validator;
// act
const expectation = test(factoryStub);
// assert
const expectedData = expectation.expectedDataToValidate;
const calls = validator.callHistory.filter((c) => c.methodName === 'assertDefined');
const names = calls.flatMap((c) => c.args[0]);
expect(names).to.include(expectedData);
const calls = validator.callHistory.filter((c) => c.methodName === 'assertType');
const args = calls.map((c) => c.args as Parameters<ExecutableValidator['assertType']>);
const validateFunctions = args.flatMap((c) => c[0]);
validateFunctions.forEach((validate) => validate(typeValidator));
expectation.assertValidation(typeValidator);
});
it('validates data with correct context', () => {
it('validates type with correct context', () => {
expectCorrectContextForFunctionCall({
methodName: 'assertDefined',
methodName: 'assertType',
act: test,
expectContext: (expectation) => expectation.expectedErrorContext,
});
@@ -185,34 +188,5 @@ function expectCorrectContextForFunctionCall<T>(testScenario: {
const providedContexts = createdValidators
.filter((v) => v.validator.callHistory.find((c) => c.methodName === methodName))
.map((v) => v.context);
expectDeepIncludes( // to.deep.include is not working
providedContexts,
expectedContext,
formatAssertionMessage([
'Error context mismatch.',
'Provided contexts do not include the expected context.',
'Expected context:',
indentText(JSON.stringify(expectedContext, undefined, 2)),
'Provided contexts:',
indentText(JSON.stringify(providedContexts, undefined, 2)),
]),
);
}
function expectDeepIncludes<T>(
array: readonly T[],
item: T,
message: string,
) {
const serializeItem = (c) => JSON.stringify(c);
const serializedContexts = array.map((c) => serializeItem(c));
const serializedExpectedContext = serializeItem(item);
expect(serializedContexts).to.include(serializedExpectedContext, formatAssertionMessage([
'Error context mismatch.',
'Provided contexts do not include the expected context.',
'Expected context:',
indentText(JSON.stringify(message, undefined, 2)),
'Provided contexts:',
indentText(JSON.stringify(message, undefined, 2)),
]));
expectDeepIncludes(providedContexts, expectedContext);
}

View File

@@ -1,12 +1,12 @@
import { describe, it, expect } from 'vitest';
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
import type { ExecutableData } from '@/application/collections/';
import { createExecutableErrorContextStub } from '@tests/unit/shared/Stubs/ExecutableErrorContextStub';
import type { ExecutableErrorContext } from '@/application/Parser/Executable/Validation/ExecutableErrorContext';
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
import { ContextualExecutableValidator, createExecutableDataValidator, type ExecutableValidator } from '@/application/Parser/Executable/Validation/ExecutableValidator';
import type { ExecutableContextErrorMessageCreator } from '@/application/Parser/Executable/Validation/ExecutableErrorContextMessage';
import { getAbsentObjectTestCases, getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub';
import type { TypeValidator } from '@/application/Parser/Common/TypeValidator';
describe('createExecutableDataValidator', () => {
it(`returns an instance of ${ContextualExecutableValidator.name}`, () => {
@@ -63,43 +63,41 @@ describe('ContextualExecutableValidator', () => {
expect(act).to.not.throw();
});
});
describe('assertDefined', () => {
describe('throws when data is missing', () => {
describe('assertType', () => {
describe('rethrows when action throws', () => {
// arrange
const testScenarios: readonly {
readonly description: string;
readonly invalidData: unknown;
}[] = [
...getAbsentObjectTestCases().map((testCase) => ({
description: `absent object (${testCase.valueName})`,
invalidData: testCase.absentValue,
})),
{
description: 'empty object',
invalidData: {},
},
];
testScenarios.forEach(({ description, invalidData }) => {
describe(`given "${description}"`, () => {
const expectedMessage = 'missing executable data';
itThrowsCorrectly({
// act
throwingAction: (sut: ExecutableValidator) => {
sut.assertDefined(invalidData as ExecutableData);
},
// assert
expectedMessage,
const expectedMessage = 'Error thrown by action';
itThrowsCorrectly({
// act
throwingAction: (sut: ExecutableValidator) => {
sut.assertType(() => {
throw new Error(expectedMessage);
});
});
},
// assert
expectedMessage,
});
});
it('does not throw if data is defined', () => {
it('provides correct validator', () => {
// arrange
const expectedValidator = new TypeValidatorStub();
const sut = new ValidatorBuilder()
.withTypeValidator(expectedValidator)
.build();
let actualValidator: TypeValidator | undefined;
// act
sut.assertType((validator) => {
actualValidator = validator;
});
// assert
expect(expectedValidator).to.equal(actualValidator);
});
it('does not throw if action does not throw', () => {
// arrange
const data = new CategoryDataStub();
const sut = new ValidatorBuilder()
.build();
// act
const act = () => sut.assertDefined(data);
const act = () => sut.assertType(() => { /* Does not throw */ });
// assert
expect(act).to.not.throw();
});
@@ -223,6 +221,8 @@ class ValidatorBuilder {
private errorMessageCreator: ExecutableContextErrorMessageCreator = () => `[${ValidatorBuilder.name}] stub error message`;
private typeValidator: TypeValidator = new TypeValidatorStub();
public withErrorMessageCreator(errorMessageCreator: ExecutableContextErrorMessageCreator): this {
this.errorMessageCreator = errorMessageCreator;
return this;
@@ -233,10 +233,16 @@ class ValidatorBuilder {
return this;
}
public withTypeValidator(typeValidator: TypeValidator): this {
this.typeValidator = typeValidator;
return this;
}
public build(): ContextualExecutableValidator {
return new ContextualExecutableValidator(
this.errorContext,
this.errorMessageCreator,
this.typeValidator,
);
}
}