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:
@@ -1,119 +1,313 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { IEntity } from '@/infrastructure/Entity/IEntity';
|
||||
import { parseCategoryCollection } from '@/application/Parser/CategoryCollectionParser';
|
||||
import { parseCategory } from '@/application/Parser/Executable/CategoryParser';
|
||||
import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub';
|
||||
import { parseCategoryCollection, type CategoryCollectionFactory } from '@/application/Parser/CategoryCollectionParser';
|
||||
import { type CategoryParser } from '@/application/Parser/Executable/CategoryParser';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||
import { ScriptingDefinitionParser } from '@/application/Parser/ScriptingDefinition/ScriptingDefinitionParser';
|
||||
import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub';
|
||||
import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub';
|
||||
import { getCategoryStub, CollectionDataStub } from '@tests/unit/shared/Stubs/CollectionDataStub';
|
||||
import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub';
|
||||
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
|
||||
import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||
import { createFunctionDataWithCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
||||
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
|
||||
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import type { CategoryData } from '@/application/collections/';
|
||||
import type { CollectionData, ScriptingDefinitionData, FunctionData } from '@/application/collections/';
|
||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||
import type { NonEmptyCollectionAssertion, ObjectAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator';
|
||||
import type { EnumParser } from '@/application/Common/Enum';
|
||||
import type { ScriptingDefinitionParser } from '@/application/Parser/ScriptingDefinition/ScriptingDefinitionParser';
|
||||
import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub';
|
||||
import type { CategoryCollectionSpecificUtilitiesFactory } from '@/application/Parser/Executable/CategoryCollectionSpecificUtilities';
|
||||
import { ScriptingDefinitionDataStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionDataStub';
|
||||
import { CategoryParserStub } from '@tests/unit/shared/Stubs/CategoryParserStub';
|
||||
import { createCategoryCollectionFactorySpy } from '@tests/unit/shared/Stubs/CategoryCollectionFactoryStub';
|
||||
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
|
||||
describe('CategoryCollectionParser', () => {
|
||||
describe('parseCategoryCollection', () => {
|
||||
it('validates object', () => {
|
||||
// arrange
|
||||
const data = new CollectionDataStub();
|
||||
const expectedAssertion: ObjectAssertion<CollectionData> = {
|
||||
value: data,
|
||||
valueName: 'collection',
|
||||
allowedProperties: [
|
||||
'os', 'scripting', 'actions', 'functions',
|
||||
],
|
||||
};
|
||||
const validator = new TypeValidatorStub();
|
||||
const context = new TestContext()
|
||||
.withData(data)
|
||||
.withTypeValidator(validator);
|
||||
// act
|
||||
context.parseCategoryCollection();
|
||||
// assert
|
||||
validator.expectObjectAssertion(expectedAssertion);
|
||||
});
|
||||
describe('actions', () => {
|
||||
describe('throws with absent actions', () => {
|
||||
itEachAbsentCollectionValue<CategoryData>((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'content does not define any action';
|
||||
const collection = new CollectionDataStub()
|
||||
.withActions(absentValue);
|
||||
const projectDetails = new ProjectDetailsStub();
|
||||
// act
|
||||
const act = () => parseCategoryCollection(collection, projectDetails);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
}, { excludeUndefined: true, excludeNull: true });
|
||||
});
|
||||
it('parses actions', () => {
|
||||
it('validates non-empty collection', () => {
|
||||
// arrange
|
||||
const actions = [getCategoryStub('test1'), getCategoryStub('test2')];
|
||||
const context = new CategoryCollectionSpecificUtilitiesStub();
|
||||
const expected = [parseCategory(actions[0], context), parseCategory(actions[1], context)];
|
||||
const collection = new CollectionDataStub()
|
||||
.withActions(actions);
|
||||
const projectDetails = new ProjectDetailsStub();
|
||||
const expectedAssertion: NonEmptyCollectionAssertion = {
|
||||
value: actions,
|
||||
valueName: '"actions" in collection',
|
||||
};
|
||||
const validator = new TypeValidatorStub();
|
||||
const context = new TestContext()
|
||||
.withData(new CollectionDataStub().withActions(actions))
|
||||
.withTypeValidator(validator);
|
||||
// act
|
||||
const actual = parseCategoryCollection(collection, projectDetails).actions;
|
||||
context.parseCategoryCollection();
|
||||
// assert
|
||||
expect(excludingId(actual)).to.be.deep.equal(excludingId(expected));
|
||||
function excludingId<TId>(array: ReadonlyArray<IEntity<TId>>) {
|
||||
return array.map((obj) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { id: omitted, ...rest } = obj;
|
||||
return rest;
|
||||
validator.expectNonEmptyCollectionAssertion(expectedAssertion);
|
||||
});
|
||||
it('parses actions correctly', () => {
|
||||
// arrange
|
||||
const {
|
||||
categoryCollectionFactorySpy,
|
||||
getInitParameters,
|
||||
} = createCategoryCollectionFactorySpy();
|
||||
const actionsData = [getCategoryStub('test1'), getCategoryStub('test2')];
|
||||
const expectedActions = [new CategoryStub(1), new CategoryStub(2)];
|
||||
const categoryParserStub = new CategoryParserStub()
|
||||
.withConfiguredParseResult(actionsData[0], expectedActions[0])
|
||||
.withConfiguredParseResult(actionsData[1], expectedActions[1]);
|
||||
const collectionData = new CollectionDataStub()
|
||||
.withActions(actionsData);
|
||||
const context = new TestContext()
|
||||
.withData(collectionData)
|
||||
.withCategoryParser(categoryParserStub.get())
|
||||
.withCategoryCollectionFactory(categoryCollectionFactorySpy);
|
||||
// act
|
||||
const actualCollection = context.parseCategoryCollection();
|
||||
// assert
|
||||
const actualActions = getInitParameters(actualCollection)?.actions;
|
||||
expect(actualActions).to.have.lengthOf(expectedActions.length);
|
||||
expect(actualActions).to.have.members(expectedActions);
|
||||
});
|
||||
describe('utilities', () => {
|
||||
it('parses actions with correct utilities', () => {
|
||||
// arrange
|
||||
const expectedUtilities = new CategoryCollectionSpecificUtilitiesStub();
|
||||
const utilitiesFactory: CategoryCollectionSpecificUtilitiesFactory = () => {
|
||||
return expectedUtilities;
|
||||
};
|
||||
const actionsData = [getCategoryStub('test1'), getCategoryStub('test2')];
|
||||
const collectionData = new CollectionDataStub()
|
||||
.withActions(actionsData);
|
||||
const categoryParserStub = new CategoryParserStub();
|
||||
const context = new TestContext()
|
||||
.withData(collectionData)
|
||||
.withCollectionUtilitiesFactory(utilitiesFactory)
|
||||
.withCategoryParser(categoryParserStub.get());
|
||||
// act
|
||||
context.parseCategoryCollection();
|
||||
// assert
|
||||
const usedUtilities = categoryParserStub.getUsedUtilities();
|
||||
expect(usedUtilities).to.have.lengthOf(2);
|
||||
expect(usedUtilities[0]).to.equal(expectedUtilities);
|
||||
expect(usedUtilities[1]).to.equal(expectedUtilities);
|
||||
});
|
||||
describe('construction', () => {
|
||||
it('creates utilities with correct functions data', () => {
|
||||
// arrange
|
||||
const expectedFunctionsData = [createFunctionDataWithCode()];
|
||||
const collectionData = new CollectionDataStub()
|
||||
.withFunctions(expectedFunctionsData);
|
||||
let actualFunctionsData: ReadonlyArray<FunctionData> | undefined;
|
||||
const utilitiesFactory: CategoryCollectionSpecificUtilitiesFactory = (data) => {
|
||||
actualFunctionsData = data;
|
||||
return new CategoryCollectionSpecificUtilitiesStub();
|
||||
};
|
||||
const context = new TestContext()
|
||||
.withData(collectionData)
|
||||
.withCollectionUtilitiesFactory(utilitiesFactory);
|
||||
// act
|
||||
context.parseCategoryCollection();
|
||||
// assert
|
||||
expect(actualFunctionsData).to.equal(expectedFunctionsData);
|
||||
});
|
||||
}
|
||||
it('creates utilities with correct scripting definition', () => {
|
||||
// arrange
|
||||
const expectedScripting = new ScriptingDefinitionStub();
|
||||
const scriptingDefinitionParser: ScriptingDefinitionParser = () => {
|
||||
return expectedScripting;
|
||||
};
|
||||
let actualScripting: IScriptingDefinition | undefined;
|
||||
const utilitiesFactory: CategoryCollectionSpecificUtilitiesFactory = (_, scripting) => {
|
||||
actualScripting = scripting;
|
||||
return new CategoryCollectionSpecificUtilitiesStub();
|
||||
};
|
||||
const context = new TestContext()
|
||||
.withCollectionUtilitiesFactory(utilitiesFactory)
|
||||
.withScriptDefinitionParser(scriptingDefinitionParser);
|
||||
// act
|
||||
context.parseCategoryCollection();
|
||||
// assert
|
||||
expect(actualScripting).to.equal(expectedScripting);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('scripting definition', () => {
|
||||
it('parses scripting definition as expected', () => {
|
||||
it('parses correctly', () => {
|
||||
// arrange
|
||||
const collection = new CollectionDataStub();
|
||||
const projectDetails = new ProjectDetailsStub();
|
||||
const expected = new ScriptingDefinitionParser()
|
||||
.parse(collection.scripting, projectDetails);
|
||||
const {
|
||||
categoryCollectionFactorySpy,
|
||||
getInitParameters,
|
||||
} = createCategoryCollectionFactorySpy();
|
||||
const expected = new ScriptingDefinitionStub();
|
||||
const scriptingDefinitionParser: ScriptingDefinitionParser = () => {
|
||||
return expected;
|
||||
};
|
||||
const context = new TestContext()
|
||||
.withCategoryCollectionFactory(categoryCollectionFactorySpy)
|
||||
.withScriptDefinitionParser(scriptingDefinitionParser);
|
||||
// act
|
||||
const actual = parseCategoryCollection(collection, projectDetails).scripting;
|
||||
const actualCategoryCollection = context.parseCategoryCollection();
|
||||
// assert
|
||||
expect(expected).to.deep.equal(actual);
|
||||
const actualScripting = getInitParameters(actualCategoryCollection)?.scripting;
|
||||
expect(expected).to.equal(actualScripting);
|
||||
});
|
||||
it('parses expected data', () => {
|
||||
// arrange
|
||||
const expectedData = new ScriptingDefinitionDataStub();
|
||||
const collection = new CollectionDataStub()
|
||||
.withScripting(expectedData);
|
||||
let actualData: ScriptingDefinitionData | undefined;
|
||||
const scriptingDefinitionParser
|
||||
: ScriptingDefinitionParser = (data: ScriptingDefinitionData) => {
|
||||
actualData = data;
|
||||
return new ScriptingDefinitionStub();
|
||||
};
|
||||
const context = new TestContext()
|
||||
.withScriptDefinitionParser(scriptingDefinitionParser)
|
||||
.withData(collection);
|
||||
// act
|
||||
context.parseCategoryCollection();
|
||||
// assert
|
||||
expect(actualData).to.equal(expectedData);
|
||||
});
|
||||
it('parses with correct project details', () => {
|
||||
// arrange
|
||||
const expectedProjectDetails = new ProjectDetailsStub();
|
||||
let actualDetails: ProjectDetails | undefined;
|
||||
const scriptingDefinitionParser
|
||||
: ScriptingDefinitionParser = (_, details: ProjectDetails) => {
|
||||
actualDetails = details;
|
||||
return new ScriptingDefinitionStub();
|
||||
};
|
||||
const context = new TestContext()
|
||||
.withProjectDetails(expectedProjectDetails)
|
||||
.withScriptDefinitionParser(scriptingDefinitionParser);
|
||||
// act
|
||||
context.parseCategoryCollection();
|
||||
// assert
|
||||
expect(actualDetails).to.equal(expectedProjectDetails);
|
||||
});
|
||||
});
|
||||
describe('os', () => {
|
||||
it('parses as expected', () => {
|
||||
it('parses correctly', () => {
|
||||
// arrange
|
||||
const {
|
||||
categoryCollectionFactorySpy,
|
||||
getInitParameters,
|
||||
} = createCategoryCollectionFactorySpy();
|
||||
const expectedOs = OperatingSystem.macOS;
|
||||
const osText = 'macos';
|
||||
const expectedName = 'os';
|
||||
const collection = new CollectionDataStub()
|
||||
const collectionData = new CollectionDataStub()
|
||||
.withOs(osText);
|
||||
const parserMock = new EnumParserStub<OperatingSystem>()
|
||||
.setup(expectedName, osText, expectedOs);
|
||||
const projectDetails = new ProjectDetailsStub();
|
||||
const context = new TestContext()
|
||||
.withOsParser(parserMock)
|
||||
.withCategoryCollectionFactory(categoryCollectionFactorySpy)
|
||||
.withData(collectionData);
|
||||
// act
|
||||
const actual = parseCategoryCollection(collection, projectDetails, parserMock);
|
||||
const actualCollection = context.parseCategoryCollection();
|
||||
// assert
|
||||
expect(actual.os).to.equal(expectedOs);
|
||||
});
|
||||
});
|
||||
describe('functions', () => {
|
||||
it('compiles script call with given function', () => {
|
||||
// arrange
|
||||
const expectedCode = 'code-from-the-function';
|
||||
const functionName = 'function-name';
|
||||
const scriptName = 'script-name';
|
||||
const script = createScriptDataWithCall()
|
||||
.withCall(new FunctionCallDataStub().withName(functionName).withParameters({}))
|
||||
.withName(scriptName);
|
||||
const func = createFunctionDataWithCode()
|
||||
.withParametersObject([])
|
||||
.withName(functionName)
|
||||
.withCode(expectedCode);
|
||||
const category = new CategoryDataStub()
|
||||
.withChildren([script,
|
||||
createScriptDataWithCode().withName('2')
|
||||
.withRecommendationLevel(RecommendationLevel.Standard),
|
||||
createScriptDataWithCode()
|
||||
.withName('3').withRecommendationLevel(RecommendationLevel.Strict),
|
||||
]);
|
||||
const collection = new CollectionDataStub()
|
||||
.withActions([category])
|
||||
.withFunctions([func]);
|
||||
const projectDetails = new ProjectDetailsStub();
|
||||
// act
|
||||
const actual = parseCategoryCollection(collection, projectDetails);
|
||||
// assert
|
||||
const actualScript = actual.getScript(scriptName);
|
||||
const actualCode = actualScript.code.execute;
|
||||
expect(actualCode).to.equal(expectedCode);
|
||||
const actualOs = getInitParameters(actualCollection)?.os;
|
||||
expect(actualOs).to.equal(expectedOs);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class TestContext {
|
||||
private data: CollectionData = new CollectionDataStub();
|
||||
|
||||
private projectDetails: ProjectDetails = new ProjectDetailsStub();
|
||||
|
||||
private validator: TypeValidator = new TypeValidatorStub();
|
||||
|
||||
private osParser: EnumParser<OperatingSystem> = new EnumParserStub<OperatingSystem>()
|
||||
.setupDefaultValue(OperatingSystem.Android);
|
||||
|
||||
private collectionUtilitiesFactory
|
||||
: CategoryCollectionSpecificUtilitiesFactory = () => {
|
||||
return new CategoryCollectionSpecificUtilitiesStub();
|
||||
};
|
||||
|
||||
private scriptDefinitionParser: ScriptingDefinitionParser = () => new ScriptingDefinitionStub();
|
||||
|
||||
private categoryParser: CategoryParser = new CategoryParserStub().get();
|
||||
|
||||
private categoryCollectionFactory
|
||||
: CategoryCollectionFactory = createCategoryCollectionFactorySpy().categoryCollectionFactorySpy;
|
||||
|
||||
public withData(data: CollectionData): this {
|
||||
this.data = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCategoryParser(categoryParser: CategoryParser): this {
|
||||
this.categoryParser = categoryParser;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCategoryCollectionFactory(categoryCollectionFactory: CategoryCollectionFactory): this {
|
||||
this.categoryCollectionFactory = categoryCollectionFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withProjectDetails(projectDetails: ProjectDetails): this {
|
||||
this.projectDetails = projectDetails;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withOsParser(osParser: EnumParser<OperatingSystem>): this {
|
||||
this.osParser = osParser;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withScriptDefinitionParser(scriptDefinitionParser: ScriptingDefinitionParser): this {
|
||||
this.scriptDefinitionParser = scriptDefinitionParser;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withTypeValidator(typeValidator: TypeValidator): this {
|
||||
this.validator = typeValidator;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCollectionUtilitiesFactory(
|
||||
collectionUtilitiesFactory: CategoryCollectionSpecificUtilitiesFactory,
|
||||
): this {
|
||||
this.collectionUtilitiesFactory = collectionUtilitiesFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public parseCategoryCollection(): ReturnType<typeof parseCategoryCollection> {
|
||||
return parseCategoryCollection(
|
||||
this.data,
|
||||
this.projectDetails,
|
||||
{
|
||||
osParser: this.osParser,
|
||||
validator: this.validator,
|
||||
parseScriptingDefinition: this.scriptDefinitionParser,
|
||||
createUtilities: this.collectionUtilitiesFactory,
|
||||
parseCategory: this.categoryParser,
|
||||
createCategoryCollection: this.categoryCollectionFactory,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user