Add type validation for parameters and fix types

This commit introduces type validation for parameter values within the
parser/compiler, aligning with the YAML schema. It aims to eliminate
dependencies on side effects in the collection files.

This update changes the treatment of data types in the Windows
collection, moving away from unintended type casting by the compiler.
Previously, numeric and boolean values were used even though only
string types were supported. This behavior was unstable and untested,
and has now been adjusted to use strings exclusively.

Changes ensure that parameter values are correctly validated
as strings, enhancing stability and maintainability.
This commit is contained in:
undergroundwires
2024-06-19 17:01:27 +02:00
parent 48761f62a2
commit fac26a6ca0
43 changed files with 873 additions and 466 deletions

View File

@@ -1,91 +1,120 @@
import { describe, it, expect } from 'vitest';
import { CodeSubstituter } from '@/application/Parser/ScriptingDefinition/CodeSubstituter';
import type { IExpressionsCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/IExpressionsCompiler';
import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub';
import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { substituteCode } from '@/application/Parser/ScriptingDefinition/CodeSubstituter';
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
import type { FunctionCallArgumentFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
import { FunctionCallArgumentFactoryStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentFactoryStub';
describe('CodeSubstituter', () => {
describe('throws if code is empty', () => {
itEachAbsentStringValue((emptyCode) => {
// arrange
const expectedError = 'missing code';
const code = emptyCode;
const projectDetails = new ProjectDetailsStub();
const sut = new CodeSubstituterBuilder().build();
// act
const act = () => sut.substitute(code, projectDetails);
// assert
expect(act).to.throw(expectedError);
}, { excludeNull: true, excludeUndefined: true });
});
describe('substitutes parameters as expected values', () => {
// arrange
const projectDetails = new ProjectDetailsStub();
const date = new Date();
const testCases: Array<{ parameter: string, argument: string }> = [
{
parameter: 'homepage',
argument: projectDetails.homepage,
},
{
parameter: 'version',
argument: projectDetails.version.toString(),
},
{
parameter: 'date',
argument: date.toUTCString(),
},
];
for (const testCase of testCases) {
it(`substitutes ${testCase.parameter} as expected`, () => {
const compilerStub = new ExpressionsCompilerStub();
const sut = new CodeSubstituterBuilder()
.withCompiler(compilerStub)
.withDate(date)
.build();
describe('substituteCode', () => {
describe('throws if code is empty', () => {
itEachAbsentStringValue((emptyCode) => {
// arrange
const expectedError = 'missing code';
const context = new TestContext()
.withCode(emptyCode);
// act
sut.substitute('non empty code', projectDetails);
const act = () => context.substitute();
// assert
expect(compilerStub.callHistory).to.have.lengthOf(1);
const parameters = compilerStub.callHistory[0].args[1];
expect(parameters.hasArgument(testCase.parameter));
const { argumentValue } = parameters.getArgument(testCase.parameter);
expect(argumentValue).to.equal(testCase.argument);
});
}
});
it('returns code as it is', () => {
// arrange
const expected = 'expected-code';
const compilerStub = new ExpressionsCompilerStub();
const sut = new CodeSubstituterBuilder()
.withCompiler(compilerStub)
.build();
// act
sut.substitute(expected, new ProjectDetailsStub());
// assert
expect(compilerStub.callHistory).to.have.lengthOf(1);
expect(compilerStub.callHistory[0].args[0]).to.equal(expected);
expect(act).to.throw(expectedError);
}, { excludeNull: true, excludeUndefined: true });
});
describe('substitutes parameters as expected values', () => {
// arrange
const projectDetails = new ProjectDetailsStub();
const date = new Date();
const testCases: Array<{ parameter: string, argument: string }> = [
{
parameter: 'homepage',
argument: projectDetails.homepage,
},
{
parameter: 'version',
argument: projectDetails.version.toString(),
},
{
parameter: 'date',
argument: date.toUTCString(),
},
];
for (const testCase of testCases) {
it(`substitutes ${testCase.parameter} as expected`, () => {
const compilerStub = new ExpressionsCompilerStub();
const context = new TestContext()
.withCompiler(compilerStub)
.withDate(date)
.withProjectDetails(projectDetails);
// act
context.substitute();
// assert
expect(compilerStub.callHistory).to.have.lengthOf(1);
const parameters = compilerStub.callHistory[0].args[1];
expect(parameters.hasArgument(testCase.parameter));
const { argumentValue } = parameters.getArgument(testCase.parameter);
expect(argumentValue).to.equal(testCase.argument);
});
}
});
it('returns code as it is', () => {
// arrange
const expected = 'expected-code';
const compilerStub = new ExpressionsCompilerStub();
const context = new TestContext()
.withCompiler(compilerStub)
.withCode(expected);
// act
context.substitute();
// assert
expect(compilerStub.callHistory).to.have.lengthOf(1);
expect(compilerStub.callHistory[0].args[0]).to.equal(expected);
});
});
});
class CodeSubstituterBuilder {
class TestContext {
private compiler: IExpressionsCompiler = new ExpressionsCompilerStub();
private date = new Date();
public withCompiler(compiler: IExpressionsCompiler) {
private code = `[${TestContext.name}] default code for testing`;
private projectDetails: ProjectDetails = new ProjectDetailsStub();
private callArgumentFactory
: FunctionCallArgumentFactory = new FunctionCallArgumentFactoryStub().factory;
public withCompiler(compiler: IExpressionsCompiler): this {
this.compiler = compiler;
return this;
}
public withDate(date: Date) {
public withDate(date: Date): this {
this.date = date;
return this;
}
public build() {
return new CodeSubstituter(this.compiler, this.date);
public withCode(code: string): this {
this.code = code;
return this;
}
public withProjectDetails(projectDetails: ProjectDetails): this {
this.projectDetails = projectDetails;
return this;
}
public substitute(): ReturnType<typeof substituteCode> {
return substituteCode(
this.code,
this.projectDetails,
{
compiler: this.compiler,
provideDate: () => this.date,
createCallArgument: this.callArgumentFactory,
},
);
}
}

View File

@@ -1,7 +1,7 @@
import { describe, it, expect } from 'vitest';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import type { EnumParser } from '@/application/Common/Enum';
import type { ICodeSubstituter } from '@/application/Parser/ScriptingDefinition/ICodeSubstituter';
import type { CodeSubstituter } from '@/application/Parser/ScriptingDefinition/CodeSubstituter';
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub';
import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub';
@@ -83,7 +83,7 @@ describe('ScriptingDefinitionParser', () => {
const context = new TestContext()
.withData(data)
.withProjectDetails(projectDetails)
.withSubstituter(substituterMock);
.withSubstituter(substituterMock.substitute);
// act
const definition = context.parseScriptingDefinition();
// assert
@@ -99,7 +99,7 @@ class TestContext {
private languageParser: EnumParser<ScriptingLanguage> = new EnumParserStub<ScriptingLanguage>()
.setupDefaultValue(ScriptingLanguage.shellscript);
private codeSubstituter: ICodeSubstituter = new CodeSubstituterStub();
private codeSubstituter: CodeSubstituter = new CodeSubstituterStub().substitute;
private validator: TypeValidator = new TypeValidatorStub();
@@ -122,7 +122,7 @@ class TestContext {
return this;
}
public withSubstituter(substituter: ICodeSubstituter): this {
public withSubstituter(substituter: CodeSubstituter): this {
this.codeSubstituter = substituter;
return this;
}