This commit unifies the concepts of executables having same ID structure. It paves the way for more complex ID structure and using IDs in collection files as part of new ID solution (#262). Using string IDs also leads to more expressive test code. This commit also refactors the rest of the code to adopt to the changes. This commit: - Separate concerns from entities for data access (in repositories) and executables. Executables use `Identifiable` meanwhile repositories use `RepositoryEntity`. - Refactor unnecessary generic parameters for enttities and ids, enforcing string gtype everwyhere. - Changes numeric IDs to string IDs for categories to unify the retrieval and construction for executables, using pseudo-ids (their names) just like scripts. - Remove `BaseEntity` for simplicity. - Simplify usage and construction of executable objects. Move factories responsible for creation of category/scripts to domain layer. Do not longer export `CollectionCategorY` and `CollectionScript`. - Use named typed for string IDs for better differentation of different ID contexts in code.
531 lines
20 KiB
TypeScript
531 lines
20 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import type { CategoryData, ExecutableData } from '@/application/collections/';
|
|
import { parseCategory } from '@/application/Parser/Executable/CategoryParser';
|
|
import { type ScriptParser } from '@/application/Parser/Executable/Script/ScriptParser';
|
|
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 { ExecutableType } from '@/application/Parser/Executable/Validation/ExecutableType';
|
|
import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
|
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';
|
|
import type { CategoryErrorContext, UnknownExecutableErrorContext } from '@/application/Parser/Executable/Validation/ExecutableErrorContext';
|
|
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
|
import { createCategoryFactorySpy } from '@tests/unit/shared/Stubs/CategoryFactoryStub';
|
|
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
|
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 type { NonEmptyCollectionAssertion, ObjectAssertion } from '@/application/Parser/Common/TypeValidator';
|
|
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
|
import type { CategoryFactory } from '@/domain/Executables/Category/CategoryFactory';
|
|
import { itThrowsContextualError } from '../Common/ContextualErrorTester';
|
|
import { itValidatesName, itValidatesType, itAsserts } from './Validation/ExecutableValidationTester';
|
|
import { generateDataValidationTestScenarios } from './Validation/DataValidationTestScenarioGenerator';
|
|
|
|
describe('CategoryParser', () => {
|
|
describe('parseCategory', () => {
|
|
describe('id', () => {
|
|
it('creates ID correctly', () => {
|
|
// arrange
|
|
const expectedId: ExecutableId = 'expected-id';
|
|
const categoryData = new CategoryDataStub()
|
|
.withName(expectedId);
|
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
|
// act
|
|
const actualScript = new TestContext()
|
|
.withData(categoryData)
|
|
.withCategoryFactory(categoryFactorySpy)
|
|
.parseCategory();
|
|
// assert
|
|
const actualId = getInitParameters(actualScript)?.executableId;
|
|
expect(actualId).to.equal(expectedId);
|
|
});
|
|
});
|
|
describe('name', () => {
|
|
it('parses name correctly', () => {
|
|
// arrange
|
|
const expectedName = 'test-expected-name';
|
|
const categoryData = new CategoryDataStub()
|
|
.withName(expectedName);
|
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
|
// act
|
|
const actualCategory = new TestContext()
|
|
.withData(categoryData)
|
|
.withCategoryFactory(categoryFactorySpy)
|
|
.parseCategory();
|
|
// assert
|
|
const actualName = getInitParameters(actualCategory)?.name;
|
|
expect(actualName).to.equal(expectedName);
|
|
});
|
|
describe('validates name', () => {
|
|
// arrange
|
|
const expectedName = 'expected category name to be validated';
|
|
const category = new CategoryDataStub()
|
|
.withName(expectedName);
|
|
const expectedContext: CategoryErrorContext = {
|
|
type: ExecutableType.Category,
|
|
self: category,
|
|
};
|
|
itValidatesName((validatorFactory) => {
|
|
// act
|
|
new TestContext()
|
|
.withData(category)
|
|
.withValidatorFactory(validatorFactory)
|
|
.parseCategory();
|
|
// assert
|
|
return {
|
|
expectedNameToValidate: expectedName,
|
|
expectedErrorContext: expectedContext,
|
|
};
|
|
});
|
|
});
|
|
});
|
|
describe('docs', () => {
|
|
it('parses docs correctly', () => {
|
|
// arrange
|
|
const url = 'https://privacy.sexy';
|
|
const categoryData = new CategoryDataStub()
|
|
.withDocs(url);
|
|
const parseDocs: DocsParser = (data) => {
|
|
return [
|
|
`parsed docs: ${JSON.stringify(data)}`,
|
|
];
|
|
};
|
|
const expectedDocs = parseDocs(categoryData);
|
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
|
// act
|
|
const actualCategory = new TestContext()
|
|
.withData(categoryData)
|
|
.withCategoryFactory(categoryFactorySpy)
|
|
.withDocsParser(parseDocs)
|
|
.parseCategory();
|
|
// assert
|
|
const actualDocs = getInitParameters(actualCategory)?.docs;
|
|
expect(actualDocs).to.deep.equal(expectedDocs);
|
|
});
|
|
});
|
|
describe('property validation', () => {
|
|
describe('validates for unknown executable', () => {
|
|
// arrange
|
|
const category = new CategoryDataStub();
|
|
const expectedContext: CategoryErrorContext = {
|
|
type: ExecutableType.Category,
|
|
self: category,
|
|
};
|
|
const expectedAssertion: ObjectAssertion<unknown> = {
|
|
value: category,
|
|
valueName: 'Executable',
|
|
};
|
|
itValidatesType(
|
|
(validatorFactory) => {
|
|
// act
|
|
new TestContext()
|
|
.withData(category)
|
|
.withValidatorFactory(validatorFactory)
|
|
.parseCategory();
|
|
// assert
|
|
return {
|
|
assertValidation: (validator) => validator.assertObject(expectedAssertion),
|
|
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 TestContext()
|
|
.withData(category)
|
|
.withValidatorFactory(validatorFactory)
|
|
.parseCategory();
|
|
// assert
|
|
return {
|
|
assertValidation: (validator) => validator.assertObject(expectedAssertion),
|
|
expectedErrorContext: expectedContext,
|
|
};
|
|
},
|
|
);
|
|
});
|
|
});
|
|
describe('children', () => {
|
|
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 TestContext()
|
|
.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
|
|
const testScenarios = generateDataValidationTestScenarios<ExecutableData>({
|
|
expectFail: [{
|
|
description: 'child has incorrect properties',
|
|
data: { property: 'non-empty-value' } as unknown as ExecutableData,
|
|
}],
|
|
expectPass: [
|
|
{
|
|
description: 'child is a category',
|
|
data: new CategoryDataStub(),
|
|
},
|
|
{
|
|
description: 'child is a script with call',
|
|
data: createScriptDataWithCall(),
|
|
},
|
|
{
|
|
description: 'child is a script with code',
|
|
data: createScriptDataWithCode(),
|
|
},
|
|
],
|
|
});
|
|
testScenarios.forEach(({
|
|
description, expectedPass, data: childData,
|
|
}) => {
|
|
describe(description, () => {
|
|
itAsserts({
|
|
expectedConditionResult: expectedPass,
|
|
test: (validatorFactory) => {
|
|
const expectedError = 'Executable is neither a category or a script.';
|
|
const parent = new CategoryDataStub()
|
|
.withName('parent')
|
|
.withChildren([new CategoryDataStub().withName('valid child'), childData]);
|
|
const expectedContext: UnknownExecutableErrorContext = {
|
|
self: childData,
|
|
parentCategory: parent,
|
|
};
|
|
// act
|
|
new TestContext()
|
|
.withData(parent)
|
|
.withValidatorFactory(validatorFactory)
|
|
.parseCategory();
|
|
// assert
|
|
return {
|
|
expectedErrorMessage: expectedError,
|
|
expectedErrorContext: expectedContext,
|
|
};
|
|
},
|
|
});
|
|
});
|
|
});
|
|
});
|
|
describe('validates children recursively', () => {
|
|
describe('validates (1th-level) child type', () => {
|
|
// arrange
|
|
const expectedName = 'child category';
|
|
const child = new CategoryDataStub()
|
|
.withName(expectedName);
|
|
const parent = new CategoryDataStub()
|
|
.withName('parent')
|
|
.withChildren([child]);
|
|
const expectedContext: UnknownExecutableErrorContext = {
|
|
self: child,
|
|
parentCategory: parent,
|
|
};
|
|
const expectedAssertion: ObjectAssertion<unknown> = {
|
|
value: child,
|
|
valueName: 'Executable',
|
|
};
|
|
itValidatesType(
|
|
(validatorFactory) => {
|
|
// act
|
|
new TestContext()
|
|
.withData(parent)
|
|
.withValidatorFactory(validatorFactory)
|
|
.parseCategory();
|
|
// assert
|
|
return {
|
|
assertValidation: (validator) => validator.assertObject(expectedAssertion),
|
|
expectedErrorContext: expectedContext,
|
|
};
|
|
},
|
|
);
|
|
});
|
|
describe('validates that (2nd-level) child name', () => {
|
|
// arrange
|
|
const expectedName = 'grandchild category';
|
|
const grandChild = new CategoryDataStub()
|
|
.withName(expectedName);
|
|
const child = new CategoryDataStub()
|
|
.withChildren([grandChild])
|
|
.withName('child category');
|
|
const parent = new CategoryDataStub()
|
|
.withName('parent')
|
|
.withChildren([child]);
|
|
const expectedContext: CategoryErrorContext = {
|
|
type: ExecutableType.Category,
|
|
self: grandChild,
|
|
parentCategory: child,
|
|
};
|
|
itValidatesName((validatorFactory) => {
|
|
// act
|
|
new TestContext()
|
|
.withData(parent)
|
|
.withValidatorFactory(validatorFactory)
|
|
.parseCategory();
|
|
// assert
|
|
return {
|
|
expectedNameToValidate: expectedName,
|
|
expectedErrorContext: expectedContext,
|
|
};
|
|
});
|
|
});
|
|
});
|
|
describe('parses correct subscript', () => {
|
|
it('parses single script correctly', () => {
|
|
// arrange
|
|
const expectedScript = new ScriptStub('expected script');
|
|
const scriptParser = new ScriptParserStub();
|
|
const childScriptData = createScriptDataWithCode();
|
|
const categoryData = new CategoryDataStub()
|
|
.withChildren([childScriptData]);
|
|
scriptParser.setupParsedResultForData(childScriptData, expectedScript);
|
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
|
// act
|
|
const actualCategory = new TestContext()
|
|
.withData(categoryData)
|
|
.withScriptParser(scriptParser.get())
|
|
.withCategoryFactory(categoryFactorySpy)
|
|
.parseCategory();
|
|
// assert
|
|
const actualScripts = getInitParameters(actualCategory)?.scripts;
|
|
expectExists(actualScripts);
|
|
expect(actualScripts).to.have.lengthOf(1);
|
|
const actualScript = actualScripts[0];
|
|
expect(actualScript).to.equal(expectedScript);
|
|
});
|
|
it('parses multiple scripts correctly', () => {
|
|
// arrange
|
|
const expectedScripts = [
|
|
new ScriptStub('expected-first-script'),
|
|
new ScriptStub('expected-second-script'),
|
|
];
|
|
const childrenData = [
|
|
createScriptDataWithCall(),
|
|
createScriptDataWithCode(),
|
|
];
|
|
const scriptParser = new ScriptParserStub();
|
|
childrenData.forEach((_, index) => {
|
|
scriptParser.setupParsedResultForData(childrenData[index], expectedScripts[index]);
|
|
});
|
|
const categoryData = new CategoryDataStub()
|
|
.withChildren(childrenData);
|
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
|
// act
|
|
const actualCategory = new TestContext()
|
|
.withScriptParser(scriptParser.get())
|
|
.withData(categoryData)
|
|
.withCategoryFactory(categoryFactorySpy)
|
|
.parseCategory();
|
|
// assert
|
|
const actualParsedScripts = getInitParameters(actualCategory)?.scripts;
|
|
expectExists(actualParsedScripts);
|
|
expect(actualParsedScripts.length).to.equal(expectedScripts.length);
|
|
expect(actualParsedScripts).to.have.members(expectedScripts);
|
|
});
|
|
it('parses all scripts with correct utilities', () => {
|
|
// arrange
|
|
const expected = new CategoryCollectionSpecificUtilitiesStub();
|
|
const scriptParser = new ScriptParserStub();
|
|
const childrenData = [
|
|
createScriptDataWithCode(),
|
|
createScriptDataWithCode(),
|
|
createScriptDataWithCode(),
|
|
];
|
|
const categoryData = new CategoryDataStub()
|
|
.withChildren(childrenData);
|
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
|
// act
|
|
const actualCategory = new TestContext()
|
|
.withData(categoryData)
|
|
.withCollectionUtilities(expected)
|
|
.withScriptParser(scriptParser.get())
|
|
.withCategoryFactory(categoryFactorySpy)
|
|
.parseCategory();
|
|
// assert
|
|
const actualParsedScripts = getInitParameters(actualCategory)?.scripts;
|
|
expectExists(actualParsedScripts);
|
|
const actualUtilities = actualParsedScripts.map(
|
|
(s) => scriptParser.getParseParameters(s)[1],
|
|
);
|
|
expect(
|
|
actualUtilities.every(
|
|
(actual) => actual === expected,
|
|
),
|
|
formatAssertionMessage([
|
|
`Expected all elements to be ${JSON.stringify(expected)}`,
|
|
'All elements:',
|
|
indentText(JSON.stringify(actualUtilities)),
|
|
]),
|
|
).to.equal(true);
|
|
});
|
|
});
|
|
it('parses correct subcategories', () => {
|
|
// arrange
|
|
const expectedChildCategory = new CategoryStub('expected-child-category');
|
|
const childCategoryData = new CategoryDataStub()
|
|
.withName('expected child category')
|
|
.withChildren([createScriptDataWithCode()]);
|
|
const categoryData = new CategoryDataStub()
|
|
.withName('category name')
|
|
.withChildren([childCategoryData]);
|
|
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
|
// act
|
|
const actualCategory = new TestContext()
|
|
.withData(categoryData)
|
|
.withCategoryFactory((parameters) => {
|
|
if (parameters.name === childCategoryData.category) {
|
|
return expectedChildCategory;
|
|
}
|
|
return categoryFactorySpy(parameters);
|
|
})
|
|
.parseCategory();
|
|
// assert
|
|
const actualSubcategories = getInitParameters(actualCategory)?.subcategories;
|
|
expectExists(actualSubcategories);
|
|
expect(actualSubcategories).to.have.lengthOf(1);
|
|
expect(actualSubcategories[0]).to.equal(expectedChildCategory);
|
|
});
|
|
});
|
|
describe('category creation', () => {
|
|
it('creates category from the factory', () => {
|
|
// arrange
|
|
const expectedCategory = new CategoryStub('expected-category');
|
|
const categoryFactory: CategoryFactory = () => expectedCategory;
|
|
// act
|
|
const actualCategory = new TestContext()
|
|
.withCategoryFactory(categoryFactory)
|
|
.parseCategory();
|
|
// assert
|
|
expect(actualCategory).to.equal(expectedCategory);
|
|
});
|
|
describe('rethrows exception if category factory fails', () => {
|
|
// arrange
|
|
const givenData = new CategoryDataStub();
|
|
const expectedContextMessage = 'Failed to parse category.';
|
|
const expectedError = new Error();
|
|
// act & assert
|
|
itThrowsContextualError({
|
|
throwingAction: (wrapError) => {
|
|
const validatorStub = new ExecutableValidatorStub();
|
|
validatorStub.createContextualErrorMessage = (message) => message;
|
|
const factoryMock: CategoryFactory = () => {
|
|
throw expectedError;
|
|
};
|
|
new TestContext()
|
|
.withCategoryFactory(factoryMock)
|
|
.withValidatorFactory(() => validatorStub)
|
|
.withErrorWrapper(wrapError)
|
|
.withData(givenData)
|
|
.parseCategory();
|
|
},
|
|
expectedWrappedError: expectedError,
|
|
expectedContextMessage,
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
class TestContext {
|
|
private data: CategoryData = new CategoryDataStub();
|
|
|
|
private collectionUtilities:
|
|
CategoryCollectionSpecificUtilitiesStub = new CategoryCollectionSpecificUtilitiesStub();
|
|
|
|
private categoryFactory: CategoryFactory = createCategoryFactorySpy().categoryFactorySpy;
|
|
|
|
private errorWrapper: ErrorWithContextWrapper = new ErrorWrapperStub().get();
|
|
|
|
private validatorFactory: ExecutableValidatorFactory = createExecutableValidatorFactoryStub;
|
|
|
|
private docsParser: DocsParser = () => ['docs'];
|
|
|
|
private scriptParser: ScriptParser = new ScriptParserStub().get();
|
|
|
|
public withData(data: CategoryData) {
|
|
this.data = data;
|
|
return this;
|
|
}
|
|
|
|
public withCollectionUtilities(
|
|
collectionUtilities: CategoryCollectionSpecificUtilitiesStub,
|
|
): this {
|
|
this.collectionUtilities = collectionUtilities;
|
|
return this;
|
|
}
|
|
|
|
public withCategoryFactory(categoryFactory: CategoryFactory): this {
|
|
this.categoryFactory = categoryFactory;
|
|
return this;
|
|
}
|
|
|
|
public withValidatorFactory(validatorFactory: ExecutableValidatorFactory): this {
|
|
this.validatorFactory = validatorFactory;
|
|
return this;
|
|
}
|
|
|
|
public withErrorWrapper(errorWrapper: ErrorWithContextWrapper): this {
|
|
this.errorWrapper = errorWrapper;
|
|
return this;
|
|
}
|
|
|
|
public withScriptParser(scriptParser: ScriptParser): this {
|
|
this.scriptParser = scriptParser;
|
|
return this;
|
|
}
|
|
|
|
public withDocsParser(docsParser: DocsParser): this {
|
|
this.docsParser = docsParser;
|
|
return this;
|
|
}
|
|
|
|
public parseCategory() {
|
|
return parseCategory(
|
|
this.data,
|
|
this.collectionUtilities,
|
|
{
|
|
createCategory: this.categoryFactory,
|
|
wrapError: this.errorWrapper,
|
|
createValidator: this.validatorFactory,
|
|
parseScript: this.scriptParser,
|
|
parseDocs: this.docsParser,
|
|
},
|
|
);
|
|
}
|
|
}
|