Compare commits

...

2 Commits

Author SHA1 Message Date
undergroundwires
58f902216b Fix disabling of Microsoft Defender $170
- Change naming from Windows Defender to Microsoft Defender to match
  latest branding.
- Add more extensive documentation.
- Add more scripts extending ways to disable Defender.
- Disable "Windows Security Center Service"
- Add missing `SetMpPreference` commands
- New disabling:
  - Disabling of Windows features related to Defender.
  - Disable Antimalware Scan Interface (AMSI)

TODO: Soft delete Defender directories, like
`$env:programdata\Microsoft\Windows Defender`

TODO: Add from here: https://learn.microsoft.com/en-us/mem/intune/protect/antivirus-security-experience-windows-settings

New scripts:

- Disable "Windows Security Center" service
- Kill SmartScreen process
- Disable "Microsoft Security Core Boot" service

Improved scripts:

- Disable Intrusion Prevention System (IPS): Add CLI command to disable
  it.

TODO: These to separate commit

TODO:

- Improve disabling of `RenameSystemFile` AsTrustedInstaller and get
  back all commented out code.
2024-07-18 09:48:06 +02:00
undergroundwires
48d6dbd700 Refactor to use string IDs for executables #262
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.
2024-07-08 23:23:05 +02:00
98 changed files with 1901 additions and 1181 deletions

View File

@@ -30,6 +30,8 @@ Related documentation:
### Executables ### Executables
They represent independently executable tweaks with documentation and reversibility.
An Executable is a logical entity that can An Executable is a logical entity that can
- execute once compiled, - execute once compiled,

View File

@@ -1,6 +1,6 @@
import type { IApplication } from '@/domain/IApplication'; import type { IApplication } from '@/domain/IApplication';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { EventSource } from '@/infrastructure/Events/EventSource'; import { EventSource } from '@/infrastructure/Events/EventSource';
import { assertInRange } from '@/application/Common/Enum'; import { assertInRange } from '@/application/Common/Enum';
import { CategoryCollectionState } from './State/CategoryCollectionState'; import { CategoryCollectionState } from './State/CategoryCollectionState';

View File

@@ -1,4 +1,4 @@
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext'; import { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext';
import { ApplicationCode } from './Code/ApplicationCode'; import { ApplicationCode } from './Code/ApplicationCode';

View File

