Files
privacy.sexy/tests/unit/application/Parser/CategoryCollectionParser.spec.ts
undergroundwires dc5c87376b Add validation for max line length in compiler
This commit adds validation logic in compiler to check for max allowed
characters per line for scripts. This allows preventing bugs caused by
limitation of terminal emulators.

Other supporting changes:

- Rename/refactor related code for clarity and better maintainability.
- Drop `I` prefix from interfaces to align with latest convention.
- Refactor CodeValidator to be functional rather than object-oriented
  for simplicity.
- Refactor syntax definition construction to be functional and be part
  of rule for better separation of concerns.
- Refactored validation logic to use an enum-based factory pattern for
  improved maintainability and scalability.
2024-08-27 11:32:52 +02:00

318 lines
13 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub';
import { parseCategoryCollection, type CategoryCollectionFactory } from '@/application/Parser/CategoryCollectionParser';
import { type CategoryParser } from '@/application/Parser/Executable/CategoryParser';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub';
import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub';
import { getCategoryStub, CollectionDataStub } from '@tests/unit/shared/Stubs/CollectionDataStub';
import { CategoryCollectionContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionContextStub';
import { createFunctionDataWithCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
import type { CollectionData, ScriptingDefinitionData, FunctionData } from '@/application/collections/';
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
import type { NonEmptyCollectionAssertion, ObjectAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator';
import type { EnumParser } from '@/application/Common/Enum';
import type { ScriptingDefinitionParser } from '@/application/Parser/ScriptingDefinition/ScriptingDefinitionParser';
import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub';
import type { CategoryCollectionContextFactory } from '@/application/Parser/Executable/CategoryCollectionContext';
import { ScriptingDefinitionDataStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionDataStub';
import { CategoryParserStub } from '@tests/unit/shared/Stubs/CategoryParserStub';
import { createCategoryCollectionFactorySpy } from '@tests/unit/shared/Stubs/CategoryCollectionFactoryStub';
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
describe('CategoryCollectionParser', () => {
describe('parseCategoryCollection', () => {
it('validates object', () => {
// arrange
const data = new CollectionDataStub();
const expectedAssertion: ObjectAssertion<CollectionData> = {
value: data,
valueName: 'Collection',
allowedProperties: [
'os', 'scripting', 'actions', 'functions',
],
};
const validator = new TypeValidatorStub();
const context = new TestContext()
.withData(data)
.withTypeValidator(validator);
// act
context.parseCategoryCollection();
// assert
validator.expectObjectAssertion(expectedAssertion);
});
describe('actions', () => {
it('validates non-empty collection', () => {
// arrange
const actions = [getCategoryStub('test1'), getCategoryStub('test2')];
const expectedAssertion: NonEmptyCollectionAssertion = {
value: actions,
valueName: '\'actions\' in collection',
};
const validator = new TypeValidatorStub();
const context = new TestContext()
.withData(new CollectionDataStub().withActions(actions))
.withTypeValidator(validator);
// act
context.parseCategoryCollection();
// assert
validator.expectNonEmptyCollectionAssertion(expectedAssertion);
});
it('parses actions correctly', () => {
// arrange
const {
categoryCollectionFactorySpy,
getInitParameters,
} = createCategoryCollectionFactorySpy();
const actionsData = [getCategoryStub('test1'), getCategoryStub('test2')];
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]);
const collectionData = new CollectionDataStub()
.withActions(actionsData);
const context = new TestContext()
.withData(collectionData)
.withCategoryParser(categoryParserStub.get())
.withCategoryCollectionFactory(categoryCollectionFactorySpy);
// act
const actualCollection = context.parseCategoryCollection();
// assert
const actualActions = getInitParameters(actualCollection)?.actions;
expect(actualActions).to.have.lengthOf(expectedActions.length);
expect(actualActions).to.have.members(expectedActions);
});
describe('context', () => {
it('parses actions with correct context', () => {
// arrange
const expectedContext = new CategoryCollectionContextStub();
const contextFactory: CategoryCollectionContextFactory = () => {
return expectedContext;
};
const actionsData = [getCategoryStub('test1'), getCategoryStub('test2')];
const collectionData = new CollectionDataStub()
.withActions(actionsData);
const categoryParserStub = new CategoryParserStub();
const context = new TestContext()
.withData(collectionData)
.withCollectionContextFactory(contextFactory)
.withCategoryParser(categoryParserStub.get());
// act
context.parseCategoryCollection();
// assert
const actualContext = categoryParserStub.getUsedContext();
expect(actualContext).to.have.lengthOf(2);
expect(actualContext[0]).to.equal(expectedContext);
expect(actualContext[1]).to.equal(expectedContext);
});
describe('construction', () => {
it('creates with correct functions data', () => {
// arrange
const expectedFunctionsData = [createFunctionDataWithCode()];
const collectionData = new CollectionDataStub()
.withFunctions(expectedFunctionsData);
let actualFunctionsData: ReadonlyArray<FunctionData> | undefined;
const contextFactory: CategoryCollectionContextFactory = (data) => {
actualFunctionsData = data;
return new CategoryCollectionContextStub();
};
const context = new TestContext()
.withData(collectionData)
.withCollectionContextFactory(contextFactory);
// act
context.parseCategoryCollection();
// assert
expect(actualFunctionsData).to.equal(expectedFunctionsData);
});
it('creates with correct language', () => {
// arrange
const expectedLanguage = ScriptingLanguage.batchfile;
const scriptingDefinitionParser: ScriptingDefinitionParser = () => {
return new ScriptingDefinitionStub()
.withLanguage(expectedLanguage);
};
let actualLanguage: ScriptingLanguage | undefined;
const contextFactory: CategoryCollectionContextFactory = (_, language) => {
actualLanguage = language;
return new CategoryCollectionContextStub();
};
const context = new TestContext()
.withCollectionContextFactory(contextFactory)
.withScriptDefinitionParser(scriptingDefinitionParser);
// act
context.parseCategoryCollection();
// assert
expect(actualLanguage).to.equal(expectedLanguage);
});
});
});
});
describe('scripting definition', () => {
it('parses correctly', () => {
// arrange
const {
categoryCollectionFactorySpy,
getInitParameters,
} = createCategoryCollectionFactorySpy();
const expected = new ScriptingDefinitionStub();
const scriptingDefinitionParser: ScriptingDefinitionParser = () => {
return expected;
};
const context = new TestContext()
.withCategoryCollectionFactory(categoryCollectionFactorySpy)
.withScriptDefinitionParser(scriptingDefinitionParser);
// act
const actualCategoryCollection = context.parseCategoryCollection();
// assert
const actualScripting = getInitParameters(actualCategoryCollection)?.scripting;
expect(expected).to.equal(actualScripting);
});
it('parses expected data', () => {
// arrange
const expectedData = new ScriptingDefinitionDataStub();
const collection = new CollectionDataStub()
.withScripting(expectedData);
let actualData: ScriptingDefinitionData | undefined;
const scriptingDefinitionParser
: ScriptingDefinitionParser = (data: ScriptingDefinitionData) => {
actualData = data;
return new ScriptingDefinitionStub();
};
const context = new TestContext()
.withScriptDefinitionParser(scriptingDefinitionParser)
.withData(collection);
// act
context.parseCategoryCollection();
// assert
expect(actualData).to.equal(expectedData);
});
it('parses with correct project details', () => {
// arrange
const expectedProjectDetails = new ProjectDetailsStub();
let actualDetails: ProjectDetails | undefined;
const scriptingDefinitionParser
: ScriptingDefinitionParser = (_, details: ProjectDetails) => {
actualDetails = details;
return new ScriptingDefinitionStub();
};
const context = new TestContext()
.withProjectDetails(expectedProjectDetails)
.withScriptDefinitionParser(scriptingDefinitionParser);
// act
context.parseCategoryCollection();
// assert
expect(actualDetails).to.equal(expectedProjectDetails);
});
});
describe('os', () => {
it('parses correctly', () => {
// arrange
const {
categoryCollectionFactorySpy,
getInitParameters,
} = createCategoryCollectionFactorySpy();
const expectedOs = OperatingSystem.macOS;
const osText = 'macos';
const expectedName = 'os';
const collectionData = new CollectionDataStub()
.withOs(osText);
const parserMock = new EnumParserStub<OperatingSystem>()
.setup(expectedName, osText, expectedOs);
const context = new TestContext()
.withOsParser(parserMock)
.withCategoryCollectionFactory(categoryCollectionFactorySpy)
.withData(collectionData);
// act
const actualCollection = context.parseCategoryCollection();
// assert
const actualOs = getInitParameters(actualCollection)?.os;
expect(actualOs).to.equal(expectedOs);
});
});
});
});
class TestContext {
private data: CollectionData = new CollectionDataStub();
private projectDetails: ProjectDetails = new ProjectDetailsStub();
private validator: TypeValidator = new TypeValidatorStub();
private osParser: EnumParser<OperatingSystem> = new EnumParserStub<OperatingSystem>()
.setupDefaultValue(OperatingSystem.Android);
private collectionContextFactory
: CategoryCollectionContextFactory = () => {
return new CategoryCollectionContextStub();
};
private scriptDefinitionParser: ScriptingDefinitionParser = () => new ScriptingDefinitionStub();
private categoryParser: CategoryParser = new CategoryParserStub().get();
private categoryCollectionFactory
: CategoryCollectionFactory = createCategoryCollectionFactorySpy().categoryCollectionFactorySpy;
public withData(data: CollectionData): this {
this.data = data;
return this;
}
public withCategoryParser(categoryParser: CategoryParser): this {
this.categoryParser = categoryParser;
return this;
}
public withCategoryCollectionFactory(categoryCollectionFactory: CategoryCollectionFactory): this {
this.categoryCollectionFactory = categoryCollectionFactory;
return this;
}
public withProjectDetails(projectDetails: ProjectDetails): this {
this.projectDetails = projectDetails;
return this;
}
public withOsParser(osParser: EnumParser<OperatingSystem>): this {
this.osParser = osParser;
return this;
}
public withScriptDefinitionParser(scriptDefinitionParser: ScriptingDefinitionParser): this {
this.scriptDefinitionParser = scriptDefinitionParser;
return this;
}
public withTypeValidator(typeValidator: TypeValidator): this {
this.validator = typeValidator;
return this;
}
public withCollectionContextFactory(
collectionContextFactory: CategoryCollectionContextFactory,
): this {
this.collectionContextFactory = collectionContextFactory;
return this;
}
public parseCategoryCollection(): ReturnType<typeof parseCategoryCollection> {
return parseCategoryCollection(
this.data,
this.projectDetails,
{
osParser: this.osParser,
validator: this.validator,
parseScriptingDefinition: this.scriptDefinitionParser,
createContext: this.collectionContextFactory,
parseCategory: this.categoryParser,
createCategoryCollection: this.categoryCollectionFactory,
},
);
}
}