Improve script/category name validation
- Use better error messages with more context. - Unify their validation logic and share tests. - Validate also type of the name. - Refactor node (Script/Category) parser tests for easier future changes and cleaner test code (using `TestBuilder` to do dirty work in unified way). - Add more tests. Custom `Error` properties are compared manually due to `chai` not supporting deep equality checks (chaijs/chai#1065, chaijs/chai#1405).
This commit is contained in:
@@ -1,16 +1,21 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { parseScript } from '@/application/Parser/Script/ScriptParser';
|
||||
import type { ScriptData } from '@/application/collections/';
|
||||
import { parseScript, ScriptFactoryType } from '@/application/Parser/Script/ScriptParser';
|
||||
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
|
||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
import { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
|
||||
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { ScriptCompilerStub } from '@tests/unit/shared/Stubs/ScriptCompilerStub';
|
||||
import { ScriptDataStub } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||
import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub';
|
||||
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
|
||||
import { CategoryCollectionParseContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionParseContextStub';
|
||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
||||
import { expectThrowsNodeError, ITestScenario, NodeValidationTestRunner } from '@tests/unit/application/Parser/NodeValidation/NodeValidatorTestRunner';
|
||||
import { Script } from '@/domain/Script';
|
||||
import { IEnumParser } from '@/application/Common/Enum';
|
||||
|
||||
describe('ScriptParser', () => {
|
||||
describe('parseScript', () => {
|
||||
@@ -19,9 +24,10 @@ describe('ScriptParser', () => {
|
||||
const expected = 'test-expected-name';
|
||||
const script = ScriptDataStub.createWithCode()
|
||||
.withName(expected);
|
||||
const parseContext = new CategoryCollectionParseContextStub();
|
||||
// act
|
||||
const actual = parseScript(script, parseContext);
|
||||
const actual = new TestBuilder()
|
||||
.withData(script)
|
||||
.parseScript();
|
||||
// assert
|
||||
expect(actual.name).to.equal(expected);
|
||||
});
|
||||
@@ -30,86 +36,42 @@ describe('ScriptParser', () => {
|
||||
const docs = ['https://expected-doc1.com', 'https://expected-doc2.com'];
|
||||
const script = ScriptDataStub.createWithCode()
|
||||
.withDocs(docs);
|
||||
const parseContext = new CategoryCollectionParseContextStub();
|
||||
const expected = parseDocUrls(script);
|
||||
// act
|
||||
const actual = parseScript(script, parseContext);
|
||||
const actual = new TestBuilder()
|
||||
.withData(script)
|
||||
.parseScript();
|
||||
// assert
|
||||
expect(actual.documentationUrls).to.deep.equal(expected);
|
||||
});
|
||||
describe('invalid script', () => {
|
||||
describe('throws when script is absent', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing script';
|
||||
const parseContext = new CategoryCollectionParseContextStub();
|
||||
const script = absentValue;
|
||||
// act
|
||||
const act = () => parseScript(script, parseContext);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('throws when both function call and code are defined', () => {
|
||||
// arrange
|
||||
const expectedError = 'cannot define both "call" and "code"';
|
||||
const parseContext = new CategoryCollectionParseContextStub();
|
||||
const script = ScriptDataStub
|
||||
.createWithCall()
|
||||
.withCode('code');
|
||||
// act
|
||||
const act = () => parseScript(script, parseContext);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws when both function call and revertCode are defined', () => {
|
||||
// arrange
|
||||
const expectedError = 'cannot define "revertCode" if "call" is defined';
|
||||
const parseContext = new CategoryCollectionParseContextStub();
|
||||
const script = ScriptDataStub
|
||||
.createWithCall()
|
||||
.withRevertCode('revert-code');
|
||||
// act
|
||||
const act = () => parseScript(script, parseContext);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws when neither call or revertCode are defined', () => {
|
||||
// arrange
|
||||
const expectedError = 'must define either "call" or "code"';
|
||||
const parseContext = new CategoryCollectionParseContextStub();
|
||||
const script = ScriptDataStub.createWithoutCallOrCodes();
|
||||
// act
|
||||
const act = () => parseScript(script, parseContext);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('level', () => {
|
||||
describe('accepts absent level', () => {
|
||||
itEachAbsentStringValue((absentValue) => {
|
||||
// arrange
|
||||
const parseContext = new CategoryCollectionParseContextStub();
|
||||
const script = ScriptDataStub.createWithCode()
|
||||
.withRecommend(absentValue);
|
||||
// act
|
||||
const actual = parseScript(script, parseContext);
|
||||
const actual = new TestBuilder()
|
||||
.withData(script)
|
||||
.parseScript();
|
||||
// assert
|
||||
expect(actual.level).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
describe('parses level as expected', () => {
|
||||
it('parses level as expected', () => {
|
||||
// arrange
|
||||
const expectedLevel = RecommendationLevel.Standard;
|
||||
const expectedName = 'level';
|
||||
const levelText = 'standard';
|
||||
const script = ScriptDataStub.createWithCode()
|
||||
.withRecommend(levelText);
|
||||
const parseContext = new CategoryCollectionParseContextStub();
|
||||
const parserMock = new EnumParserStub<RecommendationLevel>()
|
||||
.setup(expectedName, levelText, expectedLevel);
|
||||
// act
|
||||
const actual = parseScript(script, parseContext, parserMock);
|
||||
const actual = new TestBuilder()
|
||||
.withData(script)
|
||||
.withParser(parserMock)
|
||||
.parseScript();
|
||||
// assert
|
||||
expect(actual.level).to.equal(expectedLevel);
|
||||
});
|
||||
@@ -121,9 +83,10 @@ describe('ScriptParser', () => {
|
||||
const script = ScriptDataStub
|
||||
.createWithCode()
|
||||
.withCode(expected);
|
||||
const parseContext = new CategoryCollectionParseContextStub();
|
||||
// act
|
||||
const parsed = parseScript(script, parseContext);
|
||||
const parsed = new TestBuilder()
|
||||
.withData(script)
|
||||
.parseScript();
|
||||
// assert
|
||||
const actual = parsed.code.execute;
|
||||
expect(actual).to.equal(expected);
|
||||
@@ -134,9 +97,10 @@ describe('ScriptParser', () => {
|
||||
const script = ScriptDataStub
|
||||
.createWithCode()
|
||||
.withRevertCode(expected);
|
||||
const parseContext = new CategoryCollectionParseContextStub();
|
||||
// act
|
||||
const parsed = parseScript(script, parseContext);
|
||||
const parsed = new TestBuilder()
|
||||
.withData(script)
|
||||
.parseScript();
|
||||
// assert
|
||||
const actual = parsed.code.revert;
|
||||
expect(actual).to.equal(expected);
|
||||
@@ -146,10 +110,11 @@ describe('ScriptParser', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedMessage = 'missing context';
|
||||
const script = ScriptDataStub.createWithCode();
|
||||
const context: ICategoryCollectionParseContext = absentValue;
|
||||
// act
|
||||
const act = () => parseScript(script, context);
|
||||
const act = () => new TestBuilder()
|
||||
.withContext(context)
|
||||
.parseScript();
|
||||
// assert
|
||||
expect(act).to.throw(expectedMessage);
|
||||
});
|
||||
@@ -163,14 +128,17 @@ describe('ScriptParser', () => {
|
||||
const parseContext = new CategoryCollectionParseContextStub()
|
||||
.withCompiler(compiler);
|
||||
// act
|
||||
const parsed = parseScript(script, parseContext);
|
||||
const parsed = new TestBuilder()
|
||||
.withData(script)
|
||||
.withContext(parseContext)
|
||||
.parseScript();
|
||||
// assert
|
||||
const actual = parsed.code;
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
});
|
||||
describe('syntax', () => {
|
||||
it('set from the context', () => { // test through script validation logic
|
||||
it('set from the context', () => { // tests through script validation logic
|
||||
// arrange
|
||||
const commentDelimiter = 'should not throw';
|
||||
const duplicatedCode = `${commentDelimiter} duplicate-line\n${commentDelimiter} duplicate-line`;
|
||||
@@ -180,11 +148,112 @@ describe('ScriptParser', () => {
|
||||
.createWithoutCallOrCodes()
|
||||
.withCode(duplicatedCode);
|
||||
// act
|
||||
const act = () => parseScript(script, parseContext);
|
||||
const act = () => new TestBuilder()
|
||||
.withData(script)
|
||||
.withContext(parseContext);
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('invalid script data', () => {
|
||||
describe('validates script data', () => {
|
||||
// arrange
|
||||
const createTest = (script: ScriptData): ITestScenario => ({
|
||||
act: () => new TestBuilder()
|
||||
.withData(script)
|
||||
.parseScript(),
|
||||
expectedContext: {
|
||||
type: NodeType.Script,
|
||||
selfNode: script,
|
||||
},
|
||||
});
|
||||
// act and assert
|
||||
new NodeValidationTestRunner()
|
||||
.testInvalidNodeName((invalidName) => {
|
||||
return createTest(
|
||||
ScriptDataStub.createWithCall().withName(invalidName),
|
||||
);
|
||||
})
|
||||
.testMissingNodeData((node) => {
|
||||
return createTest(node as ScriptData);
|
||||
})
|
||||
.runThrowingCase({
|
||||
name: 'throws when both function call and code are defined',
|
||||
scenario: createTest(
|
||||
ScriptDataStub.createWithCall().withCode('code'),
|
||||
),
|
||||
expectedMessage: 'Cannot define both "call" and "code".',
|
||||
})
|
||||
.runThrowingCase({
|
||||
name: 'throws when both function call and revertCode are defined',
|
||||
scenario: createTest(
|
||||
ScriptDataStub.createWithCall().withRevertCode('revert-code'),
|
||||
),
|
||||
expectedMessage: 'Cannot define "revertCode" if "call" is defined.',
|
||||
})
|
||||
.runThrowingCase({
|
||||
name: 'throws when neither call or revertCode are defined',
|
||||
scenario: createTest(
|
||||
ScriptDataStub.createWithoutCallOrCodes(),
|
||||
),
|
||||
expectedMessage: 'Must define either "call" or "code".',
|
||||
});
|
||||
});
|
||||
it(`rethrows exception if ${Script.name} cannot be constructed`, () => {
|
||||
// arrange
|
||||
const expectedError = 'script creation failed';
|
||||
const factoryMock: ScriptFactoryType = () => { throw new Error(expectedError); };
|
||||
const data = ScriptDataStub.createWithCode();
|
||||
// act
|
||||
const act = () => new TestBuilder()
|
||||
.withData(data)
|
||||
.withFactory(factoryMock)
|
||||
.parseScript();
|
||||
// expect
|
||||
expectThrowsNodeError({
|
||||
act,
|
||||
expectedContext: {
|
||||
type: NodeType.Script,
|
||||
selfNode: data,
|
||||
},
|
||||
}, expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class TestBuilder {
|
||||
private data: ScriptData = ScriptDataStub.createWithCode();
|
||||
|
||||
private context: ICategoryCollectionParseContext = new CategoryCollectionParseContextStub();
|
||||
|
||||
private parser: IEnumParser<RecommendationLevel> = new EnumParserStub<RecommendationLevel>()
|
||||
.setupDefaultValue(RecommendationLevel.Standard);
|
||||
|
||||
private factory: ScriptFactoryType = undefined;
|
||||
|
||||
public withData(data: ScriptData) {
|
||||
this.data = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withContext(context: ICategoryCollectionParseContext) {
|
||||
this.context = context;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withParser(parser: IEnumParser<RecommendationLevel>) {
|
||||
this.parser = parser;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withFactory(factory: ScriptFactoryType) {
|
||||
this.factory = factory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public parseScript(): Script {
|
||||
return parseScript(this.data, this.context, this.parser, this.factory);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user