Files
privacy.sexy/tests/unit/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.spec.ts
undergroundwires fac26a6ca0 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.
2024-06-19 17:01:27 +02:00

514 lines
21 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import type {
FunctionData, CodeInstruction,
ParameterDefinitionData, FunctionCallsData,
} from '@/application/collections/';
import type { ISharedFunction } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction';
import { parseSharedFunctions } from '@/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser';
import { createFunctionDataWithCall, createFunctionDataWithCode, createFunctionDataWithoutCallOrCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
import { ParameterDefinitionDataStub } from '@tests/unit/shared/Stubs/ParameterDefinitionDataStub';
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines';
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester';
import type { FunctionParameterCollectionFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory';
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
import type { FunctionCallsParser } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser';
import { createFunctionCallsParserStub } from '@tests/unit/shared/Stubs/FunctionCallsParserStub';
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
import type { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
import type { FunctionParameterParser } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterParser';
import { createFunctionParameterParserStub } from '@tests/unit/shared/Stubs/FunctionParameterParserStub';
import { expectCallsFunctionBody, expectCodeFunctionBody } from './ExpectFunctionBodyType';
describe('SharedFunctionsParser', () => {
describe('parseSharedFunctions', () => {
describe('validates functions', () => {
it('throws when no name is provided', () => {
// arrange
const invalidFunctions = [
createFunctionDataWithCode()
.withCode('test function 1')
.withName(' '), // Whitespace,
createFunctionDataWithCode()
.withCode('test function 2')
.withName(undefined as unknown as string), // Undefined
createFunctionDataWithCode()
.withCode('test function 3')
.withName(''), // Empty
];
const expectedError = `Some function(s) have no names:\n${invalidFunctions.map((f) => JSON.stringify(f)).join('\n')}`;
// act
const act = () => new TestContext()
.withFunctions(invalidFunctions)
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
it('throws when functions have duplicate names', () => {
// arrange
const name = 'same-func-name';
const expectedError = `duplicate function name: "${name}"`;
const functions = [
createFunctionDataWithCode().withName(name),
createFunctionDataWithCode().withName(name),
];
// act
const act = () => new TestContext()
.withFunctions(functions)
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
describe('throws when functions have duplicate code', () => {
it('throws on code duplication', () => {
// arrange
const code = 'duplicate-code';
const expectedError = `duplicate "code" in functions: "${code}"`;
const functions = [
createFunctionDataWithoutCallOrCode().withName('func-1').withCode(code),
createFunctionDataWithoutCallOrCode().withName('func-2').withCode(code),
];
// act
const act = () => new TestContext()
.withFunctions(functions)
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
it('throws on revert code duplication', () => {
// arrange
const revertCode = 'duplicate-revert-code';
const expectedError = `duplicate "revertCode" in functions: "${revertCode}"`;
const functions = [
createFunctionDataWithoutCallOrCode()
.withName('func-1').withCode('code-1').withRevertCode(revertCode),
createFunctionDataWithoutCallOrCode()
.withName('func-2').withCode('code-2').withRevertCode(revertCode),
];
// act
const act = () => new TestContext()
.withFunctions(functions)
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
});
describe('throws when both or neither code and call are defined', () => {
it('throws when both code and call are defined', () => {
// arrange
const functionName = 'invalid-function';
const expectedError = `both "code" and "call" are defined in "${functionName}"`;
const invalidFunction = createFunctionDataWithoutCallOrCode()
.withName(functionName)
.withCode('code')
.withMockCall();
// act
const act = () => new TestContext()
.withFunctions([invalidFunction])
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
it('throws when neither code nor call is defined', () => {
// arrange
const functionName = 'invalid-function';
const expectedError = `neither "code" or "call" is defined in "${functionName}"`;
const invalidFunction = createFunctionDataWithoutCallOrCode()
.withName(functionName);
// act
const act = () => new TestContext()
.withFunctions([invalidFunction])
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
});
describe('throws when parameter types are invalid', () => {
const testScenarios: readonly {
readonly description: string;
readonly invalidType: unknown;
}[] = [
{
description: 'parameter is not an array',
invalidType: 5,
},
{
description: 'parameter array contains non-objects',
invalidType: ['a', { a: 'b' }],
},
];
for (const testCase of testScenarios) {
it(testCase.description, () => {
// arrange
const func = createFunctionDataWithCode()
.withParametersObject(testCase.invalidType as never);
const expectedError = `parameters must be an array of objects in function(s) "${func.name}"`;
// act
const act = () => new TestContext()
.withFunctions([func])
.parseFunctions();
// assert
expect(act).to.throw(expectedError);
});
}
});
it('validates function code as expected when code is defined', () => {
// arrange
const expectedRules = [NoEmptyLines, NoDuplicatedLines];
const functionData = createFunctionDataWithCode()
.withCode('expected code to be validated')
.withRevertCode('expected revert code to be validated');
const validator = new CodeValidatorStub();
// act
new TestContext()
.withFunctions([functionData])
.withValidator(validator)
.parseFunctions();
// assert
validator.assertHistory({
validatedCodes: [functionData.code, functionData.revertCode],
rules: expectedRules,
});
});
describe('parameter creation', () => {
describe('rethrows including function name when creating parameter throws', () => {
// arrange
const invalidParameterName = 'invalid-function-parameter-name';
const functionName = 'functionName';
const expectedErrorMessage = `Failed to create parameter: ${invalidParameterName} for function "${functionName}"`;
const expectedInnerError = new Error('injected error');
const parser: FunctionParameterParser = () => {
throw expectedInnerError;
};
const functionData = createFunctionDataWithCode()
.withName(functionName)
.withParameters(new ParameterDefinitionDataStub().withName(invalidParameterName));
itThrowsContextualError({
// act
throwingAction: (wrapError) => {
new TestContext()
.withFunctions([functionData])
.withFunctionParameterParser(parser)
.withErrorWrapper(wrapError)
.parseFunctions();
},
// assert
expectedWrappedError: expectedInnerError,
expectedContextMessage: expectedErrorMessage,
});
});
});
});
describe('handles empty function data', () => {
itEachAbsentCollectionValue<FunctionData>((absentValue) => {
// act
const actual = new TestContext()
.withFunctions(absentValue)
.parseFunctions();
// assert
expect(actual).to.not.equal(undefined);
}, { excludeUndefined: true, excludeNull: true });
});
describe('function with inline code', () => {
it('parses single function with code as expected', () => {
// arrange
const name = 'function-name';
const expected = createFunctionDataWithoutCallOrCode()
.withName(name)
.withCode('expected-code')
.withRevertCode('expected-revert-code')
.withParameters(
new ParameterDefinitionDataStub().withName('expectedParameter').withOptionality(true),
new ParameterDefinitionDataStub().withName('expectedParameter2').withOptionality(false),
);
// act
const collection = new TestContext()
.withFunctions([expected])
.parseFunctions();
// expect
const actual = collection.getFunctionByName(name);
expectEqualName(expected, actual);
expectEqualParameters(expected.parameters, actual.parameters);
expectEqualFunctionWithInlineCode(expected, actual);
});
});
describe('function with calls', () => {
describe('parses single function correctly', () => {
it('parses name correctly', () => {
// arrange
const expectedName = 'expected-function-name';
const data = createFunctionDataWithCode()
.withName(expectedName);
// act
const collection = new TestContext()
.withFunctions([data])
.parseFunctions();
// expect
const actual = collection.getFunctionByName(expectedName);
expect(actual.name).to.equal(expectedName);
expectEqualName(data, actual);
});
it('parses parameters correctly', () => {
// arrange
const functionCallsParserStub = createFunctionCallsParserStub();
const expectedParameters: readonly ParameterDefinitionData[] = [
new ParameterDefinitionDataStub().withName('expectedParameter').withOptionality(true),
new ParameterDefinitionDataStub().withName('expectedParameter2').withOptionality(false),
];
const data = createFunctionDataWithCode()
.withParameters(...expectedParameters);
// act
const collection = new TestContext()
.withFunctions([data])
.withFunctionCallsParser(functionCallsParserStub.parser)
.parseFunctions();
// expect
const actual = collection.getFunctionByName(data.name);
expectEqualParameters(expectedParameters, actual.parameters);
});
it('parses call correctly', () => {
// arrange
const functionCallsParserStub = createFunctionCallsParserStub();
const inputCallData = new FunctionCallDataStub()
.withName('function-input-call');
const data = createFunctionDataWithoutCallOrCode()
.withCall(inputCallData);
const expectedCall = new FunctionCallStub()
.withFunctionName('function-expected-call');
functionCallsParserStub.setup(inputCallData, [expectedCall]);
// act
const collection = new TestContext()
.withFunctions([data])
.withFunctionCallsParser(functionCallsParserStub.parser)
.parseFunctions();
// expect
const actualFunction = collection.getFunctionByName(data.name);
expectEqualFunctionWithCalls([expectedCall], actualFunction);
});
});
describe('parses multiple functions correctly', () => {
it('parses names correctly', () => {
// arrange
const expectedNames: readonly string[] = [
'expected-function-name-1',
'expected-function-name-2',
'expected-function-name-3',
];
const data: readonly FunctionData[] = expectedNames.map(
(functionName) => createFunctionDataWithCall()
.withName(functionName),
);
// act
const collection = new TestContext()
.withFunctions(data)
.parseFunctions();
// expect
expectedNames.forEach((name, index) => {
const compiledFunction = collection.getFunctionByName(name);
expectEqualName(data[index], compiledFunction);
});
});
it('parses parameters correctly', () => {
// arrange
const testData: readonly {
readonly functionName: string;
readonly inputParameterData: readonly ParameterDefinitionData[];
}[] = [
{
functionName: 'func1',
inputParameterData: [
new ParameterDefinitionDataStub().withName('func1-first-parameter'),
new ParameterDefinitionDataStub().withName('func1-second-parameter'),
],
},
{
functionName: 'func2',
inputParameterData: [
new ParameterDefinitionDataStub().withName('func2-optional-parameter').withOptionality(true),
new ParameterDefinitionDataStub().withName('func2-required-parameter').withOptionality(false),
],
},
];
const data: readonly FunctionData[] = testData.map(
(d) => createFunctionDataWithCall()
.withName(d.functionName)
.withParameters(...d.inputParameterData),
);
// act
const collection = new TestContext()
.withFunctions(data)
.parseFunctions();
// expect
testData.forEach(({ functionName, inputParameterData }) => {
const actualFunction = collection.getFunctionByName(functionName);
expectEqualParameters(inputParameterData, actualFunction.parameters);
});
});
it('parses call correctly', () => {
// arrange
const functionCallsParserStub = createFunctionCallsParserStub();
const callData: readonly {
readonly functionName: string;
readonly inputData: FunctionCallsData,
readonly expectedCalls: ReturnType<FunctionCallsParser>,
}[] = [
{
functionName: 'function-1',
inputData: new FunctionCallDataStub().withName('function-1-input-function-call'),
expectedCalls: [
new FunctionCallStub().withFunctionName('function-1-compiled-function-call'),
],
},
{
functionName: 'function-2',
inputData: [
new FunctionCallDataStub().withName('function-2-input-function-call-1'),
new FunctionCallDataStub().withName('function-2-input-function-call-2'),
],
expectedCalls: [
new FunctionCallStub().withFunctionName('function-2-compiled-function-call-1'),
new FunctionCallStub().withFunctionName('function-2-compiled-function-call-2'),
],
},
];
const data: readonly FunctionData[] = callData.map(
({ functionName, inputData }) => createFunctionDataWithoutCallOrCode()
.withName(functionName)
.withCall(inputData),
);
callData.forEach(({
inputData,
expectedCalls,
}) => functionCallsParserStub.setup(inputData, expectedCalls));
// act
const collection = new TestContext()
.withFunctions(data)
.withFunctionCallsParser(functionCallsParserStub.parser)
.parseFunctions();
// expect
callData.forEach(({ functionName, expectedCalls }) => {
const actualFunction = collection.getFunctionByName(functionName);
expectEqualFunctionWithCalls(expectedCalls, actualFunction);
});
});
});
});
});
});
class TestContext {
private syntax: ILanguageSyntax = new LanguageSyntaxStub();
private codeValidator: ICodeValidator = new CodeValidatorStub();
private functions: readonly FunctionData[] = [createFunctionDataWithCode()];
private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub;
private functionCallsParser: FunctionCallsParser = createFunctionCallsParserStub().parser;
private functionParameterParser: FunctionParameterParser = createFunctionParameterParserStub;
private parameterCollectionFactory
: FunctionParameterCollectionFactory = () => new FunctionParameterCollectionStub();
public withSyntax(syntax: ILanguageSyntax): this {
this.syntax = syntax;
return this;
}
public withValidator(codeValidator: ICodeValidator): this {
this.codeValidator = codeValidator;
return this;
}
public withFunctionCallsParser(functionCallsParser: FunctionCallsParser): this {
this.functionCallsParser = functionCallsParser;
return this;
}
public withFunctions(functions: readonly FunctionData[]): this {
this.functions = functions;
return this;
}
public withErrorWrapper(wrapError: ErrorWithContextWrapper): this {
this.wrapError = wrapError;
return this;
}
public withFunctionParameterParser(functionParameterParser: FunctionParameterParser): this {
this.functionParameterParser = functionParameterParser;
return this;
}
public withParameterCollectionFactory(
parameterCollectionFactory: FunctionParameterCollectionFactory,
): this {
this.parameterCollectionFactory = parameterCollectionFactory;
return this;
}
public parseFunctions(): ReturnType<typeof parseSharedFunctions> {
return parseSharedFunctions(
this.functions,
this.syntax,
{
codeValidator: this.codeValidator,
wrapError: this.wrapError,
parseParameter: this.functionParameterParser,
createParameterCollection: this.parameterCollectionFactory,
parseFunctionCalls: this.functionCallsParser,
},
);
}
}
function expectEqualName(expected: FunctionData, actual: ISharedFunction): void {
expect(actual.name).to.equal(expected.name);
}
function expectEqualParameters(
expected: readonly ParameterDefinitionData[] | undefined,
actual: IReadOnlyFunctionParameterCollection,
): void {
const actualSimplifiedParameters = actual.all.map((parameter) => ({
name: parameter.name,
optional: parameter.isOptional,
}));
const expectedSimplifiedParameters = expected?.map((parameter) => ({
name: parameter.name,
optional: parameter.optional || false,
})) || [];
expect(expectedSimplifiedParameters).to.deep.equal(actualSimplifiedParameters, 'Unequal parameters');
}
function expectEqualFunctionWithInlineCode(
expected: CodeInstruction,
actual: ISharedFunction,
): void {
expectCodeFunctionBody(actual.body);
expect(actual.body.code, `function "${actual.name}" has no code`);
expect(actual.body.code.execute).to.equal(expected.code);
expect(actual.body.code.revert).to.equal(expected.revertCode);
}
function expectEqualFunctionWithCalls(
expectedCalls: readonly FunctionCall[],
actualFunction: ISharedFunction,
): void {
expectCallsFunctionBody(actualFunction.body);
const actualCalls = actualFunction.body.calls;
expect(actualCalls.length).to.equal(expectedCalls.length);
expect(actualCalls).to.have.members(expectedCalls);
}