This commit unifies the concepts of executables having same ID structure. It paves the way for more complex ID structure and using IDs in collection files as part of new ID solution (#262). Using string IDs also leads to more expressive test code. This commit also refactors the rest of the code to adopt to the changes. This commit: - Separate concerns from entities for data access (in repositories) and executables. Executables use `Identifiable` meanwhile repositories use `RepositoryEntity`. - Refactor unnecessary generic parameters for enttities and ids, enforcing string gtype everwyhere. - Changes numeric IDs to string IDs for categories to unify the retrieval and construction for executables, using pseudo-ids (their names) just like scripts. - Remove `BaseEntity` for simplicity. - Simplify usage and construction of executable objects. Move factories responsible for creation of category/scripts to domain layer. Do not longer export `CollectionCategorY` and `CollectionScript`. - Use named typed for string IDs for better differentation of different ID contexts in code.
328 lines
11 KiB
TypeScript
328 lines
11 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import type { Category } from '@/domain/Executables/Category/Category';
|
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
|
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
|
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
|
import { getEnumValues } from '@/application/Common/Enum';
|
|
import { CategoryCollection } from '@/domain/Collection/CategoryCollection';
|
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
|
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
|
import { EnumRangeTestRunner } from '@tests/unit/application/Common/EnumRangeTestRunner';
|
|
|
|
describe('CategoryCollection', () => {
|
|
describe('getScriptsByLevel', () => {
|
|
it('filters out scripts without levels', () => {
|
|
// arrange
|
|
const recommendationLevels = getEnumValues(RecommendationLevel);
|
|
const scriptsWithLevels = recommendationLevels.map(
|
|
(level, index) => new ScriptStub(`Script${index}`).withLevel(level),
|
|
);
|
|
const toIgnore = new ScriptStub('script-to-ignore').withLevel(undefined);
|
|
for (const currentLevel of recommendationLevels) {
|
|
const category = new CategoryStub(0)
|
|
.withScripts(...scriptsWithLevels)
|
|
.withScript(toIgnore);
|
|
const sut = new CategoryCollectionBuilder()
|
|
.withActions([category])
|
|
.construct();
|
|
// act
|
|
const actual = sut.getScriptsByLevel(currentLevel);
|
|
// assert
|
|
expect(actual).to.not.include(toIgnore);
|
|
}
|
|
});
|
|
it(`${RecommendationLevel[RecommendationLevel.Standard]} filters ${RecommendationLevel[RecommendationLevel.Strict]}`, () => {
|
|
// arrange
|
|
const level = RecommendationLevel.Standard;
|
|
const expected = [
|
|
new ScriptStub('S1').withLevel(level),
|
|
new ScriptStub('S2').withLevel(level),
|
|
];
|
|
const actions = [
|
|
new CategoryStub(3).withScripts(
|
|
...expected,
|
|
new ScriptStub('S3').withLevel(RecommendationLevel.Strict),
|
|
),
|
|
];
|
|
const sut = new CategoryCollectionBuilder()
|
|
.withActions(actions)
|
|
.construct();
|
|
// act
|
|
const actual = sut.getScriptsByLevel(level);
|
|
// assert
|
|
expect(expected).to.deep.equal(actual);
|
|
});
|
|
it(`${RecommendationLevel[RecommendationLevel.Strict]} includes ${RecommendationLevel[RecommendationLevel.Standard]}`, () => {
|
|
// arrange
|
|
const level = RecommendationLevel.Strict;
|
|
const expected = [
|
|
new ScriptStub('S1').withLevel(RecommendationLevel.Standard),
|
|
new ScriptStub('S2').withLevel(RecommendationLevel.Strict),
|
|
];
|
|
const actions = [
|
|
new CategoryStub(3).withScripts(...expected),
|
|
];
|
|
const sut = new CategoryCollectionBuilder()
|
|
.withActions(actions)
|
|
.construct();
|
|
// act
|
|
const actual = sut.getScriptsByLevel(level);
|
|
// assert
|
|
expect(expected).to.deep.equal(actual);
|
|
});
|
|
describe('throws when given invalid level', () => {
|
|
new EnumRangeTestRunner<RecommendationLevel>((level) => {
|
|
// arrange
|
|
const sut = new CategoryCollectionBuilder()
|
|
.construct();
|
|
// act
|
|
sut.getScriptsByLevel(level);
|
|
})
|
|
// assert
|
|
.testOutOfRangeThrows()
|
|
.testValidValueDoesNotThrow(RecommendationLevel.Standard);
|
|
});
|
|
});
|
|
describe('actions', () => {
|
|
it('cannot construct without actions', () => {
|
|
// arrange
|
|
const categories = [];
|
|
// act
|
|
function construct() {
|
|
new CategoryCollectionBuilder()
|
|
.withActions(categories)
|
|
.construct();
|
|
}
|
|
// assert
|
|
expect(construct).to.throw('must consist of at least one category');
|
|
});
|
|
it('cannot construct without scripts', () => {
|
|
// arrange
|
|
const categories = [
|
|
new CategoryStub(3),
|
|
new CategoryStub(2),
|
|
];
|
|
// act
|
|
function construct() {
|
|
new CategoryCollectionBuilder()
|
|
.withActions(categories)
|
|
.construct();
|
|
}
|
|
// assert
|
|
expect(construct).to.throw('must consist of at least one script');
|
|
});
|
|
describe('cannot construct without any recommended scripts', () => {
|
|
describe('single missing', () => {
|
|
// arrange
|
|
const recommendationLevels = getEnumValues(RecommendationLevel);
|
|
for (const missingLevel of recommendationLevels) {
|
|
it(`when "${RecommendationLevel[missingLevel]}" is missing`, () => {
|
|
const expectedError = `none of the scripts are recommended as "${RecommendationLevel[missingLevel]}".`;
|
|
const otherLevels = recommendationLevels.filter((level) => level !== missingLevel);
|
|
const categories = otherLevels.map(
|
|
(level, index) => new CategoryStub(index)
|
|
.withScript(
|
|
new ScriptStub(`Script${index}`).withLevel(level),
|
|
),
|
|
);
|
|
// act
|
|
const construct = () => new CategoryCollectionBuilder()
|
|
.withActions(categories)
|
|
.construct();
|
|
// assert
|
|
expect(construct).to.throw(expectedError);
|
|
});
|
|
}
|
|
});
|
|
it('multiple are missing', () => {
|
|
// arrange
|
|
const expectedError = 'none of the scripts are recommended as '
|
|
+ `"${RecommendationLevel[RecommendationLevel.Standard]}, "${RecommendationLevel[RecommendationLevel.Strict]}".`;
|
|
const categories = [
|
|
new CategoryStub(0)
|
|
.withScript(
|
|
new ScriptStub(`Script${0}`).withLevel(undefined),
|
|
),
|
|
];
|
|
// act
|
|
const construct = () => new CategoryCollectionBuilder()
|
|
.withActions(categories)
|
|
.construct();
|
|
// assert
|
|
expect(construct).to.throw(expectedError);
|
|
});
|
|
});
|
|
});
|
|
describe('totalScripts', () => {
|
|
it('returns total of initial scripts', () => {
|
|
// arrange
|
|
const categories = [
|
|
new CategoryStub(1).withScripts(
|
|
new ScriptStub('S1').withLevel(RecommendationLevel.Standard),
|
|
),
|
|
new CategoryStub(2).withScripts(
|
|
new ScriptStub('S2'),
|
|
new ScriptStub('S3').withLevel(RecommendationLevel.Strict),
|
|
),
|
|
new CategoryStub(3).withCategories(
|
|
new CategoryStub(4).withScripts(new ScriptStub('S4')),
|
|
),
|
|
];
|
|
// act
|
|
const sut = new CategoryCollectionBuilder()
|
|
.withActions(categories)
|
|
.construct();
|
|
// assert
|
|
expect(sut.totalScripts).to.equal(4);
|
|
});
|
|
});
|
|
describe('totalCategories', () => {
|
|
it('returns total of initial categories', () => {
|
|
// arrange
|
|
const expected = 4;
|
|
const categories = [
|
|
new CategoryStub(1).withScripts(new ScriptStub('S1').withLevel(RecommendationLevel.Strict)),
|
|
new CategoryStub(2).withScripts(new ScriptStub('S2'), new ScriptStub('S3')),
|
|
new CategoryStub(3).withCategories(new CategoryStub(4).withScripts(new ScriptStub('S4'))),
|
|
];
|
|
// act
|
|
const sut = new CategoryCollectionBuilder()
|
|
.withActions(categories)
|
|
.construct();
|
|
// assert
|
|
expect(sut.totalCategories).to.equal(expected);
|
|
});
|
|
});
|
|
describe('os', () => {
|
|
it('sets os as expected', () => {
|
|
// arrange
|
|
const expected = OperatingSystem.macOS;
|
|
// act
|
|
const sut = new CategoryCollectionBuilder()
|
|
.withOs(expected)
|
|
.construct();
|
|
// assert
|
|
expect(sut.os).to.deep.equal(expected);
|
|
});
|
|
describe('throws when invalid', () => {
|
|
// act
|
|
const act = (os: OperatingSystem) => new CategoryCollectionBuilder()
|
|
.withOs(os)
|
|
.construct();
|
|
// assert
|
|
new EnumRangeTestRunner(act)
|
|
.testOutOfRangeThrows();
|
|
});
|
|
});
|
|
describe('scriptingDefinition', () => {
|
|
it('sets scriptingDefinition as expected', () => {
|
|
// arrange
|
|
const expected = getValidScriptingDefinition();
|
|
// act
|
|
const sut = new CategoryCollectionBuilder()
|
|
.withScripting(expected)
|
|
.construct();
|
|
// assert
|
|
expect(sut.scripting).to.deep.equal(expected);
|
|
});
|
|
});
|
|
describe('getCategory', () => {
|
|
it('throws if category is not found', () => {
|
|
// arrange
|
|
const categoryId = 123;
|
|
const expectedError = `Missing category with ID: "${categoryId}"`;
|
|
const collection = new CategoryCollectionBuilder()
|
|
.withActions([new CategoryStub(456).withMandatoryScripts()])
|
|
.construct();
|
|
// act
|
|
const act = () => collection.getCategory(categoryId);
|
|
// assert
|
|
expect(act).to.throw(expectedError);
|
|
});
|
|
it('finds correct category', () => {
|
|
// arrange
|
|
const categoryId = 123;
|
|
const expectedCategory = new CategoryStub(categoryId).withMandatoryScripts();
|
|
const collection = new CategoryCollectionBuilder()
|
|
.withActions([expectedCategory])
|
|
.construct();
|
|
// act
|
|
const actualCategory = collection.getCategory(categoryId);
|
|
// assert
|
|
expect(actualCategory).to.equal(expectedCategory);
|
|
});
|
|
});
|
|
describe('getScript', () => {
|
|
it('throws if script is not found', () => {
|
|
// arrange
|
|
const scriptId = 'missingScript';
|
|
const expectedError = `missing script: ${scriptId}`;
|
|
const collection = new CategoryCollectionBuilder()
|
|
.withActions([new CategoryStub(456).withMandatoryScripts()])
|
|
.construct();
|
|
// act
|
|
const act = () => collection.getScript(scriptId);
|
|
// assert
|
|
expect(act).to.throw(expectedError);
|
|
});
|
|
it('finds correct script', () => {
|
|
// arrange
|
|
const scriptId = 'existingScript';
|
|
const expectedScript = new ScriptStub(scriptId);
|
|
const parentCategory = new CategoryStub(123)
|
|
.withMandatoryScripts()
|
|
.withScript(expectedScript);
|
|
const collection = new CategoryCollectionBuilder()
|
|
.withActions([parentCategory])
|
|
.construct();
|
|
// act
|
|
const actualScript = collection.getScript(scriptId);
|
|
// assert
|
|
expect(actualScript).to.equal(expectedScript);
|
|
});
|
|
});
|
|
});
|
|
|
|
function getValidScriptingDefinition(): IScriptingDefinition {
|
|
return {
|
|
fileExtension: '.bat',
|
|
language: ScriptingLanguage.batchfile,
|
|
startCode: 'start',
|
|
endCode: 'end',
|
|
};
|
|
}
|
|
|
|
class CategoryCollectionBuilder {
|
|
private os = OperatingSystem.Windows;
|
|
|
|
private actions: readonly Category[] = [
|
|
new CategoryStub(1).withMandatoryScripts(),
|
|
];
|
|
|
|
private scriptingDefinition: IScriptingDefinition = getValidScriptingDefinition();
|
|
|
|
public withOs(os: OperatingSystem): this {
|
|
this.os = os;
|
|
return this;
|
|
}
|
|
|
|
public withActions(actions: readonly Category[]): this {
|
|
this.actions = actions;
|
|
return this;
|
|
}
|
|
|
|
public withScripting(scriptingDefinition: IScriptingDefinition): this {
|
|
this.scriptingDefinition = scriptingDefinition;
|
|
return this;
|
|
}
|
|
|
|
public construct(): CategoryCollection {
|
|
return new CategoryCollection({
|
|
os: this.os,
|
|
actions: this.actions,
|
|
scripting: this.scriptingDefinition,
|
|
});
|
|
}
|
|
}
|