Refactor executable IDs to use strings #262

This commit unifies executable ID structure across categories and
scripts, paving the way for more complex ID solutions for #262.
It also refactors related code to adapt to the changes.

Key changes:

- Change numeric IDs to string IDs for categories
- Use named types for string IDs to improve code clarity
- Add unit tests to verify ID uniqueness

Other supporting changes:

- Separate concerns in entities for data access and executables by using
  separate abstractions (`Identifiable` and `RepositoryEntity`)
- Simplify usage and construction of entities.
- Remove `BaseEntity` for simplicity.
- Move creation of categories/scripts to domain layer
- Refactor CategoryCollection for better validation logic isolation
- Rename some categories to keep the names (used as pseudo-IDs) unique
  on Windows.
This commit is contained in:
undergroundwires
2024-08-03 16:54:14 +02:00
parent 6fbc81675f
commit ded55a66d6
124 changed files with 2286 additions and 1331 deletions

View File

@@ -14,7 +14,7 @@ import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub'
import type { CategoryCollectionParser } from '@/application/Parser/CategoryCollectionParser';
import type { NonEmptyCollectionAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator';
import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub';
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
describe('ApplicationParser', () => {
describe('parseApplication', () => {

View File

@@ -66,7 +66,10 @@ describe('CategoryCollectionParser', () => {
getInitParameters,
} = createCategoryCollectionFactorySpy();
const actionsData = [getCategoryStub('test1'), getCategoryStub('test2')];
const expectedActions = [new CategoryStub(1), new CategoryStub(2)];
const expectedActions = [
new CategoryStub('expected-action-1'),
new CategoryStub('expected-action-2'),
];
const categoryParserStub = new CategoryParserStub()
.withConfiguredParseResult(actionsData[0], expectedActions[0])
.withConfiguredParseResult(actionsData[1], expectedActions[1]);

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import type { CategoryData, ExecutableData } from '@/application/collections/';
import { type CategoryFactory, parseCategory } from '@/application/Parser/Executable/CategoryParser';
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';
@@ -20,14 +20,48 @@ import { ScriptParserStub } from '@tests/unit/shared/Stubs/ScriptParserStub';
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
import type { NonEmptyCollectionAssertion, ObjectAssertion } from '@/application/Parser/Common/TypeValidator';
import { indentText } from '@/application/Common/Text/IndentText';
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('validation', () => {
describe('validates for name', () => {
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()
@@ -38,7 +72,7 @@ describe('CategoryParser', () => {
};
itValidatesName((validatorFactory) => {
// act
new TestBuilder()
new TestContext()
.withData(category)
.withValidatorFactory(validatorFactory)
.parseCategory();
@@ -49,7 +83,33 @@ describe('CategoryParser', () => {
};
});
});
describe('validates for unknown object', () => {
});
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 = {
@@ -63,7 +123,7 @@ describe('CategoryParser', () => {
itValidatesType(
(validatorFactory) => {
// act
new TestBuilder()
new TestContext()
.withData(category)
.withValidatorFactory(validatorFactory)
.parseCategory();
@@ -90,7 +150,7 @@ describe('CategoryParser', () => {
itValidatesType(
(validatorFactory) => {
// act
new TestBuilder()
new TestContext()
.withData(category)
.withValidatorFactory(validatorFactory)
.parseCategory();
@@ -102,6 +162,8 @@ describe('CategoryParser', () => {
},
);
});
});
describe('children', () => {
describe('validates children for non-empty collection', () => {
// arrange
const category = new CategoryDataStub()
@@ -117,7 +179,7 @@ describe('CategoryParser', () => {
itValidatesType(
(validatorFactory) => {
// act
new TestBuilder()
new TestContext()
.withData(category)
.withValidatorFactory(validatorFactory)
.parseCategory();
@@ -167,7 +229,7 @@ describe('CategoryParser', () => {
parentCategory: parent,
};
// act
new TestBuilder()
new TestContext()
.withData(parent)
.withValidatorFactory(validatorFactory)
.parseCategory();
@@ -201,7 +263,7 @@ describe('CategoryParser', () => {
itValidatesType(
(validatorFactory) => {
// act
new TestBuilder()
new TestContext()
.withData(parent)
.withValidatorFactory(validatorFactory)
.parseCategory();
@@ -231,7 +293,7 @@ describe('CategoryParser', () => {
};
itValidatesName((validatorFactory) => {
// act
new TestBuilder()
new TestContext()
.withData(parent)
.withValidatorFactory(validatorFactory)
.parseCategory();
@@ -243,178 +305,169 @@ describe('CategoryParser', () => {
});
});
});
});
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 TestBuilder()
.withCategoryFactory(factoryMock)
.withValidatorFactory(() => validatorStub)
.withErrorWrapper(wrapError)
.withData(givenData)
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();
},
expectedWrappedError: expectedError,
expectedContextMessage,
});
});
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 TestBuilder()
.withData(categoryData)
.withCategoryFactory(categoryFactorySpy)
.withDocsParser(parseDocs)
.parseCategory();
// assert
const actualDocs = getInitParameters(actualCategory)?.docs;
expect(actualDocs).to.deep.equal(expectedDocs);
});
describe('parses expected 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 TestBuilder()
.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]);
// 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);
});
const categoryData = new CategoryDataStub()
.withChildren(childrenData);
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
// act
const actualCategory = new TestBuilder()
.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', () => {
it('parses correct subcategories', () => {
// arrange
const expected = new CategoryCollectionSpecificUtilitiesStub();
const scriptParser = new ScriptParserStub();
const childrenData = [
createScriptDataWithCode(),
createScriptDataWithCode(),
createScriptDataWithCode(),
];
const expectedChildCategory = new CategoryStub('expected-child-category');
const childCategoryData = new CategoryDataStub()
.withName('expected child category')
.withChildren([createScriptDataWithCode()]);
const categoryData = new CategoryDataStub()
.withChildren(childrenData);
.withName('category name')
.withChildren([childCategoryData]);
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
// act
const actualCategory = new TestBuilder()
const actualCategory = new TestContext()
.withData(categoryData)
.withCollectionUtilities(expected)
.withScriptParser(scriptParser.get())
.withCategoryFactory(categoryFactorySpy)
.withCategoryFactory((parameters) => {
if (parameters.name === childCategoryData.category) {
return expectedChildCategory;
}
return categoryFactorySpy(parameters);
})
.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);
const actualSubcategories = getInitParameters(actualCategory)?.subcategories;
expectExists(actualSubcategories);
expect(actualSubcategories).to.have.lengthOf(1);
expect(actualSubcategories[0]).to.equal(expectedChildCategory);
});
});
it('returns expected subcategories', () => {
// arrange
const expectedChildCategory = new CategoryStub(33);
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 TestBuilder()
.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 TestBuilder {
class TestContext {
private data: CategoryData = new CategoryDataStub();
private collectionUtilities:
CategoryCollectionSpecificUtilitiesStub = new CategoryCollectionSpecificUtilitiesStub();
private categoryFactory: CategoryFactory = () => new CategoryStub(33);
private categoryFactory: CategoryFactory = createCategoryFactorySpy().categoryFactorySpy;
private errorWrapper: ErrorWithContextWrapper = new ErrorWrapperStub().get();

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import type { ScriptData, CallScriptData, CodeScriptData } from '@/application/collections/';
import { parseScript, type ScriptFactory } from '@/application/Parser/Executable/Script/ScriptParser';
import { parseScript } from '@/application/Parser/Executable/Script/ScriptParser';
import { type DocsParser } from '@/application/Parser/Executable/DocumentationParser';
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
@@ -29,53 +29,207 @@ import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/C
import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub';
import type { CategoryCollectionSpecificUtilities } from '@/application/Parser/Executable/CategoryCollectionSpecificUtilities';
import type { ObjectAssertion } from '@/application/Parser/Common/TypeValidator';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import type { ScriptFactory } from '@/domain/Executables/Script/ScriptFactory';
import { itAsserts, itValidatesType, itValidatesName } from '../Validation/ExecutableValidationTester';
import { generateDataValidationTestScenarios } from '../Validation/DataValidationTestScenarioGenerator';
describe('ScriptParser', () => {
describe('parseScript', () => {
it('parses name correctly', () => {
// arrange
const expected = 'test-expected-name';
const scriptData = createScriptDataWithCode()
.withName(expected);
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
// act
const actualScript = new TestContext()
.withData(scriptData)
.withScriptFactory(scriptFactorySpy)
.parseScript();
// assert
const actualName = getInitParameters(actualScript)?.name;
expect(actualName).to.equal(expected);
describe('property validation', () => {
describe('validates object', () => {
// arrange
const expectedScript = createScriptDataWithCall();
const expectedContext: ScriptErrorContext = {
type: ExecutableType.Script,
self: expectedScript,
};
const expectedAssertion: ObjectAssertion<CallScriptData & CodeScriptData> = {
value: expectedScript,
valueName: expectedScript.name,
allowedProperties: [
'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
],
};
itValidatesType(
(validatorFactory) => {
// act
new TestContext()
.withData(expectedScript)
.withValidatorFactory(validatorFactory)
.parseScript();
// assert
return {
expectedDataToValidate: expectedScript,
expectedErrorContext: expectedContext,
assertValidation: (validator) => validator.assertObject(expectedAssertion),
};
},
);
});
describe('validates union type', () => {
// arrange
const testScenarios = generateDataValidationTestScenarios<ScriptData>(
{
assertErrorMessage: 'Neither "call" or "code" is defined.',
expectFail: [{
description: 'with no call or code',
data: createScriptDataWithoutCallOrCodes(),
}],
expectPass: [
{
description: 'with call',
data: createScriptDataWithCall(),
},
{
description: 'with code',
data: createScriptDataWithCode(),
},
],
},
{
assertErrorMessage: 'Both "call" and "revertCode" are defined.',
expectFail: [{
description: 'with both call and revertCode',
data: createScriptDataWithCall()
.withRevertCode('revert-code'),
}],
expectPass: [
{
description: 'with call, without revertCode',
data: createScriptDataWithCall()
.withRevertCode(undefined),
},
{
description: 'with revertCode, without call',
data: createScriptDataWithCode()
.withRevertCode('revert code'),
},
],
},
{
assertErrorMessage: 'Both "call" and "code" are defined.',
expectFail: [{
description: 'with both call and code',
data: createScriptDataWithCall()
.withCode('code'),
}],
expectPass: [
{
description: 'with call, without code',
data: createScriptDataWithCall()
.withCode(''),
},
{
description: 'with code, without call',
data: createScriptDataWithCode()
.withCode('code'),
},
],
},
);
testScenarios.forEach(({
description, expectedPass, data: scriptData, expectedMessage,
}) => {
describe(description, () => {
itAsserts({
expectedConditionResult: expectedPass,
test: (validatorFactory) => {
const expectedContext: ScriptErrorContext = {
type: ExecutableType.Script,
self: scriptData,
};
// act
new TestContext()
.withData(scriptData)
.withValidatorFactory(validatorFactory)
.parseScript();
// assert
expectExists(expectedMessage);
return {
expectedErrorMessage: expectedMessage,
expectedErrorContext: expectedContext,
};
},
});
});
});
});
});
it('parses docs correctly', () => {
// arrange
const expectedDocs = ['https://expected-doc1.com', 'https://expected-doc2.com'];
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
const scriptData = createScriptDataWithCode()
.withDocs(expectedDocs);
const docsParser: DocsParser = (data) => data.docs as typeof expectedDocs;
// act
const actualScript = new TestContext()
.withData(scriptData)
.withScriptFactory(scriptFactorySpy)
.withDocsParser(docsParser)
.parseScript();
// assert
const actualDocs = getInitParameters(actualScript)?.docs;
expect(actualDocs).to.deep.equal(expectedDocs);
describe('id', () => {
it('creates ID correctly', () => {
// arrange
const expectedId: ExecutableId = 'expected-id';
const scriptData = createScriptDataWithCode()
.withName(expectedId);
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
// act
const actualScript = new TestContext()
.withData(scriptData)
.withScriptFactory(scriptFactorySpy)
.parseScript();
// assert
const actualId = getInitParameters(actualScript)?.executableId;
expect(actualId).to.equal(expectedId);
});
});
it('gets script from the factory', () => {
// arrange
const expectedScript = new ScriptStub('expected-script');
const scriptFactory: ScriptFactory = () => expectedScript;
// act
const actualScript = new TestContext()
.withScriptFactory(scriptFactory)
.parseScript();
// assert
expect(actualScript).to.equal(expectedScript);
describe('name', () => {
it('parses name correctly', () => {
// arrange
const expected = 'test-expected-name';
const scriptData = createScriptDataWithCode()
.withName(expected);
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
// act
const actualScript = new TestContext()
.withData(scriptData)
.withScriptFactory(scriptFactorySpy)
.parseScript();
// assert
const actualName = getInitParameters(actualScript)?.name;
expect(actualName).to.equal(expected);
});
describe('validates name', () => {
// arrange
const expectedName = 'expected script name to be validated';
const script = createScriptDataWithCall()
.withName(expectedName);
const expectedContext: ScriptErrorContext = {
type: ExecutableType.Script,
self: script,
};
itValidatesName((validatorFactory) => {
// act
new TestContext()
.withData(script)
.withValidatorFactory(validatorFactory)
.parseScript();
// assert
return {
expectedNameToValidate: expectedName,
expectedErrorContext: expectedContext,
};
});
});
});
describe('docs', () => {
it('parses docs correctly', () => {
// arrange
const expectedDocs = ['https://expected-doc1.com', 'https://expected-doc2.com'];
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
const scriptData = createScriptDataWithCode()
.withDocs(expectedDocs);
const docsParser: DocsParser = (data) => data.docs as typeof expectedDocs;
// act
const actualScript = new TestContext()
.withData(scriptData)
.withScriptFactory(scriptFactorySpy)
.withDocsParser(docsParser)
.parseScript();
// assert
const actualDocs = getInitParameters(actualScript)?.docs;
expect(actualDocs).to.deep.equal(expectedDocs);
});
});
describe('level', () => {
describe('generated `undefined` level if given absent value', () => {
@@ -261,175 +415,46 @@ describe('ScriptParser', () => {
});
});
});
describe('validation', () => {
describe('validates for name', () => {
describe('script creation', () => {
it('creates script from the factory', () => {
// arrange
const expectedName = 'expected script name to be validated';
const script = createScriptDataWithCall()
.withName(expectedName);
const expectedContext: ScriptErrorContext = {
type: ExecutableType.Script,
self: script,
};
itValidatesName((validatorFactory) => {
// act
new TestContext()
.withData(script)
.withValidatorFactory(validatorFactory)
.parseScript();
// assert
return {
expectedNameToValidate: expectedName,
expectedErrorContext: expectedContext,
};
});
const expectedScript = new ScriptStub('expected-script');
const scriptFactory: ScriptFactory = () => expectedScript;
// act
const actualScript = new TestContext()
.withScriptFactory(scriptFactory)
.parseScript();
// assert
expect(actualScript).to.equal(expectedScript);
});
describe('validates for defined data', () => {
describe('rethrows exception if script factory fails', () => {
// arrange
const expectedScript = createScriptDataWithCall();
const expectedContext: ScriptErrorContext = {
type: ExecutableType.Script,
self: expectedScript,
const givenData = createScriptDataWithCode();
const expectedContextMessage = 'Failed to parse script.';
const expectedError = new Error();
const validatorFactory: ExecutableValidatorFactory = () => {
const validatorStub = new ExecutableValidatorStub();
validatorStub.createContextualErrorMessage = (message) => message;
return validatorStub;
};
const expectedAssertion: ObjectAssertion<CallScriptData & CodeScriptData> = {
value: expectedScript,
valueName: expectedScript.name,
allowedProperties: [
'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
],
};
itValidatesType(
(validatorFactory) => {
// act
new TestContext()
.withData(expectedScript)
.withValidatorFactory(validatorFactory)
.parseScript();
// assert
return {
expectedDataToValidate: expectedScript,
expectedErrorContext: expectedContext,
assertValidation: (validator) => validator.assertObject(expectedAssertion),
// act & assert
itThrowsContextualError({
throwingAction: (wrapError) => {
const factoryMock: ScriptFactory = () => {
throw expectedError;
};
new TestContext()
.withScriptFactory(factoryMock)
.withErrorWrapper(wrapError)
.withValidatorFactory(validatorFactory)
.withData(givenData)
.parseScript();
},
);
});
describe('validates data', () => {
// arrange
const testScenarios = generateDataValidationTestScenarios<ScriptData>(
{
assertErrorMessage: 'Neither "call" or "code" is defined.',
expectFail: [{
description: 'with no call or code',
data: createScriptDataWithoutCallOrCodes(),
}],
expectPass: [
{
description: 'with call',
data: createScriptDataWithCall(),
},
{
description: 'with code',
data: createScriptDataWithCode(),
},
],
},
{
assertErrorMessage: 'Both "call" and "revertCode" are defined.',
expectFail: [{
description: 'with both call and revertCode',
data: createScriptDataWithCall()
.withRevertCode('revert-code'),
}],
expectPass: [
{
description: 'with call, without revertCode',
data: createScriptDataWithCall()
.withRevertCode(undefined),
},
{
description: 'with revertCode, without call',
data: createScriptDataWithCode()
.withRevertCode('revert code'),
},
],
},
{
assertErrorMessage: 'Both "call" and "code" are defined.',
expectFail: [{
description: 'with both call and code',
data: createScriptDataWithCall()
.withCode('code'),
}],
expectPass: [
{
description: 'with call, without code',
data: createScriptDataWithCall()
.withCode(''),
},
{
description: 'with code, without call',
data: createScriptDataWithCode()
.withCode('code'),
},
],
},
);
testScenarios.forEach(({
description, expectedPass, data: scriptData, expectedMessage,
}) => {
describe(description, () => {
itAsserts({
expectedConditionResult: expectedPass,
test: (validatorFactory) => {
const expectedContext: ScriptErrorContext = {
type: ExecutableType.Script,
self: scriptData,
};
// act
new TestContext()
.withData(scriptData)
.withValidatorFactory(validatorFactory)
.parseScript();
// assert
expectExists(expectedMessage);
return {
expectedErrorMessage: expectedMessage,
expectedErrorContext: expectedContext,
};
},
});
});
expectedWrappedError: expectedError,
expectedContextMessage,
});
});
});
describe('rethrows exception if script factory fails', () => {
// arrange
const givenData = createScriptDataWithCode();
const expectedContextMessage = 'Failed to parse script.';
const expectedError = new Error();
const validatorFactory: ExecutableValidatorFactory = () => {
const validatorStub = new ExecutableValidatorStub();
validatorStub.createContextualErrorMessage = (message) => message;
return validatorStub;
};
// act & assert
itThrowsContextualError({
throwingAction: (wrapError) => {
const factoryMock: ScriptFactory = () => {
throw expectedError;
};
new TestContext()
.withScriptFactory(factoryMock)
.withErrorWrapper(wrapError)
.withValidatorFactory(validatorFactory)
.withData(givenData)
.parseScript();
},
expectedWrappedError: expectedError,
expectedContextMessage,
});
});
});
});