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:
@@ -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,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user