@@ -36,12 +36,12 @@ export class CodeChangedEvent implements ICodeChangedEvent {
} }
public getScriptPositionInCode(script: Script): ICodePosition { public getScriptPositionInCode(script: Script): ICodePosition {
return this.getPositionById(script.id); return this.getPositionById(script.executableId);
} }
private getPositionById(scriptId: string): ICodePosition { private getPositionById(scriptId: string): ICodePosition {
const position = [...this.scripts.entries()] const position = [...this.scripts.entries()]
.filter(([s]) => s.id === scriptId) .filter(([s]) => s.executableId === scriptId)
.map(([, pos]) => pos) .map(([, pos]) => pos)
.at(0); .at(0);
if (!position) { if (!position) {

View File

@@ -1,5 +1,5 @@
import { EventSource } from '@/infrastructure/Events/EventSource'; import { EventSource } from '@/infrastructure/Events/EventSource';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { FilterChange } from './Event/FilterChange'; import { FilterChange } from './Event/FilterChange';
import { LinearFilterStrategy } from './Strategy/LinearFilterStrategy'; import { LinearFilterStrategy } from './Strategy/LinearFilterStrategy';
import type { FilterResult } from './Result/FilterResult'; import type { FilterResult } from './Result/FilterResult';

View File

@@ -1,4 +1,4 @@
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import type { FilterResult } from '../Result/FilterResult'; import type { FilterResult } from '../Result/FilterResult';
export interface FilterStrategy { export interface FilterStrategy {

View File

@@ -1,7 +1,7 @@
import type { Category } from '@/domain/Executables/Category/Category'; import type { Category } from '@/domain/Executables/Category/Category';
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode'; import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
import type { Documentable } from '@/domain/Executables/Documentable'; import type { Documentable } from '@/domain/Executables/Documentable';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import type { Script } from '@/domain/Executables/Script/Script'; import type { Script } from '@/domain/Executables/Script/Script';
import { AppliedFilterResult } from '../Result/AppliedFilterResult'; import { AppliedFilterResult } from '../Result/AppliedFilterResult';
import type { FilterStrategy } from './FilterStrategy'; import type { FilterStrategy } from './FilterStrategy';

View File

@@ -1,4 +1,4 @@
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import type { IApplicationCode } from './Code/IApplicationCode'; import type { IApplicationCode } from './Code/IApplicationCode';
import type { ReadonlyFilterContext, FilterContext } from './Filter/FilterContext'; import type { ReadonlyFilterContext, FilterContext } from './Filter/FilterContext';

View File

@@ -1,3 +1,5 @@
import type { ExecutableId } from '@/domain/Executables/Identifiable';
type CategorySelectionStatus = { type CategorySelectionStatus = {
readonly isSelected: true; readonly isSelected: true;
readonly isReverted: boolean; readonly isReverted: boolean;
@@ -6,7 +8,7 @@ type CategorySelectionStatus = {
}; };
export interface CategorySelectionChange { export interface CategorySelectionChange {
readonly categoryId: number; readonly categoryId: ExecutableId;
readonly newStatus: CategorySelectionStatus; readonly newStatus: CategorySelectionStatus;
} }

View File

@@ -1,5 +1,5 @@
import type { Category } from '@/domain/Executables/Category/Category'; import type { Category } from '@/domain/Executables/Category/Category';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import type { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange'; import type { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange';
import type { CategorySelection } from './CategorySelection'; import type { CategorySelection } from './CategorySelection';
import type { ScriptSelection } from '../Script/ScriptSelection'; import type { ScriptSelection } from '../Script/ScriptSelection';
@@ -23,7 +23,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
return false; return false;
} }
return scripts.every( return scripts.every(
(script) => selectedScripts.some((selected) => selected.id === script.id), (script) => selectedScripts.some((selected) => selected.id === script.executableId),
); );
} }
@@ -50,7 +50,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
const scripts = category.getAllScriptsRecursively(); const scripts = category.getAllScriptsRecursively();
const scriptsChangesInCategory = scripts const scriptsChangesInCategory = scripts
.map((script): ScriptSelectionChange => ({ .map((script): ScriptSelectionChange => ({
scriptId: script.id, scriptId: script.executableId,
newStatus: { newStatus: {
...change.newStatus, ...change.newStatus,
}, },

View File

@@ -2,7 +2,7 @@ import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryReposito
import type { Script } from '@/domain/Executables/Script/Script'; import type { Script } from '@/domain/Executables/Script/Script';
import { EventSource } from '@/infrastructure/Events/EventSource'; import { EventSource } from '@/infrastructure/Events/EventSource';
import type { ReadonlyRepository, Repository } from '@/application/Repository/Repository'; import type { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce'; import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce';
import { UserSelectedScript } from './UserSelectedScript'; import { UserSelectedScript } from './UserSelectedScript';
import type { ScriptSelection } from './ScriptSelection'; import type { ScriptSelection } from './ScriptSelection';
@@ -16,7 +16,7 @@ export type DebounceFunction = typeof batchedDebounce<ScriptSelectionChangeComma
export class DebouncedScriptSelection implements ScriptSelection { export class DebouncedScriptSelection implements ScriptSelection {
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>(); public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
private readonly scripts: Repository<string, SelectedScript>; private readonly scripts: Repository<SelectedScript>;
public readonly processChanges: ScriptSelection['processChanges']; public readonly processChanges: ScriptSelection['processChanges'];
@@ -25,7 +25,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
selectedScripts: ReadonlyArray<SelectedScript>, selectedScripts: ReadonlyArray<SelectedScript>,
debounce: DebounceFunction = batchedDebounce, debounce: DebounceFunction = batchedDebounce,
) { ) {
this.scripts = new InMemoryRepository<string, SelectedScript>(); this.scripts = new InMemoryRepository<SelectedScript>();
for (const script of selectedScripts) { for (const script of selectedScripts) {
this.scripts.addItem(script); this.scripts.addItem(script);
} }
@@ -49,7 +49,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
public selectAll(): void { public selectAll(): void {
const scriptsToSelect = this.collection const scriptsToSelect = this.collection
.getAllScripts() .getAllScripts()
.filter((script) => !this.scripts.exists(script.id)) .filter((script) => !this.scripts.exists(script.executableId))
.map((script) => new UserSelectedScript(script, false)); .map((script) => new UserSelectedScript(script, false));
if (scriptsToSelect.length === 0) { if (scriptsToSelect.length === 0) {
return; return;
@@ -116,9 +116,9 @@ export class DebouncedScriptSelection implements ScriptSelection {
private applyChange(change: ScriptSelectionChange): number { private applyChange(change: ScriptSelectionChange): number {
const script = this.collection.getScript(change.scriptId); const script = this.collection.getScript(change.scriptId);
if (change.newStatus.isSelected) { if (change.newStatus.isSelected) {
return this.addOrUpdateScript(script.id, change.newStatus.isReverted); return this.addOrUpdateScript(script.executableId, change.newStatus.isReverted);
} }
return this.removeScript(script.id); return this.removeScript(script.executableId);
} }
private addOrUpdateScript(scriptId: string, revert: boolean): number { private addOrUpdateScript(scriptId: string, revert: boolean): number {
@@ -152,24 +152,24 @@ function assertNonEmptyScriptSelection(selectedItems: readonly Script[]) {
} }
function getScriptIdsToBeSelected( function getScriptIdsToBeSelected(
existingItems: ReadonlyRepository<string, SelectedScript>, existingItems: ReadonlyRepository<SelectedScript>,
desiredScripts: readonly Script[], desiredScripts: readonly Script[],
): string[] { ): string[] {
return desiredScripts return desiredScripts
.filter((script) => !existingItems.exists(script.id)) .filter((script) => !existingItems.exists(script.executableId))
.map((script) => script.id); .map((script) => script.executableId);
} }
function getScriptIdsToBeDeselected( function getScriptIdsToBeDeselected(
existingItems: ReadonlyRepository<string, SelectedScript>, existingItems: ReadonlyRepository<SelectedScript>,
desiredScripts: readonly Script[], desiredScripts: readonly Script[],
): string[] { ): string[] {
return existingItems return existingItems
.getItems() .getItems()
.filter((existing) => !desiredScripts.some((script) => existing.id === script.id)) .filter((existing) => !desiredScripts.some((script) => existing.id === script.executableId))
.map((script) => script.id); .map((script) => script.id);
} }
function equals(a: SelectedScript, b: SelectedScript): boolean { function equals(a: SelectedScript, b: SelectedScript): boolean {
return a.script.equals(b.script.id) && a.revert === b.revert; return a.script.executableId === b.script.executableId && a.revert === b.revert;
} }

View File

@@ -1,9 +1,7 @@
import type { IEntity } from '@/infrastructure/Entity/IEntity';
import type { Script } from '@/domain/Executables/Script/Script'; import type { Script } from '@/domain/Executables/Script/Script';
import type { RepositoryEntity } from '@/application/Repository/RepositoryEntity';
type ScriptId = Script['id']; export interface SelectedScript extends RepositoryEntity {
export interface SelectedScript extends IEntity<ScriptId> {
readonly script: Script; readonly script: Script;
readonly revert: boolean; readonly revert: boolean;
} }

View File

@@ -1,17 +1,16 @@
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import type { Script } from '@/domain/Executables/Script/Script'; import type { Script } from '@/domain/Executables/Script/Script';
import type { SelectedScript } from './SelectedScript'; import type { RepositoryEntity } from '@/application/Repository/RepositoryEntity';
type SelectedScriptId = SelectedScript['id']; export class UserSelectedScript implements RepositoryEntity {
public readonly id: string;
export class UserSelectedScript extends BaseEntity<SelectedScriptId> {
constructor( constructor(
public readonly script: Script, public readonly script: Script,
public readonly revert: boolean, public readonly revert: boolean,
) { ) {
super(script.id); this.id = script.executableId;
if (revert && !script.canRevert()) { if (revert && !script.canRevert()) {
throw new Error(`The script with ID '${script.id}' is not reversible and cannot be reverted.`); throw new Error(`The script with ID '${script.executableId}' is not reversible and cannot be reverted.`);
} }
} }
} }

View File

@@ -1,4 +1,4 @@
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper'; import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper';
import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection'; import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection';
import type { CategorySelection } from './Category/CategorySelection'; import type { CategorySelection } from './Category/CategorySelection';

View File

@@ -1,7 +1,7 @@
import type { CollectionData } from '@/application/collections/'; import type { CollectionData } from '@/application/collections/';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { CategoryCollection } from '@/domain/CategoryCollection'; import { CategoryCollection } from '@/domain/Collection/CategoryCollection';
import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
import { createEnumParser, type EnumParser } from '../Common/Enum'; import { createEnumParser, type EnumParser } from '../Common/Enum';
import { parseCategory, type CategoryParser } from './Executable/CategoryParser'; import { parseCategory, type CategoryParser } from './Executable/CategoryParser';

View File

@@ -3,16 +3,14 @@ import type {
} from '@/application/collections/'; } from '@/application/collections/';
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError'; import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import type { Category } from '@/domain/Executables/Category/Category'; import type { Category } from '@/domain/Executables/Category/Category';
import { CollectionCategory } from '@/domain/Executables/Category/CollectionCategory';
import type { Script } from '@/domain/Executables/Script/Script'; import type { Script } from '@/domain/Executables/Script/Script';
import { createCategory, type CategoryFactory } from '@/domain/Executables/Category/CategoryFactory';
import { parseDocs, type DocsParser } from './DocumentationParser'; import { parseDocs, type DocsParser } from './DocumentationParser';
import { parseScript, type ScriptParser } from './Script/ScriptParser'; import { parseScript, type ScriptParser } from './Script/ScriptParser';
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from './Validation/ExecutableValidator'; import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from './Validation/ExecutableValidator';
import { ExecutableType } from './Validation/ExecutableType'; import { ExecutableType } from './Validation/ExecutableType';
import type { CategoryCollectionSpecificUtilities } from './CategoryCollectionSpecificUtilities'; import type { CategoryCollectionSpecificUtilities } from './CategoryCollectionSpecificUtilities';
let categoryIdCounter = 0;
export const parseCategory: CategoryParser = ( export const parseCategory: CategoryParser = (
category: CategoryData, category: CategoryData,
collectionUtilities: CategoryCollectionSpecificUtilities, collectionUtilities: CategoryCollectionSpecificUtilities,
@@ -59,7 +57,7 @@ function parseCategoryRecursively(
} }
try { try {
return context.categoryUtilities.createCategory({ return context.categoryUtilities.createCategory({
id: categoryIdCounter++, executableId: context.categoryData.category, // arbitrary ID
name: context.categoryData.category, name: context.categoryData.category,
docs: context.categoryUtilities.parseDocs(context.categoryData), docs: context.categoryUtilities.parseDocs(context.categoryData),
subcategories: children.subcategories, subcategories: children.subcategories,
@@ -166,10 +164,6 @@ function hasProperty(
return Object.prototype.hasOwnProperty.call(object, propertyName); return Object.prototype.hasOwnProperty.call(object, propertyName);
} }
export type CategoryFactory = (
...parameters: ConstructorParameters<typeof CollectionCategory>
) => Category;
interface CategoryParserUtilities { interface CategoryParserUtilities {
readonly createCategory: CategoryFactory; readonly createCategory: CategoryFactory;
readonly wrapError: ErrorWithContextWrapper; readonly wrapError: ErrorWithContextWrapper;
@@ -179,7 +173,7 @@ interface CategoryParserUtilities {
} }
const DefaultCategoryParserUtilities: CategoryParserUtilities = { const DefaultCategoryParserUtilities: CategoryParserUtilities = {
createCategory: (...parameters) => new CollectionCategory(...parameters), createCategory,
wrapError: wrapErrorWithAdditionalContext, wrapError: wrapErrorWithAdditionalContext,
createValidator: createExecutableDataValidator, createValidator: createExecutableDataValidator,
parseScript, parseScript,

View File

@@ -1,7 +1,6 @@
import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/'; import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/';
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines'; import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax'; import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
import { CollectionScript } from '@/domain/Executables/Script/CollectionScript';
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel'; import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode'; import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator'; import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
@@ -10,6 +9,7 @@ import type { ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptC
import { createScriptCode } from '@/domain/Executables/Script/Code/ScriptCodeFactory'; import { createScriptCode } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
import type { Script } from '@/domain/Executables/Script/Script'; import type { Script } from '@/domain/Executables/Script/Script';
import { createEnumParser, type EnumParser } from '@/application/Common/Enum'; import { createEnumParser, type EnumParser } from '@/application/Common/Enum';
import { createScript, type ScriptFactory } from '@/domain/Executables/Script/ScriptFactory';
import { parseDocs, type DocsParser } from '../DocumentationParser'; import { parseDocs, type DocsParser } from '../DocumentationParser';
import { ExecutableType } from '../Validation/ExecutableType'; import { ExecutableType } from '../Validation/ExecutableType';
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from '../Validation/ExecutableValidator'; import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from '../Validation/ExecutableValidator';
@@ -37,6 +37,7 @@ export const parseScript: ScriptParser = (
validateScript(data, validator); validateScript(data, validator);
try { try {
const script = scriptUtilities.createScript({ const script = scriptUtilities.createScript({
executableId: data.name, // arbitrary ID
name: data.name, name: data.name,
code: parseCode( code: parseCode(
data, data,
@@ -132,14 +133,6 @@ interface ScriptParserUtilities {
readonly parseDocs: DocsParser; readonly parseDocs: DocsParser;
} }
export type ScriptFactory = (
...parameters: ConstructorParameters<typeof CollectionScript>
) => Script;
const createScript: ScriptFactory = (...parameters) => {
return new CollectionScript(...parameters);
};
const DefaultUtilities: ScriptParserUtilities = { const DefaultUtilities: ScriptParserUtilities = {
levelParser: createEnumParser(RecommendationLevel), levelParser: createEnumParser(RecommendationLevel),
createScript, createScript,

View File

@@ -1,17 +1,19 @@
import type { IEntity } from '@/infrastructure/Entity/IEntity'; import type { RepositoryEntity } from './RepositoryEntity';
export interface ReadonlyRepository<TKey, TEntity extends IEntity<TKey>> { type EntityId = RepositoryEntity['id'];
export interface ReadonlyRepository<TEntity extends RepositoryEntity> {
readonly length: number; readonly length: number;
getItems(predicate?: (entity: TEntity) => boolean): readonly TEntity[]; getItems(predicate?: (entity: TEntity) => boolean): readonly TEntity[];
getById(id: TKey): TEntity; getById(id: EntityId): TEntity;
exists(id: TKey): boolean; exists(id: EntityId): boolean;
} }
export interface MutableRepository<TKey, TEntity extends IEntity<TKey>> { export interface MutableRepository<TEntity extends RepositoryEntity> {
addItem(item: TEntity): void; addItem(item: TEntity): void;
addOrUpdateItem(item: TEntity): void; addOrUpdateItem(item: TEntity): void;
removeItem(id: TKey): void; removeItem(id: EntityId): void;
} }
export interface Repository<TKey, TEntity extends IEntity<TKey>> export interface Repository<TEntity extends RepositoryEntity>
extends ReadonlyRepository<TKey, TEntity>, MutableRepository<TKey, TEntity> { } extends ReadonlyRepository<TEntity>, MutableRepository<TEntity> { }

View File

@@ -0,0 +1,6 @@
/** Aggregate root */
export type RepositoryEntityId = string;
export interface RepositoryEntity {
readonly id: RepositoryEntityId;
}

View File

@@ -3389,7 +3389,8 @@ actions:
function: DisableService function: DisableService
parameters: parameters:
serviceName: PcaSvc # Check: (Get-Service -Name 'PcaSvc').StartType serviceName: PcaSvc # Check: (Get-Service -Name 'PcaSvc').StartType
defaultStartupMode: Automatic # Allowed values: Automatic | Manual # Windows 10 21H1: Manual | Windows 11 22H2: Automatic
defaultStartupMode: Automatic # Allowed values: Automatic | Manual | Boot
- -
category: Disable Windows telemetry and data collection category: Disable Windows telemetry and data collection
children: children:
@@ -3424,7 +3425,7 @@ actions:
function: DisableService function: DisableService
parameters: parameters:
serviceName: DiagTrack # Check: (Get-Service -Name DiagTrack).StartType serviceName: DiagTrack # Check: (Get-Service -Name DiagTrack).StartType
defaultStartupMode: Automatic # Allowed values: Automatic | Manual defaultStartupMode: Automatic # Allowed values: Automatic | Manual | Boot
- -
name: Disable WAP push notification routing service # Device Management Wireless Application Protocol (WAP) Push message Routing Service name: Disable WAP push notification routing service # Device Management Wireless Application Protocol (WAP) Push message Routing Service
recommend: standard recommend: standard
@@ -3441,7 +3442,7 @@ actions:
function: DisableService function: DisableService
parameters: parameters:
serviceName: dmwappushservice # Check: (Get-Service -Name dmwappushservice).StartType serviceName: dmwappushservice # Check: (Get-Service -Name dmwappushservice).StartType
defaultStartupMode: Manual # Allowed values: Automatic | Manual defaultStartupMode: Manual # Allowed values: Automatic | Manual | Boot
- -
name: Disable "Diagnostics Hub Standard Collector" service name: Disable "Diagnostics Hub Standard Collector" service
docs: |- docs: |-
@@ -3457,7 +3458,7 @@ actions:
function: DisableService function: DisableService
parameters: parameters:
serviceName: diagnosticshub.standardcollector.service # Check: (Get-Service -Name diagnosticshub.standardcollector.service).StartType serviceName: diagnosticshub.standardcollector.service # Check: (Get-Service -Name diagnosticshub.standardcollector.service).StartType
defaultStartupMode: Manual # Allowed values: Automatic | Manual defaultStartupMode: Manual # Allowed values: Automatic | Manual | Boot
- -
name: Disable "Diagnostic Execution Service" (`diagsvc`) name: Disable "Diagnostic Execution Service" (`diagsvc`)
docs: |- docs: |-
@@ -3473,7 +3474,7 @@ actions:
function: DisableService function: DisableService
parameters: parameters:
serviceName: diagsvc # Check: (Get-Service -Name diagsvc).StartType serviceName: diagsvc # Check: (Get-Service -Name diagsvc).StartType
defaultStartupMode: Manual # Allowed values: Automatic | Manual defaultStartupMode: Manual # Allowed values: Automatic | Manual | Boot
- -
name: Disable "Customer Experience Improvement Program" scheduled tasks name: Disable "Customer Experience Improvement Program" scheduled tasks
recommend: standard recommend: standard
@@ -3959,6 +3960,9 @@ actions:
[3]: https://web.archive.org/web/20231018135918/https://www.stigviewer.com/stig/windows_10/2016-06-24/finding/V-63493 "The system must be configured to allow a local or DOD-wide collector to request additional error reporting diagnostic data to be sent. | stigviewer.com" [3]: https://web.archive.org/web/20231018135918/https://www.stigviewer.com/stig/windows_10/2016-06-24/finding/V-63493 "The system must be configured to allow a local or DOD-wide collector to request additional error reporting diagnostic data to be sent. | stigviewer.com"
[4]: https://web.archive.org/web/20231018135930/https://batcmd.com/windows/10/services/wersvc/ "Windows Error Reporting Service - Windows 10 Service - batcmd.com" [4]: https://web.archive.org/web/20231018135930/https://batcmd.com/windows/10/services/wersvc/ "Windows Error Reporting Service - Windows 10 Service - batcmd.com"
[5]: https://web.archive.org/web/20231019222221/https://batcmd.com/windows/10/services/wercplsupport/ "Problem Reports Control Panel Support - Windows 10 Service - batcmd.com" [5]: https://web.archive.org/web/20231019222221/https://batcmd.com/windows/10/services/wercplsupport/ "Problem Reports Control Panel Support - Windows 10 Service - batcmd.com"
# TODO: Windows Error Reporting Service sends error back to Microsoft:
- https://web.archive.org/web/20210926064024/https://docs.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-antivirus-on-windows-server?view=o365-worldwide
call: call:
- -
function: Comment function: Comment
@@ -4037,12 +4041,12 @@ actions:
function: DisableService function: DisableService
parameters: parameters:
serviceName: wersvc # Check: (Get-Service -Name wersvc).StartType serviceName: wersvc # Check: (Get-Service -Name wersvc).StartType
defaultStartupMode: Manual # Allowed values: Automatic | Manual defaultStartupMode: Manual # Allowed values: Automatic | Manual | Boot
- # Problem Reports Control Panel Support - # Problem Reports Control Panel Support
function: DisableService function: DisableService
parameters: parameters:
serviceName: wercplsupport # Check: (Get-Service -Name wercplsupport).StartType serviceName: wercplsupport # Check: (Get-Service -Name wercplsupport).StartType
defaultStartupMode: Manual # Allowed values: Automatic | Manual defaultStartupMode: Manual # Allowed values: Automatic | Manual | Boot
- -
category: Disable Windows Update data collection category: Disable Windows Update data collection
children: children:
@@ -4200,7 +4204,7 @@ actions:
# "Set-Service" returns "Access is denied" since Windows 10 1809. # "Set-Service" returns "Access is denied" since Windows 10 1809.
parameters: parameters:
serviceName: DoSvc # Check: (Get-Service -Name 'DoSvc').StartType serviceName: DoSvc # Check: (Get-Service -Name 'DoSvc').StartType
defaultStartupMode: Automatic # Allowed values: Automatic | Manual defaultStartupMode: Automatic # Allowed values: Automatic | Manual | Boot
- -
name: Disable cloud-based speech recognition name: Disable cloud-based speech recognition
recommend: standard recommend: standard
@@ -5299,7 +5303,7 @@ actions:
function: DisableService function: DisableService
parameters: parameters:
serviceName: WbioSrvc # Check: (Get-Service -Name WbioSrvc).StartType serviceName: WbioSrvc # Check: (Get-Service -Name WbioSrvc).StartType
defaultStartupMode: Manual # Allowed values: Automatic | Manual defaultStartupMode: Manual # Allowed values: Automatic | Manual | Boot
- -
name: Disable Wi-Fi Sense name: Disable Wi-Fi Sense
recommend: standard recommend: standard
@@ -5452,7 +5456,7 @@ actions:
function: DisableService function: DisableService
parameters: parameters:
serviceName: wisvc # Check: (Get-Service -Name wisvc).StartType serviceName: wisvc # Check: (Get-Service -Name wisvc).StartType
defaultStartupMode: Manual # Allowed values: Automatic | Manual defaultStartupMode: Manual # Allowed values: Automatic | Manual | Boot
- -
name: Disable Microsoft feature trials name: Disable Microsoft feature trials
docs: https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.DataCollection::EnableExperimentation docs: https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.DataCollection::EnableExperimentation
@@ -6377,7 +6381,7 @@ actions:
# function: DisableService # function: DisableService
# parameters: # parameters:
# serviceName: ClickToRunSvc # Check: (Get-Service -Name ClickToRunSvc).StartType # serviceName: ClickToRunSvc # Check: (Get-Service -Name ClickToRunSvc).StartType
# defaultStartupMode: Automatic # Allowed values: Automatic | Manual # defaultStartupMode: Automatic # Allowed values: Automatic | Manual | Boot
- -
name: Disable "Microsoft Office Subscription Heartbeat" task name: Disable "Microsoft Office Subscription Heartbeat" task
docs: |- docs: |-
@@ -9266,7 +9270,7 @@ actions:
function: DisableService function: DisableService
parameters: parameters:
serviceName: LogiRegistryService # Check: (Get-Service -Name 'LogiRegistryService').StartType serviceName: LogiRegistryService # Check: (Get-Service -Name 'LogiRegistryService').StartType
defaultStartupMode: Automatic # Allowed values: Automatic | Manual defaultStartupMode: Automatic # Allowed values: Automatic | Manual | Boot
- -
category: Disable Dropbox background automatic updates category: Disable Dropbox background automatic updates
docs: |- docs: |-
@@ -9412,7 +9416,7 @@ actions:
function: DisableService function: DisableService
parameters: parameters:
serviceName: WMPNetworkSvc # Check: (Get-Service -Name 'WMPNetworkSvc').StartType serviceName: WMPNetworkSvc # Check: (Get-Service -Name 'WMPNetworkSvc').StartType
defaultStartupMode: Manual # Allowed values: Automatic | Manual defaultStartupMode: Manual # Allowed values: Automatic | Manual | Boot
- -
name: Disable CCleaner data collection name: Disable CCleaner data collection
call: call:
@@ -12478,6 +12482,12 @@ actions:
[11]: https://web.archive.org/web/20240409171421/https://learn.microsoft.com/en-us/defender/ "Microsoft Defender products and services | Microsoft Learn" [11]: https://web.archive.org/web/20240409171421/https://learn.microsoft.com/en-us/defender/ "Microsoft Defender products and services | Microsoft Learn"
# See defender status: Get-MpComputerStatus # See defender status: Get-MpComputerStatus
children: children:
# TODO:
# - `HKLM\Software\Policies\Microsoft\Windows Defender!AllowFastServiceStartup` -> 0
# - `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock!AllowDevelopmentWithoutDevLicense` > 1
# - `HKLM\SOFTWARE\Policies\Microsoft\Windows\Appx!AllowDevelopmentWithoutDevLicense` > 1
# - `HKLM\SYSTEM\CurrentControlSet\Control\CI\Policy!VerifiedAndReputablePolicyState` > 1
# TODO: serach for `Policies\Microsoft\Windows Defender\Features`, theres stuff not added here
- -
category: Disable Microsoft Defender firewall category: Disable Microsoft Defender firewall
docs: |- docs: |-
@@ -12690,6 +12700,26 @@ actions:
grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
- -
function: ShowComputerRestartSuggestion function: ShowComputerRestartSuggestion
-
name: Disable Windows Filtering Platform (WFP) and Base Filtering Engine (BFE)
docs: |-
Windows Filtering Platform
A service that controls the operation of the **Windows Filtering Platform** [1].
Windows Filtering Platform (WFP) is a network traffic processing platform designed
to replace the Windows XP and Windows Server 2003 network traffic filtering interfaces [1].
WFP consists of a set of hooks into the network stack and a filtering engine that
coordinates network stack interactions [1].
It performs the following tasks:
- Accepts filters and other configuration settings for the platform [1].
- Reports the current state of the system, including statistics [1].
- Enforces the security model for accepting configuration in the platform [1].
For example, a local administrator can add filters but other users can only view them [1].
. Plumbs configuration settings to other modules in the system [1]
For example, IPsec negotiation polices go to IKE/AuthIP keying modules, filters go to the filter engine [1].
code: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\BFE # TODO: not tested
- -
name: Disable firewall via command-line utility name: Disable firewall via command-line utility
# ❗️ Following must be enabled and in running state: # ❗️ Following must be enabled and in running state:
@@ -12817,21 +12847,92 @@ actions:
- https://web.archive.org/web/20240314125156/https://learn.microsoft.com/en-us/windows-hardware/customize/desktop/unattend/security-malware-windows-defender-disableantispyware - https://web.archive.org/web/20240314125156/https://learn.microsoft.com/en-us/windows-hardware/customize/desktop/unattend/security-malware-windows-defender-disableantispyware
- https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::DisableAntiSpywareDefender - https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::DisableAntiSpywareDefender
call: call:
function: SetRegistryValue -
parameters: function: SetRegistryValue
keyPath: HKLM\SOFTWARE\Policies\Microsoft\Windows Defender parameters:
valueName: DisableAntiSpyware keyPath: HKLM\SOFTWARE\Policies\Microsoft\Windows Defender
dataType: REG_DWORD valueName: DisableAntiSpyware
data: "1" dataType: REG_DWORD
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2) data: "1"
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
-
# Disable Firewall through PowerShell cmdled # TODO: same as CLI?
function: RunPowerShell
parameters:
code: Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled True
- #TODO: Test permissions and doc this:
function: SetRegistryValue
parameters:
keyPath: HKLM\SOFTWARE\Policies\Microsoft\Windows Advanced Threat Protection
valueName: ForceDefenderPassiveMode
dataType: REG_DWORD
data: "1"
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
-
name: Disable Microsoft Defender Antivirus # Deprecated since Windows 10 version 1903
docs: |-
This script deactivates Microsoft Defender Antivirus on Windows versions before the August 2020 update (version 4.18.2007.8) [1] [2].
Newer versions of Microsoft Defender Antivirus, especially from Windows 10 version 1903 onwards [1], do not support deactivation through system policy [1] [2].
Microsoft Defender Antivirus offers protection against malware, including spyware. The **DisableAntiSpyware** setting, when set to `false` (i.e., `1`),
previously disabled Microsoft Defender Antivirus and other non-Microsoft antivirus solutions [1]. However, this setting is now obsolete for devices running
platform version 4.18.2108.4 or newer [1]. Additionally, Microsoft Defender for Endpoint ignores this setting [1]. Tamper protection, introduced in Windows
10 version 1903, prevents unauthorized changes to this setting [1]. The related registry key is
`HKLM\SOFTWARE\Policies\Microsoft\Windows Defender!DisableAntiSpyware` [2] [3].
Similarly, the **DisableAntiVirus** policy, intended to deactivate Microsoft Defender Antivirus [2], is applicable only to versions before the
August 2020 update [2]. Post-update, this policy cannot turn off Microsoft Defender Antivirus on client devices [2]. Its associated registry key
is `HKLM\SOFTWARE\Policies\Microsoft\Windows Defender!DisableAntiVirus` [2].
> **Caution**: Disabling antivirus can increase privacy by reducing data collection from Microsoft and may enhance system performance.
> However, it poses a significant security risk by reducing protection against malware and other threats. Users should consider the
> trade-offs between privacy, system performance, and security before disabling antivirus protection.
[1]: https://web.archive.org/web/20231126024121/https://learn.microsoft.com/en-us/windows-hardware/customize/desktop/unattend/security-malware-windows-defender-disableantispyware "DisableAntiSpyware | Microsoft Learn | learn.microsoft.com"
[2]: https://web.archive.org/web/20231126024330/https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/troubleshoot-onboarding?view=o365-worldwide "Troubleshoot Microsoft Defender for Endpoint onboarding issues | Microsoft Learn | learn.microsoft.com"
[3]: https://web.archive.org/web/20210926064024/https://docs.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-antivirus-on-windows-server?view=o365-worldwide "Microsoft Defender Antivirus on Windows Server | Microsoft Docs | docs.microsoft.com"
call:
-
function: RunInlineCode
parameters:
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender" /v "DisableAntiSpyware" /t REG_DWORD /d 1 /f
revertCode: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender" /v "DisableAntiSpyware" /f 2>nul
-
function: RunInlineCode
parameters:
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender" /v "DisableAntiVirus" /t REG_DWORD /d 1 /f
revertCode: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender" /v "DisableAntiVirus" /f 2>nul
# TODO: Soft-delete defender directory.
# TODO: Make above category
# name: Remove Windows Defender Definition FilesPermalink
# docs: |-
# https://unit42.paloaltonetworks.com/unit42-gorgon-group-slithering-nation-state-cybercrime/
# Removing definition files would cause ATP to not fire for AntiMalware.
# https://atomicredteam.io/defense-evasion/T1562.001/#atomic-test-20---remove-windows-defender-definition-files
# https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-antivirus-windows?view=o365-worldwide
# code: "%ProgramFiles%\Windows Defender\MpCmdRun.exe" -RemoveDefinitions -All
# revertCode: "%ProgramFiles%\Windows Defender\MpCmdRun.exe" -SignatureUpdate
# TODO: MpDlpService https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-antivirus-windows?view=o365-worldwide
# MDDlpSvc
# TODO:
# - Reg.exe add "HKLM\SOFTWARE\Microsoft\Windows Defender\Scan" /v "AutomaticallyCleanAfterScan" /t REG_DWORD /d "0" /f
# - HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows Defender!DisableSpecialRunningModes > 1
# - HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows Defender!ServiceKeepAlive > 1
- -
category: Disable Defender features category: Disable Defender features
# Status: Get-MpPreference # Status: Get-MpPreference
children: children:
- -
category: Disable Defender Antivirus cloud protection service category: Disable Defender Antivirus cloud protection service
docs: https://web.archive.org/web/20240523173753/https://learn.microsoft.com/en-us/defender-endpoint/enable-cloud-protection-microsoft-defender-antivirus?view=o365-worldwide docs: |-
# Formerly known as: Microsoft MAPS (Microsoft Active Protection Service), Microsoft SpyNet Microsoft Defender Antivirus cloud protection helps protect against malware on your endpoints and across your network.
It's formerly known as *Microsoft Active Protection Service (MAPS)* [2] [3], or *Microsoft SpyNet* [2]). MAPS leverages
user data to identify potentially malicious programs, sharing details such as file information, IP address, computer
identification, and system/browser information [2] [3].
[1]: https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/enable-cloud-protection-microsoft-defender-antivirus?view=o365-worldwide
[2]: https://en.wikipedia.org/wiki/Microsoft_Active_Protection_Service
[3]: https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/jj618314(v=ws.11)
children: children:
- -
category: Disable Defender cloud protection features category: Disable Defender cloud protection features
@@ -12863,14 +12964,24 @@ actions:
docs: docs:
- https://web.archive.org/web/20240314122554/https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-defender#cloudextendedtimeout - https://web.archive.org/web/20240314122554/https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-defender#cloudextendedtimeout
- https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::MpEngine_MpBafsExtendedTimeout - https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::MpEngine_MpBafsExtendedTimeout
# Managing with MpPreference module:
- https://docs.microsoft.com/fr-fr/powershell/module/defender/set-mppreference
call: call:
function: SetRegistryValue -
parameters: function: SetMpPreference
keyPath: HKLM\Software\Policies\Microsoft\Windows Defender\MpEngine parameters:
valueName: MpBafsExtendedTimeout property: CloudExtendedTimeout # Status: Get-MpPreference | Select-Object -Property CloudExtendedTimeout
dataType: REG_DWORD value: "'50'" # Set: Set-MpPreference -Force -CloudExtendedTimeout '50'
data: "50" default: $False # Default: 0 | Set-MpPreference -Force -CloudExtendedTimeout '0'
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2) setDefaultOnWindows11: true # `Remove-MpPreference` sets it to 0 instead 1 (OS default) in Windows 11
-
function: SetRegistryValue
parameters:
keyPath: HKLM\Software\Policies\Microsoft\Windows Defender\MpEngine
valueName: MpBafsExtendedTimeout
dataType: REG_DWORD
data: "50"
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
- -
name: Minimize cloud protection level # Requires "Join Microsoft MAPS" name: Minimize cloud protection level # Requires "Join Microsoft MAPS"
docs: docs:
@@ -13044,14 +13155,23 @@ actions:
- https://web.archive.org/web/20240314124546/https://learn.microsoft.com/en-us/windows/client-management/mdm/defender-csp#configuration-enablefilehashcomputation - https://web.archive.org/web/20240314124546/https://learn.microsoft.com/en-us/windows/client-management/mdm/defender-csp#configuration-enablefilehashcomputation
- https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::MpEngine_EnableFileHashComputation - https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::MpEngine_EnableFileHashComputation
- https://techcommunity.microsoft.com/t5/microsoft-security-baselines/security-baseline-final-windows-10-and-windows-server-version/ba-p/1543631 - https://techcommunity.microsoft.com/t5/microsoft-security-baselines/security-baseline-final-windows-10-and-windows-server-version/ba-p/1543631
# Managing with MpPreference module:
- https://docs.microsoft.com/en-us/powershell/module/defender/set-mppreference
call: call:
function: SetRegistryValue -
parameters: function: SetRegistryValue
keyPath: HKLM\Software\Policies\Microsoft\Windows Defender\MpEngine parameters:
valueName: EnableFileHashComputation keyPath: HKLM\Software\Policies\Microsoft\Windows Defender\MpEngine
dataType: REG_DWORD valueName: EnableFileHashComputation
data: "0" dataType: REG_DWORD
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2) data: "0"
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
-
function: SetMpPreference
parameters:
property: EnableFileHashComputation # Status: Get-MpPreference | Select-Object -Property EnableFileHashComputation
value: $True # Set: Set-MpPreference -Force -EnableFileHashComputation $True
default: $False # Default: False (Enabled) | Remove-MpPreference -Force -EnableFileHashComputation | Set-MpPreference -Force -EnableFileHashComputation $False
- -
category: Disable "Windows Defender Exploit Guard" category: Disable "Windows Defender Exploit Guard"
docs: https://web.archive.org/web/20231020130741/https://www.microsoft.com/en-us/security/blog/2017/10/23/windows-defender-exploit-guard-reduce-the-attack-surface-against-next-generation-malware/ docs: https://web.archive.org/web/20231020130741/https://www.microsoft.com/en-us/security/blog/2017/10/23/windows-defender-exploit-guard-reduce-the-attack-surface-against-next-generation-malware/
@@ -13069,17 +13189,35 @@ actions:
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2) deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
- -
name: Disable controlled folder access name: Disable controlled folder access
docs: docs: |-
- https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::ExploitGuard_ControlledFolderAccess_EnableControlledFolderAccess This script turns of controlled folder access feature.
- https://web.archive.org/web/20240314124339/https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/enable-controlled-folders?view=o365-worldwide
Controlled folder access helps you protect valuable data from malicious apps and threats, such as ransomware [1].
This feature is disabled by default [2].
It can be controlled using PowerShell MpPreference module using `EnableControlledFolderAccess` key [2] [1], the feature is disabled using `Disabled` value.
It can also be disabled using `Software\Policies\Microsoft\Windows Defender\Windows Defender Exploit Guard\Controlled Folder Access` registry key [3].
[1]: https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/enable-controlled-folders
[2]: https://learn.microsoft.com/en-us/powershell/module/defender/set-mppreference?view=windowsserver2022-ps#-enablecontrolledfolderaccess
[3]: https://web.archive.org/web/20230422135736/https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::ExploitGuard_ControlledFolderAccess_EnableControlledFolderAccess
call: call:
function: SetRegistryValue -
parameters: function: SetMpPreference
keyPath: HKLM\Software\Policies\Microsoft\Windows Defender\Windows Defender Exploit Guard\Controlled Folder Access parameters:
valueName: EnableControlledFolderAccess property: EnableControlledFolderAccess # Status: Get-MpPreference | Select-Object -Property EnableControlledFolderAccess
dataType: REG_DWORD value: 'Disabled' # Set: Set-MpPreference -Force -EnableControlledFolderAccess 'Enabled'
data: "0" default: 'Disabled' # Default: Disabled | Remove-MpPreference -Force -EnableControlledFolderAccess | Set-MpPreference -Force -EnableControlledFolderAccess $False
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2) -
function: SetRegistryValue
parameters:
keyPath: HKLM\Software\Policies\Microsoft\Windows Defender\Windows Defender Exploit Guard\Controlled Folder Access
valueName: EnableControlledFolderAccess
dataType: REG_DWORD
data: "0"
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
- -
category: Disable network inspection system features category: Disable network inspection system features
children: children:
@@ -13137,7 +13275,6 @@ actions:
value: $True # Set: Set-MpPreference -Force -DisableRealtimeMonitoring $True value: $True # Set: Set-MpPreference -Force -DisableRealtimeMonitoring $True
# ❌ Windows 11: Does not fail but does not set $True value | ✅ Windows 10: Works as expected # ❌ Windows 11: Does not fail but does not set $True value | ✅ Windows 10: Works as expected
default: $False # Default: False (Enabled) | Remove-MpPreference -Force -DisableRealtimeMonitoring | Set-MpPreference -Force -DisableRealtimeMonitoring $False default: $False # Default: False (Enabled) | Remove-MpPreference -Force -DisableRealtimeMonitoring | Set-MpPreference -Force -DisableRealtimeMonitoring $False
- -
function: SetRegistryValue function: SetRegistryValue
parameters: parameters:
@@ -13146,6 +13283,11 @@ actions:
dataType: REG_DWORD dataType: REG_DWORD
data: "1" data: "1"
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2) deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
- # TODO: https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/migrating-asr-rules?view=o365-worldwide
function: RunInlineCode
parameters:
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender\Policy Manager" /v "AllowRealTimeMonitoring" /t REG_DWORD /d "1" /f
revertCode: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender\Policy Manager" /v "AllowRealTimeMonitoring" /f 2>nul
- -
name: Disable intrusion prevention system (IPS) name: Disable intrusion prevention system (IPS)
docs: docs:
@@ -13267,7 +13409,7 @@ actions:
function: SetRegistryValue function: SetRegistryValue
parameters: parameters:
keyPath: HKLM\Software\Policies\Microsoft\Windows Defender\Real-Time Protection keyPath: HKLM\Software\Policies\Microsoft\Windows Defender\Real-Time Protection
valueName: DisableWindowsSpotlightFeatures valueName: DisableOnAccessProtection
dataType: REG_DWORD dataType: REG_DWORD
data: "1" data: "1"
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2) deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
@@ -13309,6 +13451,67 @@ actions:
dataType: REG_DWORD dataType: REG_DWORD
data: "1" data: "1"
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2) deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
-
name: Disable synchronous real-time scanning of Dev Drive
docs: |-
This script disables synchronous real-time scanning in Dev Drive on Windows 11.
This way, it enables a performance mode in Defender [1].
Dev Drive, a new storage volume type, is designed for developers to improve performance using ReFS technology [1] [2].
By default, Dev Drive operates in asynchronous scan mode, balancing threat protection and performance [1].
This script switches scanning from synchronous (real-time protection) to asynchronous (scanning after file operations),
resulting in faster performance but potentially reduced security [1].
Synchronous scanning initiates a real-time protection scan when opening a file, while asynchronous scanning defers the
security scan until after the file operation [1]. Disabling synchronous scanning can impact performance, especially in
development environments with frequent file operations [2].
To enable performance mode, real-time protection must be active, and Dev Drive must be designated as trusted [1].
This script uses `SetMpPreference` command [1] and `HKLM\Software\Microsoft\Windows Defender\Real-Time Protection!DisableAsyncScanOnOpen`
registry key modification [3] to alter the scanning behavior.
> **Caution**: Changing these settings can lower security by prioritizing performance over immediate threat scanning.
> It is recommended to understand the security implications before proceeding.
[1]: https://web.archive.org/web/20231126014947/https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-endpoint-antivirus-performance-mode?view=o365-worldwide "Protect Dev Drive using performance mode | Microsoft Learn | learn.microsoft.com"
[2]: https://web.archive.org/web/20231126014908/https://blogs.windows.com/windowsdeveloper/2023/09/26/new-experiences-designed-to-make-every-developer-more-productive-on-windows-11/ "New experiences designed to make every developer more productive on Windows 11 - Windows Developer Blog | blogs.windows.com"
[3]: https://www.elevenforum.com/t/enable-or-disable-performance-mode-for-dev-drive-protection-in-windows-11.17215/ "Enable or Disable Performance Mode for Dev Drive Protection in Windows 11 Tutorial | Windows 11 Forum | www.elevenforum.com"
call:
-
function: RunInlineCodeAsTrustedInstaller # Otherwise we get "ERROR: Access is denied." (>= 22H2)
parameters:
code: reg add "HKLM\Software\Microsoft\Windows Defender\Real-Time Protection" /v "DisableAsyncScanOnOpen" /t REG_DWORD /d "0" /f
revertCode: reg delete "HKLM\Software\Policies\Microsoft\Windows Defender\Real-Time Protection" /v "DisableAsyncScanOnOpen" /f 2>nul
-
function: SetMpPreference
parameters:
property: PerformanceModeStatus # Status: Get-MpPreference | Select-Object -Property PerformanceModeStatus
value: 'Enabled' # Set: Set-MpPreference -Force -PerformanceModeStatus 'Enabled'
default: 'Disabled' # Default: Disabled | Remove-MpPreference -Force -PerformanceModeStatus | Set-MpPreference -Force -PerformanceModeStatus 'Disabled'
-
name: Disable Dynamic Protection Analysis (DPA) feature
docs: |-
This script disables the Dynamic Protection Analysis (DPA) feature in Microsoft Defender.
DPA, part of Microsoft Defender's real-time protection conducts continuous behavioral analysis to identify potential threats.
However, this monitoring may lead to increased data collection by Microsoft, raising privacy concerns.
Disabling DPA aims to mitigate this data collection, enhancing user privacy by reducing the scope of Microsoft Defender's surveillance.
Additionally, this action may yield performance improvements, particularly in scenarios where real-time scanning imposes a significant
burden on system resources. Yet, users should be aware that disabling DPA reduces the system's security and defensive capabilities against
threats, as it limits the efficacy of Microsoft Defender's real-time response.
The script modifies the `HKLM\SOFTWARE\Microsoft\Windows Defender\Real-Time Protection!DpaDisabled` registry key to achieve this.
> **Caution:** Users need to weigh the privacy advantages against the potential decrease in security effectiveness.
> This setting change is significant for systems with modern versions of Windows, where DPA is a default-enabled feature.
call:
function: RunInlineCodeAsTrustedInstaller # Otherwise we get "ERROR: Access is denied." (>= 22H2)
parameters:
code: reg add "HKLM\Software\Microsoft\Windows Defender\Real-Time Protection" /v "DpaDisabled" /t REG_DWORD /d "1" /f
revertCode: |- # This value exists with value `0` by default since Windows 10 >= 22H2 and Windows 11 >= 22H2
reg add "HKLM\Software\Microsoft\Windows Defender\Real-Time Protection" /v "DpaDisabled" /t REG_DWORD /d "0" /f 2>nul
- -
category: Disable Defender remediation category: Disable Defender remediation
children: children:
@@ -13456,7 +13659,7 @@ actions:
dataType: REG_DWORD dataType: REG_DWORD
data: '1' data: '1'
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2) deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
# - # Too good to disable # - Too good to disable, also no reported privacy issues
# category: Disable Microsoft Defender "Device Guard" and "Credential Guard" # category: Disable Microsoft Defender "Device Guard" and "Credential Guard"
# docs: https://techcommunity.microsoft.com/t5/iis-support-blog/windows-10-device-guard-and-credential-guard-demystified/ba-p/376419 # docs: https://techcommunity.microsoft.com/t5/iis-support-blog/windows-10-device-guard-and-credential-guard-demystified/ba-p/376419
# children: # children:
@@ -14963,13 +15166,15 @@ actions:
# 3. Try `DisableServiceInRegistryAsTrustedInstaller` as last effort. # 3. Try `DisableServiceInRegistryAsTrustedInstaller` as last effort.
children: children:
- -
name: Disable "Microsoft Defender Antivirus Service" name: Disable "Microsoft Defender Antivirus service" service
# ❗️ Breaks `Set-MpPreference` PowerShell cmdlet that helps to manage Defender # ❗️ Breaks `Set-MpPreference` PowerShell cmdlet that helps to manage Defender
# E.g. `Set-MpPreference -Force -MAPSReporting 0` throws: # E.g. `Set-MpPreference -Force -MAPSReporting 0` throws:
# `Set-MpPreference: Operation failed with the following error: 0x800106ba. Operation: Set-MpPreference.` # `Set-MpPreference: Operation failed with the following error: 0x800106ba. Operation: Set-MpPreference.`
# `Target: MAPS_MAPSReporting. FullyQualifiedErrorId : HRESULT 0x800106ba,Set-MpPreference` # `Target: MAPS_MAPSReporting. FullyQualifiedErrorId : HRESULT 0x800106ba,Set-MpPreference`
docs: |- docs: |-
https://web.archive.org/web/20240314091238/https://batcmd.com/windows/10/services/windefend/ It is a service used by Microsoft Defender [2] [3].
It's named as "Microsoft Defender Antivirus service", "Antimalware Service Executable" and "Microsoft Defender Antivirus" [3].
### Overview of default service statuses ### Overview of default service statuses
@@ -14977,6 +15182,14 @@ actions:
| ---------- | -------| ---------- | | ---------- | -------| ---------- |
| Windows 10 (≥ 22H2) | 🟢 Running | Automatic | | Windows 10 (≥ 22H2) | 🟢 Running | Automatic |
| Windows 11 (≥ 23H2) | 🟢 Running | Automatic | | Windows 11 (≥ 23H2) | 🟢 Running | Automatic |
[2]: https://web.archive.org/web/20231126024330/https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/troubleshoot-onboarding?view=o365-worldwide "Troubleshoot Microsoft Defender for Endpoint onboarding issues | Microsoft Learn | learn.microsoft.com"
[3]: https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-antivirus-windows?view=o365-worldwide
TODO:
- https://web.archive.org/web/20240314091238/https://batcmd.com/windows/10/services/windefend/
# Microsoft Defender Antivirus service, source:
- https://web.archive.org/web/20210926064024/https://docs.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-antivirus-on-windows-server?view=o365-worldwide
call: call:
- -
# Windows 10 (22H2): ❌ `DisableService` | ❌ `DisableServiceInRegistry` | ✅ `DisableServiceInRegistryAsTrustedInstaller` # Windows 10 (22H2): ❌ `DisableService` | ❌ `DisableServiceInRegistry` | ✅ `DisableServiceInRegistryAsTrustedInstaller`
@@ -14991,13 +15204,22 @@ actions:
# fileGlob: '%PROGRAMFILES%\Windows Defender\MsMpEng.exe' # Found also in C:\ProgramData\Microsoft\Windows Defender\Platform\4.18.2107.4-0 and \4.18.2103.7-0 ... # fileGlob: '%PROGRAMFILES%\Windows Defender\MsMpEng.exe' # Found also in C:\ProgramData\Microsoft\Windows Defender\Platform\4.18.2107.4-0 and \4.18.2103.7-0 ...
# grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 # grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
- -
category: Disable Defender kernel-level drivers category: Disable kernel-level Microsoft Defender drivers
children: children:
# - Skipping wdnsfltr ("Windows Defender Network Stream Filter Driver") as it's Windows 1709 only # Commented out drivers:
# - `wdnsfltr`: "Windows Defender Network Stream Filter Driver" as it's Windows 1709 only
- -
name: Disable "Microsoft Defender Antivirus Network Inspection System Driver" service name: Disable "Microsoft Defender Antivirus Network Inspection System Driver" driver
docs: |- docs: |-
https://web.archive.org/web/20240314062056/https://batcmd.com/windows/10/services/wdnisdrv/ This script disables `WdNisDrv` service, known as "Microsoft Defender Antivirus Network Inspection System Driver" [1].
It's a service used by Windows Defender [2].
This service helps guard against intrusion attempts targeting known and newly discovered vulnerabilities in
network protocols [1].
[1]: https://web.archive.org/web/20240314062056/https://batcmd.com/windows/10/services/wdnisdrv/
[2]: https://web.archive.org/web/20231126024330/https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/troubleshoot-onboarding?view=o365-worldwide "Troubleshoot Microsoft Defender for Endpoint onboarding issues | Microsoft Learn | learn.microsoft.com"
### Overview of default service statuses ### Overview of default service statuses
@@ -15022,8 +15244,14 @@ actions:
fileGlob: '%SYSTEMROOT%\System32\drivers\WdNisDrv.sys' fileGlob: '%SYSTEMROOT%\System32\drivers\WdNisDrv.sys'
grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
- -
name: Disable "Microsoft Defender Antivirus Mini-Filter Driver" service name: Disable "Microsoft Defender Antivirus Mini-Filter Driver" driver
docs: |- docs: |-
It is a service used by Windows Defender [2]
[2]: https://web.archive.org/web/20231126024330/https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/troubleshoot-onboarding?view=o365-worldwide "Troubleshoot Microsoft Defender for Endpoint onboarding issues | Microsoft Learn | learn.microsoft.com"
TODO:
- https://web.archive.org/web/20240314091638/https://n4r1b.com/posts/2020/01/dissecting-the-windows-defender-driver-wdfilter-part-1/ - https://web.archive.org/web/20240314091638/https://n4r1b.com/posts/2020/01/dissecting-the-windows-defender-driver-wdfilter-part-1/
- https://web.archive.org/web/20240314062047/https://batcmd.com/windows/10/services/wdfilter/ - https://web.archive.org/web/20240314062047/https://batcmd.com/windows/10/services/wdfilter/
@@ -15044,15 +15272,20 @@ actions:
serviceName: WdFilter # Check: (Get-Service -Name 'WdFilter').StartType serviceName: WdFilter # Check: (Get-Service -Name 'WdFilter').StartType
defaultStartupMode: Boot # Allowed values: Boot | System | Automatic | Manual defaultStartupMode: Boot # Allowed values: Boot | System | Automatic | Manual
# notStoppable: true # See `sc queryex WdFilter`, tested since Windows 10 22H2, Windows 11 22H2. # notStoppable: true # See `sc queryex WdFilter`, tested since Windows 10 22H2, Windows 11 22H2.
# TODO: Stopping this service does not work, fails with:
# The requested control is not valid for this service.
- -
function: SoftDeleteFiles function: SoftDeleteFiles
parameters: parameters:
fileGlob: '%SYSTEMROOT%\System32\drivers\WdFilter.sys' fileGlob: '%SYSTEMROOT%\System32\drivers\WdFilter.sys'
grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
- -
name: Disable "Microsoft Defender Antivirus Boot Driver" service name: Disable "Microsoft Defender Antivirus Boot Driver" driver
docs: |- docs: |-
https://web.archive.org/web/20240314062057/https://batcmd.com/windows/10/services/wdboot/ It is a service used by Windows Defender [2].
[1]: https://web.archive.org/web/20240314062057/https://batcmd.com/windows/10/services/wdboot/
[2]: https://web.archive.org/web/20231126024330/https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/troubleshoot-onboarding?view=o365-worldwide "Troubleshoot Microsoft Defender for Endpoint onboarding issues | Microsoft Learn | learn.microsoft.com"
### Overview of default service statuses ### Overview of default service statuses
@@ -15161,14 +15394,115 @@ actions:
parameters: parameters:
fileGlob: '%WINDIR%\System32\SecurityHealthService.exe' fileGlob: '%WINDIR%\System32\SecurityHealthService.exe'
grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2 grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
-
category: Disable Defender Windows features
docs: |-
`Get-WindowsOptionalFeature -Online -FeatureName "*Defender*"` to see related features.
children:
-
name: Disable "Windows-Defender" feature
docs: |-
Windows 10 > 22H2: Feature does not exist
https://github.com/MicrosoftDocs/microsoft-365-docs/blob/b3c6d838ad6c823c5e541a556761ab5faa240bfd/microsoft-365/security/defender-endpoint/enable-update-mdav-to-latest-ws.md?plain=1#L76
https://github.com/Ariantor/microsoft-365-docs/blob/cba6edb3bf31d3d9f86ef2271dbd78133dcd8118/microsoft-365/security/defender-endpoint/switch-to-mde-phase-2.md?plain=1#L84
https://github.com/isabella232/microsoft-365-docs-pr.it-IT/blob/d3a567aa6c70fd7ef8b400bf24b52632794041e3/microsoft-365/security/defender-endpoint/switch-to-microsoft-defender-setup.md?plain=1#L101
https://web.archive.org/web/20210926064024/https://docs.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-antivirus-on-windows-server?view=o365-worldwide
call:
function: DisableFeature
parameters:
featureName: Windows-Defender # TODO: Access is denied.
-
name: Disable "Windows-Defender-Gui" feature
docs: |-
Windows 10 > 22H2: Feature does not exist
https://github.com/MicrosoftDocs/microsoft-365-docs/blob/b3c6d838ad6c823c5e541a556761ab5faa240bfd/microsoft-365/security/defender-endpoint/enable-update-mdav-to-latest-ws.md?plain=1#L76
https://github.com/Ariantor/microsoft-365-docs/blob/cba6edb3bf31d3d9f86ef2271dbd78133dcd8118/microsoft-365/security/defender-endpoint/switch-to-mde-phase-2.md?plain=1#L84
https://github.com/isabella232/microsoft-365-docs-pr.it-IT/blob/d3a567aa6c70fd7ef8b400bf24b52632794041e3/microsoft-365/security/defender-endpoint/switch-to-microsoft-defender-setup.md?plain=1#L101
https://web.archive.org/web/20210926064024/https://docs.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-antivirus-on-windows-server?view=o365-worldwide
call:
function: DisableFeature
parameters:
featureName: Windows-Defender-Gui # TODO: Access is denied.
-
name: Disable "Windows-Defender-Features" feature
docs: |-
Windows 10 > 22H2: Feature does not exist
https://github.com/MicrosoftDocs/microsoft-365-docs/blob/b3c6d838ad6c823c5e541a556761ab5faa240bfd/microsoft-365/security/defender-endpoint/enable-update-mdav-to-latest-ws.md?plain=1#L76
https://github.com/Ariantor/microsoft-365-docs/blob/cba6edb3bf31d3d9f86ef2271dbd78133dcd8118/microsoft-365/security/defender-endpoint/switch-to-mde-phase-2.md?plain=1#L84
https://github.com/isabella232/microsoft-365-docs-pr.it-IT/blob/d3a567aa6c70fd7ef8b400bf24b52632794041e3/microsoft-365/security/defender-endpoint/switch-to-microsoft-defender-setup.md?plain=1#L101
call:
function: DisableFeature
parameters:
featureName: Windows-Defender-Features # TODO: Access is denied.
-
name: Disable "Application Guard" feature
docs: |-
FeatureName : Windows-Defender-ApplicationGuard
DisplayName : Microsoft Defender Application Guard
Description : Offers a secure container for internet browsing
RestartRequired : Possible
State : Disabled
CustomProperties :
call:
function: DisableFeature
parameters:
featureName: Windows-Defender-ApplicationGuard # Get-WindowsOptionalFeature -Online -FeatureName "Windows-Defender-ApplicationGuard"
# TODO: Should disable on revert too
-
name: Disable "Windows-Defender-Default-Definitions" feature
docs: |-
FeatureName : Windows-Defender-Default-Definitions
DisplayName :
Description :
RestartRequired : Possible
State : Enabled
CustomProperties :
call:
function: DisableFeature
parameters:
featureName: Windows-Defender-Default-Definitions # Get-WindowsOptionalFeature -Online -FeatureName "Windows-Defender-Default-Definitions"
-
name: Disable Antimalware Scan Interface (AMSI)
docs: https://learn.microsoft.com/en-us/windows/win32/amsi/antimalware-scan-interface-portal
code: Remove-Item -Path "HKLM:\SOFTWARE\Microsoft\AMSI\Providers\{2781761E-28E0-4109-99FE-B9D127C57AFE}" -Recurse
revertCode: New-Item -Path "HKLM:\SOFTWARE\Microsoft\AMSI\Providers" -Name "{2781761E-28E0-4109-99FE-B9D127C57AFE}" -ErrorAction Ignore | Out-Null
-
name: DisallowExploitProtectionOverride # TODO: Fix
code: HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows Defender Security Center\App and Browser protection!DisallowExploitProtectionOverride
- -
category: Disable SmartScreen category: Disable SmartScreen
docs: docs: |-
- https://en.wikipedia.org/wiki/Microsoft_SmartScreen Microsoft Defender SmartScreen helps safeguard users from phishing, malware websites, and potentially harmful downloads [2].
- https://web.archive.org/web/20240314131452/https://learn.microsoft.com/en-us/windows/security/operating-system-security/virus-and-threat-protection/microsoft-defender-smartscreen/ It assesses webpage safety by analyzing behavior and comparing sites to a list of known malicious ones [2].
For downloads, it cross-references with lists of known malicious software and frequently downloaded files, issuing warnings for potential threats.
SmartScreen is also known as "Windows SmartScreen" [1], "Windows Defender SmartScreen", "Microsoft Defender SmartScreen" [2]
and "SmartScreen Filter" [1].
[1]: https://en.wikipedia.org/wiki/Microsoft_SmartScreen
[2]: https://docs.microsoft.com/en-us/windows/security/threat-protection/microsoft-defender-smartscreen/microsoft-defender-smartscreen-overview
children: children:
-
name: Kill SmartScreen process
recommend: strict
docs: |-
This script stops execution of `smartscreen.exe` which is the main process for SmartScreen [1] [2] [3].
`smartscreen.exe` is located in the `%WinDir%\System32` [1] [2] folder.
[1]: https://www.howtogeek.com/320711/what-is-smartscreen-and-why-is-it-running-on-my-pc/
[2]: https://www.file.net/process/smartscreen.exe.html
[3]: https://strontic.github.io/xcyclopedia/library/smartscreen.exe-B75FA41284409A6134BF824BEAE59B4E.html
call:
function: KillProcess
parameters:
processName: smartscreen.exe
processStartPath: '%WinDir%\System32\smartscreen.exe'
- -
category: Disable SmartScreen for apps and files category: Disable SmartScreen for apps and files
docs: https://docs.microsoft.com/en-us/windows/security/threat-protection/microsoft-defender-smartscreen/microsoft-defender-smartscreen-overview
children: children:
- -
name: Disable SmartScreen for apps and files name: Disable SmartScreen for apps and files
@@ -15397,18 +15731,80 @@ actions:
valueName: PreventOverride valueName: PreventOverride
dwordData: "0" dwordData: "0"
- -
name: Disable SmartScreen in Internet Explorer name: Disable outdated SmartScreen in Internet Explorer
docs: https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.InternetExplorer::IZ_Policy_Phishing_9 docs: |-
This script disables SmartScreen in outdated Internet Explorer.
SmartScreen is also known as *Phishing Filter* [1].
Internet Explorer 11 is retired and out-of-support [1].
Internet Explorer 11 desktop application has been permanently disabled through
a Microsoft Edge update on certain versions of Windows [1].
This script only applies to old versions of Windows with Internet Explorer.
This script configures `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\<ZoneNumber>\2301` registry key [1].
Different zones have different meaning [1]:
| Security Zone | Meaning |
| ------------- | ------- |
| `0` | My Computer |
| `1` | Local Intranet Zone |
| `2` | Trusted sites Zone |
| `3` | Internet Zone |
| `4` | Restricted Sites Zone |
This script configures `2301` setting which configures whether to use Phishing Filter [1] to disable
SmartScreen.
[1]: https://web.archive.org/web/20240709095151/https://learn.microsoft.com/en-us/troubleshoot/developer/browsers/security-privacy/ie-security-zones-registry-entries "IE security zones registry entries for advanced users - Browsers | Microsoft Learn | learn.microsoft.com"
- https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.InternetExplorer::IZ_Policy_Phishing_9
- https://www.stigviewer.com/stig/microsoft_internet_explorer_11/2018-06-08/finding/V-64719
call: call:
function: SetRegistryValue -
parameters: function: SetRegistryValue
keyPath: HKLM\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\0 parameters:
valueName: '2301' keyPath: HKLM\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\0
dataType: REG_DWORD valueName: '2301'
data: '1' dataType: REG_DWORD
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2) data: '1'
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
-
function: SetRegistryValue
parameters:
keyPath: HKLM\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\1
valueName: '2301'
dataType: REG_DWORD
data: '1'
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
-
function: SetRegistryValue
parameters:
keyPath: HKLM\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\2
valueName: '2301'
dataType: REG_DWORD
data: '1'
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
-
function: SetRegistryValue
parameters:
keyPath: HKLM\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\3
valueName: '2301'
dataType: REG_DWORD
data: '1'
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
-
function: SetRegistryValue
parameters:
keyPath: HKLM\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\4
valueName: '2301'
dataType: REG_DWORD
data: '1'
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
- -
category: Disable SmartScreen for Windows Store apps category: Disable SmartScreen for Windows Store apps
docs: https://docs.microsoft.com/en-us/windows/security/threat-protection/microsoft-defender-smartscreen/microsoft-defender-smartscreen-overview
children: children:
- -
name: Disable SmartScreen's "App Install Control" feature name: Disable SmartScreen's "App Install Control" feature
@@ -15646,7 +16042,7 @@ actions:
function: DisableService function: DisableService
parameters: parameters:
serviceName: wuauserv # Check: (Get-Service -Name 'wuauserv').StartType serviceName: wuauserv # Check: (Get-Service -Name 'wuauserv').StartType
defaultStartupMode: Manual # Allowed values: Automatic | Manual defaultStartupMode: Manual # Allowed values: Automatic | Manual | Boot
- -
name: Disable "Update Orchestrator Service" (`UsoSvc`) name: Disable "Update Orchestrator Service" (`UsoSvc`)
docs: |- docs: |-
@@ -15681,7 +16077,7 @@ actions:
function: DisableService function: DisableService
parameters: parameters:
serviceName: UsoSvc # Check: (Get-Service -Name 'UsoSvc').StartType serviceName: UsoSvc # Check: (Get-Service -Name 'UsoSvc').StartType
defaultStartupMode: Automatic # Allowed values: Automatic | Manual defaultStartupMode: Automatic # Allowed values: Automatic | Manual | Boot
- -
name: Disable "Windows Update Medic Service" (`WaaSMedicSvc`) name: Disable "Windows Update Medic Service" (`WaaSMedicSvc`)
docs: |- docs: |-

View File

@@ -1,6 +1,6 @@
import { OperatingSystem } from './OperatingSystem'; import { OperatingSystem } from './OperatingSystem';
import type { IApplication } from './IApplication'; import type { IApplication } from './IApplication';
import type { ICategoryCollection } from './ICategoryCollection'; import type { ICategoryCollection } from './Collection/ICategoryCollection';
import type { ProjectDetails } from './Project/ProjectDetails'; import type { ProjectDetails } from './Project/ProjectDetails';
export class Application implements IApplication { export class Application implements IApplication {

View File

@@ -1,10 +1,10 @@
import { getEnumValues, assertInRange } from '@/application/Common/Enum'; import { getEnumValues, assertInRange } from '@/application/Common/Enum';
import { RecommendationLevel } from './Executables/Script/RecommendationLevel'; import { RecommendationLevel } from '../Executables/Script/RecommendationLevel';
import { OperatingSystem } from './OperatingSystem'; import { OperatingSystem } from '../OperatingSystem';
import type { IEntity } from '../infrastructure/Entity/IEntity'; import type { ExecutableId, Identifiable } from '../Executables/Identifiable';
import type { Category } from './Executables/Category/Category'; import type { Category } from '../Executables/Category/Category';
import type { Script } from './Executables/Script/Script'; import type { Script } from '../Executables/Script/Script';
import type { IScriptingDefinition } from './IScriptingDefinition'; import type { IScriptingDefinition } from '../IScriptingDefinition';
import type { ICategoryCollection } from './ICategoryCollection'; import type { ICategoryCollection } from './ICategoryCollection';
export class CategoryCollection implements ICategoryCollection { export class CategoryCollection implements ICategoryCollection {
@@ -30,14 +30,14 @@ export class CategoryCollection implements ICategoryCollection {
this.queryable = makeQueryable(this.actions); this.queryable = makeQueryable(this.actions);
assertInRange(this.os, OperatingSystem); assertInRange(this.os, OperatingSystem);
ensureValid(this.queryable); ensureValid(this.queryable);
ensureNoDuplicates(this.queryable.allCategories); ensureNoDuplicateIds(this.queryable.allCategories);
ensureNoDuplicates(this.queryable.allScripts); ensureNoDuplicateIds(this.queryable.allScripts);
} }
public getCategory(categoryId: number): Category { public getCategory(executableId: ExecutableId): Category {
const category = this.queryable.allCategories.find((c) => c.id === categoryId); const category = this.queryable.allCategories.find((c) => c.executableId === executableId);
if (!category) { if (!category) {
throw new Error(`Missing category with ID: "${categoryId}"`); throw new Error(`Missing category with ID: "${executableId}"`);
} }
return category; return category;
} }
@@ -48,10 +48,10 @@ export class CategoryCollection implements ICategoryCollection {
return scripts ?? []; return scripts ?? [];
} }
public getScript(scriptId: string): Script { public getScript(executableId: string): Script {
const script = this.queryable.allScripts.find((s) => s.id === scriptId); const script = this.queryable.allScripts.find((s) => s.executableId === executableId);
if (!script) { if (!script) {
throw new Error(`missing script: ${scriptId}`); throw new Error(`missing script: ${executableId}`);
} }
return script; return script;
} }
@@ -65,17 +65,14 @@ export class CategoryCollection implements ICategoryCollection {
} }
} }
function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) { function ensureNoDuplicateIds(executables: ReadonlyArray<Identifiable>) { // TODO: Unit test this
const isUniqueInArray = (id: TKey, index: number, array: readonly TKey[]) => array const duplicatedIds = executables
.findIndex((otherId) => otherId === id) !== index; .map((e) => e.executableId)
const duplicatedIds = entities .filter((id, index, array) => array.findIndex((otherId) => otherId === id) !== index);
.map((entity) => entity.id)
.filter((id, index, array) => !isUniqueInArray(id, index, array))
.filter(isUniqueInArray);
if (duplicatedIds.length > 0) { if (duplicatedIds.length > 0) {
const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(','); const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(',');
throw new Error( throw new Error(
`Duplicate entities are detected with following id(s): ${duplicatedIdsText}`, `Duplicate executables are detected with following id(s): ${duplicatedIdsText}`,
); );
} }
} }
@@ -120,7 +117,7 @@ function flattenApplication(
): [Category[], Script[]] { ): [Category[], Script[]] {
const [subCategories, subScripts] = (categories || []) const [subCategories, subScripts] = (categories || [])
// Parse children // Parse children
.map((category) => flattenApplication(category.subCategories)) .map((category) => flattenApplication(category.subcategories))
// Flatten results // Flatten results
.reduce(([previousCategories, previousScripts], [currentCategories, currentScripts]) => { .reduce(([previousCategories, previousScripts], [currentCategories, currentScripts]) => {
return [ return [

View File

@@ -3,6 +3,7 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel'; import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
import type { Script } from '@/domain/Executables/Script/Script'; import type { Script } from '@/domain/Executables/Script/Script';
import type { Category } from '@/domain/Executables/Category/Category'; import type { Category } from '@/domain/Executables/Category/Category';
import type { ExecutableId } from '../Executables/Identifiable';
export interface ICategoryCollection { export interface ICategoryCollection {
readonly scripting: IScriptingDefinition; readonly scripting: IScriptingDefinition;
@@ -12,8 +13,8 @@ export interface ICategoryCollection {
readonly actions: ReadonlyArray<Category>; readonly actions: ReadonlyArray<Category>;
getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<Script>; getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<Script>;
getCategory(categoryId: number): Category; getCategory(categoryId: ExecutableId): Category;
getScript(scriptId: string): Script; getScript(scriptId: ExecutableId): Script;
getAllScripts(): ReadonlyArray<Script>; getAllScripts(): ReadonlyArray<Script>;
getAllCategories(): ReadonlyArray<Category>; getAllCategories(): ReadonlyArray<Category>;
} }

View File

@@ -1,10 +1,9 @@
import type { Script } from '../Script/Script'; import type { Script } from '../Script/Script';
import type { Executable } from '../Executable'; import type { Executable } from '../Executable';
export interface Category extends Executable<number> { export interface Category extends Executable {
readonly id: number;
readonly name: string; readonly name: string;
readonly subCategories: ReadonlyArray<Category>; readonly subcategories: ReadonlyArray<Category>;
readonly scripts: ReadonlyArray<Script>; readonly scripts: ReadonlyArray<Script>;
includes(script: Script): boolean; includes(script: Script): boolean;
getAllScriptsRecursively(): ReadonlyArray<Script>; getAllScriptsRecursively(): ReadonlyArray<Script>;

View File

@@ -1,29 +1,51 @@
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity'; import type { Category } from '@/domain/Executables/Category/Category';
import type { Category } from './Category'; import type { Script } from '@/domain/Executables/Script/Script';
import type { Script } from '../Script/Script'; import type { ExecutableId } from '../Identifiable';
export class CollectionCategory extends BaseEntity<number> implements Category { export type CategoryFactory = (
private allSubScripts?: ReadonlyArray<Script> = undefined; parameters: CategoryInitParameters,
) => Category;
export interface CategoryInitParameters {
readonly executableId: ExecutableId;
readonly name: string;
readonly docs: ReadonlyArray<string>;
readonly subcategories: ReadonlyArray<Category>;
readonly scripts: ReadonlyArray<Script>;
}
export const createCategory: CategoryFactory = (
parameters,
) => {
return new CollectionCategory(parameters);
};
class CollectionCategory implements Category {
public readonly executableId: ExecutableId;
public readonly name: string; public readonly name: string;
public readonly docs: ReadonlyArray<string>; public readonly docs: ReadonlyArray<string>;
public readonly subCategories: ReadonlyArray<Category>; public readonly subcategories: ReadonlyArray<Category>;
public readonly scripts: ReadonlyArray<Script>; public readonly scripts: ReadonlyArray<Script>;
private allSubScripts?: ReadonlyArray<Script> = undefined;
constructor(parameters: CategoryInitParameters) { constructor(parameters: CategoryInitParameters) {
super(parameters.id);
validateParameters(parameters); validateParameters(parameters);
this.executableId = parameters.executableId;
this.name = parameters.name; this.name = parameters.name;
this.docs = parameters.docs; this.docs = parameters.docs;
this.subCategories = parameters.subcategories; this.subcategories = parameters.subcategories;
this.scripts = parameters.scripts; this.scripts = parameters.scripts;
} }
public includes(script: Script): boolean { public includes(script: Script): boolean {
return this.getAllScriptsRecursively().some((childScript) => childScript.id === script.id); return this
.getAllScriptsRecursively()
.some((childScript) => childScript.executableId === script.executableId);
} }
public getAllScriptsRecursively(): readonly Script[] { public getAllScriptsRecursively(): readonly Script[] {
@@ -34,22 +56,17 @@ export class CollectionCategory extends BaseEntity<number> implements Category {
} }
} }
export interface CategoryInitParameters {
readonly id: number;
readonly name: string;
readonly docs: ReadonlyArray<string>;
readonly subcategories: ReadonlyArray<Category>;
readonly scripts: ReadonlyArray<Script>;
}
function parseScriptsRecursively(category: Category): ReadonlyArray<Script> { function parseScriptsRecursively(category: Category): ReadonlyArray<Script> {
return [ return [
...category.scripts, ...category.scripts,
...category.subCategories.flatMap((c) => c.getAllScriptsRecursively()), ...category.subcategories.flatMap((c) => c.getAllScriptsRecursively()),
]; ];
} }
function validateParameters(parameters: CategoryInitParameters) { function validateParameters(parameters: CategoryInitParameters) {
if (!parameters.executableId) {
throw new Error('missing ID');
}
if (!parameters.name) { if (!parameters.name) {
throw new Error('missing name'); throw new Error('missing name');
} }

View File

@@ -1,6 +1,6 @@
import type { IEntity } from '@/infrastructure/Entity/IEntity';
import type { Documentable } from './Documentable'; import type { Documentable } from './Documentable';
import type { Identifiable } from './Identifiable';
export interface Executable<TExecutableKey> export interface Executable
extends Documentable, IEntity<TExecutableKey> { extends Documentable, Identifiable {
} }

View File

@@ -0,0 +1,5 @@
export type ExecutableId = string;
export interface Identifiable {
readonly executableId: ExecutableId;
}

View File

@@ -3,7 +3,7 @@ import type { Executable } from '../Executable';
import type { Documentable } from '../Documentable'; import type { Documentable } from '../Documentable';
import type { ScriptCode } from './Code/ScriptCode'; import type { ScriptCode } from './Code/ScriptCode';
export interface Script extends Executable<string>, Documentable { export interface Script extends Executable, Documentable {
readonly name: string; readonly name: string;
readonly level?: RecommendationLevel; readonly level?: RecommendationLevel;
readonly code: ScriptCode; readonly code: ScriptCode;

View File

@@ -1,9 +1,26 @@
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import { RecommendationLevel } from './RecommendationLevel'; import { RecommendationLevel } from './RecommendationLevel';
import type { Script } from './Script';
import type { ScriptCode } from './Code/ScriptCode'; import type { ScriptCode } from './Code/ScriptCode';
import type { Script } from './Script';
export interface ScriptInitParameters {
readonly executableId: string;
readonly name: string;
readonly code: ScriptCode;
readonly docs: ReadonlyArray<string>;
readonly level?: RecommendationLevel;
}
export type ScriptFactory = (
parameters: ScriptInitParameters,
) => Script;
export const createScript: ScriptFactory = (parameters) => {
return new CollectionScript(parameters);
};
class CollectionScript implements Script {
public readonly executableId: string;
export class CollectionScript extends BaseEntity<string> implements Script {
public readonly name: string; public readonly name: string;
public readonly code: ScriptCode; public readonly code: ScriptCode;
@@ -13,7 +30,7 @@ export class CollectionScript extends BaseEntity<string> implements Script {
public readonly level?: RecommendationLevel; public readonly level?: RecommendationLevel;
constructor(parameters: ScriptInitParameters) { constructor(parameters: ScriptInitParameters) {
super(parameters.name); this.executableId = parameters.executableId;
this.name = parameters.name; this.name = parameters.name;
this.code = parameters.code; this.code = parameters.code;
this.docs = parameters.docs; this.docs = parameters.docs;
@@ -26,13 +43,6 @@ export class CollectionScript extends BaseEntity<string> implements Script {
} }
} }
export interface ScriptInitParameters {
readonly name: string;
readonly code: ScriptCode;
readonly docs: ReadonlyArray<string>;
readonly level?: RecommendationLevel;
}
function validateLevel(level?: RecommendationLevel) { function validateLevel(level?: RecommendationLevel) {
if (level !== undefined && !(level in RecommendationLevel)) { if (level !== undefined && !(level in RecommendationLevel)) {
throw new Error(`invalid level: ${level}`); throw new Error(`invalid level: ${level}`);

View File

@@ -1,4 +1,4 @@
import type { ICategoryCollection } from './ICategoryCollection'; import type { ICategoryCollection } from './Collection/ICategoryCollection';
import type { ProjectDetails } from './Project/ProjectDetails'; import type { ProjectDetails } from './Project/ProjectDetails';
import type { OperatingSystem } from './OperatingSystem'; import type { OperatingSystem } from './OperatingSystem';

View File

@@ -1,14 +0,0 @@
import { isNumber } from '@/TypeHelpers';
import type { IEntity } from './IEntity';
export abstract class BaseEntity<TId> implements IEntity<TId> {
protected constructor(public id: TId) {
if (!isNumber(id) && !id) {
throw new Error('Id cannot be null or empty');
}
}
public equals(otherId: TId): boolean {
return this.id === otherId;
}
}

View File

@@ -1,5 +0,0 @@
/** Aggregate root */
export interface IEntity<TId> {
id: TId;
equals(other: TId): boolean;
}

View File

@@ -1,12 +1,15 @@
import type { Repository } from '../../application/Repository/Repository'; import type { Repository } from '../../application/Repository/Repository';
import type { IEntity } from '../Entity/IEntity'; import type { RepositoryEntity } from '../../application/Repository/RepositoryEntity';
export class InMemoryRepository<TKey, TEntity extends IEntity<TKey>> export class InMemoryRepository<TEntity extends RepositoryEntity>
implements Repository<TKey, TEntity> { implements Repository<TEntity> {
private readonly items: TEntity[]; private readonly items: TEntity[];
constructor(items?: TEntity[]) { constructor(items?: readonly TEntity[]) {
this.items = items ?? new Array<TEntity>(); this.items = new Array<TEntity>();
if (items) {
this.items.push(...items);
}
} }
public get length(): number { public get length(): number {
@@ -17,7 +20,7 @@ implements Repository<TKey, TEntity> {
return predicate ? this.items.filter(predicate) : this.items; return predicate ? this.items.filter(predicate) : this.items;
} }
public getById(id: TKey): TEntity { public getById(id: string): TEntity {
const items = this.getItems((entity) => entity.id === id); const items = this.getItems((entity) => entity.id === id);
if (!items.length) { if (!items.length) {
throw new Error(`missing item: ${id}`); throw new Error(`missing item: ${id}`);
@@ -39,7 +42,7 @@ implements Repository<TKey, TEntity> {
this.items.push(item); this.items.push(item);
} }
public removeItem(id: TKey): void { public removeItem(id: string): void {
const index = this.items.findIndex((item) => item.id === id); const index = this.items.findIndex((item) => item.id === id);
if (index === -1) { if (index === -1) {
throw new Error(`Cannot remove (id: ${id}) as it does not exist`); throw new Error(`Cannot remove (id: ${id}) as it does not exist`);
@@ -47,7 +50,7 @@ implements Repository<TKey, TEntity> {
this.items.splice(index, 1); this.items.splice(index, 1);
} }
public exists(id: TKey): boolean { public exists(id: string): boolean {
const index = this.items.findIndex((item) => item.id === id); const index = this.items.findIndex((item) => item.id === id);
return index !== -1; return index !== -1;
} }

View File

@@ -1,7 +1,7 @@
import type { Script } from '@/domain/Executables/Script/Script'; import type { Script } from '@/domain/Executables/Script/Script';
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel'; import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
import { scrambledEqual } from '@/application/Common/Array'; import { scrambledEqual } from '@/application/Common/Array';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import type { ReadonlyScriptSelection, ScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection'; import type { ReadonlyScriptSelection, ScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection';
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import { RecommendationStatusType } from './RecommendationStatusType'; import { RecommendationStatusType } from './RecommendationStatusType';
@@ -99,6 +99,6 @@ function areAllSelected(
if (expectedScripts.length < selectedScriptIds.length) { if (expectedScripts.length < selectedScriptIds.length) {
return false; return false;
} }
const expectedScriptIds = expectedScripts.map((script) => script.id); const expectedScriptIds = expectedScripts.map((script) => script.executableId);
return scrambledEqual(selectedScriptIds, expectedScriptIds); return scrambledEqual(selectedScriptIds, expectedScriptIds);
} }

View File

@@ -90,7 +90,7 @@ import {
} from 'vue'; } from 'vue';
import { injectKey } from '@/presentation/injectionSymbols'; import { injectKey } from '@/presentation/injectionSymbols';
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue'; import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import MenuOptionList from '../MenuOptionList.vue'; import MenuOptionList from '../MenuOptionList.vue';
import MenuOptionListItem from '../MenuOptionListItem.vue'; import MenuOptionListItem from '../MenuOptionListItem.vue';
import { setCurrentRecommendationStatus, getCurrentRecommendationStatus } from './RecommendationStatusHandler'; import { setCurrentRecommendationStatus, getCurrentRecommendationStatus } from './RecommendationStatusHandler';
@@ -142,3 +142,4 @@ export default defineComponent({
}, },
}); });
</script> </script>
@/domain/Collection/ICategoryCollection

View File

@@ -44,6 +44,7 @@ import {
} from 'vue'; } from 'vue';
import { injectKey } from '@/presentation/injectionSymbols'; import { injectKey } from '@/presentation/injectionSymbols';
import SizeObserver from '@/presentation/components/Shared/SizeObserver.vue'; import SizeObserver from '@/presentation/components/Shared/SizeObserver.vue';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import { hasDirective } from './NonCollapsingDirective'; import { hasDirective } from './NonCollapsingDirective';
import CardListItem from './CardListItem.vue'; import CardListItem from './CardListItem.vue';
@@ -58,12 +59,12 @@ export default defineComponent({
const width = ref<number | undefined>(); const width = ref<number | undefined>();
const categoryIds = computed<readonly number[]>( const categoryIds = computed<readonly ExecutableId[]>(
() => currentState.value.collection.actions.map((category) => category.id), () => currentState.value.collection.actions.map((category) => category.executableId),
); );
const activeCategoryId = ref<number | undefined>(undefined); const activeCategoryId = ref<ExecutableId | undefined>(undefined);
function onSelected(categoryId: number, isExpanded: boolean) { function onSelected(categoryId: ExecutableId, isExpanded: boolean) {
activeCategoryId.value = isExpanded ? categoryId : undefined; activeCategoryId.value = isExpanded ? categoryId : undefined;
} }

View File

@@ -56,12 +56,14 @@
<script lang="ts"> <script lang="ts">
import { import {
defineComponent, computed, shallowRef, defineComponent, computed, shallowRef,
type PropType,
} from 'vue'; } from 'vue';
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue'; import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
import FlatButton from '@/presentation/components/Shared/FlatButton.vue'; import FlatButton from '@/presentation/components/Shared/FlatButton.vue';
import { injectKey } from '@/presentation/injectionSymbols'; import { injectKey } from '@/presentation/injectionSymbols';
import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue'; import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue';
import { sleep } from '@/infrastructure/Threading/AsyncSleep'; import { sleep } from '@/infrastructure/Threading/AsyncSleep';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import CardSelectionIndicator from './CardSelectionIndicator.vue'; import CardSelectionIndicator from './CardSelectionIndicator.vue';
import CardExpandTransition from './CardExpandTransition.vue'; import CardExpandTransition from './CardExpandTransition.vue';
import CardExpansionArrow from './CardExpansionArrow.vue'; import CardExpansionArrow from './CardExpansionArrow.vue';
@@ -77,11 +79,11 @@ export default defineComponent({
}, },
props: { props: {
categoryId: { categoryId: {
type: Number, type: String as PropType<ExecutableId>,
required: true, required: true,
}, },
activeCategoryId: { activeCategoryId: {
type: Number, type: String as PropType<ExecutableId>,
default: undefined, default: undefined,
}, },
}, },

View File

@@ -12,11 +12,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, computed } from 'vue'; import { defineComponent, computed, type PropType } from 'vue';
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue'; import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
import { injectKey } from '@/presentation/injectionSymbols'; import { injectKey } from '@/presentation/injectionSymbols';
import type { Category } from '@/domain/Executables/Category/Category'; import type { Category } from '@/domain/Executables/Category/Category';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
export default defineComponent({ export default defineComponent({
components: { components: {
@@ -24,7 +25,7 @@ export default defineComponent({
}, },
props: { props: {
categoryId: { categoryId: {
type: Number, type: String as PropType<ExecutableId>,
required: true, required: true,
}, },
}, },
@@ -60,3 +61,4 @@ export default defineComponent({
font-size: $font-size-absolute-normal; font-size: $font-size-absolute-normal;
} }
</style> </style>
@/domain/Collection/ICategoryCollection

View File

@@ -1,10 +1,12 @@
import type { ExecutableId } from '@/domain/Executables/Identifiable';
export enum NodeType { export enum NodeType {
Script, Script,
Category, Category,
} }
export interface NodeMetadata { export interface NodeMetadata {
readonly id: string; readonly id: ExecutableId;
readonly text: string; readonly text: string;
readonly isReversible: boolean; readonly isReversible: boolean;
readonly docs: ReadonlyArray<string>; readonly docs: ReadonlyArray<string>;

View File

@@ -12,7 +12,7 @@ import {
} from 'vue'; } from 'vue';
import { injectKey } from '@/presentation/injectionSymbols'; import { injectKey } from '@/presentation/injectionSymbols';
import type { NodeMetadata } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata'; import type { NodeMetadata } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { getReverter } from './Reverter/ReverterFactory'; import { getReverter } from './Reverter/ReverterFactory';
import ToggleSwitch from './ToggleSwitch.vue'; import ToggleSwitch from './ToggleSwitch.vue';
import type { Reverter } from './Reverter/Reverter'; import type { Reverter } from './Reverter/Reverter';
@@ -64,3 +64,4 @@ export default defineComponent({
}, },
}); });
</script> </script>
@/domain/Collection/ICategoryCollection

View File

@@ -1,17 +1,18 @@
import type { UserSelection } from '@/application/Context/State/Selection/UserSelection'; import type { UserSelection } from '@/application/Context/State/Selection/UserSelection';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import { getCategoryId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter'; import type { ExecutableId } from '@/domain/Executables/Identifiable';
import { createExecutableIdFromNodeId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter';
import { ScriptReverter } from './ScriptReverter'; import { ScriptReverter } from './ScriptReverter';
import type { Reverter } from './Reverter'; import type { Reverter } from './Reverter';
export class CategoryReverter implements Reverter { export class CategoryReverter implements Reverter {
private readonly categoryId: number; private readonly categoryId: ExecutableId;
private readonly scriptReverters: ReadonlyArray<ScriptReverter>; private readonly scriptReverters: ReadonlyArray<ScriptReverter>;
constructor(nodeId: string, collection: ICategoryCollection) { constructor(nodeId: string, collection: ICategoryCollection) {
this.categoryId = getCategoryId(nodeId); this.categoryId = createExecutableIdFromNodeId(nodeId);
this.scriptReverters = createScriptReverters(this.categoryId, collection); this.scriptReverters = createScriptReverters(this.categoryId, collection);
} }
@@ -37,12 +38,12 @@ export class CategoryReverter implements Reverter {
} }
function createScriptReverters( function createScriptReverters(
categoryId: number, categoryId: ExecutableId,
collection: ICategoryCollection, collection: ICategoryCollection,
): ScriptReverter[] { ): ScriptReverter[] {
const category = collection.getCategory(categoryId); const category = collection.getCategory(categoryId);
const scripts = category const scripts = category
.getAllScriptsRecursively() .getAllScriptsRecursively()
.filter((script) => script.canRevert()); .filter((script) => script.canRevert());
return scripts.map((script) => new ScriptReverter(script.id)); return scripts.map((script) => new ScriptReverter(script.executableId));
} }

View File

@@ -1,4 +1,4 @@
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { type NodeMetadata, NodeType } from '../NodeMetadata'; import { type NodeMetadata, NodeType } from '../NodeMetadata';
import { ScriptReverter } from './ScriptReverter'; import { ScriptReverter } from './ScriptReverter';
import { CategoryReverter } from './CategoryReverter'; import { CategoryReverter } from './CategoryReverter';

View File

@@ -1,13 +1,13 @@
import type { UserSelection } from '@/application/Context/State/Selection/UserSelection'; import type { UserSelection } from '@/application/Context/State/Selection/UserSelection';
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import { getScriptId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter'; import { createExecutableIdFromNodeId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter';
import type { Reverter } from './Reverter'; import type { Reverter } from './Reverter';
export class ScriptReverter implements Reverter { export class ScriptReverter implements Reverter {
private readonly scriptId: string; private readonly scriptId: string;
constructor(nodeId: string) { constructor(nodeId: string) {
this.scriptId = getScriptId(nodeId); this.scriptId = createExecutableIdFromNodeId(nodeId);
} }
public getState(selectedScripts: ReadonlyArray<SelectedScript>): boolean { public getState(selectedScripts: ReadonlyArray<SelectedScript>): boolean {

View File

@@ -24,8 +24,9 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, toRef } from 'vue'; import { defineComponent, toRef, type PropType } from 'vue';
import { injectKey } from '@/presentation/injectionSymbols'; import { injectKey } from '@/presentation/injectionSymbols';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import TreeView from './TreeView/TreeView.vue'; import TreeView from './TreeView/TreeView.vue';
import NodeContent from './NodeContent/NodeContent.vue'; import NodeContent from './NodeContent/NodeContent.vue';
import { useTreeViewFilterEvent } from './TreeViewAdapter/UseTreeViewFilterEvent'; import { useTreeViewFilterEvent } from './TreeViewAdapter/UseTreeViewFilterEvent';
@@ -41,7 +42,7 @@ export default defineComponent({
}, },
props: { props: {
categoryId: { categoryId: {
type: [Number], type: String as PropType<ExecutableId>,
default: undefined, default: undefined,
}, },
hasTopPadding: { hasTopPadding: {

View File

@@ -1,5 +1,7 @@
export type TreeInputNodeDataId = string;
export interface TreeInputNodeData { export interface TreeInputNodeData {
readonly id: string; readonly id: TreeInputNodeDataId;
readonly children?: readonly TreeInputNodeData[]; readonly children?: readonly TreeInputNodeData[];
readonly parent?: TreeInputNodeData | null; readonly parent?: TreeInputNodeData | null;
readonly data?: object; readonly data?: object;

View File

@@ -56,7 +56,7 @@ import { useNodeState } from './UseNodeState';
import LeafTreeNode from './LeafTreeNode.vue'; import LeafTreeNode from './LeafTreeNode.vue';
import InteractableNode from './InteractableNode.vue'; import InteractableNode from './InteractableNode.vue';
import type { TreeRoot } from '../TreeRoot/TreeRoot'; import type { TreeRoot } from '../TreeRoot/TreeRoot';
import type { TreeNode } from './TreeNode'; import type { TreeNode, TreeNodeId } from './TreeNode';
import type { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStrategy'; import type { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStrategy';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
@@ -69,7 +69,7 @@ export default defineComponent({
}, },
props: { props: {
nodeId: { nodeId: {
type: String, type: String as PropType<TreeNodeId>,
required: true, required: true,
}, },
treeRoot: { treeRoot: {

View File

@@ -18,13 +18,13 @@ import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
import { useNodeState } from './UseNodeState'; import { useNodeState } from './UseNodeState';
import { useKeyboardInteractionState } from './UseKeyboardInteractionState'; import { useKeyboardInteractionState } from './UseKeyboardInteractionState';
import type { TreeRoot } from '../TreeRoot/TreeRoot'; import type { TreeRoot } from '../TreeRoot/TreeRoot';
import type { TreeNode } from './TreeNode'; import type { TreeNode, TreeNodeId } from './TreeNode';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
export default defineComponent({ export default defineComponent({
props: { props: {
nodeId: { nodeId: {
type: String, type: String as PropType<TreeNodeId>,
required: true, required: true,
}, },
treeRoot: { treeRoot: {

View File

@@ -28,7 +28,7 @@ import { defineComponent, computed, toRef } from 'vue';
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes'; import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
import NodeCheckbox from './NodeCheckbox.vue'; import NodeCheckbox from './NodeCheckbox.vue';
import InteractableNode from './InteractableNode.vue'; import InteractableNode from './InteractableNode.vue';
import type { TreeNode } from './TreeNode'; import type { TreeNode, TreeNodeId } from './TreeNode';
import type { TreeRoot } from '../TreeRoot/TreeRoot'; import type { TreeRoot } from '../TreeRoot/TreeRoot';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
@@ -39,7 +39,7 @@ export default defineComponent({
}, },
props: { props: {
nodeId: { nodeId: {
type: String, type: String as PropType<TreeNodeId>,
required: true, required: true,
}, },
treeRoot: { treeRoot: {

View File

@@ -14,13 +14,13 @@ import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
import { useNodeState } from './UseNodeState'; import { useNodeState } from './UseNodeState';
import { TreeNodeCheckState } from './State/CheckState'; import { TreeNodeCheckState } from './State/CheckState';
import type { TreeRoot } from '../TreeRoot/TreeRoot'; import type { TreeRoot } from '../TreeRoot/TreeRoot';
import type { TreeNode } from './TreeNode'; import type { TreeNode, TreeNodeId } from './TreeNode';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
export default defineComponent({ export default defineComponent({
props: { props: {
nodeId: { nodeId: {
type: String, type: String as PropType<TreeNodeId>,
required: true, required: true,
}, },
treeRoot: { treeRoot: {

View File

@@ -1,8 +1,10 @@
import type { HierarchyAccess, HierarchyReader } from './Hierarchy/HierarchyAccess'; import type { HierarchyAccess, HierarchyReader } from './Hierarchy/HierarchyAccess';
import type { TreeNodeStateAccess, TreeNodeStateReader } from './State/StateAccess'; import type { TreeNodeStateAccess, TreeNodeStateReader } from './State/StateAccess';
export type TreeNodeId = string;
export interface ReadOnlyTreeNode { export interface ReadOnlyTreeNode {
readonly id: string; readonly id: TreeNodeId;
readonly state: TreeNodeStateReader; readonly state: TreeNodeStateReader;
readonly hierarchy: HierarchyReader; readonly hierarchy: HierarchyReader;
readonly metadata?: object; readonly metadata?: object;

View File

@@ -1,6 +1,6 @@
import { TreeNodeHierarchy } from './Hierarchy/TreeNodeHierarchy'; import { TreeNodeHierarchy } from './Hierarchy/TreeNodeHierarchy';
import { TreeNodeState } from './State/TreeNodeState'; import { TreeNodeState } from './State/TreeNodeState';
import type { TreeNode } from './TreeNode'; import type { TreeNode, TreeNodeId } from './TreeNode';
import type { TreeNodeStateAccess } from './State/StateAccess'; import type { TreeNodeStateAccess } from './State/StateAccess';
import type { HierarchyAccess } from './Hierarchy/HierarchyAccess'; import type { HierarchyAccess } from './Hierarchy/HierarchyAccess';
@@ -9,7 +9,7 @@ export class TreeNodeManager implements TreeNode {
public readonly hierarchy: HierarchyAccess; public readonly hierarchy: HierarchyAccess;
constructor(public readonly id: string, public readonly metadata?: object) { constructor(public readonly id: TreeNodeId, public readonly metadata?: object) {
if (!id) { if (!id) {
throw new Error('missing id'); throw new Error('missing id');
} }

View File

@@ -22,6 +22,7 @@ import {
} from 'vue'; } from 'vue';
import HierarchicalTreeNode from '../Node/HierarchicalTreeNode.vue'; import HierarchicalTreeNode from '../Node/HierarchicalTreeNode.vue';
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes'; import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
import { type TreeNodeId } from '../Node/TreeNode';
import type { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStrategy'; import type { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStrategy';
import type { TreeRoot } from './TreeRoot'; import type { TreeRoot } from './TreeRoot';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
@@ -43,7 +44,7 @@ export default defineComponent({
setup(props) { setup(props) {
const { nodes } = useCurrentTreeNodes(toRef(props, 'treeRoot')); const { nodes } = useCurrentTreeNodes(toRef(props, 'treeRoot'));
const renderedNodeIds = computed<string[]>(() => { const renderedNodeIds = computed<TreeNodeId[]>(() => {
return nodes return nodes
.value .value
.rootNodes .rootNodes

View File

@@ -26,6 +26,7 @@ import { useLeafNodeCheckedStateUpdater } from './UseLeafNodeCheckedStateUpdater
import { useAutoUpdateParentCheckState } from './UseAutoUpdateParentCheckState'; import { useAutoUpdateParentCheckState } from './UseAutoUpdateParentCheckState';
import { useAutoUpdateChildrenCheckState } from './UseAutoUpdateChildrenCheckState'; import { useAutoUpdateChildrenCheckState } from './UseAutoUpdateChildrenCheckState';
import { useGradualNodeRendering, type NodeRenderingControl } from './Rendering/UseGradualNodeRendering'; import { useGradualNodeRendering, type NodeRenderingControl } from './Rendering/UseGradualNodeRendering';
import { type TreeNodeId } from './Node/TreeNode';
import type { TreeNodeStateChangedEmittedEvent } from './Bindings/TreeNodeStateChangedEmittedEvent'; import type { TreeNodeStateChangedEmittedEvent } from './Bindings/TreeNodeStateChangedEmittedEvent';
import type { TreeInputNodeData } from './Bindings/TreeInputNodeData'; import type { TreeInputNodeData } from './Bindings/TreeInputNodeData';
import type { TreeViewFilterEvent } from './Bindings/TreeInputFilterEvent'; import type { TreeViewFilterEvent } from './Bindings/TreeInputFilterEvent';
@@ -45,7 +46,7 @@ export default defineComponent({
default: () => undefined, default: () => undefined,
}, },
selectedLeafNodeIds: { selectedLeafNodeIds: {
type: Array as PropType<ReadonlyArray<string>>, type: Array as PropType<ReadonlyArray<TreeNodeId>>,
default: () => [], default: () => [],
}, },
}, },

View File

@@ -1,14 +1,17 @@
import type { Category } from '@/domain/Executables/Category/Category'; import type { Category } from '@/domain/Executables/Category/Category';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import type { Script } from '@/domain/Executables/Script/Script'; import type { Script } from '@/domain/Executables/Script/Script';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import type { Executable } from '@/domain/Executables/Executable';
import { type NodeMetadata, NodeType } from '../NodeContent/NodeMetadata'; import { type NodeMetadata, NodeType } from '../NodeContent/NodeMetadata';
import type { TreeNodeId } from '../TreeView/Node/TreeNode';
export function parseAllCategories(collection: ICategoryCollection): NodeMetadata[] { export function parseAllCategories(collection: ICategoryCollection): NodeMetadata[] {
return createCategoryNodes(collection.actions); return createCategoryNodes(collection.actions);
} }
export function parseSingleCategory( export function parseSingleCategory(
categoryId: number, categoryId: ExecutableId,
collection: ICategoryCollection, collection: ICategoryCollection,
): NodeMetadata[] { ): NodeMetadata[] {
const category = collection.getCategory(categoryId); const category = collection.getCategory(categoryId);
@@ -16,27 +19,19 @@ export function parseSingleCategory(
return tree; return tree;
} }
export function getScriptNodeId(script: Script): string { export function createNodeIdForExecutable(executable: Executable): TreeNodeId {
return script.id; return executable.executableId;
} }
export function getScriptId(nodeId: string): string { export function createExecutableIdFromNodeId(nodeId: TreeNodeId): ExecutableId {
return nodeId; return nodeId;
} }
export function getCategoryId(nodeId: string): number {
return +nodeId;
}
export function getCategoryNodeId(category: Category): string {
return `${category.id}`;
}
function parseCategoryRecursively( function parseCategoryRecursively(
parentCategory: Category, parentCategory: Category,
): NodeMetadata[] { ): NodeMetadata[] {
return [ return [
...createCategoryNodes(parentCategory.subCategories), ...createCategoryNodes(parentCategory.subcategories),
...createScriptNodes(parentCategory.scripts), ...createScriptNodes(parentCategory.scripts),
]; ];
} }
@@ -57,7 +52,7 @@ function convertCategoryToNode(
children: readonly NodeMetadata[], children: readonly NodeMetadata[],
): NodeMetadata { ): NodeMetadata {
return { return {
id: getCategoryNodeId(category), id: createNodeIdForExecutable(category),
type: NodeType.Category, type: NodeType.Category,
text: category.name, text: category.name,
children, children,
@@ -68,7 +63,7 @@ function convertCategoryToNode(
function convertScriptToNode(script: Script): NodeMetadata { function convertScriptToNode(script: Script): NodeMetadata {
return { return {
id: getScriptNodeId(script), id: createNodeIdForExecutable(script),
type: NodeType.Script, type: NodeType.Script,
text: script.name, text: script.name,
children: [], children: [],

View File

@@ -0,0 +1,3 @@
export function UseExecutableFromTreeNodeId(treeNodeId: string) {
}

View File

@@ -2,20 +2,21 @@ import {
computed, shallowReadonly, computed, shallowReadonly,
} from 'vue'; } from 'vue';
import type { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState'; import type { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState';
import { getScriptNodeId } from './CategoryNodeMetadataConverter'; import { createNodeIdForExecutable } from './CategoryNodeMetadataConverter';
import type { TreeNodeId } from '../TreeView/Node/TreeNode';
export function useSelectedScriptNodeIds( export function useSelectedScriptNodeIds(
useSelectionStateHook: ReturnType<typeof useUserSelectionState>, useSelectionStateHook: ReturnType<typeof useUserSelectionState>,
scriptNodeIdParser = getScriptNodeId, convertToNodeId = createNodeIdForExecutable,
) { ) {
const { currentSelection } = useSelectionStateHook; const { currentSelection } = useSelectionStateHook;
const selectedNodeIds = computed<readonly string[]>(() => { const selectedNodeIds = computed<readonly TreeNodeId[]>(() => {
return currentSelection return currentSelection
.value .value
.scripts .scripts
.selectedScripts .selectedScripts
.map((selected) => scriptNodeIdParser(selected.script)); .map((selected) => convertToNodeId(selected.script));
}); });
return { return {

View File

@@ -1,16 +1,15 @@
import { import {
type Ref, shallowReadonly, shallowRef, type Ref, shallowReadonly, shallowRef,
} from 'vue'; } from 'vue';
import type { Script } from '@/domain/Executables/Script/Script';
import type { Category } from '@/domain/Executables/Category/Category'; import type { Category } from '@/domain/Executables/Category/Category';
import { injectKey } from '@/presentation/injectionSymbols'; import { injectKey } from '@/presentation/injectionSymbols';
import type { ReadonlyFilterContext } from '@/application/Context/State/Filter/FilterContext'; import type { ReadonlyFilterContext } from '@/application/Context/State/Filter/FilterContext';
import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult'; import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import type { Executable } from '@/domain/Executables/Executable';
import { type TreeViewFilterEvent, createFilterRemovedEvent, createFilterTriggeredEvent } from '../TreeView/Bindings/TreeInputFilterEvent'; import { type TreeViewFilterEvent, createFilterRemovedEvent, createFilterTriggeredEvent } from '../TreeView/Bindings/TreeInputFilterEvent';
import { getNodeMetadata } from './TreeNodeMetadataConverter'; import { createExecutableIdFromNodeId } from './CategoryNodeMetadataConverter';
import { getCategoryNodeId, getScriptNodeId } from './CategoryNodeMetadataConverter'; import type { ReadOnlyTreeNode, TreeNodeId } from '../TreeView/Node/TreeNode';
import type { NodeMetadata } from '../NodeContent/NodeMetadata';
import type { ReadOnlyTreeNode } from '../TreeView/Node/TreeNode';
type TreeNodeFilterResultPredicate = ( type TreeNodeFilterResultPredicate = (
node: ReadOnlyTreeNode, node: ReadOnlyTreeNode,
@@ -24,7 +23,7 @@ export function useTreeViewFilterEvent() {
const latestFilterEvent = shallowRef<TreeViewFilterEvent | undefined>(undefined); const latestFilterEvent = shallowRef<TreeViewFilterEvent | undefined>(undefined);
const treeNodePredicate: TreeNodeFilterResultPredicate = (node, filterResult) => filterMatches( const treeNodePredicate: TreeNodeFilterResultPredicate = (node, filterResult) => filterMatches(
getNodeMetadata(node), node.id,
filterResult, filterResult,
); );
@@ -71,15 +70,17 @@ function createFilterEvent(
); );
} }
function filterMatches(node: NodeMetadata, filter: FilterResult): boolean { function filterMatches(nodeId: TreeNodeId, filter: FilterResult): boolean {
return containsScript(node, filter.scriptMatches) const executableId = createExecutableIdFromNodeId(nodeId);
|| containsCategory(node, filter.categoryMatches); return containsExecutable(executableId, filter.scriptMatches)
|| containsExecutable(executableId, filter.categoryMatches);
} }
function containsScript(expected: NodeMetadata, scripts: readonly Script[]) { function containsExecutable(
return scripts.some((existing: Script) => expected.id === getScriptNodeId(existing)); expectedId: ExecutableId,
} executables: readonly Executable[],
): boolean {
function containsCategory(expected: NodeMetadata, categories: readonly Category[]) { return executables.some(
return categories.some((existing: Category) => expected.id === getCategoryNodeId(existing)); (existing: Category) => existing.executableId === expectedId,
);
} }

View File

@@ -1,15 +1,16 @@
import { import {
type Ref, computed, shallowReadonly, type Ref, computed, shallowReadonly,
} from 'vue'; } from 'vue';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { injectKey } from '@/presentation/injectionSymbols'; import { injectKey } from '@/presentation/injectionSymbols';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import { parseSingleCategory, parseAllCategories } from './CategoryNodeMetadataConverter'; import { parseSingleCategory, parseAllCategories } from './CategoryNodeMetadataConverter';
import { convertToNodeInput } from './TreeNodeMetadataConverter'; import { convertToNodeInput } from './TreeNodeMetadataConverter';
import type { TreeInputNodeData } from '../TreeView/Bindings/TreeInputNodeData'; import type { TreeInputNodeData } from '../TreeView/Bindings/TreeInputNodeData';
import type { NodeMetadata } from '../NodeContent/NodeMetadata'; import type { NodeMetadata } from '../NodeContent/NodeMetadata';
export function useTreeViewNodeInput( export function useTreeViewNodeInput(
categoryIdRef: Readonly<Ref<number | undefined>>, categoryIdRef: Readonly<Ref<ExecutableId | undefined>>,
parser: CategoryNodeParser = { parser: CategoryNodeParser = {
parseSingle: parseSingleCategory, parseSingle: parseSingleCategory,
parseAll: parseAllCategories, parseAll: parseAllCategories,
@@ -30,7 +31,7 @@ export function useTreeViewNodeInput(
} }
function parseNodes( function parseNodes(
categoryId: number | undefined, categoryId: ExecutableId | undefined,
categoryCollection: ICategoryCollection, categoryCollection: ICategoryCollection,
parser: CategoryNodeParser, parser: CategoryNodeParser,
): NodeMetadata[] { ): NodeMetadata[] {

16
test.ps1 Normal file
View File

@@ -0,0 +1,16 @@
# (Command only avalable in Windows Server)
# name: Uninstall Windows Defender from Windows Server
# docs: https://web.archive.org/web/20210926064024/https://docs.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-antivirus-on-windows-server?view=o365-worldwide
# Do
Uninstall-WindowsFeature -Name Windows-Defender
Uninstall-WindowsFeature -Name Windows-Defender-GUI
# Revert:
Install-WindowsFeature -Name Windows-Defender
Install-WindowsFeature -Name Windows-Defender-GUI

View File

@@ -225,7 +225,7 @@ function collectAllDocumentedExecutables(): DocumentedExecutable[] {
]); ]);
const allDocumentedExecutables = allExecutables.filter((e) => e.docs.length > 0); const allDocumentedExecutables = allExecutables.filter((e) => e.docs.length > 0);
return allDocumentedExecutables.map((executable): DocumentedExecutable => ({ return allDocumentedExecutables.map((executable): DocumentedExecutable => ({
executableLabel: `${executable.name} (${executable.id})`, executableLabel: `${executable.name} (${executable.executableId})`,
docs: executable.docs.join('\n'), docs: executable.docs.join('\n'),
})); }));
} }

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { buildContext } from '@/application/Context/ApplicationContextFactory'; import { buildContext } from '@/application/Context/ApplicationContextFactory';
import type { IApplicationFactory } from '@/application/IApplicationFactory'; import type { IApplicationFactory } from '@/application/IApplicationFactory';
import type { IApplication } from '@/domain/IApplication'; import type { IApplication } from '@/domain/IApplication';

View File

@@ -4,7 +4,7 @@ import { CategoryCollectionState } from '@/application/Context/State/CategoryCol
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub'; import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub'; import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { ApplicationCodeStub } from '@tests/unit/shared/Stubs/ApplicationCodeStub'; import { ApplicationCodeStub } from '@tests/unit/shared/Stubs/ApplicationCodeStub';
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { expectExists } from '@tests/shared/Assertions/ExpectExists'; import { expectExists } from '@tests/shared/Assertions/ExpectExists';

View File

@@ -8,7 +8,7 @@ import { FilterResultStub } from '@tests/unit/shared/Stubs/FilterResultStub';
import { FilterStrategyStub } from '@tests/unit/shared/Stubs/FilterStrategyStub'; import { FilterStrategyStub } from '@tests/unit/shared/Stubs/FilterStrategyStub';
import type { FilterStrategy } from '@/application/Context/State/Filter/Strategy/FilterStrategy'; import type { FilterStrategy } from '@/application/Context/State/Filter/Strategy/FilterStrategy';
import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult'; import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
describe('AdaptiveFilterContext', () => { describe('AdaptiveFilterContext', () => {
describe('clearFilter', () => { describe('clearFilter', () => {

View File

@@ -48,7 +48,7 @@ describe('AppliedFilterResult', () => {
const expected = true; const expected = true;
const result = new ResultBuilder() const result = new ResultBuilder()
.withScriptMatches([]) .withScriptMatches([])
.withCategoryMatches([new CategoryStub(5)]) .withCategoryMatches([new CategoryStub('matched-category-id')])
.build(); .build();
// act // act
const actual = result.hasAnyMatches(); const actual = result.hasAnyMatches();
@@ -58,8 +58,8 @@ describe('AppliedFilterResult', () => {
// arrange // arrange
const expected = true; const expected = true;
const result = new ResultBuilder() const result = new ResultBuilder()
.withScriptMatches([new ScriptStub('id')]) .withScriptMatches([new ScriptStub('matched-script-id')])
.withCategoryMatches([new CategoryStub(5)]) .withCategoryMatches([new CategoryStub('matched-category-id')])
.build(); .build();
// act // act
const actual = result.hasAnyMatches(); const actual = result.hasAnyMatches();
@@ -69,9 +69,13 @@ describe('AppliedFilterResult', () => {
}); });
class ResultBuilder { class ResultBuilder {
private scriptMatches: readonly Script[] = [new ScriptStub('id')]; private scriptMatches: readonly Script[] = [
new ScriptStub(`[${ResultBuilder.name}]matched-script-id`),
];
private categoryMatches: readonly Category[] = [new CategoryStub(5)]; private categoryMatches: readonly Category[] = [
new CategoryStub(`[${ResultBuilder.name}]matched-category-id`),
];
private query: string = `[${ResultBuilder.name}]query`; private query: string = `[${ResultBuilder.name}]query`;

View File

@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub'; import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub'; import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import type { Category } from '@/domain/Executables/Category/Category'; import type { Category } from '@/domain/Executables/Category/Category';
import type { Script } from '@/domain/Executables/Script/Script'; import type { Script } from '@/domain/Executables/Script/Script';
import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult'; import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult';
@@ -37,7 +37,10 @@ describe('LinearFilterStrategy', () => {
// arrange // arrange
const matchingFilter = 'matching filter'; const matchingFilter = 'matching filter';
const collection = new CategoryCollectionStub() const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(2).withScript(createMatchingScript(matchingFilter))); .withAction(
new CategoryStub('parent-category-of-matching-script')
.withScript(createMatchingScript(matchingFilter)),
);
const strategy = new FilterStrategyTestBuilder() const strategy = new FilterStrategyTestBuilder()
.withFilter(matchingFilter) .withFilter(matchingFilter)
.withCollection(collection); .withCollection(collection);

View File

@@ -2,14 +2,13 @@ import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub'; import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
import type { ScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection'; import type { ScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { ScriptToCategorySelectionMapper } from '@/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper'; import { ScriptToCategorySelectionMapper } from '@/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper';
import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub'; import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub';
import type { CategorySelectionChange } from '@/application/Context/State/Selection/Category/CategorySelectionChange'; import type { CategorySelectionChange } from '@/application/Context/State/Selection/Category/CategorySelectionChange';
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from '@/application/Context/State/Selection/Script/ScriptSelectionChange'; import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from '@/application/Context/State/Selection/Script/ScriptSelectionChange';
import { expectExists } from '@tests/shared/Assertions/ExpectExists'; import { expectExists } from '@tests/shared/Assertions/ExpectExists';
import type { Category } from '@/domain/Executables/Category/Category'; import type { ExecutableId } from '@/domain/Executables/Identifiable';
import type { Script } from '@/domain/Executables/Script/Script';
describe('ScriptToCategorySelectionMapper', () => { describe('ScriptToCategorySelectionMapper', () => {
describe('areAllScriptsSelected', () => { describe('areAllScriptsSelected', () => {
@@ -65,18 +64,18 @@ describe('ScriptToCategorySelectionMapper', () => {
readonly description: string; readonly description: string;
readonly changes: readonly CategorySelectionChange[]; readonly changes: readonly CategorySelectionChange[];
readonly categories: ReadonlyArray<{ readonly categories: ReadonlyArray<{
readonly categoryId: Category['id'], readonly categoryId: ExecutableId,
readonly scriptIds: readonly Script['id'][], readonly scriptIds: readonly ExecutableId[],
}>; }>;
readonly expected: readonly ScriptSelectionChange[], readonly expected: readonly ScriptSelectionChange[],
}> = [ }> = [
{ {
description: 'single script: select without revert', description: 'single script: select without revert',
categories: [ categories: [
{ categoryId: 1, scriptIds: ['single-script'] }, { categoryId: 'category-1', scriptIds: ['single-script'] },
], ],
changes: [ changes: [
{ categoryId: 1, newStatus: { isSelected: true, isReverted: false } }, { categoryId: 'category-1', newStatus: { isSelected: true, isReverted: false } },
], ],
expected: [ expected: [
{ scriptId: 'single-script', newStatus: { isSelected: true, isReverted: false } }, { scriptId: 'single-script', newStatus: { isSelected: true, isReverted: false } },
@@ -85,12 +84,12 @@ describe('ScriptToCategorySelectionMapper', () => {
{ {
description: 'multiple scripts: select without revert', description: 'multiple scripts: select without revert',
categories: [ categories: [
{ categoryId: 1, scriptIds: ['script1-cat1', 'script2-cat1'] }, { categoryId: 'category-1', scriptIds: ['script1-cat1', 'script2-cat1'] },
{ categoryId: 2, scriptIds: ['script3-cat2'] }, { categoryId: 'category-2', scriptIds: ['script3-cat2'] },
], ],
changes: [ changes: [
{ categoryId: 1, newStatus: { isSelected: true, isReverted: false } }, { categoryId: 'category-1', newStatus: { isSelected: true, isReverted: false } },
{ categoryId: 2, newStatus: { isSelected: true, isReverted: false } }, { categoryId: 'category-2', newStatus: { isSelected: true, isReverted: false } },
], ],
expected: [ expected: [
{ scriptId: 'script1-cat1', newStatus: { isSelected: true, isReverted: false } }, { scriptId: 'script1-cat1', newStatus: { isSelected: true, isReverted: false } },
@@ -101,10 +100,10 @@ describe('ScriptToCategorySelectionMapper', () => {
{ {
description: 'single script: select with revert', description: 'single script: select with revert',
categories: [ categories: [
{ categoryId: 1, scriptIds: ['single-script'] }, { categoryId: 'category-1', scriptIds: ['single-script'] },
], ],
changes: [ changes: [
{ categoryId: 1, newStatus: { isSelected: true, isReverted: true } }, { categoryId: 'category-1', newStatus: { isSelected: true, isReverted: true } },
], ],
expected: [ expected: [
{ scriptId: 'single-script', newStatus: { isSelected: true, isReverted: true } }, { scriptId: 'single-script', newStatus: { isSelected: true, isReverted: true } },
@@ -113,14 +112,14 @@ describe('ScriptToCategorySelectionMapper', () => {
{ {
description: 'multiple scripts: select with revert', description: 'multiple scripts: select with revert',
categories: [ categories: [
{ categoryId: 1, scriptIds: ['script-1-cat-1'] }, { categoryId: 'category-1', scriptIds: ['script-1-cat-1'] },
{ categoryId: 2, scriptIds: ['script-2-cat-2'] }, { categoryId: 'category-2', scriptIds: ['script-2-cat-2'] },
{ categoryId: 3, scriptIds: ['script-3-cat-3'] }, { categoryId: 'category-3', scriptIds: ['script-3-cat-3'] },
], ],
changes: [ changes: [
{ categoryId: 1, newStatus: { isSelected: true, isReverted: true } }, { categoryId: 'category-1', newStatus: { isSelected: true, isReverted: true } },
{ categoryId: 2, newStatus: { isSelected: true, isReverted: true } }, { categoryId: 'category-2', newStatus: { isSelected: true, isReverted: true } },
{ categoryId: 3, newStatus: { isSelected: true, isReverted: true } }, { categoryId: 'category-3', newStatus: { isSelected: true, isReverted: true } },
], ],
expected: [ expected: [
{ scriptId: 'script-1-cat-1', newStatus: { isSelected: true, isReverted: true } }, { scriptId: 'script-1-cat-1', newStatus: { isSelected: true, isReverted: true } },
@@ -131,10 +130,10 @@ describe('ScriptToCategorySelectionMapper', () => {
{ {
description: 'single script: deselect', description: 'single script: deselect',
categories: [ categories: [
{ categoryId: 1, scriptIds: ['single-script'] }, { categoryId: 'category-1', scriptIds: ['single-script'] },
], ],
changes: [ changes: [
{ categoryId: 1, newStatus: { isSelected: false } }, { categoryId: 'category-1', newStatus: { isSelected: false } },
], ],
expected: [ expected: [
{ scriptId: 'single-script', newStatus: { isSelected: false } }, { scriptId: 'single-script', newStatus: { isSelected: false } },
@@ -143,12 +142,12 @@ describe('ScriptToCategorySelectionMapper', () => {
{ {
description: 'multiple scripts: deselect', description: 'multiple scripts: deselect',
categories: [ categories: [
{ categoryId: 1, scriptIds: ['script-1-cat1'] }, { categoryId: 'category-1', scriptIds: ['script-1-cat1'] },
{ categoryId: 2, scriptIds: ['script-2-cat2'] }, { categoryId: 'category-2', scriptIds: ['script-2-cat2'] },
], ],
changes: [ changes: [
{ categoryId: 1, newStatus: { isSelected: false } }, { categoryId: 'category-1', newStatus: { isSelected: false } },
{ categoryId: 2, newStatus: { isSelected: false } }, { categoryId: 'category-2', newStatus: { isSelected: false } },
], ],
expected: [ expected: [
{ scriptId: 'script-1-cat1', newStatus: { isSelected: false } }, { scriptId: 'script-1-cat1', newStatus: { isSelected: false } },
@@ -158,14 +157,14 @@ describe('ScriptToCategorySelectionMapper', () => {
{ {
description: 'mixed operations (select, revert, deselect)', description: 'mixed operations (select, revert, deselect)',
categories: [ categories: [
{ categoryId: 1, scriptIds: ['to-revert'] }, { categoryId: 'category-1', scriptIds: ['to-revert'] },
{ categoryId: 2, scriptIds: ['not-revert'] }, { categoryId: 'category-2', scriptIds: ['not-revert'] },
{ categoryId: 3, scriptIds: ['to-deselect'] }, { categoryId: 'category-3', scriptIds: ['to-deselect'] },
], ],
changes: [ changes: [
{ categoryId: 1, newStatus: { isSelected: true, isReverted: true } }, { categoryId: 'category-1', newStatus: { isSelected: true, isReverted: true } },
{ categoryId: 2, newStatus: { isSelected: true, isReverted: false } }, { categoryId: 'category-2', newStatus: { isSelected: true, isReverted: false } },
{ categoryId: 3, newStatus: { isSelected: false } }, { categoryId: 'category-3', newStatus: { isSelected: false } },
], ],
expected: [ expected: [
{ scriptId: 'to-revert', newStatus: { isSelected: true, isReverted: true } }, { scriptId: 'to-revert', newStatus: { isSelected: true, isReverted: true } },
@@ -176,12 +175,12 @@ describe('ScriptToCategorySelectionMapper', () => {
{ {
description: 'affecting selected categories only', description: 'affecting selected categories only',
categories: [ categories: [
{ categoryId: 1, scriptIds: ['relevant-1', 'relevant-2'] }, { categoryId: 'category-1', scriptIds: ['relevant-1', 'relevant-2'] },
{ categoryId: 2, scriptIds: ['not-relevant-1', 'not-relevant-2'] }, { categoryId: 'category-2', scriptIds: ['not-relevant-1', 'not-relevant-2'] },
{ categoryId: 3, scriptIds: ['not-relevant-3', 'not-relevant-4'] }, { categoryId: 'category-3', scriptIds: ['not-relevant-3', 'not-relevant-4'] },
], ],
changes: [ changes: [
{ categoryId: 1, newStatus: { isSelected: true, isReverted: true } }, { categoryId: 'category-1', newStatus: { isSelected: true, isReverted: true } },
], ],
expected: [ expected: [
{ scriptId: 'relevant-1', newStatus: { isSelected: true, isReverted: true } }, { scriptId: 'relevant-1', newStatus: { isSelected: true, isReverted: true } },
@@ -198,7 +197,7 @@ describe('ScriptToCategorySelectionMapper', () => {
const sut = new ScriptToCategorySelectionMapperBuilder() const sut = new ScriptToCategorySelectionMapperBuilder()
.withScriptSelection(scriptSelectionStub) .withScriptSelection(scriptSelectionStub)
.withCollection(new CategoryCollectionStub().withAction( .withCollection(new CategoryCollectionStub().withAction(
new CategoryStub(99) new CategoryStub('single-parent-category-action')
// Register scripts to test for nested items // Register scripts to test for nested items
.withAllScriptIdsRecursively(...categories.flatMap((c) => c.scriptIds)) .withAllScriptIdsRecursively(...categories.flatMap((c) => c.scriptIds))
.withCategories(...categories.map( .withCategories(...categories.map(
@@ -256,7 +255,7 @@ function setupTestWithPreselectedScripts(options: {
new ScriptStub('third-script'), new ScriptStub('third-script'),
]; ];
const preselectedScripts = options.preselect(allScripts); const preselectedScripts = options.preselect(allScripts);
const category = new CategoryStub(1) const category = new CategoryStub('single-parent-category-action')
.withAllScriptsRecursively(...allScripts); // Register scripts to test for nested items .withAllScriptsRecursively(...allScripts); // Register scripts to test for nested items
const collection = new CategoryCollectionStub().withAction(category); const collection = new CategoryCollectionStub().withAction(category);
const sut = new ScriptToCategorySelectionMapperBuilder() const sut = new ScriptToCategorySelectionMapperBuilder()

View File

@@ -4,7 +4,7 @@ import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub'; import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub'; import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import { BatchedDebounceStub } from '@tests/unit/shared/Stubs/BatchedDebounceStub'; import { BatchedDebounceStub } from '@tests/unit/shared/Stubs/BatchedDebounceStub';
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from '@/application/Context/State/Selection/Script/ScriptSelectionChange'; import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from '@/application/Context/State/Selection/Script/ScriptSelectionChange';
@@ -104,7 +104,7 @@ describe('DebouncedScriptSelection', () => {
const { scriptSelection, unselectedScripts } = setupTestWithPreselectedScripts({ const { scriptSelection, unselectedScripts } = setupTestWithPreselectedScripts({
preselect: (allScripts) => [allScripts[0]], preselect: (allScripts) => [allScripts[0]],
}); });
const scriptIdToCheck = unselectedScripts[0].id; const scriptIdToCheck = unselectedScripts[0].executableId;
// act // act
const actual = scriptSelection.isSelected(scriptIdToCheck); const actual = scriptSelection.isSelected(scriptIdToCheck);
// assert // assert
@@ -300,7 +300,7 @@ describe('DebouncedScriptSelection', () => {
preselect: (allScripts) => [allScripts[0], allScripts[1]] preselect: (allScripts) => [allScripts[0], allScripts[1]]
.map((s) => s.toSelectedScript()), .map((s) => s.toSelectedScript()),
getChanges: (allScripts) => [ getChanges: (allScripts) => [
{ scriptId: allScripts[2].id, newStatus: { isReverted: true, isSelected: true } }, { scriptId: allScripts[2].executableId, newStatus: { isReverted: true, isSelected: true } },
], ],
getExpectedFinalSelection: (allScripts) => [ getExpectedFinalSelection: (allScripts) => [
allScripts[0].toSelectedScript(), allScripts[0].toSelectedScript(),
@@ -313,7 +313,7 @@ describe('DebouncedScriptSelection', () => {
preselect: (allScripts) => [allScripts[0], allScripts[1]] preselect: (allScripts) => [allScripts[0], allScripts[1]]
.map((s) => s.toSelectedScript()), .map((s) => s.toSelectedScript()),
getChanges: (allScripts) => [ getChanges: (allScripts) => [
{ scriptId: allScripts[2].id, newStatus: { isReverted: false, isSelected: true } }, { scriptId: allScripts[2].executableId, newStatus: { isReverted: false, isSelected: true } },
], ],
getExpectedFinalSelection: (allScripts) => [ getExpectedFinalSelection: (allScripts) => [
allScripts[0].toSelectedScript(), allScripts[0].toSelectedScript(),
@@ -326,7 +326,7 @@ describe('DebouncedScriptSelection', () => {
preselect: (allScripts) => [allScripts[0], allScripts[1]] preselect: (allScripts) => [allScripts[0], allScripts[1]]
.map((s) => s.toSelectedScript()), .map((s) => s.toSelectedScript()),
getChanges: (allScripts) => [ getChanges: (allScripts) => [
{ scriptId: allScripts[0].id, newStatus: { isSelected: false } }, { scriptId: allScripts[0].executableId, newStatus: { isSelected: false } },
], ],
getExpectedFinalSelection: (allScripts) => [ getExpectedFinalSelection: (allScripts) => [
allScripts[1].toSelectedScript(), allScripts[1].toSelectedScript(),
@@ -339,7 +339,7 @@ describe('DebouncedScriptSelection', () => {
allScripts[1].toSelectedScript(), allScripts[1].toSelectedScript(),
], ],
getChanges: (allScripts) => [ getChanges: (allScripts) => [
{ scriptId: allScripts[0].id, newStatus: { isSelected: true, isReverted: true } }, { scriptId: allScripts[0].executableId, newStatus: { isSelected: true, isReverted: true } },
], ],
getExpectedFinalSelection: (allScripts) => [ getExpectedFinalSelection: (allScripts) => [
allScripts[0].toSelectedScript().withRevert(true), allScripts[0].toSelectedScript().withRevert(true),
@@ -353,7 +353,7 @@ describe('DebouncedScriptSelection', () => {
allScripts[1].toSelectedScript(), allScripts[1].toSelectedScript(),
], ],
getChanges: (allScripts) => [ getChanges: (allScripts) => [
{ scriptId: allScripts[0].id, newStatus: { isSelected: true, isReverted: false } }, { scriptId: allScripts[0].executableId, newStatus: { isSelected: true, isReverted: false } },
], ],
getExpectedFinalSelection: (allScripts) => [ getExpectedFinalSelection: (allScripts) => [
allScripts[0].toSelectedScript().withRevert(false), allScripts[0].toSelectedScript().withRevert(false),
@@ -367,9 +367,9 @@ describe('DebouncedScriptSelection', () => {
allScripts[2].toSelectedScript(), // remove allScripts[2].toSelectedScript(), // remove
], ],
getChanges: (allScripts) => [ getChanges: (allScripts) => [
{ scriptId: allScripts[0].id, newStatus: { isSelected: true, isReverted: false } }, { scriptId: allScripts[0].executableId, newStatus: { isSelected: true, isReverted: false } },
{ scriptId: allScripts[1].id, newStatus: { isSelected: true, isReverted: true } }, { scriptId: allScripts[1].executableId, newStatus: { isSelected: true, isReverted: true } },
{ scriptId: allScripts[2].id, newStatus: { isSelected: false } }, { scriptId: allScripts[2].executableId, newStatus: { isSelected: false } },
], ],
getExpectedFinalSelection: (allScripts) => [ getExpectedFinalSelection: (allScripts) => [
allScripts[0].toSelectedScript().withRevert(false), allScripts[0].toSelectedScript().withRevert(false),
@@ -408,7 +408,7 @@ describe('DebouncedScriptSelection', () => {
description: 'does not change selection for an already selected script', description: 'does not change selection for an already selected script',
preselect: (allScripts) => [allScripts[0].toSelectedScript().withRevert(true)], preselect: (allScripts) => [allScripts[0].toSelectedScript().withRevert(true)],
getChanges: (allScripts) => [ getChanges: (allScripts) => [
{ scriptId: allScripts[0].id, newStatus: { isReverted: true, isSelected: true } }, { scriptId: allScripts[0].executableId, newStatus: { isReverted: true, isSelected: true } },
], ],
}, },
{ {
@@ -416,15 +416,15 @@ describe('DebouncedScriptSelection', () => {
preselect: (allScripts) => [allScripts[0], allScripts[1]] preselect: (allScripts) => [allScripts[0], allScripts[1]]
.map((s) => s.toSelectedScript()), .map((s) => s.toSelectedScript()),
getChanges: (allScripts) => [ getChanges: (allScripts) => [
{ scriptId: allScripts[2].id, newStatus: { isSelected: false } }, { scriptId: allScripts[2].executableId, newStatus: { isSelected: false } },
], ],
}, },
{ {
description: 'handles no mutations for mixed unchanged operations', description: 'handles no mutations for mixed unchanged operations',
preselect: (allScripts) => [allScripts[0].toSelectedScript().withRevert(false)], preselect: (allScripts) => [allScripts[0].toSelectedScript().withRevert(false)],
getChanges: (allScripts) => [ getChanges: (allScripts) => [
{ scriptId: allScripts[0].id, newStatus: { isSelected: true, isReverted: false } }, { scriptId: allScripts[0].executableId, newStatus: { isSelected: true, isReverted: false } },
{ scriptId: allScripts[1].id, newStatus: { isSelected: false } }, { scriptId: allScripts[1].executableId, newStatus: { isSelected: false } },
], ],
}, },
]; ];
@@ -459,7 +459,7 @@ describe('DebouncedScriptSelection', () => {
.build(); .build();
const expectedCommand: ScriptSelectionChangeCommand = { const expectedCommand: ScriptSelectionChangeCommand = {
changes: [ changes: [
{ scriptId: script.id, newStatus: { isReverted: true, isSelected: true } }, { scriptId: script.executableId, newStatus: { isReverted: true, isSelected: true } },
], ],
}; };
// act // act
@@ -481,7 +481,7 @@ describe('DebouncedScriptSelection', () => {
// act // act
selection.processChanges({ selection.processChanges({
changes: [ changes: [
{ scriptId: script.id, newStatus: { isReverted: true, isSelected: true } }, { scriptId: script.executableId, newStatus: { isReverted: true, isSelected: true } },
], ],
}); });
// assert // assert
@@ -502,7 +502,7 @@ describe('DebouncedScriptSelection', () => {
// act // act
selection.processChanges({ selection.processChanges({
changes: [ changes: [
{ scriptId: script.id, newStatus: { isReverted: true, isSelected: true } }, { scriptId: script.executableId, newStatus: { isReverted: true, isSelected: true } },
], ],
}); });
debounceStub.execute(); debounceStub.execute();
@@ -525,7 +525,7 @@ describe('DebouncedScriptSelection', () => {
for (const script of scripts) { for (const script of scripts) {
selection.processChanges({ selection.processChanges({
changes: [ changes: [
{ scriptId: script.id, newStatus: { isReverted: true, isSelected: true } }, { scriptId: script.executableId, newStatus: { isReverted: true, isSelected: true } },
], ],
}); });
} }
@@ -572,7 +572,7 @@ function setupTestWithPreselectedScripts(options: {
return initialSelection; return initialSelection;
})(); })();
const unselectedScripts = allScripts.filter( const unselectedScripts = allScripts.filter(
(s) => !preselectedScripts.map((selected) => selected.id).includes(s.id), (s) => !preselectedScripts.map((selected) => selected.id).includes(s.executableId),
); );
const collection = createCollectionWithScripts(...allScripts); const collection = createCollectionWithScripts(...allScripts);
const scriptSelection = new DebouncedScriptSelectionBuilder() const scriptSelection = new DebouncedScriptSelectionBuilder()

View File

@@ -1,7 +1,7 @@
import { describe, it } from 'vitest'; import { describe, it } from 'vitest';
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import { UserSelectionFacade } from '@/application/Context/State/Selection/UserSelectionFacade'; import { UserSelectionFacade } from '@/application/Context/State/Selection/UserSelectionFacade';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub'; import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
import type { ScriptsFactory, CategoriesFactory } from '@/application/Context/State/Selection/UserSelectionFacade'; import type { ScriptsFactory, CategoriesFactory } from '@/application/Context/State/Selection/UserSelectionFacade';
import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub'; import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub';

View File

@@ -14,7 +14,7 @@ import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub'
import type { CategoryCollectionParser } from '@/application/Parser/CategoryCollectionParser'; import type { CategoryCollectionParser } from '@/application/Parser/CategoryCollectionParser';
import type { NonEmptyCollectionAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator'; import type { NonEmptyCollectionAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator';
import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub'; import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
describe('ApplicationParser', () => { describe('ApplicationParser', () => {
describe('parseApplication', () => { describe('parseApplication', () => {

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import type { CategoryData, ExecutableData } from '@/application/collections/'; import type { CategoryData, ExecutableData } from '@/application/collections/';
import { type CategoryFactory, parseCategory } from '@/application/Parser/Executable/CategoryParser'; import { parseCategory } from '@/application/Parser/Executable/CategoryParser';
import { type ScriptParser } from '@/application/Parser/Executable/Script/ScriptParser'; import { type ScriptParser } from '@/application/Parser/Executable/Script/ScriptParser';
import { type DocsParser } from '@/application/Parser/Executable/DocumentationParser'; import { type DocsParser } from '@/application/Parser/Executable/DocumentationParser';
import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub'; import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub';
@@ -20,14 +20,48 @@ import { ScriptParserStub } from '@tests/unit/shared/Stubs/ScriptParserStub';
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
import { indentText } from '@tests/shared/Text'; import { indentText } from '@tests/shared/Text';
import type { NonEmptyCollectionAssertion, ObjectAssertion } from '@/application/Parser/Common/TypeValidator'; import type { NonEmptyCollectionAssertion, ObjectAssertion } from '@/application/Parser/Common/TypeValidator';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import type { CategoryFactory } from '@/domain/Executables/Category/CategoryFactory';
import { itThrowsContextualError } from '../Common/ContextualErrorTester'; import { itThrowsContextualError } from '../Common/ContextualErrorTester';
import { itValidatesName, itValidatesType, itAsserts } from './Validation/ExecutableValidationTester'; import { itValidatesName, itValidatesType, itAsserts } from './Validation/ExecutableValidationTester';
import { generateDataValidationTestScenarios } from './Validation/DataValidationTestScenarioGenerator'; import { generateDataValidationTestScenarios } from './Validation/DataValidationTestScenarioGenerator';
describe('CategoryParser', () => { describe('CategoryParser', () => {
describe('parseCategory', () => { describe('parseCategory', () => {
describe('validation', () => { describe('id', () => {
describe('validates for name', () => { it('creates ID correctly', () => {
// arrange
const expectedId: ExecutableId = 'expected-id';
const categoryData = new CategoryDataStub()
.withName(expectedId);
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
// act
const actualScript = new TestContext()
.withData(categoryData)
.withCategoryFactory(categoryFactorySpy)
.parseCategory();
// assert
const actualId = getInitParameters(actualScript)?.executableId;
expect(actualId).to.equal(expectedId);
});
});
describe('name', () => {
it('parses name correctly', () => {
// arrange
const expectedName = 'test-expected-name';
const categoryData = new CategoryDataStub()
.withName(expectedName);
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
// act
const actualCategory = new TestContext()
.withData(categoryData)
.withCategoryFactory(categoryFactorySpy)
.parseCategory();
// assert
const actualName = getInitParameters(actualCategory)?.name;
expect(actualName).to.equal(expectedName);
});
describe('validates name', () => {
// arrange // arrange
const expectedName = 'expected category name to be validated'; const expectedName = 'expected category name to be validated';
const category = new CategoryDataStub() const category = new CategoryDataStub()
@@ -38,7 +72,7 @@ describe('CategoryParser', () => {
}; };
itValidatesName((validatorFactory) => { itValidatesName((validatorFactory) => {
// act // act
new TestBuilder() new TestContext()
.withData(category) .withData(category)
.withValidatorFactory(validatorFactory) .withValidatorFactory(validatorFactory)
.parseCategory(); .parseCategory();
@@ -49,7 +83,33 @@ describe('CategoryParser', () => {
}; };
}); });
}); });
describe('validates for unknown object', () => { });
describe('docs', () => {
it('parses docs correctly', () => {
// arrange
const url = 'https://privacy.sexy';
const categoryData = new CategoryDataStub()
.withDocs(url);
const parseDocs: DocsParser = (data) => {
return [
`parsed docs: ${JSON.stringify(data)}`,
];
};
const expectedDocs = parseDocs(categoryData);
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
// act
const actualCategory = new TestContext()
.withData(categoryData)
.withCategoryFactory(categoryFactorySpy)
.withDocsParser(parseDocs)
.parseCategory();
// assert
const actualDocs = getInitParameters(actualCategory)?.docs;
expect(actualDocs).to.deep.equal(expectedDocs);
});
});
describe('property validation', () => {
describe('validates for unknown executable', () => {
// arrange // arrange
const category = new CategoryDataStub(); const category = new CategoryDataStub();
const expectedContext: CategoryErrorContext = { const expectedContext: CategoryErrorContext = {
@@ -63,7 +123,7 @@ describe('CategoryParser', () => {
itValidatesType( itValidatesType(
(validatorFactory) => { (validatorFactory) => {
// act // act
new TestBuilder() new TestContext()
.withData(category) .withData(category)
.withValidatorFactory(validatorFactory) .withValidatorFactory(validatorFactory)
.parseCategory(); .parseCategory();
@@ -90,7 +150,7 @@ describe('CategoryParser', () => {
itValidatesType( itValidatesType(
(validatorFactory) => { (validatorFactory) => {
// act // act
new TestBuilder() new TestContext()
.withData(category) .withData(category)
.withValidatorFactory(validatorFactory) .withValidatorFactory(validatorFactory)
.parseCategory(); .parseCategory();
@@ -102,6 +162,8 @@ describe('CategoryParser', () => {
}, },
); );
}); });
});
describe('children', () => {
describe('validates children for non-empty collection', () => { describe('validates children for non-empty collection', () => {
// arrange // arrange
const category = new CategoryDataStub() const category = new CategoryDataStub()
@@ -117,7 +179,7 @@ describe('CategoryParser', () => {
itValidatesType( itValidatesType(
(validatorFactory) => { (validatorFactory) => {
// act // act
new TestBuilder() new TestContext()
.withData(category) .withData(category)
.withValidatorFactory(validatorFactory) .withValidatorFactory(validatorFactory)
.parseCategory(); .parseCategory();
@@ -167,7 +229,7 @@ describe('CategoryParser', () => {
parentCategory: parent, parentCategory: parent,
}; };
// act // act
new TestBuilder() new TestContext()
.withData(parent) .withData(parent)
.withValidatorFactory(validatorFactory) .withValidatorFactory(validatorFactory)
.parseCategory(); .parseCategory();
@@ -201,7 +263,7 @@ describe('CategoryParser', () => {
itValidatesType( itValidatesType(
(validatorFactory) => { (validatorFactory) => {
// act // act
new TestBuilder() new TestContext()
.withData(parent) .withData(parent)
.withValidatorFactory(validatorFactory) .withValidatorFactory(validatorFactory)
.parseCategory(); .parseCategory();
@@ -231,7 +293,7 @@ describe('CategoryParser', () => {
}; };
itValidatesName((validatorFactory) => { itValidatesName((validatorFactory) => {
// act // act
new TestBuilder() new TestContext()
.withData(parent) .withData(parent)
.withValidatorFactory(validatorFactory) .withValidatorFactory(validatorFactory)
.parseCategory(); .parseCategory();
@@ -243,178 +305,169 @@ describe('CategoryParser', () => {
}); });
}); });
}); });
}); describe('parses correct subscript', () => {
describe('rethrows exception if category factory fails', () => { it('parses single script correctly', () => {
// arrange // arrange
const givenData = new CategoryDataStub(); const expectedScript = new ScriptStub('expected script');
const expectedContextMessage = 'Failed to parse category.'; const scriptParser = new ScriptParserStub();
const expectedError = new Error(); const childScriptData = createScriptDataWithCode();
// act & assert const categoryData = new CategoryDataStub()
itThrowsContextualError({ .withChildren([childScriptData]);
throwingAction: (wrapError) => { scriptParser.setupParsedResultForData(childScriptData, expectedScript);
const validatorStub = new ExecutableValidatorStub(); const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
validatorStub.createContextualErrorMessage = (message) => message; // act
const factoryMock: CategoryFactory = () => { const actualCategory = new TestContext()
throw expectedError; .withData(categoryData)
}; .withScriptParser(scriptParser.get())
new TestBuilder() .withCategoryFactory(categoryFactorySpy)
.withCategoryFactory(factoryMock)
.withValidatorFactory(() => validatorStub)
.withErrorWrapper(wrapError)
.withData(givenData)
.parseCategory(); .parseCategory();
}, // assert
expectedWrappedError: expectedError, const actualScripts = getInitParameters(actualCategory)?.scripts;
expectedContextMessage, expectExists(actualScripts);
}); expect(actualScripts).to.have.lengthOf(1);
}); const actualScript = actualScripts[0];
it('parses docs correctly', () => { expect(actualScript).to.equal(expectedScript);
// arrange });
const url = 'https://privacy.sexy'; it('parses multiple scripts correctly', () => {
const categoryData = new CategoryDataStub() // arrange
.withDocs(url); const expectedScripts = [
const parseDocs: DocsParser = (data) => { new ScriptStub('expected-first-script'),
return [ new ScriptStub('expected-second-script'),
`parsed docs: ${JSON.stringify(data)}`, ];
]; const childrenData = [
}; createScriptDataWithCall(),
const expectedDocs = parseDocs(categoryData); createScriptDataWithCode(),
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy(); ];
// act const scriptParser = new ScriptParserStub();
const actualCategory = new TestBuilder() childrenData.forEach((_, index) => {
.withData(categoryData) scriptParser.setupParsedResultForData(childrenData[index], expectedScripts[index]);
.withCategoryFactory(categoryFactorySpy) });
.withDocsParser(parseDocs) const categoryData = new CategoryDataStub()
.parseCategory(); .withChildren(childrenData);
// assert const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
const actualDocs = getInitParameters(actualCategory)?.docs; // act
expect(actualDocs).to.deep.equal(expectedDocs); const actualCategory = new TestContext()
}); .withScriptParser(scriptParser.get())
describe('parses expected subscript', () => { .withData(categoryData)
it('parses single script correctly', () => { .withCategoryFactory(categoryFactorySpy)
// arrange .parseCategory();
const expectedScript = new ScriptStub('expected script'); // assert
const scriptParser = new ScriptParserStub(); const actualParsedScripts = getInitParameters(actualCategory)?.scripts;
const childScriptData = createScriptDataWithCode(); expectExists(actualParsedScripts);
const categoryData = new CategoryDataStub() expect(actualParsedScripts.length).to.equal(expectedScripts.length);
.withChildren([childScriptData]); expect(actualParsedScripts).to.have.members(expectedScripts);
scriptParser.setupParsedResultForData(childScriptData, expectedScript); });
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy(); it('parses all scripts with correct utilities', () => {
// act // arrange
const actualCategory = new TestBuilder() const expected = new CategoryCollectionSpecificUtilitiesStub();
.withData(categoryData) const scriptParser = new ScriptParserStub();
.withScriptParser(scriptParser.get()) const childrenData = [
.withCategoryFactory(categoryFactorySpy) createScriptDataWithCode(),
.parseCategory(); createScriptDataWithCode(),
// assert createScriptDataWithCode(),
const actualScripts = getInitParameters(actualCategory)?.scripts; ];
expectExists(actualScripts); const categoryData = new CategoryDataStub()
expect(actualScripts).to.have.lengthOf(1); .withChildren(childrenData);
const actualScript = actualScripts[0]; const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
expect(actualScript).to.equal(expectedScript); // act
}); const actualCategory = new TestContext()
it('parses multiple scripts correctly', () => { .withData(categoryData)
// arrange .withCollectionUtilities(expected)
const expectedScripts = [ .withScriptParser(scriptParser.get())
new ScriptStub('expected-first-script'), .withCategoryFactory(categoryFactorySpy)
new ScriptStub('expected-second-script'), .parseCategory();
]; // assert
const childrenData = [ const actualParsedScripts = getInitParameters(actualCategory)?.scripts;
createScriptDataWithCall(), expectExists(actualParsedScripts);
createScriptDataWithCode(), const actualUtilities = actualParsedScripts.map(
]; (s) => scriptParser.getParseParameters(s)[1],
const scriptParser = new ScriptParserStub(); );
childrenData.forEach((_, index) => { expect(
scriptParser.setupParsedResultForData(childrenData[index], expectedScripts[index]); actualUtilities.every(
(actual) => actual === expected,
),
formatAssertionMessage([
`Expected all elements to be ${JSON.stringify(expected)}`,
'All elements:',
indentText(JSON.stringify(actualUtilities)),
]),
).to.equal(true);
}); });
const categoryData = new CategoryDataStub()
.withChildren(childrenData);
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
// act
const actualCategory = new TestBuilder()
.withScriptParser(scriptParser.get())
.withData(categoryData)
.withCategoryFactory(categoryFactorySpy)
.parseCategory();
// assert
const actualParsedScripts = getInitParameters(actualCategory)?.scripts;
expectExists(actualParsedScripts);
expect(actualParsedScripts.length).to.equal(expectedScripts.length);
expect(actualParsedScripts).to.have.members(expectedScripts);
}); });
it('parses all scripts with correct utilities', () => { it('parses correct subcategories', () => {
// arrange // arrange
const expected = new CategoryCollectionSpecificUtilitiesStub(); const expectedChildCategory = new CategoryStub('expected-child-category');
const scriptParser = new ScriptParserStub(); const childCategoryData = new CategoryDataStub()
const childrenData = [ .withName('expected child category')
createScriptDataWithCode(), .withChildren([createScriptDataWithCode()]);
createScriptDataWithCode(),
createScriptDataWithCode(),
];
const categoryData = new CategoryDataStub() const categoryData = new CategoryDataStub()
.withChildren(childrenData); .withName('category name')
.withChildren([childCategoryData]);
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy(); const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
// act // act
const actualCategory = new TestBuilder() const actualCategory = new TestContext()
.withData(categoryData) .withData(categoryData)
.withCollectionUtilities(expected) .withCategoryFactory((parameters) => {
.withScriptParser(scriptParser.get()) if (parameters.name === childCategoryData.category) {
.withCategoryFactory(categoryFactorySpy) return expectedChildCategory;
}
return categoryFactorySpy(parameters);
})
.parseCategory(); .parseCategory();
// assert // assert
const actualParsedScripts = getInitParameters(actualCategory)?.scripts; const actualSubcategories = getInitParameters(actualCategory)?.subcategories;
expectExists(actualParsedScripts); expectExists(actualSubcategories);
const actualUtilities = actualParsedScripts.map( expect(actualSubcategories).to.have.lengthOf(1);
(s) => scriptParser.getParseParameters(s)[1], expect(actualSubcategories[0]).to.equal(expectedChildCategory);
);
expect(
actualUtilities.every(
(actual) => actual === expected,
),
formatAssertionMessage([
`Expected all elements to be ${JSON.stringify(expected)}`,
'All elements:',
indentText(JSON.stringify(actualUtilities)),
]),
).to.equal(true);
}); });
}); });
it('returns expected subcategories', () => { describe('category creation', () => {
// arrange it('creates category from the factory', () => {
const expectedChildCategory = new CategoryStub(33); // arrange
const childCategoryData = new CategoryDataStub() const expectedCategory = new CategoryStub('expected-category');
.withName('expected child category') const categoryFactory: CategoryFactory = () => expectedCategory;
.withChildren([createScriptDataWithCode()]); // act
const categoryData = new CategoryDataStub() const actualCategory = new TestContext()
.withName('category name') .withCategoryFactory(categoryFactory)
.withChildren([childCategoryData]); .parseCategory();
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy(); // assert
// act expect(actualCategory).to.equal(expectedCategory);
const actualCategory = new TestBuilder() });
.withData(categoryData) describe('rethrows exception if category factory fails', () => {
.withCategoryFactory((parameters) => { // arrange
if (parameters.name === childCategoryData.category) { const givenData = new CategoryDataStub();
return expectedChildCategory; const expectedContextMessage = 'Failed to parse category.';
} const expectedError = new Error();
return categoryFactorySpy(parameters); // act & assert
}) itThrowsContextualError({
.parseCategory(); throwingAction: (wrapError) => {
// assert const validatorStub = new ExecutableValidatorStub();
const actualSubcategories = getInitParameters(actualCategory)?.subcategories; validatorStub.createContextualErrorMessage = (message) => message;
expectExists(actualSubcategories); const factoryMock: CategoryFactory = () => {
expect(actualSubcategories).to.have.lengthOf(1); throw expectedError;
expect(actualSubcategories[0]).to.equal(expectedChildCategory); };
new TestContext()
.withCategoryFactory(factoryMock)
.withValidatorFactory(() => validatorStub)
.withErrorWrapper(wrapError)
.withData(givenData)
.parseCategory();
},
expectedWrappedError: expectedError,
expectedContextMessage,
});
});
}); });
}); });
}); });
class TestBuilder { class TestContext {
private data: CategoryData = new CategoryDataStub(); private data: CategoryData = new CategoryDataStub();
private collectionUtilities: private collectionUtilities:
CategoryCollectionSpecificUtilitiesStub = new CategoryCollectionSpecificUtilitiesStub(); CategoryCollectionSpecificUtilitiesStub = new CategoryCollectionSpecificUtilitiesStub();
private categoryFactory: CategoryFactory = () => new CategoryStub(33); private categoryFactory: CategoryFactory = createCategoryFactorySpy().categoryFactorySpy;
private errorWrapper: ErrorWithContextWrapper = new ErrorWrapperStub().get(); private errorWrapper: ErrorWithContextWrapper = new ErrorWrapperStub().get();

View File

@@ -29,53 +29,206 @@ import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/C
import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub'; import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub';
import type { CategoryCollectionSpecificUtilities } from '@/application/Parser/Executable/CategoryCollectionSpecificUtilities'; import type { CategoryCollectionSpecificUtilities } from '@/application/Parser/Executable/CategoryCollectionSpecificUtilities';
import type { ObjectAssertion } from '@/application/Parser/Common/TypeValidator'; import type { ObjectAssertion } from '@/application/Parser/Common/TypeValidator';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import { itAsserts, itValidatesType, itValidatesName } from '../Validation/ExecutableValidationTester'; import { itAsserts, itValidatesType, itValidatesName } from '../Validation/ExecutableValidationTester';
import { generateDataValidationTestScenarios } from '../Validation/DataValidationTestScenarioGenerator'; import { generateDataValidationTestScenarios } from '../Validation/DataValidationTestScenarioGenerator';
describe('ScriptParser', () => { describe('ScriptParser', () => {
describe('parseScript', () => { describe('parseScript', () => {
it('parses name correctly', () => { describe('property validation', () => {
// arrange describe('validates object', () => {
const expected = 'test-expected-name'; // arrange
const scriptData = createScriptDataWithCode() const expectedScript = createScriptDataWithCall();
.withName(expected); const expectedContext: ScriptErrorContext = {
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy(); type: ExecutableType.Script,
// act self: expectedScript,
const actualScript = new TestContext() };
.withData(scriptData) const expectedAssertion: ObjectAssertion<CallScriptData & CodeScriptData> = {
.withScriptFactory(scriptFactorySpy) value: expectedScript,
.parseScript(); valueName: expectedScript.name,
// assert allowedProperties: [
const actualName = getInitParameters(actualScript)?.name; 'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
expect(actualName).to.equal(expected); ],
};
itValidatesType(
(validatorFactory) => {
// act
new TestContext()
.withData(expectedScript)
.withValidatorFactory(validatorFactory)
.parseScript();
// assert
return {
expectedDataToValidate: expectedScript,
expectedErrorContext: expectedContext,
assertValidation: (validator) => validator.assertObject(expectedAssertion),
};
},
);
});
describe('validates union type', () => {
// arrange
const testScenarios = generateDataValidationTestScenarios<ScriptData>(
{
assertErrorMessage: 'Neither "call" or "code" is defined.',
expectFail: [{
description: 'with no call or code',
data: createScriptDataWithoutCallOrCodes(),
}],
expectPass: [
{
description: 'with call',
data: createScriptDataWithCall(),
},
{
description: 'with code',
data: createScriptDataWithCode(),
},
],
},
{
assertErrorMessage: 'Both "call" and "revertCode" are defined.',
expectFail: [{
description: 'with both call and revertCode',
data: createScriptDataWithCall()
.withRevertCode('revert-code'),
}],
expectPass: [
{
description: 'with call, without revertCode',
data: createScriptDataWithCall()
.withRevertCode(undefined),
},
{
description: 'with revertCode, without call',
data: createScriptDataWithCode()
.withRevertCode('revert code'),
},
],
},
{
assertErrorMessage: 'Both "call" and "code" are defined.',
expectFail: [{
description: 'with both call and code',
data: createScriptDataWithCall()
.withCode('code'),
}],
expectPass: [
{
description: 'with call, without code',
data: createScriptDataWithCall()
.withCode(''),
},
{
description: 'with code, without call',
data: createScriptDataWithCode()
.withCode('code'),
},
],
},
);
testScenarios.forEach(({
description, expectedPass, data: scriptData, expectedMessage,
}) => {
describe(description, () => {
itAsserts({
expectedConditionResult: expectedPass,
test: (validatorFactory) => {
const expectedContext: ScriptErrorContext = {
type: ExecutableType.Script,
self: scriptData,
};
// act
new TestContext()
.withData(scriptData)
.withValidatorFactory(validatorFactory)
.parseScript();
// assert
expectExists(expectedMessage);
return {
expectedErrorMessage: expectedMessage,
expectedErrorContext: expectedContext,
};
},
});
});
});
});
}); });
it('parses docs correctly', () => { describe('id', () => {
// arrange it('creates ID correctly', () => {
const expectedDocs = ['https://expected-doc1.com', 'https://expected-doc2.com']; // arrange
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy(); const expectedId: ExecutableId = 'expected-id';
const scriptData = createScriptDataWithCode() const scriptData = createScriptDataWithCode()
.withDocs(expectedDocs); .withName(expectedId);
const docsParser: DocsParser = (data) => data.docs as typeof expectedDocs; const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
// act // act
const actualScript = new TestContext() const actualScript = new TestContext()
.withData(scriptData) .withData(scriptData)
.withScriptFactory(scriptFactorySpy) .withScriptFactory(scriptFactorySpy)
.withDocsParser(docsParser) .parseScript();
.parseScript(); // assert
// assert const actualId = getInitParameters(actualScript)?.executableId;
const actualDocs = getInitParameters(actualScript)?.docs; expect(actualId).to.equal(expectedId);
expect(actualDocs).to.deep.equal(expectedDocs); });
}); });
it('gets script from the factory', () => { describe('name', () => {
// arrange it('parses name correctly', () => {
const expectedScript = new ScriptStub('expected-script'); // arrange
const scriptFactory: ScriptFactory = () => expectedScript; const expected = 'test-expected-name';
// act const scriptData = createScriptDataWithCode()
const actualScript = new TestContext() .withName(expected);
.withScriptFactory(scriptFactory) const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
.parseScript(); // act
// assert const actualScript = new TestContext()
expect(actualScript).to.equal(expectedScript); .withData(scriptData)
.withScriptFactory(scriptFactorySpy)
.parseScript();
// assert
const actualName = getInitParameters(actualScript)?.name;
expect(actualName).to.equal(expected);
});
describe('validates name', () => {
// arrange
const expectedName = 'expected script name to be validated';
const script = createScriptDataWithCall()
.withName(expectedName);
const expectedContext: ScriptErrorContext = {
type: ExecutableType.Script,
self: script,
};
itValidatesName((validatorFactory) => {
// act
new TestContext()
.withData(script)
.withValidatorFactory(validatorFactory)
.parseScript();
// assert
return {
expectedNameToValidate: expectedName,
expectedErrorContext: expectedContext,
};
});
});
});
describe('docs', () => {
it('parses docs correctly', () => {
// arrange
const expectedDocs = ['https://expected-doc1.com', 'https://expected-doc2.com'];
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
const scriptData = createScriptDataWithCode()
.withDocs(expectedDocs);
const docsParser: DocsParser = (data) => data.docs as typeof expectedDocs;
// act
const actualScript = new TestContext()
.withData(scriptData)
.withScriptFactory(scriptFactorySpy)
.withDocsParser(docsParser)
.parseScript();
// assert
const actualDocs = getInitParameters(actualScript)?.docs;
expect(actualDocs).to.deep.equal(expectedDocs);
});
}); });
describe('level', () => { describe('level', () => {
describe('generated `undefined` level if given absent value', () => { describe('generated `undefined` level if given absent value', () => {
@@ -261,175 +414,46 @@ describe('ScriptParser', () => {
}); });
}); });
}); });
describe('validation', () => { describe('script creation', () => {
describe('validates for name', () => { it('creates script from the factory', () => {
// arrange // arrange
const expectedName = 'expected script name to be validated'; const expectedScript = new ScriptStub('expected-script');
const script = createScriptDataWithCall() const scriptFactory: ScriptFactory = () => expectedScript;
.withName(expectedName); // act
const expectedContext: ScriptErrorContext = { const actualScript = new TestContext()
type: ExecutableType.Script, .withScriptFactory(scriptFactory)
self: script, .parseScript();
}; // assert
itValidatesName((validatorFactory) => { expect(actualScript).to.equal(expectedScript);
// act
new TestContext()
.withData(script)
.withValidatorFactory(validatorFactory)
.parseScript();
// assert
return {
expectedNameToValidate: expectedName,
expectedErrorContext: expectedContext,
};
});
}); });
describe('validates for defined data', () => { describe('rethrows exception if script factory fails', () => {
// arrange // arrange
const expectedScript = createScriptDataWithCall(); const givenData = createScriptDataWithCode();
const expectedContext: ScriptErrorContext = { const expectedContextMessage = 'Failed to parse script.';
type: ExecutableType.Script, const expectedError = new Error();
self: expectedScript, const validatorFactory: ExecutableValidatorFactory = () => {
const validatorStub = new ExecutableValidatorStub();
validatorStub.createContextualErrorMessage = (message) => message;
return validatorStub;
}; };
const expectedAssertion: ObjectAssertion<CallScriptData & CodeScriptData> = { // act & assert
value: expectedScript, itThrowsContextualError({
valueName: expectedScript.name, throwingAction: (wrapError) => {
allowedProperties: [ const factoryMock: ScriptFactory = () => {
'name', 'recommend', 'code', 'revertCode', 'call', 'docs', throw expectedError;
],
};
itValidatesType(
(validatorFactory) => {
// act
new TestContext()
.withData(expectedScript)
.withValidatorFactory(validatorFactory)
.parseScript();
// assert
return {
expectedDataToValidate: expectedScript,
expectedErrorContext: expectedContext,
assertValidation: (validator) => validator.assertObject(expectedAssertion),
}; };
new TestContext()
.withScriptFactory(factoryMock)
.withErrorWrapper(wrapError)
.withValidatorFactory(validatorFactory)
.withData(givenData)
.parseScript();
}, },
); expectedWrappedError: expectedError,
}); expectedContextMessage,
describe('validates data', () => {
// arrange
const testScenarios = generateDataValidationTestScenarios<ScriptData>(
{
assertErrorMessage: 'Neither "call" or "code" is defined.',
expectFail: [{
description: 'with no call or code',
data: createScriptDataWithoutCallOrCodes(),
}],
expectPass: [
{
description: 'with call',
data: createScriptDataWithCall(),
},
{
description: 'with code',
data: createScriptDataWithCode(),
},
],
},
{
assertErrorMessage: 'Both "call" and "revertCode" are defined.',
expectFail: [{
description: 'with both call and revertCode',
data: createScriptDataWithCall()
.withRevertCode('revert-code'),
}],
expectPass: [
{
description: 'with call, without revertCode',
data: createScriptDataWithCall()
.withRevertCode(undefined),
},
{
description: 'with revertCode, without call',
data: createScriptDataWithCode()
.withRevertCode('revert code'),
},
],
},
{
assertErrorMessage: 'Both "call" and "code" are defined.',
expectFail: [{
description: 'with both call and code',
data: createScriptDataWithCall()
.withCode('code'),
}],
expectPass: [
{
description: 'with call, without code',
data: createScriptDataWithCall()
.withCode(''),
},
{
description: 'with code, without call',
data: createScriptDataWithCode()
.withCode('code'),
},
],
},
);
testScenarios.forEach(({
description, expectedPass, data: scriptData, expectedMessage,
}) => {
describe(description, () => {
itAsserts({
expectedConditionResult: expectedPass,
test: (validatorFactory) => {
const expectedContext: ScriptErrorContext = {
type: ExecutableType.Script,
self: scriptData,
};
// act
new TestContext()
.withData(scriptData)
.withValidatorFactory(validatorFactory)
.parseScript();
// assert
expectExists(expectedMessage);
return {
expectedErrorMessage: expectedMessage,
expectedErrorContext: expectedContext,
};
},
});
});
}); });
}); });
}); });
describe('rethrows exception if script factory fails', () => {
// arrange
const givenData = createScriptDataWithCode();
const expectedContextMessage = 'Failed to parse script.';
const expectedError = new Error();
const validatorFactory: ExecutableValidatorFactory = () => {
const validatorStub = new ExecutableValidatorStub();
validatorStub.createContextualErrorMessage = (message) => message;
return validatorStub;
};
// act & assert
itThrowsContextualError({
throwingAction: (wrapError) => {
const factoryMock: ScriptFactory = () => {
throw expectedError;
};
new TestContext()
.withScriptFactory(factoryMock)
.withErrorWrapper(wrapError)
.withValidatorFactory(validatorFactory)
.withData(givenData)
.parseScript();
},
expectedWrappedError: expectedError,
expectedContextMessage,
});
});
}); });
}); });

View File

@@ -3,7 +3,7 @@ import { Application } from '@/domain/Application';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub'; import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub'; import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { getAbsentCollectionTestCases } from '@tests/unit/shared/TestCases/AbsentTests'; import { getAbsentCollectionTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
describe('Application', () => { describe('Application', () => {

View File

@@ -5,7 +5,7 @@ import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel'; import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
import { getEnumValues } from '@/application/Common/Enum'; import { getEnumValues } from '@/application/Common/Enum';
import { CategoryCollection } from '@/domain/CategoryCollection'; import { CategoryCollection } from '@/domain/Collection/CategoryCollection';
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub'; import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
import { EnumRangeTestRunner } from '@tests/unit/application/Common/EnumRangeTestRunner'; import { EnumRangeTestRunner } from '@tests/unit/application/Common/EnumRangeTestRunner';

View File

@@ -0,0 +1,316 @@
import { describe, it, expect } from 'vitest';
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import type { Category } from '@/domain/Executables/Category/Category';
import type { Script } from '@/domain/Executables/Script/Script';
import { createCategory } from '@/domain/Executables/Category/CategoryFactory';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
describe('CategoryFactory', () => {
describe('createCategory', () => {
describe('id', () => {
it('assigns id correctly', () => {
// arrange
const expectedId: ExecutableId = 'expected category id';
// act
const category = new TestContext()
.withId(expectedId)
.build();
// assert
const actualId = category.executableId;
expect(actualId).to.equal(expectedId);
});
describe('throws error if id is absent', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
const expectedError = 'missing ID';
const id = absentValue;
// act
const construct = () => new TestContext()
.withId(id)
.build();
// assert
expect(construct).to.throw(expectedError);
}, { excludeNull: true, excludeUndefined: true });
});
});
describe('name', () => {
it('assigns name correctly', () => {
// arrange
const expectedName = 'expected category name';
// act
const category = new TestContext()
.withName(expectedName)
.build();
// assert
const actualName = category.name;
expect(actualName).to.equal(expectedName);
});
describe('throws error if name is absent', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
const expectedError = 'missing name';
const name = absentValue;
// act
const construct = () => new TestContext()
.withName(name)
.build();
// assert
expect(construct).to.throw(expectedError);
}, { excludeNull: true, excludeUndefined: true });
});
});
describe('docs', () => {
it('assigns docs correctly', () => {
// arrange
const expectedDocs = ['expected', 'docs'];
// act
const category = new TestContext()
.withDocs(expectedDocs)
.build();
// assert
const actualDocs = category.docs;
expect(actualDocs).to.equal(expectedDocs);
});
});
describe('children', () => {
it('assigns scripts correctly', () => {
// arrange
const expectedScripts = [
new ScriptStub('expected-script-1'),
new ScriptStub('expected-script-2'),
];
// act
const category = new TestContext()
.withScripts(expectedScripts)
.build();
// assert
const actualScripts = category.docs;
expect(actualScripts).to.equal(expectedScripts);
});
it('assigns categories correctly', () => {
// arrange
const expectedCategories = [
new CategoryStub('expected-subcategory-1'),
new CategoryStub('expected-subcategory-2'),
];
// act
const category = new TestContext()
.withSubcategories(expectedCategories)
.build();
// assert
const actualCategories = category.subcategories;
expect(actualCategories).to.equal(expectedCategories);
});
it('throws error if no children are present', () => {
// arrange
const expectedError = 'A category must have at least one sub-category or script';
const scriptChildren: readonly Script[] = [];
const categoryChildren: readonly Category[] = [];
// act
const construct = () => new TestContext()
.withSubcategories(categoryChildren)
.withScripts(scriptChildren)
.build();
// assert
expect(construct).to.throw(expectedError);
});
});
describe('getAllScriptsRecursively', () => {
it('retrieves direct child scripts', () => {
// arrange
const expectedScripts: readonly Script[] = [
new ScriptStub('expected-script-1'),
new ScriptStub('expected-script-2'),
];
const category = new TestContext()
.withScripts(expectedScripts)
.build();
// act
const actual = category.getAllScriptsRecursively();
// assert
expect(actual).to.have.deep.members(expectedScripts);
});
it('retrieves scripts from direct child categories', () => {
// arrange
const expectedScriptIds: readonly string[] = [
'1', '2', '3', '4',
];
const subcategories: readonly Category[] = [
new CategoryStub('subcategory-1').withScriptIds('1', '2'),
new CategoryStub('subcategory-2').withScriptIds('3', '4'),
];
const category = new TestContext()
.withScripts([])
.withSubcategories(subcategories)
.build();
// act
const actualIds = category
.getAllScriptsRecursively()
.map((s) => s.executableId);
// assert
expect(actualIds).to.have.deep.members(expectedScriptIds);
});
it('retrieves scripts from both direct children and child categories', () => {
// arrange
const expectedScriptIds: readonly string[] = [
'1', '2', '3', '4', '5', '6',
];
const subcategories: readonly Category[] = [
new CategoryStub('subcategory-1').withScriptIds('1', '2'),
new CategoryStub('subcategory-2').withScriptIds('3', '4'),
];
const scripts: readonly Script[] = [
new ScriptStub('1'),
new ScriptStub('2'),
];
const category = new TestContext()
.withSubcategories(subcategories)
.withScripts(scripts)
.build();
// act
const actualIds = category
.getAllScriptsRecursively()
.map((s) => s.executableId);
// assert
expect(actualIds).to.have.deep.members(expectedScriptIds);
});
it('retrieves scripts from nested categories recursively', () => {
// arrange
const expectedScriptIds: readonly string[] = [
'1', '2', '3', '4', '5', '6',
];
const subcategories: readonly Category[] = [
new CategoryStub('subcategory-1')
.withScriptIds('1', '2')
.withCategory(
new CategoryStub('subcategory-1-subcategory-1')
.withScriptIds('3', '4'),
),
new CategoryStub('subcategory-2')
.withCategories(
new CategoryStub('subcategory-2-subcategory-1')
.withScriptIds('5')
.withCategory(
new CategoryStub('subcategory-2-subcategory-1-subcategory-1')
.withCategory(
new CategoryStub('subcategory-2-subcategory-1-subcategory-1-subcategory-1')
.withScriptIds('6'),
),
),
),
];
// assert
const category = new TestContext()
.withScripts([])
.withSubcategories(subcategories)
.build();
// act
const actualIds = category
.getAllScriptsRecursively()
.map((s) => s.executableId);
// assert
expect(actualIds).to.have.deep.members(expectedScriptIds);
});
});
describe('includes', () => {
it('returns false for scripts not included', () => {
// assert
const expectedResult = false;
const script = new ScriptStub('3');
const childCategory = new CategoryStub('subcategory')
.withScriptIds('1', '2');
const category = new TestContext()
.withSubcategories([childCategory])
.build();
// act
const actual = category.includes(script);
// assert
expect(actual).to.equal(expectedResult);
});
it('returns true for scripts directly included', () => {
// assert
const expectedResult = true;
const script = new ScriptStub('3');
const childCategory = new CategoryStub('subcategory')
.withScript(script)
.withScriptIds('non-related');
const category = new TestContext()
.withSubcategories([childCategory])
.build();
// act
const actual = category.includes(script);
// assert
expect(actual).to.equal(expectedResult);
});
it('returns true for scripts included in nested categories', () => {
// assert
const expectedResult = true;
const script = new ScriptStub('3');
const childCategory = new CategoryStub('subcategory')
.withScriptIds('non-related')
.withCategory(
new CategoryStub('nested-subcategory')
.withScript(script),
);
const category = new TestContext()
.withSubcategories([childCategory])
.build();
// act
const actual = category.includes(script);
// assert
expect(actual).to.equal(expectedResult);
});
});
});
});
class TestContext {
private id = `[${TestContext.name}] test category`;
private name = 'test-category';
private docs: ReadonlyArray<string> = [];
private subcategories: ReadonlyArray<Category> = [];
private scripts: ReadonlyArray<Script> = [
new ScriptStub(`[${TestContext.name}] script`),
];
public withId(id: string): this {
this.id = id;
return this;
}
public withName(name: string): this {
this.name = name;
return this;
}
public withDocs(docs: ReadonlyArray<string>): this {
this.docs = docs;
return this;
}
public withScripts(scripts: ReadonlyArray<Script>): this {
this.scripts = scripts;
return this;
}
public withSubcategories(subcategories: ReadonlyArray<Category>): this {
this.subcategories = subcategories;
return this;
}
public build(): ReturnType<typeof createCategory> {
return createCategory({
executableId: this.id,
name: this.name,
docs: this.docs,
subcategories: this.subcategories,
scripts: this.scripts,
});
}
}

View File

@@ -1,217 +0,0 @@
import { describe, it, expect } from 'vitest';
import { CollectionCategory } from '@/domain/Executables/Category/CollectionCategory';
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import type { Category } from '@/domain/Executables/Category/Category';
import type { Script } from '@/domain/Executables/Script/Script';
describe('CollectionCategory', () => {
describe('ctor', () => {
describe('throws error if name is absent', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
const expectedError = 'missing name';
const name = absentValue;
// act
const construct = () => new CategoryBuilder()
.withName(name)
.build();
// assert
expect(construct).to.throw(expectedError);
}, { excludeNull: true, excludeUndefined: true });
});
it('throws error if no children are present', () => {
// arrange
const expectedError = 'A category must have at least one sub-category or script';
const scriptChildren: readonly Script[] = [];
const categoryChildren: readonly Category[] = [];
// act
const construct = () => new CategoryBuilder()
.withSubcategories(categoryChildren)
.withScripts(scriptChildren)
.build();
// assert
expect(construct).to.throw(expectedError);
});
});
describe('getAllScriptsRecursively', () => {
it('retrieves direct child scripts', () => {
// arrange
const expectedScripts = [new ScriptStub('1'), new ScriptStub('2')];
const sut = new CategoryBuilder()
.withScripts(expectedScripts)
.build();
// act
const actual = sut.getAllScriptsRecursively();
// assert
expect(actual).to.have.deep.members(expectedScripts);
});
it('retrieves scripts from direct child categories', () => {
// arrange
const expectedScriptIds = ['1', '2', '3', '4'];
const categories = [
new CategoryStub(31).withScriptIds('1', '2'),
new CategoryStub(32).withScriptIds('3', '4'),
];
const sut = new CategoryBuilder()
.withScripts([])
.withSubcategories(categories)
.build();
// act
const actualIds = sut
.getAllScriptsRecursively()
.map((s) => s.id);
// assert
expect(actualIds).to.have.deep.members(expectedScriptIds);
});
it('retrieves scripts from both direct children and child categories', () => {
// arrange
const expectedScriptIds = ['1', '2', '3', '4', '5', '6'];
const categories = [
new CategoryStub(31).withScriptIds('1', '2'),
new CategoryStub(32).withScriptIds('3', '4'),
];
const scripts = [new ScriptStub('5'), new ScriptStub('6')];
const sut = new CategoryBuilder()
.withSubcategories(categories)
.withScripts(scripts)
.build();
// act
const actualIds = sut
.getAllScriptsRecursively()
.map((s) => s.id);
// assert
expect(actualIds).to.have.deep.members(expectedScriptIds);
});
it('retrieves scripts from nested categories recursively', () => {
// arrange
const expectedScriptIds = ['1', '2', '3', '4', '5', '6'];
const categories = [
new CategoryStub(31)
.withScriptIds('1', '2')
.withCategory(
new CategoryStub(32)
.withScriptIds('3', '4'),
),
new CategoryStub(33)
.withCategories(
new CategoryStub(34)
.withScriptIds('5')
.withCategory(
new CategoryStub(35)
.withCategory(
new CategoryStub(35).withScriptIds('6'),
),
),
),
];
// assert
const sut = new CategoryBuilder()
.withScripts([])
.withSubcategories(categories)
.build();
// act
const actualIds = sut
.getAllScriptsRecursively()
.map((s) => s.id);
// assert
expect(actualIds).to.have.deep.members(expectedScriptIds);
});
});
describe('includes', () => {
it('returns false for scripts not included', () => {
// assert
const expectedResult = false;
const script = new ScriptStub('3');
const childCategory = new CategoryStub(33)
.withScriptIds('1', '2');
const sut = new CategoryBuilder()
.withSubcategories([childCategory])
.build();
// act
const actual = sut.includes(script);
// assert
expect(actual).to.equal(expectedResult);
});
it('returns true for scripts directly included', () => {
// assert
const expectedResult = true;
const script = new ScriptStub('3');
const childCategory = new CategoryStub(33)
.withScript(script)
.withScriptIds('non-related');
const sut = new CategoryBuilder()
.withSubcategories([childCategory])
.build();
// act
const actual = sut.includes(script);
// assert
expect(actual).to.equal(expectedResult);
});
it('returns true for scripts included in nested categories', () => {
// assert
const expectedResult = true;
const script = new ScriptStub('3');
const childCategory = new CategoryStub(22)
.withScriptIds('non-related')
.withCategory(new CategoryStub(33).withScript(script));
const sut = new CategoryBuilder()
.withSubcategories([childCategory])
.build();
// act
const actual = sut.includes(script);
// assert
expect(actual).to.equal(expectedResult);
});
});
});
class CategoryBuilder {
private id = 3264;
private name = 'test-script';
private docs: ReadonlyArray<string> = [];
private subcategories: ReadonlyArray<Category> = [];
private scripts: ReadonlyArray<Script> = [
new ScriptStub(`[${CategoryBuilder.name}] script`),
];
public withId(id: number): this {
this.id = id;
return this;
}
public withName(name: string): this {
this.name = name;
return this;
}
public withDocs(docs: ReadonlyArray<string>): this {
this.docs = docs;
return this;
}
public withScripts(scripts: ReadonlyArray<Script>): this {
this.scripts = scripts;
return this;
}
public withSubcategories(subcategories: ReadonlyArray<Category>): this {
this.subcategories = subcategories;
return this;
}
public build(): CollectionCategory {
return new CollectionCategory({
id: this.id,
name: this.name,
docs: this.docs,
subcategories: this.subcategories,
scripts: this.scripts,
});
}
}

View File

@@ -1,21 +1,35 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { getEnumValues } from '@/application/Common/Enum'; import { getEnumValues } from '@/application/Common/Enum';
import { CollectionScript } from '@/domain/Executables/Script/CollectionScript';
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel'; import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode'; import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub'; import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
import { createScript } from '@/domain/Executables/Script/ScriptFactory';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
describe('CollectionScript', () => { describe('ScriptFactory', () => {
describe('ctor', () => { describe('createScript', () => {
describe('id', () => {
it('correctly assigns id', () => {
// arrange
const expectedId: ExecutableId = 'expected-id';
// act
const script = new TestContext()
.withId(expectedId)
.build();
// assert
const actualId = script.executableId;
expect(actualId).to.equal(expectedId);
});
});
describe('scriptCode', () => { describe('scriptCode', () => {
it('assigns code correctly', () => { it('assigns code correctly', () => {
// arrange // arrange
const expected = new ScriptCodeStub(); const expected = new ScriptCodeStub();
const sut = new ScriptBuilder() const script = new TestContext()
.withCode(expected) .withCode(expected)
.build(); .build();
// act // act
const actual = sut.code; const actual = script.code;
// assert // assert
expect(actual).to.deep.equal(expected); expect(actual).to.deep.equal(expected);
}); });
@@ -23,21 +37,21 @@ describe('CollectionScript', () => {
describe('canRevert', () => { describe('canRevert', () => {
it('returns false without revert code', () => { it('returns false without revert code', () => {
// arrange // arrange
const sut = new ScriptBuilder() const script = new TestContext()
.withCodes('code') .withCodes('code')
.build(); .build();
// act // act
const actual = sut.canRevert(); const actual = script.canRevert();
// assert // assert
expect(actual).to.equal(false); expect(actual).to.equal(false);
}); });
it('returns true with revert code', () => { it('returns true with revert code', () => {
// arrange // arrange
const sut = new ScriptBuilder() const script = new TestContext()
.withCodes('code', 'non empty revert code') .withCodes('code', 'non empty revert code')
.build(); .build();
// act // act
const actual = sut.canRevert(); const actual = script.canRevert();
// assert // assert
expect(actual).to.equal(true); expect(actual).to.equal(true);
}); });
@@ -48,7 +62,7 @@ describe('CollectionScript', () => {
const invalidValue: RecommendationLevel = 55 as never; const invalidValue: RecommendationLevel = 55 as never;
const expectedError = 'invalid level'; const expectedError = 'invalid level';
// act // act
const construct = () => new ScriptBuilder() const construct = () => new TestContext()
.withRecommendationLevel(invalidValue) .withRecommendationLevel(invalidValue)
.build(); .build();
// assert // assert
@@ -58,43 +72,46 @@ describe('CollectionScript', () => {
// arrange // arrange
const expected = undefined; const expected = undefined;
// act // act
const sut = new ScriptBuilder() const script = new TestContext()
.withRecommendationLevel(expected) .withRecommendationLevel(expected)
.build(); .build();
// assert // assert
expect(sut.level).to.equal(expected); expect(script.level).to.equal(expected);
}); });
it('correctly assigns valid recommendation levels', () => { it('correctly assigns valid recommendation levels', () => {
// arrange getEnumValues(RecommendationLevel).forEach((enumValue) => {
for (const expected of getEnumValues(RecommendationLevel)) { // arrange
const expectedRecommendationLevel = enumValue;
// act // act
const sut = new ScriptBuilder() const script = new TestContext()
.withRecommendationLevel(expected) .withRecommendationLevel(expectedRecommendationLevel)
.build(); .build();
// assert // assert
const actual = sut.level; const actualRecommendationLevel = script.level;
expect(actual).to.equal(expected); expect(actualRecommendationLevel).to.equal(expectedRecommendationLevel);
} });
}); });
}); });
describe('docs', () => { describe('docs', () => {
it('correctly assigns docs', () => { it('correctly assigns docs', () => {
// arrange // arrange
const expected = ['doc1', 'doc2']; const expectedDocs = ['doc1', 'doc2'];
// act // act
const sut = new ScriptBuilder() const script = new TestContext()
.withDocs(expected) .withDocs(expectedDocs)
.build(); .build();
const actual = sut.docs;
// assert // assert
expect(actual).to.equal(expected); const actualDocs = script.docs;
expect(actualDocs).to.equal(expectedDocs);
}); });
}); });
}); });
}); });
class ScriptBuilder { class TestContext {
private name = 'test-script'; private name = `[${TestContext.name}]test-script`;
private id: ExecutableId = `[${TestContext.name}]id`;
private code: ScriptCode = new ScriptCodeStub(); private code: ScriptCode = new ScriptCodeStub();
@@ -109,6 +126,11 @@ class ScriptBuilder {
return this; return this;
} }
public withId(id: ExecutableId): this {
this.id = id;
return this;
}
public withCode(code: ScriptCode): this { public withCode(code: ScriptCode): this {
this.code = code; this.code = code;
return this; return this;
@@ -129,8 +151,9 @@ class ScriptBuilder {
return this; return this;
} }
public build(): CollectionScript { public build(): ReturnType<typeof createScript> {
return new CollectionScript({ return createScript({
executableId: this.id,
name: this.name, name: this.name,
code: this.code, code: this.code,
docs: this.docs, docs: this.docs,

View File

@@ -1,125 +1,180 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { NumericEntityStub } from '@tests/unit/shared/Stubs/NumericEntityStub';
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository'; import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
import { RepositoryEntityStub } from '@tests/unit/shared/Stubs/RepositoryEntityStub';
import type { RepositoryEntity, RepositoryEntityId } from '@/application/Repository/RepositoryEntity';
describe('InMemoryRepository', () => { describe('InMemoryRepository', () => {
describe('exists', () => { describe('exists', () => {
const sut = new InMemoryRepository<number, NumericEntityStub>( it('returns true when item exists', () => {
[new NumericEntityStub(1), new NumericEntityStub(2), new NumericEntityStub(3)], // arrange
); const expectedExistence = true;
const existingItemId: RepositoryEntityId = 'existing-entity-id';
describe('item exists', () => { const items: readonly RepositoryEntity[] = [
const actual = sut.exists(1); new RepositoryEntityStub('unrelated-entity-1'),
it('returns true', () => expect(actual).to.be.true); new RepositoryEntityStub(existingItemId),
new RepositoryEntityStub('unrelated-entity-2'),
];
const sut = new InMemoryRepository(items);
// act
const actualExistence = sut.exists(existingItemId);
// assert
expect(actualExistence).to.equal(expectedExistence);
}); });
describe('item does not exist', () => { it('returns false when item does not exist', () => {
const actual = sut.exists(99); // arrange
it('returns false', () => expect(actual).to.be.false); const expectedExistence = false;
const absentItemId: RepositoryEntityId = 'id-that-does-not-belong';
const items: readonly RepositoryEntity[] = [
new RepositoryEntityStub('unrelated-entity-1'),
new RepositoryEntityStub('unrelated-entity-2'),
];
const sut = new InMemoryRepository(items);
// act
const actualExistence = sut.exists(absentItemId);
// assert
expect(actualExistence).to.equal(expectedExistence);
}); });
}); });
it('getItems gets initial items', () => { describe('getItems', () => {
// arrange it('returns initial items', () => {
const expected = [ // arrange
new NumericEntityStub(1), new NumericEntityStub(2), const expectedItems: readonly RepositoryEntity[] = [
new NumericEntityStub(3), new NumericEntityStub(4), new RepositoryEntityStub('expected-item-1'),
]; new RepositoryEntityStub('expected-item-2'),
new RepositoryEntityStub('expected-item-3'),
// act ];
const sut = new InMemoryRepository<number, NumericEntityStub>(expected); // act
const actual = sut.getItems(); const sut = new InMemoryRepository(expectedItems);
const actualItems = sut.getItems();
// assert // assert
expect(actual).to.deep.equal(expected); expect(actualItems).to.have.lengthOf(expectedItems.length);
expect(actualItems).to.deep.members(expectedItems);
});
}); });
describe('addItem', () => { describe('addItem', () => {
it('adds', () => { it('increases length', () => {
// arrange // arrange
const sut = new InMemoryRepository<number, NumericEntityStub>(); const sut = new InMemoryRepository<RepositoryEntity>();
const expected = { const expectedLength = 1;
length: 1,
item: new NumericEntityStub(1),
};
// act // act
sut.addItem(expected.item); sut.addItem(new RepositoryEntityStub('unrelated-id'));
const actual = {
length: sut.length,
item: sut.getItems()[0],
};
// assert // assert
expect(actual.length).to.equal(expected.length); const actualLength = sut.length;
expect(actual.item).to.deep.equal(expected.item); expect(actualLength).to.equal(expectedLength);
});
it('adds as item', () => {
// arrange
const sut = new InMemoryRepository<RepositoryEntity>();
const expectedItem = new RepositoryEntityStub('expected-entity-id');
// act
sut.addItem(expectedItem);
// assert
const actualItems = sut.getItems();
expect(actualItems).to.have.lengthOf(1);
expect(actualItems).to.deep.include(expectedItem);
}); });
}); });
it('removeItem removes', () => { describe('removeItem', () => {
// arrange it('decreases length', () => {
const initialItems = [ // arrange
new NumericEntityStub(1), new NumericEntityStub(2), const itemIdToDelete: RepositoryEntityId = 'entity-id-to-be-deleted';
new NumericEntityStub(3), new NumericEntityStub(4), const initialItems: readonly RepositoryEntity[] = [
]; new RepositoryEntityStub('entity-to-be-retained-1'),
const idToDelete = 3; new RepositoryEntityStub(itemIdToDelete),
const expected = { new RepositoryEntityStub('entity-to-be-retained-2'),
length: 3, ];
items: [new NumericEntityStub(1), new NumericEntityStub(2), new NumericEntityStub(4)], const expectedLength = 2;
}; const sut = new InMemoryRepository<RepositoryEntity>(initialItems);
const sut = new InMemoryRepository<number, NumericEntityStub>(initialItems); // act
sut.removeItem(itemIdToDelete);
// act // assert
sut.removeItem(idToDelete); const actualLength = sut.length;
const actual = { expect(actualLength).to.equal(expectedLength);
length: sut.length, });
items: sut.getItems(), it('removes from items', () => {
}; // arrange
const expectedItems: readonly RepositoryEntity[] = [
// assert new RepositoryEntityStub('entity-to-be-retained-1'),
expect(actual.length).to.equal(expected.length); new RepositoryEntityStub('entity-to-be-retained-2'),
expect(actual.items).to.deep.equal(expected.items); ];
const itemIdToDelete: RepositoryEntityId = 'entity-id-to-be-deleted';
const initialItems: readonly RepositoryEntity[] = [
...expectedItems,
new RepositoryEntityStub(itemIdToDelete),
];
const sut = new InMemoryRepository<RepositoryEntity>(initialItems);
// act
sut.removeItem(itemIdToDelete);
// assert
const actualItems = sut.getItems();
expect(actualItems).to.have.lengthOf(expectedItems.length);
expect(actualItems).to.have.deep.members(expectedItems);
});
}); });
describe('addOrUpdateItem', () => { describe('addOrUpdateItem', () => {
it('adds when item does not exist', () => { it('adds when item does not exist', () => {
// arrange // arrange
const initialItems = [new NumericEntityStub(1), new NumericEntityStub(2)]; const initialItems: readonly RepositoryEntity[] = [
const newItem = new NumericEntityStub(3); new RepositoryEntityStub('existing-item-1'),
const expected = [...initialItems, newItem]; new RepositoryEntityStub('existing-item-2'),
const sut = new InMemoryRepository<number, NumericEntityStub>(initialItems); ];
const newItem = new RepositoryEntityStub('new-item');
const expectedItems: readonly RepositoryEntity[] = [
...initialItems,
newItem,
];
const sut = new InMemoryRepository(initialItems);
// act // act
sut.addOrUpdateItem(newItem); sut.addOrUpdateItem(newItem);
// assert // assert
const actual = sut.getItems(); const actualItems = sut.getItems();
expect(actual).to.deep.equal(expected); expect(actualItems).to.have.lengthOf(expectedItems.length);
expect(actualItems).to.have.members(expectedItems);
}); });
it('updates when item exists', () => { it('updates when item exists', () => {
// arrange // arrange
const initialItems = [new NumericEntityStub(1).withCustomProperty('bca')]; const itemId: RepositoryEntityId = 'same-item-id';
const updatedItem = new NumericEntityStub(1).withCustomProperty('abc'); const initialItems: readonly RepositoryEntity[] = [
const expected = [updatedItem]; new RepositoryEntityStub(itemId)
const sut = new InMemoryRepository<number, NumericEntityStub>(initialItems); .withCustomPropertyValue('initial-property-value'),
];
const updatedItem = new RepositoryEntityStub(itemId)
.withCustomPropertyValue('changed-property-value');
const sut = new InMemoryRepository(initialItems);
// act // act
sut.addOrUpdateItem(updatedItem); sut.addOrUpdateItem(updatedItem);
// assert // assert
const actual = sut.getItems(); const actualItems = sut.getItems();
expect(actual).to.deep.equal(expected); expect(actualItems).to.have.lengthOf(1);
expect(actualItems[0]).to.equal(updatedItem);
}); });
}); });
describe('getById', () => { describe('getById', () => {
it('returns entity if it exists', () => { it('returns entity if it exists', () => {
// arrange // arrange
const expected = new NumericEntityStub(1).withCustomProperty('bca'); const existingId: RepositoryEntityId = 'existing-item-id';
const sut = new InMemoryRepository<number, NumericEntityStub>([ const expectedItem = new RepositoryEntityStub(existingId)
expected, new NumericEntityStub(2).withCustomProperty('bca'), .withCustomPropertyValue('bca');
new NumericEntityStub(3).withCustomProperty('bca'), new NumericEntityStub(4).withCustomProperty('bca'), const initialItems: readonly RepositoryEntity[] = [
]); new RepositoryEntityStub('unrelated-entity'),
expectedItem,
new RepositoryEntityStub('different-id-same-property').withCustomPropertyValue('bca'),
];
const sut = new InMemoryRepository(initialItems);
// act // act
const actual = sut.getById(expected.id); const actualItem = sut.getById(expectedItem.id);
// assert // assert
expect(actual).to.deep.equal(expected); expect(actualItem).to.deep.equal(expectedItem);
}); });
it('throws if item does not exist', () => { it('throws if item does not exist', () => {
// arrange // arrange
const id = 31; const id: RepositoryEntityId = 'id-that-does-not-exist';
const expectedError = `missing item: ${id}`; const expectedError = `missing item: ${id}`;
const sut = new InMemoryRepository<number, NumericEntityStub>([]); const sut = new InMemoryRepository<RepositoryEntityStub>();
// act // act
const act = () => sut.getById(id); const act = () => sut.getById(id);
// assert // assert

View File

@@ -145,7 +145,7 @@ describe('RecommendationStatusHandler', () => {
return `total: ${testCase.selection.length}\n` return `total: ${testCase.selection.length}\n`
+ 'scripts:\n' + 'scripts:\n'
+ testCase.selection + testCase.selection
.map((s) => `{ id: ${s.script.id}, level: ${s.script.level === undefined ? 'unknown' : RecommendationLevel[s.script.level]} }`) .map((s) => `{ id: ${s.script.executableId}, level: ${s.script.level === undefined ? 'unknown' : RecommendationLevel[s.script.level]} }`)
.join(' | '); .join(' | ');
} }
}); });

View File

@@ -5,7 +5,7 @@ import { CategoryReverter } from '@/presentation/components/Scripts/View/Tree/No
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub'; import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub'; import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
import { getCategoryNodeId, getScriptNodeId } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter'; import { getCategoryNodeId, createNodeIdForExecutable } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
import { type NodeMetadata, NodeType } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata'; import { type NodeMetadata, NodeType } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
describe('ReverterFactory', () => { describe('ReverterFactory', () => {
@@ -24,7 +24,7 @@ describe('ReverterFactory', () => {
it('gets ScriptReverter for script node', () => { it('gets ScriptReverter for script node', () => {
// arrange // arrange
const script = new ScriptStub('test'); const script = new ScriptStub('test');
const node = getNodeContentStub(getScriptNodeId(script), NodeType.Script); const node = getNodeContentStub(createNodeIdForExecutable(script), NodeType.Script);
const collection = new CategoryCollectionStub() const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(0).withScript(script)); .withAction(new CategoryStub(0).withScript(script));
// act // act

View File

@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
import { ScriptReverter } from '@/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ScriptReverter'; import { ScriptReverter } from '@/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ScriptReverter';
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub'; import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
import { getScriptNodeId } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter'; import { createNodeIdForExecutable } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
import { UserSelectionStub } from '@tests/unit/shared/Stubs/UserSelectionStub'; import { UserSelectionStub } from '@tests/unit/shared/Stubs/UserSelectionStub';
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub'; import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub';
@@ -11,7 +11,7 @@ describe('ScriptReverter', () => {
describe('getState', () => { describe('getState', () => {
// arrange // arrange
const script = new ScriptStub('id'); const script = new ScriptStub('id');
const nodeId = getScriptNodeId(script); const nodeId = createNodeIdForExecutable(script);
const testScenarios: ReadonlyArray<{ const testScenarios: ReadonlyArray<{
readonly description: string; readonly description: string;
readonly selectedScripts: readonly SelectedScript[]; readonly selectedScripts: readonly SelectedScript[];
@@ -98,7 +98,7 @@ describe('ScriptReverter', () => {
expectedRevert: false, expectedRevert: false,
}, },
]; ];
const nodeId = getScriptNodeId(script); const nodeId = createNodeIdForExecutable(script);
testScenarios.forEach(( testScenarios.forEach((
{ description, selection, expectedRevert }, { description, selection, expectedRevert },
) => { ) => {
@@ -111,7 +111,7 @@ describe('ScriptReverter', () => {
// act // act
sut.selectWithRevertState(revertState, userSelection); sut.selectWithRevertState(revertState, userSelection);
// assert // assert
expect(scriptSelection.isScriptSelected(script.id, expectedRevert)).to.equal(true); expect(scriptSelection.isScriptSelected(script.executableId, expectedRevert)).to.equal(true);
}); });
}); });
}); });

View File

@@ -3,13 +3,14 @@ import { TreeNodeManager } from '@/presentation/components/Scripts/View/Tree/Tre
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests'; import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { TreeNodeHierarchy } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/Hierarchy/TreeNodeHierarchy'; import { TreeNodeHierarchy } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/Hierarchy/TreeNodeHierarchy';
import { TreeNodeState } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/TreeNodeState'; import { TreeNodeState } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/TreeNodeState';
import type { TreeNodeId } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
describe('TreeNodeManager', () => { describe('TreeNodeManager', () => {
describe('constructor', () => { describe('constructor', () => {
describe('id', () => { describe('id', () => {
it('should initialize with the provided id', () => { it('should initialize with the provided id', () => {
// arrange // arrange
const expectedId = 'test-id'; const expectedId: TreeNodeId = 'test-id';
// act // act
const node = new TreeNodeManager(expectedId); const node = new TreeNodeManager(expectedId);
// assert // assert
@@ -18,9 +19,10 @@ describe('TreeNodeManager', () => {
describe('should throw an error if id is not provided', () => { describe('should throw an error if id is not provided', () => {
itEachAbsentStringValue((absentId) => { itEachAbsentStringValue((absentId) => {
// arrange // arrange
const id = absentId as TreeNodeId;
const expectedError = 'missing id'; const expectedError = 'missing id';
// act // act
const act = () => new TreeNodeManager(absentId); const act = () => new TreeNodeManager(id);
// assert // assert
expect(act).to.throw(expectedError); expect(act).to.throw(expectedError);
}, { excludeNull: true, excludeUndefined: true }); }, { excludeNull: true, excludeUndefined: true });

View File

@@ -5,31 +5,36 @@ import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub'; import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
import { import {
getCategoryId, getCategoryNodeId, getScriptId, createExecutableIdFromNodeId,
getScriptNodeId, parseAllCategories, parseSingleCategory, createNodeIdForExecutable,
parseAllCategories,
parseSingleCategory,
} from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter'; } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
import { ExecutableType } from '@/application/Parser/Executable/Validation/ExecutableType'; import { ExecutableType } from '@/application/Parser/Executable/Validation/ExecutableType';
import type { NodeMetadata } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata'; import type { NodeMetadata } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
import { expectExists } from '@tests/shared/Assertions/ExpectExists'; import { expectExists } from '@tests/shared/Assertions/ExpectExists';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
describe('CategoryNodeMetadataConverter', () => { describe('CategoryNodeMetadataConverter', () => {
it('can convert script id and back', () => { it('can convert script id and back', () => {
// arrange // arrange
const script = new ScriptStub('test'); const expectedScriptId: ExecutableId = 'expected-script-id';
const script = new ScriptStub(expectedScriptId);
// act // act
const nodeId = getScriptNodeId(script); const nodeId = createNodeIdForExecutable(script);
const scriptId = getScriptId(nodeId); const actualScriptId = createExecutableIdFromNodeId(nodeId);
// assert // assert
expect(scriptId).to.equal(script.id); expect(actualScriptId).to.equal(expectedScriptId);
}); });
it('can convert category id and back', () => { it('can convert category id and back', () => {
// arrange // arrange
const category = new CategoryStub(55); const expectedCategoryId: ExecutableId = 'expected-category-id';
const category = new CategoryStub(expectedCategoryId);
// act // act
const nodeId = getCategoryNodeId(category); const nodeId = createNodeIdForExecutable(category);
const scriptId = getCategoryId(nodeId); const actualCategoryId = createExecutableIdFromNodeId(nodeId);
// assert // assert
expect(scriptId).to.equal(category.id); expect(actualCategoryId).to.equal(expectedCategoryId);
}); });
describe('parseSingleCategory', () => { describe('parseSingleCategory', () => {
it('throws error if parent category cannot be retrieved', () => { it('throws error if parent category cannot be retrieved', () => {
@@ -38,32 +43,45 @@ describe('CategoryNodeMetadataConverter', () => {
const collection = new CategoryCollectionStub(); const collection = new CategoryCollectionStub();
collection.getCategory = () => { throw new Error(expectedError); }; collection.getCategory = () => { throw new Error(expectedError); };
// act // act
const act = () => parseSingleCategory(31, collection); const act = () => parseSingleCategory('unimportant-id', collection);
// assert // assert
expect(act).to.throw(expectedError); expect(act).to.throw(expectedError);
}); });
it('can parse when category has sub categories', () => { it('can parse when category has sub categories', () => {
// arrange // arrange
const categoryId = 31; const parentCategoryId: ExecutableId = 'parent-category';
const firstSubCategory = new CategoryStub(11).withScriptIds('111', '112'); const firstSubcategory = new CategoryStub('subcategory-1')
const secondSubCategory = new CategoryStub(categoryId) .withScriptIds('subcategory-1-script-1', 'subcategory-1-script-2');
.withCategory(new CategoryStub(33).withScriptIds('331', '331')) const secondSubCategory = new CategoryStub('subcategory-2')
.withCategory(new CategoryStub(44).withScriptIds('44')); .withCategory(
const collection = new CategoryCollectionStub().withAction(new CategoryStub(categoryId) new CategoryStub('subcategory-2-subcategory-1')
.withCategory(firstSubCategory) .withScriptIds('subcategory-2-subcategory-1-script-1', 'subcategory-2-subcategory-1-script-2'),
.withCategory(secondSubCategory)); )
.withCategory(
new CategoryStub('subcategory-2-subcategory-2')
.withScriptIds('subcategory-2-subcategory-2-script-1'),
);
const collection = new CategoryCollectionStub().withAction(
new CategoryStub(parentCategoryId)
.withCategory(firstSubcategory)
.withCategory(secondSubCategory),
);
// act // act
const nodes = parseSingleCategory(categoryId, collection); const nodes = parseSingleCategory(parentCategoryId, collection);
// assert // assert
expectExists(nodes); expectExists(nodes);
expect(nodes).to.have.lengthOf(2); expect(nodes).to.have.lengthOf(2);
expectSameCategory(nodes[0], firstSubCategory); expectSameCategory(nodes[0], firstSubcategory);
expectSameCategory(nodes[1], secondSubCategory); expectSameCategory(nodes[1], secondSubCategory);
}); });
it('can parse when category has sub scripts', () => { it('can parse when category has sub scripts', () => {
// arrange // arrange
const categoryId = 31; const categoryId: ExecutableId = 'expected-category-id';
const scripts = [new ScriptStub('script1'), new ScriptStub('script2'), new ScriptStub('script3')]; const scripts: readonly Script[] = [
new ScriptStub('script1'),
new ScriptStub('script2'),
new ScriptStub('script3'),
];
const collection = new CategoryCollectionStub() const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(categoryId).withScripts(...scripts)); .withAction(new CategoryStub(categoryId).withScripts(...scripts));
// act // act
@@ -79,10 +97,11 @@ describe('CategoryNodeMetadataConverter', () => {
it('parseAllCategories parses as expected', () => { it('parseAllCategories parses as expected', () => {
// arrange // arrange
const collection = new CategoryCollectionStub() const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(0).withScriptIds('1, 2')) .withAction(new CategoryStub('category-1').withScriptIds('1, 2'))
.withAction(new CategoryStub(1).withCategories( .withAction(new CategoryStub('category-2').withCategories(
new CategoryStub(3).withScriptIds('3', '4'), new CategoryStub('category-2-subcategory-1').withScriptIds('3', '4'),
new CategoryStub(4).withCategory(new CategoryStub(5).withScriptIds('6')), new CategoryStub('category-2-subcategory-1')
.withCategory(new CategoryStub('category-2-subcategory-1-subcategory-1').withScriptIds('6')),
)); ));
// act // act
const nodes = parseAllCategories(collection); const nodes = parseAllCategories(collection);
@@ -100,8 +119,8 @@ function isReversible(category: Category): boolean {
return false; return false;
} }
} }
if (category.subCategories) { if (category.subcategories) {
if (category.subCategories.some((c) => !isReversible(c))) { if (category.subcategories.some((c) => !isReversible(c))) {
return false; return false;
} }
} }
@@ -110,17 +129,17 @@ function isReversible(category: Category): boolean {
function expectSameCategory(node: NodeMetadata, category: Category): void { function expectSameCategory(node: NodeMetadata, category: Category): void {
expect(node.type).to.equal(ExecutableType.Category, getErrorMessage('type')); expect(node.type).to.equal(ExecutableType.Category, getErrorMessage('type'));
expect(node.id).to.equal(getCategoryNodeId(category), getErrorMessage('id')); expect(node.id).to.equal(createNodeIdForExecutable(category), getErrorMessage('id'));
expect(node.docs).to.equal(category.docs, getErrorMessage('docs')); expect(node.docs).to.equal(category.docs, getErrorMessage('docs'));
expect(node.text).to.equal(category.name, getErrorMessage('name')); expect(node.text).to.equal(category.name, getErrorMessage('name'));
expect(node.isReversible).to.equal(isReversible(category), getErrorMessage('isReversible')); expect(node.isReversible).to.equal(isReversible(category), getErrorMessage('isReversible'));
expect(node.children).to.have.lengthOf( expect(node.children).to.have.lengthOf(
category.scripts.length + category.subCategories.length, category.scripts.length + category.subcategories.length,
getErrorMessage('total children'), getErrorMessage('total children'),
); );
if (category.subCategories) { if (category.subcategories) {
for (let i = 0; i < category.subCategories.length; i++) { for (let i = 0; i < category.subcategories.length; i++) {
expectSameCategory(node.children[i], category.subCategories[i]); expectSameCategory(node.children[i], category.subcategories[i]);
} }
} }
if (category.scripts) { if (category.scripts) {
@@ -137,7 +156,7 @@ function expectSameCategory(node: NodeMetadata, category: Category): void {
function expectSameScript(node: NodeMetadata, script: Script): void { function expectSameScript(node: NodeMetadata, script: Script): void {
expect(node.type).to.equal(ExecutableType.Script, getErrorMessage('type')); expect(node.type).to.equal(ExecutableType.Script, getErrorMessage('type'));
expect(node.id).to.equal(getScriptNodeId(script), getErrorMessage('id')); expect(node.id).to.equal(createNodeIdForExecutable(script), getErrorMessage('id'));
expect(node.docs).to.equal(script.docs, getErrorMessage('docs')); expect(node.docs).to.equal(script.docs, getErrorMessage('docs'));
expect(node.text).to.equal(script.name, getErrorMessage('name')); expect(node.text).to.equal(script.name, getErrorMessage('name'));
expect(node.isReversible).to.equal(script.canRevert(), getErrorMessage('canRevert')); expect(node.isReversible).to.equal(script.canRevert(), getErrorMessage('canRevert'));

View File

@@ -1,10 +1,12 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { useSelectedScriptNodeIds } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseSelectedScriptNodeIds'; import { useSelectedScriptNodeIds } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseSelectedScriptNodeIds';
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub'; import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
import { getScriptNodeId } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter'; import { createNodeIdForExecutable } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
import type { Script } from '@/domain/Executables/Script/Script'; import type { Script } from '@/domain/Executables/Script/Script';
import { UseUserSelectionStateStub } from '@tests/unit/shared/Stubs/UseUserSelectionStateStub'; import { UseUserSelectionStateStub } from '@tests/unit/shared/Stubs/UseUserSelectionStateStub';
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
import type { TreeNodeId } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
import type { Executable } from '@/domain/Executables/Executable';
describe('useSelectedScriptNodeIds', () => { describe('useSelectedScriptNodeIds', () => {
it('returns an empty array when no scripts are selected', () => { it('returns an empty array when no scripts are selected', () => {
@@ -23,7 +25,7 @@ describe('useSelectedScriptNodeIds', () => {
new SelectedScriptStub(new ScriptStub('id-1')), new SelectedScriptStub(new ScriptStub('id-1')),
new SelectedScriptStub(new ScriptStub('id-2')), new SelectedScriptStub(new ScriptStub('id-2')),
]; ];
const parsedNodeIds = new Map<Script, string>([ const parsedNodeIds = new Map<Script, TreeNodeId>([
[selectedScripts[0].script, 'expected-id-1'], [selectedScripts[0].script, 'expected-id-1'],
[selectedScripts[1].script, 'expected-id-2'], [selectedScripts[1].script, 'expected-id-2'],
]); ]);
@@ -47,7 +49,7 @@ describe('useSelectedScriptNodeIds', () => {
new SelectedScriptStub(new ScriptStub('id-1')), new SelectedScriptStub(new ScriptStub('id-1')),
new SelectedScriptStub(new ScriptStub('id-2')), new SelectedScriptStub(new ScriptStub('id-2')),
]; ];
const parsedNodeIds = new Map<Script, string>([ const parsedNodeIds = new Map<Script, TreeNodeId>([
[changedScripts[0].script, 'expected-id-1'], [changedScripts[0].script, 'expected-id-1'],
[changedScripts[1].script, 'expected-id-2'], [changedScripts[1].script, 'expected-id-2'],
]); ]);
@@ -68,9 +70,9 @@ describe('useSelectedScriptNodeIds', () => {
}); });
}); });
type ScriptNodeIdParser = typeof getScriptNodeId; type NodeIdParser = typeof createNodeIdForExecutable;
function createNodeIdParserFromMap(scriptToIdMap: Map<Script, string>): ScriptNodeIdParser { function createNodeIdParserFromMap(scriptToIdMap: Map<Executable, TreeNodeId>): NodeIdParser {
return (script) => { return (script) => {
const expectedId = scriptToIdMap.get(script); const expectedId = scriptToIdMap.get(script);
if (!expectedId) { if (!expectedId) {
@@ -81,12 +83,12 @@ function createNodeIdParserFromMap(scriptToIdMap: Map<Script, string>): ScriptNo
} }
function runHook(scenario?: { function runHook(scenario?: {
readonly scriptNodeIdParser?: ScriptNodeIdParser, readonly scriptNodeIdParser?: NodeIdParser,
readonly useSelectionState?: UseUserSelectionStateStub, readonly useSelectionState?: UseUserSelectionStateStub,
}) { }) {
const useSelectionStateStub = scenario?.useSelectionState ?? new UseUserSelectionStateStub(); const useSelectionStateStub = scenario?.useSelectionState ?? new UseUserSelectionStateStub();
const nodeIdParser: ScriptNodeIdParser = scenario?.scriptNodeIdParser const nodeIdParser: NodeIdParser = scenario?.scriptNodeIdParser
?? ((script) => script.id); ?? ((script) => script.executableId);
const returnObject = useSelectedScriptNodeIds(useSelectionStateStub.get(), nodeIdParser); const returnObject = useSelectedScriptNodeIds(useSelectionStateStub.get(), nodeIdParser);
return { return {
returnObject, returnObject,

View File

@@ -216,29 +216,29 @@ function itExpectedFilterTriggeredEvent(
{ {
description: 'returns true when category exists', description: 'returns true when category exists',
scriptMatches: [], scriptMatches: [],
categoryMatches: [new CategoryStub(1)], categoryMatches: [new CategoryStub('category-match-1')],
givenNode: createNode({ id: '1', hasParent: false }), givenNode: createNode({ id: 'category-match-1', hasParent: false }),
expectedPredicateResult: true, expectedPredicateResult: true,
}, },
{ {
description: 'returns true when script exists', description: 'returns true when script exists',
scriptMatches: [new ScriptStub('a')], scriptMatches: [new ScriptStub('script-match-1')],
categoryMatches: [], categoryMatches: [],
givenNode: createNode({ id: 'a', hasParent: true }), givenNode: createNode({ id: 'script-match-1', hasParent: true }),
expectedPredicateResult: true, expectedPredicateResult: true,
}, },
{ {
description: 'returns false when category is missing', description: 'returns false when category is missing',
scriptMatches: [new ScriptStub('b')], scriptMatches: [new ScriptStub('script-match-1')],
categoryMatches: [new CategoryStub(2)], categoryMatches: [new CategoryStub('category-match-1')],
givenNode: createNode({ id: '1', hasParent: false }), givenNode: createNode({ id: 'unrelated-node', hasParent: false }),
expectedPredicateResult: false, expectedPredicateResult: false,
}, },
{ {
description: 'finds false when script is missing', description: 'finds false when script is missing',
scriptMatches: [new ScriptStub('b')], scriptMatches: [new ScriptStub('script-match-1')],
categoryMatches: [new CategoryStub(1)], categoryMatches: [new CategoryStub('category-match-1')],
givenNode: createNode({ id: 'a', hasParent: true }), givenNode: createNode({ id: 'unrelated-node', hasParent: true }),
expectedPredicateResult: false, expectedPredicateResult: false,
}, },
]; ];
@@ -261,8 +261,8 @@ function itExpectedFilterTriggeredEvent(
expect(event.value.predicate).toBeDefined(); expect(event.value.predicate).toBeDefined();
const actualPredicateResult = event.value.predicate(givenNode); const actualPredicateResult = event.value.predicate(givenNode);
expect(actualPredicateResult).to.equal(expectedPredicateResult, formatAssertionMessage([ expect(actualPredicateResult).to.equal(expectedPredicateResult, formatAssertionMessage([
`Script matches (${scriptMatches.length}): [${scriptMatches.map((s) => s.id).join(', ')}]`, `Script matches (${scriptMatches.length}): [${scriptMatches.map((s) => s.executableId).join(', ')}]`,
`Category matches (${categoryMatches.length}): [${categoryMatches.map((s) => s.id).join(', ')}]`, `Category matches (${categoryMatches.length}): [${categoryMatches.map((s) => s.executableId).join(', ')}]`,
`Expected node: "${givenNode.id}"`, `Expected node: "${givenNode.id}"`,
])); ]));
}); });

View File

@@ -7,7 +7,7 @@ import { CategoryCollectionStateStub } from '@tests/unit/shared/Stubs/CategoryCo
import { UseCollectionStateStub } from '@tests/unit/shared/Stubs/UseCollectionStateStub'; import { UseCollectionStateStub } from '@tests/unit/shared/Stubs/UseCollectionStateStub';
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub'; import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub'; import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import type { NodeMetadata } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata'; import type { NodeMetadata } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
import { NodeMetadataStub } from '@tests/unit/shared/Stubs/NodeMetadataStub'; import { NodeMetadataStub } from '@tests/unit/shared/Stubs/NodeMetadataStub';
import { convertToNodeInput } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/TreeNodeMetadataConverter'; import { convertToNodeInput } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/TreeNodeMetadataConverter';

View File

@@ -1,5 +1,5 @@
import type { IApplication } from '@/domain/IApplication'; import type { IApplication } from '@/domain/IApplication';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
import { ProjectDetailsStub } from './ProjectDetailsStub'; import { ProjectDetailsStub } from './ProjectDetailsStub';

View File

@@ -1,6 +1,6 @@
import type { CategoryCollectionFactory } from '@/application/Parser/CategoryCollectionParser'; import type { CategoryCollectionFactory } from '@/application/Parser/CategoryCollectionParser';
import type { CategoryCollectionInitParameters } from '@/domain/CategoryCollection'; import type { CategoryCollectionInitParameters } from '@/domain/Collection/CategoryCollection';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { CategoryCollectionStub } from './CategoryCollectionStub'; import { CategoryCollectionStub } from './CategoryCollectionStub';
export function createCategoryCollectionFactorySpy(): { export function createCategoryCollectionFactorySpy(): {

View File

@@ -1,5 +1,5 @@
import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { getEnumValues } from '@/application/Common/Enum'; import { getEnumValues } from '@/application/Common/Enum';
import type { CollectionData } from '@/application/collections/'; import type { CollectionData } from '@/application/collections/';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';

View File

@@ -3,7 +3,7 @@ import type { ICategoryCollectionState } from '@/application/Context/State/ICate
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import type { Script } from '@/domain/Executables/Script/Script'; import type { Script } from '@/domain/Executables/Script/Script';
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub'; import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import type { UserSelection } from '@/application/Context/State/Selection/UserSelection'; import type { UserSelection } from '@/application/Context/State/Selection/UserSelection';
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import type { FilterContext } from '@/application/Context/State/Filter/FilterContext'; import type { FilterContext } from '@/application/Context/State/Filter/FilterContext';

View File

@@ -2,8 +2,9 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import type { Script } from '@/domain/Executables/Script/Script'; import type { Script } from '@/domain/Executables/Script/Script';
import type { Category } from '@/domain/Executables/Category/Category'; import type { Category } from '@/domain/Executables/Category/Category';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel'; import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import { ScriptStub } from './ScriptStub'; import { ScriptStub } from './ScriptStub';
import { ScriptingDefinitionStub } from './ScriptingDefinitionStub'; import { ScriptingDefinitionStub } from './ScriptingDefinitionStub';
import { CategoryStub } from './CategoryStub'; import { CategoryStub } from './CategoryStub';
@@ -22,9 +23,9 @@ export class CategoryCollectionStub implements ICategoryCollection {
public readonly actions = new Array<Category>(); public readonly actions = new Array<Category>();
public withSomeActions(): this { public withSomeActions(): this {
this.withAction(new CategoryStub(1)); this.withAction(new CategoryStub(`[${CategoryCollectionStub}]-action-1`));
this.withAction(new CategoryStub(2)); this.withAction(new CategoryStub(`[${CategoryCollectionStub}]-action-2`));
this.withAction(new CategoryStub(3)); this.withAction(new CategoryStub(`[${CategoryCollectionStub}]-action-3`));
return this; return this;
} }
@@ -60,9 +61,9 @@ export class CategoryCollectionStub implements ICategoryCollection {
return this; return this;
} }
public getCategory(categoryId: number): Category { public getCategory(categoryId: ExecutableId): Category {
return this.getAllCategories() return this.getAllCategories()
.find((category) => category.id === categoryId) .find((category) => category.executableId === categoryId)
?? new CategoryStub(categoryId); ?? new CategoryStub(categoryId);
} }
@@ -73,7 +74,7 @@ export class CategoryCollectionStub implements ICategoryCollection {
public getScript(scriptId: string): Script { public getScript(scriptId: string): Script {
return this.getAllScripts() return this.getAllScripts()
.find((script) => scriptId === script.id) .find((script) => scriptId === script.executableId)
?? new ScriptStub(scriptId); ?? new ScriptStub(scriptId);
} }
@@ -89,7 +90,7 @@ export class CategoryCollectionStub implements ICategoryCollection {
} }
function getSubCategoriesRecursively(category: Category): ReadonlyArray<Category> { function getSubCategoriesRecursively(category: Category): ReadonlyArray<Category> {
return (category.subCategories || []).flatMap( return (category.subcategories || []).flatMap(
(subCategory) => [subCategory, ...getSubCategoriesRecursively(subCategory)], (subCategory) => [subCategory, ...getSubCategoriesRecursively(subCategory)],
); );
} }
@@ -97,7 +98,7 @@ function getSubCategoriesRecursively(category: Category): ReadonlyArray<Category
function getScriptsRecursively(category: Category): ReadonlyArray<Script> { function getScriptsRecursively(category: Category): ReadonlyArray<Script> {
return [ return [
...(category.scripts || []), ...(category.scripts || []),
...(category.subCategories || []).flatMap( ...(category.subcategories || []).flatMap(
(subCategory) => getScriptsRecursively(subCategory), (subCategory) => getScriptsRecursively(subCategory),
), ),
]; ];

View File

@@ -1,6 +1,5 @@
import type { CategoryFactory } from '@/application/Parser/Executable/CategoryParser';
import type { CategoryInitParameters } from '@/domain/Executables/Category/CollectionCategory';
import type { Category } from '@/domain/Executables/Category/Category'; import type { Category } from '@/domain/Executables/Category/Category';
import type { CategoryFactory, CategoryInitParameters } from '@/domain/Executables/Category/CategoryFactory';
import { CategoryStub } from './CategoryStub'; import { CategoryStub } from './CategoryStub';
export function createCategoryFactorySpy(): { export function createCategoryFactorySpy(): {
@@ -10,7 +9,7 @@ export function createCategoryFactorySpy(): {
const createdCategories = new Map<Category, CategoryInitParameters>(); const createdCategories = new Map<Category, CategoryInitParameters>();
return { return {
categoryFactorySpy: (parameters) => { categoryFactorySpy: (parameters) => {
const category = new CategoryStub(55); const category = new CategoryStub('category-from-factory-stub');
createdCategories.set(category, parameters); createdCategories.set(category, parameters);
return category; return category;
}, },

View File

@@ -1,13 +1,13 @@
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import type { Category } from '@/domain/Executables/Category/Category'; import type { Category } from '@/domain/Executables/Category/Category';
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel'; import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
import type { Script } from '@/domain/Executables/Script/Script'; import type { Script } from '@/domain/Executables/Script/Script';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import { ScriptStub } from './ScriptStub'; import { ScriptStub } from './ScriptStub';
export class CategoryStub extends BaseEntity<number> implements Category { export class CategoryStub implements Category {
public name = `category-with-id-${this.id}`; public name = `[${CategoryStub.name}] name (ID: ${this.executableId})`;
public readonly subCategories = new Array<Category>(); public readonly subcategories = new Array<Category>();
public readonly scripts = new Array<Script>(); public readonly scripts = new Array<Script>();
@@ -15,25 +15,25 @@ export class CategoryStub extends BaseEntity<number> implements Category {
private allScriptsRecursively: (readonly Script[]) | undefined; private allScriptsRecursively: (readonly Script[]) | undefined;
public constructor(id: number) { public constructor(
super(id); readonly executableId: ExecutableId,
} ) { }
public includes(script: Script): boolean { public includes(script: Script): boolean {
return this.getAllScriptsRecursively().some((s) => s.id === script.id); return this.getAllScriptsRecursively().some((s) => s.executableId === script.executableId);
} }
public getAllScriptsRecursively(): readonly Script[] { public getAllScriptsRecursively(): readonly Script[] {
if (this.allScriptsRecursively === undefined) { if (this.allScriptsRecursively === undefined) {
return [ return [
...this.scripts, ...this.scripts,
...this.subCategories.flatMap((c) => c.getAllScriptsRecursively()), ...this.subcategories.flatMap((c) => c.getAllScriptsRecursively()),
]; ];
} }
return this.allScriptsRecursively; return this.allScriptsRecursively;
} }
public withScriptIds(...scriptIds: readonly string[]): this { public withScriptIds(...scriptIds: readonly ExecutableId[]): this {
return this.withScripts( return this.withScripts(
...scriptIds.map((id) => new ScriptStub(id)), ...scriptIds.map((id) => new ScriptStub(id)),
); );
@@ -70,7 +70,7 @@ export class CategoryStub extends BaseEntity<number> implements Category {
} }
public withCategory(category: Category): this { public withCategory(category: Category): this {
this.subCategories.push(category); this.subcategories.push(category);
return this; return this;
} }

View File

@@ -1,6 +1,6 @@
import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult'; import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult';
import type { FilterStrategy } from '@/application/Context/State/Filter/Strategy/FilterStrategy'; import type { FilterStrategy } from '@/application/Context/State/Filter/Strategy/FilterStrategy';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { FilterResultStub } from './FilterResultStub'; import { FilterResultStub } from './FilterResultStub';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls'; import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';

View File

@@ -1,14 +0,0 @@
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
export class NumericEntityStub extends BaseEntity<number> {
public customProperty = 'customProperty';
public constructor(id: number) {
super(id);
}
public withCustomProperty(value: string): NumericEntityStub {
this.customProperty = value;
return this;
}
}

View File

@@ -0,0 +1,14 @@
import type { RepositoryEntity, RepositoryEntityId } from '@/application/Repository/RepositoryEntity';
export class RepositoryEntityStub implements RepositoryEntity {
public customProperty = 'customProperty';
public constructor(
public readonly id: RepositoryEntityId,
) { }
public withCustomPropertyValue(value: string): RepositoryEntityStub {
this.customProperty = value;
return this;
}
}

View File

@@ -1,6 +1,5 @@
import type { ScriptFactory } from '@/application/Parser/Executable/Script/ScriptParser';
import type { Script } from '@/domain/Executables/Script/Script'; import type { Script } from '@/domain/Executables/Script/Script';
import type { ScriptInitParameters } from '@/domain/Executables/Script/CollectionScript'; import type { ScriptFactory, ScriptInitParameters } from '@/domain/Executables/Script/ScriptFactory';
import { ScriptStub } from './ScriptStub'; import { ScriptStub } from './ScriptStub';
export function createScriptFactorySpy(): { export function createScriptFactorySpy(): {

View File

@@ -1,15 +1,15 @@
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import type { Script } from '@/domain/Executables/Script/Script'; import type { Script } from '@/domain/Executables/Script/Script';
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel'; import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode'; import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
import { SelectedScriptStub } from './SelectedScriptStub'; import { SelectedScriptStub } from './SelectedScriptStub';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
export class ScriptStub extends BaseEntity<string> implements Script { export class ScriptStub implements Script {
public name = `name${this.id}`; public name = `name${this.executableId}`;
public code: ScriptCode = { public code: ScriptCode = {
execute: `REM execute-code (${this.id})`, execute: `REM execute-code (${this.executableId})`,
revert: `REM revert-code (${this.id})`, revert: `REM revert-code (${this.executableId})`,
}; };
public docs: readonly string[] = new Array<string>(); public docs: readonly string[] = new Array<string>();
@@ -18,9 +18,7 @@ export class ScriptStub extends BaseEntity<string> implements Script {
private isReversible: boolean | undefined = undefined; private isReversible: boolean | undefined = undefined;
constructor(public readonly id: string) { constructor(public readonly executableId: ExecutableId) { }
super(id);
}
public canRevert(): boolean { public canRevert(): boolean {
if (this.isReversible === undefined) { if (this.isReversible === undefined) {

View File

@@ -1,17 +1,18 @@
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import type { RepositoryEntityId } from '@/application/Repository/RepositoryEntity';
import type { Script } from '@/domain/Executables/Script/Script'; import type { Script } from '@/domain/Executables/Script/Script';
export class SelectedScriptStub implements SelectedScript { export class SelectedScriptStub implements SelectedScript {
public readonly script: Script; public readonly script: Script;
public readonly id: string; public readonly id: RepositoryEntityId;
public revert: boolean; public revert: boolean;
constructor( constructor(
script: Script, script: Script,
) { ) {
this.id = script.id; this.id = script.executableId;
this.script = script; this.script = script;
} }