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
They represent independently executable tweaks with documentation and reversibility.
An Executable is a logical entity that can
- execute once compiled,

View File

@@ -1,6 +1,6 @@
import type { IApplication } from '@/domain/IApplication';
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 { assertInRange } from '@/application/Common/Enum';
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 { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext';
import { ApplicationCode } from './Code/ApplicationCode';

View File

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

View File

@@ -1,5 +1,5 @@
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 { LinearFilterStrategy } from './Strategy/LinearFilterStrategy';
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';
export interface FilterStrategy {

View File

@@ -1,7 +1,7 @@
import type { Category } from '@/domain/Executables/Category/Category';
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
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 { AppliedFilterResult } from '../Result/AppliedFilterResult';
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 type { IApplicationCode } from './Code/IApplicationCode';
import type { ReadonlyFilterContext, FilterContext } from './Filter/FilterContext';

View File

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

View File

@@ -1,5 +1,5 @@
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 { CategorySelection } from './CategorySelection';
import type { ScriptSelection } from '../Script/ScriptSelection';
@@ -23,7 +23,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
return false;
}
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 scriptsChangesInCategory = scripts
.map((script): ScriptSelectionChange => ({
scriptId: script.id,
scriptId: script.executableId,
newStatus: {
...change.newStatus,
},

View File

@@ -2,7 +2,7 @@ import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryReposito
import type { Script } from '@/domain/Executables/Script/Script';
import { EventSource } from '@/infrastructure/Events/EventSource';
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 { UserSelectedScript } from './UserSelectedScript';
import type { ScriptSelection } from './ScriptSelection';
@@ -16,7 +16,7 @@ export type DebounceFunction = typeof batchedDebounce<ScriptSelectionChangeComma
export class DebouncedScriptSelection implements ScriptSelection {
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
private readonly scripts: Repository<string, SelectedScript>;
private readonly scripts: Repository<SelectedScript>;
public readonly processChanges: ScriptSelection['processChanges'];
@@ -25,7 +25,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
selectedScripts: ReadonlyArray<SelectedScript>,
debounce: DebounceFunction = batchedDebounce,
) {
this.scripts = new InMemoryRepository<string, SelectedScript>();
this.scripts = new InMemoryRepository<SelectedScript>();
for (const script of selectedScripts) {
this.scripts.addItem(script);
}
@@ -49,7 +49,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
public selectAll(): void {
const scriptsToSelect = this.collection
.getAllScripts()
.filter((script) => !this.scripts.exists(script.id))
.filter((script) => !this.scripts.exists(script.executableId))
.map((script) => new UserSelectedScript(script, false));
if (scriptsToSelect.length === 0) {
return;
@@ -116,9 +116,9 @@ export class DebouncedScriptSelection implements ScriptSelection {
private applyChange(change: ScriptSelectionChange): number {
const script = this.collection.getScript(change.scriptId);
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 {
@@ -152,24 +152,24 @@ function assertNonEmptyScriptSelection(selectedItems: readonly Script[]) {
}
function getScriptIdsToBeSelected(
existingItems: ReadonlyRepository<string, SelectedScript>,
existingItems: ReadonlyRepository<SelectedScript>,
desiredScripts: readonly Script[],
): string[] {
return desiredScripts
.filter((script) => !existingItems.exists(script.id))
.map((script) => script.id);
.filter((script) => !existingItems.exists(script.executableId))
.map((script) => script.executableId);
}
function getScriptIdsToBeDeselected(
existingItems: ReadonlyRepository<string, SelectedScript>,
existingItems: ReadonlyRepository<SelectedScript>,
desiredScripts: readonly Script[],
): string[] {
return existingItems
.getItems()
.filter((existing) => !desiredScripts.some((script) => existing.id === script.id))
.filter((existing) => !desiredScripts.some((script) => existing.id === script.executableId))
.map((script) => script.id);
}
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 { RepositoryEntity } from '@/application/Repository/RepositoryEntity';
type ScriptId = Script['id'];
export interface SelectedScript extends IEntity<ScriptId> {
export interface SelectedScript extends RepositoryEntity {
readonly script: Script;
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 { 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(
public readonly script: Script,
public readonly revert: boolean,
) {
super(script.id);
this.id = script.executableId;
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 { DebouncedScriptSelection } from './Script/DebouncedScriptSelection';
import type { CategorySelection } from './Category/CategorySelection';

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/';
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
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 type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
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 type { Script } from '@/domain/Executables/Script/Script';
import { createEnumParser, type EnumParser } from '@/application/Common/Enum';
import { createScript, type ScriptFactory } from '@/domain/Executables/Script/ScriptFactory';
import { parseDocs, type DocsParser } from '../DocumentationParser';
import { ExecutableType } from '../Validation/ExecutableType';
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from '../Validation/ExecutableValidator';
@@ -37,6 +37,7 @@ export const parseScript: ScriptParser = (
validateScript(data, validator);
try {
const script = scriptUtilities.createScript({
executableId: data.name, // arbitrary ID
name: data.name,
code: parseCode(
data,
@@ -132,14 +133,6 @@ interface ScriptParserUtilities {
readonly parseDocs: DocsParser;
}
export type ScriptFactory = (
...parameters: ConstructorParameters<typeof CollectionScript>
) => Script;
const createScript: ScriptFactory = (...parameters) => {
return new CollectionScript(...parameters);
};
const DefaultUtilities: ScriptParserUtilities = {
levelParser: createEnumParser(RecommendationLevel),
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;
getItems(predicate?: (entity: TEntity) => boolean): readonly TEntity[];
getById(id: TKey): TEntity;
exists(id: TKey): boolean;
getById(id: EntityId): TEntity;
exists(id: EntityId): boolean;
}
export interface MutableRepository<TKey, TEntity extends IEntity<TKey>> {
export interface MutableRepository<TEntity extends RepositoryEntity> {
addItem(item: TEntity): void;
addOrUpdateItem(item: TEntity): void;
removeItem(id: TKey): void;
removeItem(id: EntityId): void;
}
export interface Repository<TKey, TEntity extends IEntity<TKey>>
extends ReadonlyRepository<TKey, TEntity>, MutableRepository<TKey, TEntity> { }
export interface Repository<TEntity extends RepositoryEntity>
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
parameters:
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
children:
@@ -3424,7 +3425,7 @@ actions:
function: DisableService
parameters:
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
recommend: standard
@@ -3441,7 +3442,7 @@ actions:
function: DisableService
parameters:
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
docs: |-
@@ -3457,7 +3458,7 @@ actions:
function: DisableService
parameters:
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`)
docs: |-
@@ -3473,7 +3474,7 @@ actions:
function: DisableService
parameters:
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
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"
[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"
# 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:
-
function: Comment
@@ -4037,12 +4041,12 @@ actions:
function: DisableService
parameters:
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
function: DisableService
parameters:
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
children:
@@ -4200,7 +4204,7 @@ actions:
# "Set-Service" returns "Access is denied" since Windows 10 1809.
parameters:
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
recommend: standard
@@ -5299,7 +5303,7 @@ actions:
function: DisableService
parameters:
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
recommend: standard
@@ -5452,7 +5456,7 @@ actions:
function: DisableService
parameters:
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
docs: https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.DataCollection::EnableExperimentation
@@ -6377,7 +6381,7 @@ actions:
# function: DisableService
# parameters:
# 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
docs: |-
@@ -9266,7 +9270,7 @@ actions:
function: DisableService
parameters:
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
docs: |-
@@ -9412,7 +9416,7 @@ actions:
function: DisableService
parameters:
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
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"
# See defender status: Get-MpComputerStatus
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
docs: |-
@@ -12690,6 +12700,26 @@ actions:
grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
-
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
# ❗️ Following must be enabled and in running state:
@@ -12817,6 +12847,7 @@ actions:
- 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
call:
-
function: SetRegistryValue
parameters:
keyPath: HKLM\SOFTWARE\Policies\Microsoft\Windows Defender
@@ -12824,14 +12855,84 @@ actions:
dataType: REG_DWORD
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
# Status: Get-MpPreference
children:
-
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
# Formerly known as: Microsoft MAPS (Microsoft Active Protection Service), Microsoft SpyNet
docs: |-
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:
-
category: Disable Defender cloud protection features
@@ -12863,7 +12964,17 @@ actions:
docs:
- 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
# Managing with MpPreference module:
- https://docs.microsoft.com/fr-fr/powershell/module/defender/set-mppreference
call:
-
function: SetMpPreference
parameters:
property: CloudExtendedTimeout # Status: Get-MpPreference | Select-Object -Property CloudExtendedTimeout
value: "'50'" # Set: Set-MpPreference -Force -CloudExtendedTimeout '50'
default: $False # Default: 0 | Set-MpPreference -Force -CloudExtendedTimeout '0'
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
@@ -13044,7 +13155,10 @@ actions:
- 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://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:
-
function: SetRegistryValue
parameters:
keyPath: HKLM\Software\Policies\Microsoft\Windows Defender\MpEngine
@@ -13052,6 +13166,12 @@ actions:
dataType: REG_DWORD
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"
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,10 +13189,28 @@ actions:
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
-
name: Disable controlled folder access
docs:
- https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::ExploitGuard_ControlledFolderAccess_EnableControlledFolderAccess
- https://web.archive.org/web/20240314124339/https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/enable-controlled-folders?view=o365-worldwide
docs: |-
This script turns of controlled folder access feature.
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:
-
function: SetMpPreference
parameters:
property: EnableControlledFolderAccess # Status: Get-MpPreference | Select-Object -Property EnableControlledFolderAccess
value: 'Disabled' # Set: Set-MpPreference -Force -EnableControlledFolderAccess 'Enabled'
default: 'Disabled' # Default: Disabled | Remove-MpPreference -Force -EnableControlledFolderAccess | Set-MpPreference -Force -EnableControlledFolderAccess $False
-
function: SetRegistryValue
parameters:
keyPath: HKLM\Software\Policies\Microsoft\Windows Defender\Windows Defender Exploit Guard\Controlled Folder Access
@@ -13137,7 +13275,6 @@ actions:
value: $True # Set: Set-MpPreference -Force -DisableRealtimeMonitoring $True
# ❌ 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
-
function: SetRegistryValue
parameters:
@@ -13146,6 +13283,11 @@ actions:
dataType: REG_DWORD
data: "1"
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)
docs:
@@ -13267,7 +13409,7 @@ actions:
function: SetRegistryValue
parameters:
keyPath: HKLM\Software\Policies\Microsoft\Windows Defender\Real-Time Protection
valueName: DisableWindowsSpotlightFeatures
valueName: DisableOnAccessProtection
dataType: REG_DWORD
data: "1"
deleteOnRevert: 'true' # Missing by default since Windows 10 Pro (≥ 22H2) and Windows 11 Pro (≥ 23H2)
@@ -13309,6 +13451,67 @@ actions:
dataType: REG_DWORD
data: "1"
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
children:
@@ -13456,7 +13659,7 @@ actions:
dataType: REG_DWORD
data: '1'
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"
# docs: https://techcommunity.microsoft.com/t5/iis-support-blog/windows-10-device-guard-and-credential-guard-demystified/ba-p/376419
# children:
@@ -14963,13 +15166,15 @@ actions:
# 3. Try `DisableServiceInRegistryAsTrustedInstaller` as last effort.
children:
-
name: Disable "Microsoft Defender Antivirus Service"
name: Disable "Microsoft Defender Antivirus service" service
# ❗️ Breaks `Set-MpPreference` PowerShell cmdlet that helps to manage Defender
# E.g. `Set-MpPreference -Force -MAPSReporting 0` throws:
# `Set-MpPreference: Operation failed with the following error: 0x800106ba. Operation: Set-MpPreference.`
# `Target: MAPS_MAPSReporting. FullyQualifiedErrorId : HRESULT 0x800106ba,Set-MpPreference`
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
@@ -14977,6 +15182,14 @@ actions:
| ---------- | -------| ---------- |
| Windows 10 (≥ 22H2) | 🟢 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:
-
# 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 ...
# 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:
# - 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: |-
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
@@ -15022,8 +15244,14 @@ actions:
fileGlob: '%SYSTEMROOT%\System32\drivers\WdNisDrv.sys'
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: |-
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/20240314062047/https://batcmd.com/windows/10/services/wdfilter/
@@ -15044,15 +15272,20 @@ actions:
serviceName: WdFilter # Check: (Get-Service -Name 'WdFilter').StartType
defaultStartupMode: Boot # Allowed values: Boot | System | Automatic | Manual
# 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
parameters:
fileGlob: '%SYSTEMROOT%\System32\drivers\WdFilter.sys'
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: |-
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
@@ -15162,13 +15395,114 @@ actions:
fileGlob: '%WINDIR%\System32\SecurityHealthService.exe'
grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
-
category: Disable SmartScreen
docs:
- https://en.wikipedia.org/wiki/Microsoft_SmartScreen
- https://web.archive.org/web/20240314131452/https://learn.microsoft.com/en-us/windows/security/operating-system-security/virus-and-threat-protection/microsoft-defender-smartscreen/
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
docs: |-
Microsoft Defender SmartScreen helps safeguard users from phishing, malware websites, and potentially harmful downloads [2].
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:
-
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
docs: https://docs.microsoft.com/en-us/windows/security/threat-protection/microsoft-defender-smartscreen/microsoft-defender-smartscreen-overview
children:
-
name: Disable SmartScreen for apps and files
@@ -15397,9 +15731,38 @@ actions:
valueName: PreventOverride
dwordData: "0"
-
name: Disable SmartScreen in Internet Explorer
docs: https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.InternetExplorer::IZ_Policy_Phishing_9
name: Disable outdated SmartScreen in Internet Explorer
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:
-
function: SetRegistryValue
parameters:
keyPath: HKLM\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\0
@@ -15407,8 +15770,41 @@ actions:
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\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
docs: https://docs.microsoft.com/en-us/windows/security/threat-protection/microsoft-defender-smartscreen/microsoft-defender-smartscreen-overview
children:
-
name: Disable SmartScreen's "App Install Control" feature
@@ -15646,7 +16042,7 @@ actions:
function: DisableService
parameters:
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`)
docs: |-
@@ -15681,7 +16077,7 @@ actions:
function: DisableService
parameters:
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`)
docs: |-

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,29 +1,51 @@
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import type { Category } from './Category';
import type { Script } from '../Script/Script';
import type { Category } from '@/domain/Executables/Category/Category';
import type { Script } from '@/domain/Executables/Script/Script';
import type { ExecutableId } from '../Identifiable';
export class CollectionCategory extends BaseEntity<number> implements Category {
private allSubScripts?: ReadonlyArray<Script> = undefined;
export type CategoryFactory = (
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 docs: ReadonlyArray<string>;
public readonly subCategories: ReadonlyArray<Category>;
public readonly subcategories: ReadonlyArray<Category>;
public readonly scripts: ReadonlyArray<Script>;
private allSubScripts?: ReadonlyArray<Script> = undefined;
constructor(parameters: CategoryInitParameters) {
super(parameters.id);
validateParameters(parameters);
this.executableId = parameters.executableId;
this.name = parameters.name;
this.docs = parameters.docs;
this.subCategories = parameters.subcategories;
this.subcategories = parameters.subcategories;
this.scripts = parameters.scripts;
}
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[] {
@@ -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> {
return [
...category.scripts,
...category.subCategories.flatMap((c) => c.getAllScriptsRecursively()),
...category.subcategories.flatMap((c) => c.getAllScriptsRecursively()),
];
}
function validateParameters(parameters: CategoryInitParameters) {
if (!parameters.executableId) {
throw new Error('missing ID');
}
if (!parameters.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 { Identifiable } from './Identifiable';
export interface Executable<TExecutableKey>
extends Documentable, IEntity<TExecutableKey> {
export interface Executable
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 { ScriptCode } from './Code/ScriptCode';
export interface Script extends Executable<string>, Documentable {
export interface Script extends Executable, Documentable {
readonly name: string;
readonly level?: RecommendationLevel;
readonly code: ScriptCode;

View File

@@ -1,9 +1,26 @@
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import { RecommendationLevel } from './RecommendationLevel';
import type { Script } from './Script';
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 code: ScriptCode;
@@ -13,7 +30,7 @@ export class CollectionScript extends BaseEntity<string> implements Script {
public readonly level?: RecommendationLevel;
constructor(parameters: ScriptInitParameters) {
super(parameters.name);
this.executableId = parameters.executableId;
this.name = parameters.name;
this.code = parameters.code;
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) {
if (level !== undefined && !(level in RecommendationLevel)) {
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 { 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 { IEntity } from '../Entity/IEntity';
import type { RepositoryEntity } from '../../application/Repository/RepositoryEntity';
export class InMemoryRepository<TKey, TEntity extends IEntity<TKey>>
implements Repository<TKey, TEntity> {
export class InMemoryRepository<TEntity extends RepositoryEntity>
implements Repository<TEntity> {
private readonly items: TEntity[];
constructor(items?: TEntity[]) {
this.items = items ?? new Array<TEntity>();
constructor(items?: readonly TEntity[]) {
this.items = new Array<TEntity>();
if (items) {
this.items.push(...items);
}
}
public get length(): number {
@@ -17,7 +20,7 @@ implements Repository<TKey, TEntity> {
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);
if (!items.length) {
throw new Error(`missing item: ${id}`);
@@ -39,7 +42,7 @@ implements Repository<TKey, TEntity> {
this.items.push(item);
}
public removeItem(id: TKey): void {
public removeItem(id: string): void {
const index = this.items.findIndex((item) => item.id === id);
if (index === -1) {
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);
}
public exists(id: TKey): boolean {
public exists(id: string): boolean {
const index = this.items.findIndex((item) => item.id === id);
return index !== -1;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,11 +12,12 @@
</template>
<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 { injectKey } from '@/presentation/injectionSymbols';
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({
components: {
@@ -24,7 +25,7 @@ export default defineComponent({
},
props: {
categoryId: {
type: Number,
type: String as PropType<ExecutableId>,
required: true,
},
},
@@ -60,3 +61,4 @@ export default defineComponent({
font-size: $font-size-absolute-normal;
}
</style>
@/domain/Collection/ICategoryCollection

View File

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

View File

@@ -12,7 +12,7 @@ import {
} from 'vue';
import { injectKey } from '@/presentation/injectionSymbols';
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 ToggleSwitch from './ToggleSwitch.vue';
import type { Reverter } from './Reverter/Reverter';
@@ -64,3 +64,4 @@ export default defineComponent({
},
});
</script>
@/domain/Collection/ICategoryCollection

View File

@@ -1,17 +1,18 @@
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 { getCategoryId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import { createExecutableIdFromNodeId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter';
import { ScriptReverter } from './ScriptReverter';
import type { Reverter } from './Reverter';
export class CategoryReverter implements Reverter {
private readonly categoryId: number;
private readonly categoryId: ExecutableId;
private readonly scriptReverters: ReadonlyArray<ScriptReverter>;
constructor(nodeId: string, collection: ICategoryCollection) {
this.categoryId = getCategoryId(nodeId);
this.categoryId = createExecutableIdFromNodeId(nodeId);
this.scriptReverters = createScriptReverters(this.categoryId, collection);
}
@@ -37,12 +38,12 @@ export class CategoryReverter implements Reverter {
}
function createScriptReverters(
categoryId: number,
categoryId: ExecutableId,
collection: ICategoryCollection,
): ScriptReverter[] {
const category = collection.getCategory(categoryId);
const scripts = category
.getAllScriptsRecursively()
.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 { ScriptReverter } from './ScriptReverter';
import { CategoryReverter } from './CategoryReverter';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,20 +2,21 @@ import {
computed, shallowReadonly,
} from 'vue';
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(
useSelectionStateHook: ReturnType<typeof useUserSelectionState>,
scriptNodeIdParser = getScriptNodeId,
convertToNodeId = createNodeIdForExecutable,
) {
const { currentSelection } = useSelectionStateHook;
const selectedNodeIds = computed<readonly string[]>(() => {
const selectedNodeIds = computed<readonly TreeNodeId[]>(() => {
return currentSelection
.value
.scripts
.selectedScripts
.map((selected) => scriptNodeIdParser(selected.script));
.map((selected) => convertToNodeId(selected.script));
});
return {

View File

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

View File

@@ -1,15 +1,16 @@
import {
type Ref, computed, shallowReadonly,
} from 'vue';
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { injectKey } from '@/presentation/injectionSymbols';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import { parseSingleCategory, parseAllCategories } from './CategoryNodeMetadataConverter';
import { convertToNodeInput } from './TreeNodeMetadataConverter';
import type { TreeInputNodeData } from '../TreeView/Bindings/TreeInputNodeData';
import type { NodeMetadata } from '../NodeContent/NodeMetadata';
export function useTreeViewNodeInput(
categoryIdRef: Readonly<Ref<number | undefined>>,
categoryIdRef: Readonly<Ref<ExecutableId | undefined>>,
parser: CategoryNodeParser = {
parseSingle: parseSingleCategory,
parseAll: parseAllCategories,
@@ -30,7 +31,7 @@ export function useTreeViewNodeInput(
}
function parseNodes(
categoryId: number | undefined,
categoryId: ExecutableId | undefined,
categoryCollection: ICategoryCollection,
parser: CategoryNodeParser,
): 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);
return allDocumentedExecutables.map((executable): DocumentedExecutable => ({
executableLabel: `${executable.name} (${executable.id})`,
executableLabel: `${executable.name} (${executable.executableId})`,
docs: executable.docs.join('\n'),
}));
}

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
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 type { IApplicationFactory } from '@/application/IApplicationFactory';
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 { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
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 type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
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 type { FilterStrategy } from '@/application/Context/State/Filter/Strategy/FilterStrategy';
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('clearFilter', () => {

View File

@@ -48,7 +48,7 @@ describe('AppliedFilterResult', () => {
const expected = true;
const result = new ResultBuilder()
.withScriptMatches([])
.withCategoryMatches([new CategoryStub(5)])
.withCategoryMatches([new CategoryStub('matched-category-id')])
.build();
// act
const actual = result.hasAnyMatches();
@@ -58,8 +58,8 @@ describe('AppliedFilterResult', () => {
// arrange
const expected = true;
const result = new ResultBuilder()
.withScriptMatches([new ScriptStub('id')])
.withCategoryMatches([new CategoryStub(5)])
.withScriptMatches([new ScriptStub('matched-script-id')])
.withCategoryMatches([new CategoryStub('matched-category-id')])
.build();
// act
const actual = result.hasAnyMatches();
@@ -69,9 +69,13 @@ describe('AppliedFilterResult', () => {
});
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`;

View File

@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
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 { Script } from '@/domain/Executables/Script/Script';
import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult';
@@ -37,7 +37,10 @@ describe('LinearFilterStrategy', () => {
// arrange
const matchingFilter = 'matching filter';
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()
.withFilter(matchingFilter)
.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 { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
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 { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub';
import type { CategorySelectionChange } from '@/application/Context/State/Selection/Category/CategorySelectionChange';
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from '@/application/Context/State/Selection/Script/ScriptSelectionChange';
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
import type { Category } from '@/domain/Executables/Category/Category';
import type { Script } from '@/domain/Executables/Script/Script';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
describe('ScriptToCategorySelectionMapper', () => {
describe('areAllScriptsSelected', () => {
@@ -65,18 +64,18 @@ describe('ScriptToCategorySelectionMapper', () => {
readonly description: string;
readonly changes: readonly CategorySelectionChange[];
readonly categories: ReadonlyArray<{
readonly categoryId: Category['id'],
readonly scriptIds: readonly Script['id'][],
readonly categoryId: ExecutableId,
readonly scriptIds: readonly ExecutableId[],
}>;
readonly expected: readonly ScriptSelectionChange[],
}> = [
{
description: 'single script: select without revert',
categories: [
{ categoryId: 1, scriptIds: ['single-script'] },
{ categoryId: 'category-1', scriptIds: ['single-script'] },
],
changes: [
{ categoryId: 1, newStatus: { isSelected: true, isReverted: false } },
{ categoryId: 'category-1', newStatus: { isSelected: true, isReverted: false } },
],
expected: [
{ scriptId: 'single-script', newStatus: { isSelected: true, isReverted: false } },
@@ -85,12 +84,12 @@ describe('ScriptToCategorySelectionMapper', () => {
{
description: 'multiple scripts: select without revert',
categories: [
{ categoryId: 1, scriptIds: ['script1-cat1', 'script2-cat1'] },
{ categoryId: 2, scriptIds: ['script3-cat2'] },
{ categoryId: 'category-1', scriptIds: ['script1-cat1', 'script2-cat1'] },
{ categoryId: 'category-2', scriptIds: ['script3-cat2'] },
],
changes: [
{ categoryId: 1, newStatus: { isSelected: true, isReverted: false } },
{ categoryId: 2, newStatus: { isSelected: true, isReverted: false } },
{ categoryId: 'category-1', newStatus: { isSelected: true, isReverted: false } },
{ categoryId: 'category-2', newStatus: { isSelected: true, isReverted: false } },
],
expected: [
{ scriptId: 'script1-cat1', newStatus: { isSelected: true, isReverted: false } },
@@ -101,10 +100,10 @@ describe('ScriptToCategorySelectionMapper', () => {
{
description: 'single script: select with revert',
categories: [
{ categoryId: 1, scriptIds: ['single-script'] },
{ categoryId: 'category-1', scriptIds: ['single-script'] },
],
changes: [
{ categoryId: 1, newStatus: { isSelected: true, isReverted: true } },
{ categoryId: 'category-1', newStatus: { isSelected: true, isReverted: true } },
],
expected: [
{ scriptId: 'single-script', newStatus: { isSelected: true, isReverted: true } },
@@ -113,14 +112,14 @@ describe('ScriptToCategorySelectionMapper', () => {
{
description: 'multiple scripts: select with revert',
categories: [
{ categoryId: 1, scriptIds: ['script-1-cat-1'] },
{ categoryId: 2, scriptIds: ['script-2-cat-2'] },
{ categoryId: 3, scriptIds: ['script-3-cat-3'] },
{ categoryId: 'category-1', scriptIds: ['script-1-cat-1'] },
{ categoryId: 'category-2', scriptIds: ['script-2-cat-2'] },
{ categoryId: 'category-3', scriptIds: ['script-3-cat-3'] },
],
changes: [
{ categoryId: 1, newStatus: { isSelected: true, isReverted: true } },
{ categoryId: 2, newStatus: { isSelected: true, isReverted: true } },
{ categoryId: 3, newStatus: { isSelected: true, isReverted: true } },
{ categoryId: 'category-1', newStatus: { isSelected: true, isReverted: true } },
{ categoryId: 'category-2', newStatus: { isSelected: true, isReverted: true } },
{ categoryId: 'category-3', newStatus: { isSelected: true, isReverted: true } },
],
expected: [
{ scriptId: 'script-1-cat-1', newStatus: { isSelected: true, isReverted: true } },
@@ -131,10 +130,10 @@ describe('ScriptToCategorySelectionMapper', () => {
{
description: 'single script: deselect',
categories: [
{ categoryId: 1, scriptIds: ['single-script'] },
{ categoryId: 'category-1', scriptIds: ['single-script'] },
],
changes: [
{ categoryId: 1, newStatus: { isSelected: false } },
{ categoryId: 'category-1', newStatus: { isSelected: false } },
],
expected: [
{ scriptId: 'single-script', newStatus: { isSelected: false } },
@@ -143,12 +142,12 @@ describe('ScriptToCategorySelectionMapper', () => {
{
description: 'multiple scripts: deselect',
categories: [
{ categoryId: 1, scriptIds: ['script-1-cat1'] },
{ categoryId: 2, scriptIds: ['script-2-cat2'] },
{ categoryId: 'category-1', scriptIds: ['script-1-cat1'] },
{ categoryId: 'category-2', scriptIds: ['script-2-cat2'] },
],
changes: [
{ categoryId: 1, newStatus: { isSelected: false } },
{ categoryId: 2, newStatus: { isSelected: false } },
{ categoryId: 'category-1', newStatus: { isSelected: false } },
{ categoryId: 'category-2', newStatus: { isSelected: false } },
],
expected: [
{ scriptId: 'script-1-cat1', newStatus: { isSelected: false } },
@@ -158,14 +157,14 @@ describe('ScriptToCategorySelectionMapper', () => {
{
description: 'mixed operations (select, revert, deselect)',
categories: [
{ categoryId: 1, scriptIds: ['to-revert'] },
{ categoryId: 2, scriptIds: ['not-revert'] },
{ categoryId: 3, scriptIds: ['to-deselect'] },
{ categoryId: 'category-1', scriptIds: ['to-revert'] },
{ categoryId: 'category-2', scriptIds: ['not-revert'] },
{ categoryId: 'category-3', scriptIds: ['to-deselect'] },
],
changes: [
{ categoryId: 1, newStatus: { isSelected: true, isReverted: true } },
{ categoryId: 2, newStatus: { isSelected: true, isReverted: false } },
{ categoryId: 3, newStatus: { isSelected: false } },
{ categoryId: 'category-1', newStatus: { isSelected: true, isReverted: true } },
{ categoryId: 'category-2', newStatus: { isSelected: true, isReverted: false } },
{ categoryId: 'category-3', newStatus: { isSelected: false } },
],
expected: [
{ scriptId: 'to-revert', newStatus: { isSelected: true, isReverted: true } },
@@ -176,12 +175,12 @@ describe('ScriptToCategorySelectionMapper', () => {
{
description: 'affecting selected categories only',
categories: [
{ categoryId: 1, scriptIds: ['relevant-1', 'relevant-2'] },
{ categoryId: 2, scriptIds: ['not-relevant-1', 'not-relevant-2'] },
{ categoryId: 3, scriptIds: ['not-relevant-3', 'not-relevant-4'] },
{ categoryId: 'category-1', scriptIds: ['relevant-1', 'relevant-2'] },
{ categoryId: 'category-2', scriptIds: ['not-relevant-1', 'not-relevant-2'] },
{ categoryId: 'category-3', scriptIds: ['not-relevant-3', 'not-relevant-4'] },
],
changes: [
{ categoryId: 1, newStatus: { isSelected: true, isReverted: true } },
{ categoryId: 'category-1', newStatus: { isSelected: true, isReverted: true } },
],
expected: [
{ scriptId: 'relevant-1', newStatus: { isSelected: true, isReverted: true } },
@@ -198,7 +197,7 @@ describe('ScriptToCategorySelectionMapper', () => {
const sut = new ScriptToCategorySelectionMapperBuilder()
.withScriptSelection(scriptSelectionStub)
.withCollection(new CategoryCollectionStub().withAction(
new CategoryStub(99)
new CategoryStub('single-parent-category-action')
// Register scripts to test for nested items
.withAllScriptIdsRecursively(...categories.flatMap((c) => c.scriptIds))
.withCategories(...categories.map(
@@ -256,7 +255,7 @@ function setupTestWithPreselectedScripts(options: {
new ScriptStub('third-script'),
];
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
const collection = new CategoryCollectionStub().withAction(category);
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 { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
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 { BatchedDebounceStub } from '@tests/unit/shared/Stubs/BatchedDebounceStub';
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from '@/application/Context/State/Selection/Script/ScriptSelectionChange';
@@ -104,7 +104,7 @@ describe('DebouncedScriptSelection', () => {
const { scriptSelection, unselectedScripts } = setupTestWithPreselectedScripts({
preselect: (allScripts) => [allScripts[0]],
});
const scriptIdToCheck = unselectedScripts[0].id;
const scriptIdToCheck = unselectedScripts[0].executableId;
// act
const actual = scriptSelection.isSelected(scriptIdToCheck);
// assert
@@ -300,7 +300,7 @@ describe('DebouncedScriptSelection', () => {
preselect: (allScripts) => [allScripts[0], allScripts[1]]
.map((s) => s.toSelectedScript()),
getChanges: (allScripts) => [
{ scriptId: allScripts[2].id, newStatus: { isReverted: true, isSelected: true } },
{ scriptId: allScripts[2].executableId, newStatus: { isReverted: true, isSelected: true } },
],
getExpectedFinalSelection: (allScripts) => [
allScripts[0].toSelectedScript(),
@@ -313,7 +313,7 @@ describe('DebouncedScriptSelection', () => {
preselect: (allScripts) => [allScripts[0], allScripts[1]]
.map((s) => s.toSelectedScript()),
getChanges: (allScripts) => [
{ scriptId: allScripts[2].id, newStatus: { isReverted: false, isSelected: true } },
{ scriptId: allScripts[2].executableId, newStatus: { isReverted: false, isSelected: true } },
],
getExpectedFinalSelection: (allScripts) => [
allScripts[0].toSelectedScript(),
@@ -326,7 +326,7 @@ describe('DebouncedScriptSelection', () => {
preselect: (allScripts) => [allScripts[0], allScripts[1]]
.map((s) => s.toSelectedScript()),
getChanges: (allScripts) => [
{ scriptId: allScripts[0].id, newStatus: { isSelected: false } },
{ scriptId: allScripts[0].executableId, newStatus: { isSelected: false } },
],
getExpectedFinalSelection: (allScripts) => [
allScripts[1].toSelectedScript(),
@@ -339,7 +339,7 @@ describe('DebouncedScriptSelection', () => {
allScripts[1].toSelectedScript(),
],
getChanges: (allScripts) => [
{ scriptId: allScripts[0].id, newStatus: { isSelected: true, isReverted: true } },
{ scriptId: allScripts[0].executableId, newStatus: { isSelected: true, isReverted: true } },
],
getExpectedFinalSelection: (allScripts) => [
allScripts[0].toSelectedScript().withRevert(true),
@@ -353,7 +353,7 @@ describe('DebouncedScriptSelection', () => {
allScripts[1].toSelectedScript(),
],
getChanges: (allScripts) => [
{ scriptId: allScripts[0].id, newStatus: { isSelected: true, isReverted: false } },
{ scriptId: allScripts[0].executableId, newStatus: { isSelected: true, isReverted: false } },
],
getExpectedFinalSelection: (allScripts) => [
allScripts[0].toSelectedScript().withRevert(false),
@@ -367,9 +367,9 @@ describe('DebouncedScriptSelection', () => {
allScripts[2].toSelectedScript(), // remove
],
getChanges: (allScripts) => [
{ scriptId: allScripts[0].id, newStatus: { isSelected: true, isReverted: false } },
{ scriptId: allScripts[1].id, newStatus: { isSelected: true, isReverted: true } },
{ scriptId: allScripts[2].id, newStatus: { isSelected: false } },
{ scriptId: allScripts[0].executableId, newStatus: { isSelected: true, isReverted: false } },
{ scriptId: allScripts[1].executableId, newStatus: { isSelected: true, isReverted: true } },
{ scriptId: allScripts[2].executableId, newStatus: { isSelected: false } },
],
getExpectedFinalSelection: (allScripts) => [
allScripts[0].toSelectedScript().withRevert(false),
@@ -408,7 +408,7 @@ describe('DebouncedScriptSelection', () => {
description: 'does not change selection for an already selected script',
preselect: (allScripts) => [allScripts[0].toSelectedScript().withRevert(true)],
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]]
.map((s) => s.toSelectedScript()),
getChanges: (allScripts) => [
{ scriptId: allScripts[2].id, newStatus: { isSelected: false } },
{ scriptId: allScripts[2].executableId, newStatus: { isSelected: false } },
],
},
{
description: 'handles no mutations for mixed unchanged operations',
preselect: (allScripts) => [allScripts[0].toSelectedScript().withRevert(false)],
getChanges: (allScripts) => [
{ scriptId: allScripts[0].id, newStatus: { isSelected: true, isReverted: false } },
{ scriptId: allScripts[1].id, newStatus: { isSelected: false } },
{ scriptId: allScripts[0].executableId, newStatus: { isSelected: true, isReverted: false } },
{ scriptId: allScripts[1].executableId, newStatus: { isSelected: false } },
],
},
];
@@ -459,7 +459,7 @@ describe('DebouncedScriptSelection', () => {
.build();
const expectedCommand: ScriptSelectionChangeCommand = {
changes: [
{ scriptId: script.id, newStatus: { isReverted: true, isSelected: true } },
{ scriptId: script.executableId, newStatus: { isReverted: true, isSelected: true } },
],
};
// act
@@ -481,7 +481,7 @@ describe('DebouncedScriptSelection', () => {
// act
selection.processChanges({
changes: [
{ scriptId: script.id, newStatus: { isReverted: true, isSelected: true } },
{ scriptId: script.executableId, newStatus: { isReverted: true, isSelected: true } },
],
});
// assert
@@ -502,7 +502,7 @@ describe('DebouncedScriptSelection', () => {
// act
selection.processChanges({
changes: [
{ scriptId: script.id, newStatus: { isReverted: true, isSelected: true } },
{ scriptId: script.executableId, newStatus: { isReverted: true, isSelected: true } },
],
});
debounceStub.execute();
@@ -525,7 +525,7 @@ describe('DebouncedScriptSelection', () => {
for (const script of scripts) {
selection.processChanges({
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;
})();
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 scriptSelection = new DebouncedScriptSelectionBuilder()

View File

@@ -1,7 +1,7 @@
import { describe, it } from 'vitest';
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
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 type { ScriptsFactory, CategoriesFactory } from '@/application/Context/State/Selection/UserSelectionFacade';
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 { NonEmptyCollectionAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator';
import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub';
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
describe('ApplicationParser', () => {
describe('parseApplication', () => {

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
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 DocsParser } from '@/application/Parser/Executable/DocumentationParser';
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 { indentText } from '@tests/shared/Text';
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 { itValidatesName, itValidatesType, itAsserts } from './Validation/ExecutableValidationTester';
import { generateDataValidationTestScenarios } from './Validation/DataValidationTestScenarioGenerator';
describe('CategoryParser', () => {
describe('parseCategory', () => {
describe('validation', () => {
describe('validates for name', () => {
describe('id', () => {
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
const expectedName = 'expected category name to be validated';
const category = new CategoryDataStub()
@@ -38,7 +72,7 @@ describe('CategoryParser', () => {
};
itValidatesName((validatorFactory) => {
// act
new TestBuilder()
new TestContext()
.withData(category)
.withValidatorFactory(validatorFactory)
.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
const category = new CategoryDataStub();
const expectedContext: CategoryErrorContext = {
@@ -63,7 +123,7 @@ describe('CategoryParser', () => {
itValidatesType(
(validatorFactory) => {
// act
new TestBuilder()
new TestContext()
.withData(category)
.withValidatorFactory(validatorFactory)
.parseCategory();
@@ -90,7 +150,7 @@ describe('CategoryParser', () => {
itValidatesType(
(validatorFactory) => {
// act
new TestBuilder()
new TestContext()
.withData(category)
.withValidatorFactory(validatorFactory)
.parseCategory();
@@ -102,6 +162,8 @@ describe('CategoryParser', () => {
},
);
});
});
describe('children', () => {
describe('validates children for non-empty collection', () => {
// arrange
const category = new CategoryDataStub()
@@ -117,7 +179,7 @@ describe('CategoryParser', () => {
itValidatesType(
(validatorFactory) => {
// act
new TestBuilder()
new TestContext()
.withData(category)
.withValidatorFactory(validatorFactory)
.parseCategory();
@@ -167,7 +229,7 @@ describe('CategoryParser', () => {
parentCategory: parent,
};
// act
new TestBuilder()
new TestContext()
.withData(parent)
.withValidatorFactory(validatorFactory)
.parseCategory();
@@ -201,7 +263,7 @@ describe('CategoryParser', () => {
itValidatesType(
(validatorFactory) => {
// act
new TestBuilder()
new TestContext()
.withData(parent)
.withValidatorFactory(validatorFactory)
.parseCategory();
@@ -231,7 +293,7 @@ describe('CategoryParser', () => {
};
itValidatesName((validatorFactory) => {
// act
new TestBuilder()
new TestContext()
.withData(parent)
.withValidatorFactory(validatorFactory)
.parseCategory();
@@ -243,54 +305,7 @@ describe('CategoryParser', () => {
});
});
});
});
describe('rethrows exception if category factory fails', () => {
// arrange
const givenData = new CategoryDataStub();
const expectedContextMessage = 'Failed to parse category.';
const expectedError = new Error();
// act & assert
itThrowsContextualError({
throwingAction: (wrapError) => {
const validatorStub = new ExecutableValidatorStub();
validatorStub.createContextualErrorMessage = (message) => message;
const factoryMock: CategoryFactory = () => {
throw expectedError;
};
new TestBuilder()
.withCategoryFactory(factoryMock)
.withValidatorFactory(() => validatorStub)
.withErrorWrapper(wrapError)
.withData(givenData)
.parseCategory();
},
expectedWrappedError: expectedError,
expectedContextMessage,
});
});
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 TestBuilder()
.withData(categoryData)
.withCategoryFactory(categoryFactorySpy)
.withDocsParser(parseDocs)
.parseCategory();
// assert
const actualDocs = getInitParameters(actualCategory)?.docs;
expect(actualDocs).to.deep.equal(expectedDocs);
});
describe('parses expected subscript', () => {
describe('parses correct subscript', () => {
it('parses single script correctly', () => {
// arrange
const expectedScript = new ScriptStub('expected script');
@@ -301,7 +316,7 @@ describe('CategoryParser', () => {
scriptParser.setupParsedResultForData(childScriptData, expectedScript);
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
// act
const actualCategory = new TestBuilder()
const actualCategory = new TestContext()
.withData(categoryData)
.withScriptParser(scriptParser.get())
.withCategoryFactory(categoryFactorySpy)
@@ -331,7 +346,7 @@ describe('CategoryParser', () => {
.withChildren(childrenData);
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
// act
const actualCategory = new TestBuilder()
const actualCategory = new TestContext()
.withScriptParser(scriptParser.get())
.withData(categoryData)
.withCategoryFactory(categoryFactorySpy)
@@ -355,7 +370,7 @@ describe('CategoryParser', () => {
.withChildren(childrenData);
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
// act
const actualCategory = new TestBuilder()
const actualCategory = new TestContext()
.withData(categoryData)
.withCollectionUtilities(expected)
.withScriptParser(scriptParser.get())
@@ -379,9 +394,9 @@ describe('CategoryParser', () => {
).to.equal(true);
});
});
it('returns expected subcategories', () => {
it('parses correct subcategories', () => {
// arrange
const expectedChildCategory = new CategoryStub(33);
const expectedChildCategory = new CategoryStub('expected-child-category');
const childCategoryData = new CategoryDataStub()
.withName('expected child category')
.withChildren([createScriptDataWithCode()]);
@@ -390,7 +405,7 @@ describe('CategoryParser', () => {
.withChildren([childCategoryData]);
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
// act
const actualCategory = new TestBuilder()
const actualCategory = new TestContext()
.withData(categoryData)
.withCategoryFactory((parameters) => {
if (parameters.name === childCategoryData.category) {
@@ -406,15 +421,53 @@ describe('CategoryParser', () => {
expect(actualSubcategories[0]).to.equal(expectedChildCategory);
});
});
describe('category creation', () => {
it('creates category from the factory', () => {
// arrange
const expectedCategory = new CategoryStub('expected-category');
const categoryFactory: CategoryFactory = () => expectedCategory;
// act
const actualCategory = new TestContext()
.withCategoryFactory(categoryFactory)
.parseCategory();
// assert
expect(actualCategory).to.equal(expectedCategory);
});
describe('rethrows exception if category factory fails', () => {
// arrange
const givenData = new CategoryDataStub();
const expectedContextMessage = 'Failed to parse category.';
const expectedError = new Error();
// act & assert
itThrowsContextualError({
throwingAction: (wrapError) => {
const validatorStub = new ExecutableValidatorStub();
validatorStub.createContextualErrorMessage = (message) => message;
const factoryMock: CategoryFactory = () => {
throw expectedError;
};
new TestContext()
.withCategoryFactory(factoryMock)
.withValidatorFactory(() => validatorStub)
.withErrorWrapper(wrapError)
.withData(givenData)
.parseCategory();
},
expectedWrappedError: expectedError,
expectedContextMessage,
});
});
});
});
});
class TestBuilder {
class TestContext {
private data: CategoryData = new CategoryDataStub();
private collectionUtilities:
CategoryCollectionSpecificUtilitiesStub = new CategoryCollectionSpecificUtilitiesStub();
private categoryFactory: CategoryFactory = () => new CategoryStub(33);
private categoryFactory: CategoryFactory = createCategoryFactorySpy().categoryFactorySpy;
private errorWrapper: ErrorWithContextWrapper = new ErrorWrapperStub().get();

View File

@@ -29,11 +29,150 @@ import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/C
import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub';
import type { CategoryCollectionSpecificUtilities } from '@/application/Parser/Executable/CategoryCollectionSpecificUtilities';
import type { ObjectAssertion } from '@/application/Parser/Common/TypeValidator';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import { itAsserts, itValidatesType, itValidatesName } from '../Validation/ExecutableValidationTester';
import { generateDataValidationTestScenarios } from '../Validation/DataValidationTestScenarioGenerator';
describe('ScriptParser', () => {
describe('parseScript', () => {
describe('property validation', () => {
describe('validates object', () => {
// arrange
const expectedScript = createScriptDataWithCall();
const expectedContext: ScriptErrorContext = {
type: ExecutableType.Script,
self: expectedScript,
};
const expectedAssertion: ObjectAssertion<CallScriptData & CodeScriptData> = {
value: expectedScript,
valueName: expectedScript.name,
allowedProperties: [
'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
],
};
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,
};
},
});
});
});
});
});
describe('id', () => {
it('creates ID correctly', () => {
// arrange
const expectedId: ExecutableId = 'expected-id';
const scriptData = createScriptDataWithCode()
.withName(expectedId);
const { scriptFactorySpy, getInitParameters } = createScriptFactorySpy();
// act
const actualScript = new TestContext()
.withData(scriptData)
.withScriptFactory(scriptFactorySpy)
.parseScript();
// assert
const actualId = getInitParameters(actualScript)?.executableId;
expect(actualId).to.equal(expectedId);
});
});
describe('name', () => {
it('parses name correctly', () => {
// arrange
const expected = 'test-expected-name';
@@ -49,6 +188,30 @@ describe('ScriptParser', () => {
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'];
@@ -66,16 +229,6 @@ describe('ScriptParser', () => {
const actualDocs = getInitParameters(actualScript)?.docs;
expect(actualDocs).to.deep.equal(expectedDocs);
});
it('gets script from the factory', () => {
// arrange
const expectedScript = new ScriptStub('expected-script');
const scriptFactory: ScriptFactory = () => expectedScript;
// act
const actualScript = new TestContext()
.withScriptFactory(scriptFactory)
.parseScript();
// assert
expect(actualScript).to.equal(expectedScript);
});
describe('level', () => {
describe('generated `undefined` level if given absent value', () => {
@@ -261,147 +414,17 @@ describe('ScriptParser', () => {
});
});
});
describe('validation', () => {
describe('validates for name', () => {
describe('script creation', () => {
it('creates script from the factory', () => {
// arrange
const expectedName = 'expected script name to be validated';
const script = createScriptDataWithCall()
.withName(expectedName);
const expectedContext: ScriptErrorContext = {
type: ExecutableType.Script,
self: script,
};
itValidatesName((validatorFactory) => {
const expectedScript = new ScriptStub('expected-script');
const scriptFactory: ScriptFactory = () => expectedScript;
// act
new TestContext()
.withData(script)
.withValidatorFactory(validatorFactory)
const actualScript = new TestContext()
.withScriptFactory(scriptFactory)
.parseScript();
// assert
return {
expectedNameToValidate: expectedName,
expectedErrorContext: expectedContext,
};
});
});
describe('validates for defined data', () => {
// arrange
const expectedScript = createScriptDataWithCall();
const expectedContext: ScriptErrorContext = {
type: ExecutableType.Script,
self: expectedScript,
};
const expectedAssertion: ObjectAssertion<CallScriptData & CodeScriptData> = {
value: expectedScript,
valueName: expectedScript.name,
allowedProperties: [
'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
],
};
itValidatesType(
(validatorFactory) => {
// act
new TestContext()
.withData(expectedScript)
.withValidatorFactory(validatorFactory)
.parseScript();
// assert
return {
expectedDataToValidate: expectedScript,
expectedErrorContext: expectedContext,
assertValidation: (validator) => validator.assertObject(expectedAssertion),
};
},
);
});
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,
};
},
});
});
});
});
expect(actualScript).to.equal(expectedScript);
});
describe('rethrows exception if script factory fails', () => {
// arrange
@@ -432,6 +455,7 @@ describe('ScriptParser', () => {
});
});
});
});
class TestContext {
private data: ScriptData = createScriptDataWithCode();

View File

@@ -3,7 +3,7 @@ import { Application } from '@/domain/Application';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
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';
describe('Application', () => {

View File

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

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

View File

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

View File

@@ -145,7 +145,7 @@ describe('RecommendationStatusHandler', () => {
return `total: ${testCase.selection.length}\n`
+ 'scripts:\n'
+ 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(' | ');
}
});

View File

@@ -5,7 +5,7 @@ import { CategoryReverter } from '@/presentation/components/Scripts/View/Tree/No
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
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';
describe('ReverterFactory', () => {
@@ -24,7 +24,7 @@ describe('ReverterFactory', () => {
it('gets ScriptReverter for script node', () => {
// arrange
const script = new ScriptStub('test');
const node = getNodeContentStub(getScriptNodeId(script), NodeType.Script);
const node = getNodeContentStub(createNodeIdForExecutable(script), NodeType.Script);
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(0).withScript(script));
// 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 { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
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 type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import { ScriptSelectionStub } from '@tests/unit/shared/Stubs/ScriptSelectionStub';
@@ -11,7 +11,7 @@ describe('ScriptReverter', () => {
describe('getState', () => {
// arrange
const script = new ScriptStub('id');
const nodeId = getScriptNodeId(script);
const nodeId = createNodeIdForExecutable(script);
const testScenarios: ReadonlyArray<{
readonly description: string;
readonly selectedScripts: readonly SelectedScript[];
@@ -98,7 +98,7 @@ describe('ScriptReverter', () => {
expectedRevert: false,
},
];
const nodeId = getScriptNodeId(script);
const nodeId = createNodeIdForExecutable(script);
testScenarios.forEach((
{ description, selection, expectedRevert },
) => {
@@ -111,7 +111,7 @@ describe('ScriptReverter', () => {
// act
sut.selectWithRevertState(revertState, userSelection);
// 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 { TreeNodeHierarchy } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/Hierarchy/TreeNodeHierarchy';
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('constructor', () => {
describe('id', () => {
it('should initialize with the provided id', () => {
// arrange
const expectedId = 'test-id';
const expectedId: TreeNodeId = 'test-id';
// act
const node = new TreeNodeManager(expectedId);
// assert
@@ -18,9 +19,10 @@ describe('TreeNodeManager', () => {
describe('should throw an error if id is not provided', () => {
itEachAbsentStringValue((absentId) => {
// arrange
const id = absentId as TreeNodeId;
const expectedError = 'missing id';
// act
const act = () => new TreeNodeManager(absentId);
const act = () => new TreeNodeManager(id);
// assert
expect(act).to.throw(expectedError);
}, { 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 { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
import {
getCategoryId, getCategoryNodeId, getScriptId,
getScriptNodeId, parseAllCategories, parseSingleCategory,
createExecutableIdFromNodeId,
createNodeIdForExecutable,
parseAllCategories,
parseSingleCategory,
} from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
import { ExecutableType } from '@/application/Parser/Executable/Validation/ExecutableType';
import type { NodeMetadata } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
describe('CategoryNodeMetadataConverter', () => {
it('can convert script id and back', () => {
// arrange
const script = new ScriptStub('test');
const expectedScriptId: ExecutableId = 'expected-script-id';
const script = new ScriptStub(expectedScriptId);
// act
const nodeId = getScriptNodeId(script);
const scriptId = getScriptId(nodeId);
const nodeId = createNodeIdForExecutable(script);
const actualScriptId = createExecutableIdFromNodeId(nodeId);
// assert
expect(scriptId).to.equal(script.id);
expect(actualScriptId).to.equal(expectedScriptId);
});
it('can convert category id and back', () => {
// arrange
const category = new CategoryStub(55);
const expectedCategoryId: ExecutableId = 'expected-category-id';
const category = new CategoryStub(expectedCategoryId);
// act
const nodeId = getCategoryNodeId(category);
const scriptId = getCategoryId(nodeId);
const nodeId = createNodeIdForExecutable(category);
const actualCategoryId = createExecutableIdFromNodeId(nodeId);
// assert
expect(scriptId).to.equal(category.id);
expect(actualCategoryId).to.equal(expectedCategoryId);
});
describe('parseSingleCategory', () => {
it('throws error if parent category cannot be retrieved', () => {
@@ -38,32 +43,45 @@ describe('CategoryNodeMetadataConverter', () => {
const collection = new CategoryCollectionStub();
collection.getCategory = () => { throw new Error(expectedError); };
// act
const act = () => parseSingleCategory(31, collection);
const act = () => parseSingleCategory('unimportant-id', collection);
// assert
expect(act).to.throw(expectedError);
});
it('can parse when category has sub categories', () => {
// arrange
const categoryId = 31;
const firstSubCategory = new CategoryStub(11).withScriptIds('111', '112');
const secondSubCategory = new CategoryStub(categoryId)
.withCategory(new CategoryStub(33).withScriptIds('331', '331'))
.withCategory(new CategoryStub(44).withScriptIds('44'));
const collection = new CategoryCollectionStub().withAction(new CategoryStub(categoryId)
.withCategory(firstSubCategory)
.withCategory(secondSubCategory));
const parentCategoryId: ExecutableId = 'parent-category';
const firstSubcategory = new CategoryStub('subcategory-1')
.withScriptIds('subcategory-1-script-1', 'subcategory-1-script-2');
const secondSubCategory = new CategoryStub('subcategory-2')
.withCategory(
new CategoryStub('subcategory-2-subcategory-1')
.withScriptIds('subcategory-2-subcategory-1-script-1', 'subcategory-2-subcategory-1-script-2'),
)
.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
const nodes = parseSingleCategory(categoryId, collection);
const nodes = parseSingleCategory(parentCategoryId, collection);
// assert
expectExists(nodes);
expect(nodes).to.have.lengthOf(2);
expectSameCategory(nodes[0], firstSubCategory);
expectSameCategory(nodes[0], firstSubcategory);
expectSameCategory(nodes[1], secondSubCategory);
});
it('can parse when category has sub scripts', () => {
// arrange
const categoryId = 31;
const scripts = [new ScriptStub('script1'), new ScriptStub('script2'), new ScriptStub('script3')];
const categoryId: ExecutableId = 'expected-category-id';
const scripts: readonly Script[] = [
new ScriptStub('script1'),
new ScriptStub('script2'),
new ScriptStub('script3'),
];
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(categoryId).withScripts(...scripts));
// act
@@ -79,10 +97,11 @@ describe('CategoryNodeMetadataConverter', () => {
it('parseAllCategories parses as expected', () => {
// arrange
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(0).withScriptIds('1, 2'))
.withAction(new CategoryStub(1).withCategories(
new CategoryStub(3).withScriptIds('3', '4'),
new CategoryStub(4).withCategory(new CategoryStub(5).withScriptIds('6')),
.withAction(new CategoryStub('category-1').withScriptIds('1, 2'))
.withAction(new CategoryStub('category-2').withCategories(
new CategoryStub('category-2-subcategory-1').withScriptIds('3', '4'),
new CategoryStub('category-2-subcategory-1')
.withCategory(new CategoryStub('category-2-subcategory-1-subcategory-1').withScriptIds('6')),
));
// act
const nodes = parseAllCategories(collection);
@@ -100,8 +119,8 @@ function isReversible(category: Category): boolean {
return false;
}
}
if (category.subCategories) {
if (category.subCategories.some((c) => !isReversible(c))) {
if (category.subcategories) {
if (category.subcategories.some((c) => !isReversible(c))) {
return false;
}
}
@@ -110,17 +129,17 @@ function isReversible(category: Category): boolean {
function expectSameCategory(node: NodeMetadata, category: Category): void {
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.text).to.equal(category.name, getErrorMessage('name'));
expect(node.isReversible).to.equal(isReversible(category), getErrorMessage('isReversible'));
expect(node.children).to.have.lengthOf(
category.scripts.length + category.subCategories.length,
category.scripts.length + category.subcategories.length,
getErrorMessage('total children'),
);
if (category.subCategories) {
for (let i = 0; i < category.subCategories.length; i++) {
expectSameCategory(node.children[i], category.subCategories[i]);
if (category.subcategories) {
for (let i = 0; i < category.subcategories.length; i++) {
expectSameCategory(node.children[i], category.subcategories[i]);
}
}
if (category.scripts) {
@@ -137,7 +156,7 @@ function expectSameCategory(node: NodeMetadata, category: Category): void {
function expectSameScript(node: NodeMetadata, script: Script): void {
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.text).to.equal(script.name, getErrorMessage('name'));
expect(node.isReversible).to.equal(script.canRevert(), getErrorMessage('canRevert'));

View File

@@ -1,10 +1,12 @@
import { describe, it, expect } from 'vitest';
import { useSelectedScriptNodeIds } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseSelectedScriptNodeIds';
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 { UseUserSelectionStateStub } from '@tests/unit/shared/Stubs/UseUserSelectionStateStub';
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', () => {
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-2')),
];
const parsedNodeIds = new Map<Script, string>([
const parsedNodeIds = new Map<Script, TreeNodeId>([
[selectedScripts[0].script, 'expected-id-1'],
[selectedScripts[1].script, 'expected-id-2'],
]);
@@ -47,7 +49,7 @@ describe('useSelectedScriptNodeIds', () => {
new SelectedScriptStub(new ScriptStub('id-1')),
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[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) => {
const expectedId = scriptToIdMap.get(script);
if (!expectedId) {
@@ -81,12 +83,12 @@ function createNodeIdParserFromMap(scriptToIdMap: Map<Script, string>): ScriptNo
}
function runHook(scenario?: {
readonly scriptNodeIdParser?: ScriptNodeIdParser,
readonly scriptNodeIdParser?: NodeIdParser,
readonly useSelectionState?: UseUserSelectionStateStub,
}) {
const useSelectionStateStub = scenario?.useSelectionState ?? new UseUserSelectionStateStub();
const nodeIdParser: ScriptNodeIdParser = scenario?.scriptNodeIdParser
?? ((script) => script.id);
const nodeIdParser: NodeIdParser = scenario?.scriptNodeIdParser
?? ((script) => script.executableId);
const returnObject = useSelectedScriptNodeIds(useSelectionStateStub.get(), nodeIdParser);
return {
returnObject,

View File

@@ -216,29 +216,29 @@ function itExpectedFilterTriggeredEvent(
{
description: 'returns true when category exists',
scriptMatches: [],
categoryMatches: [new CategoryStub(1)],
givenNode: createNode({ id: '1', hasParent: false }),
categoryMatches: [new CategoryStub('category-match-1')],
givenNode: createNode({ id: 'category-match-1', hasParent: false }),
expectedPredicateResult: true,
},
{
description: 'returns true when script exists',
scriptMatches: [new ScriptStub('a')],
scriptMatches: [new ScriptStub('script-match-1')],
categoryMatches: [],
givenNode: createNode({ id: 'a', hasParent: true }),
givenNode: createNode({ id: 'script-match-1', hasParent: true }),
expectedPredicateResult: true,
},
{
description: 'returns false when category is missing',
scriptMatches: [new ScriptStub('b')],
categoryMatches: [new CategoryStub(2)],
givenNode: createNode({ id: '1', hasParent: false }),
scriptMatches: [new ScriptStub('script-match-1')],
categoryMatches: [new CategoryStub('category-match-1')],
givenNode: createNode({ id: 'unrelated-node', hasParent: false }),
expectedPredicateResult: false,
},
{
description: 'finds false when script is missing',
scriptMatches: [new ScriptStub('b')],
categoryMatches: [new CategoryStub(1)],
givenNode: createNode({ id: 'a', hasParent: true }),
scriptMatches: [new ScriptStub('script-match-1')],
categoryMatches: [new CategoryStub('category-match-1')],
givenNode: createNode({ id: 'unrelated-node', hasParent: true }),
expectedPredicateResult: false,
},
];
@@ -261,8 +261,8 @@ function itExpectedFilterTriggeredEvent(
expect(event.value.predicate).toBeDefined();
const actualPredicateResult = event.value.predicate(givenNode);
expect(actualPredicateResult).to.equal(expectedPredicateResult, formatAssertionMessage([
`Script matches (${scriptMatches.length}): [${scriptMatches.map((s) => s.id).join(', ')}]`,
`Category matches (${categoryMatches.length}): [${categoryMatches.map((s) => s.id).join(', ')}]`,
`Script matches (${scriptMatches.length}): [${scriptMatches.map((s) => s.executableId).join(', ')}]`,
`Category matches (${categoryMatches.length}): [${categoryMatches.map((s) => s.executableId).join(', ')}]`,
`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 { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
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 { NodeMetadataStub } from '@tests/unit/shared/Stubs/NodeMetadataStub';
import { convertToNodeInput } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/TreeNodeMetadataConverter';

View File

@@ -1,5 +1,5 @@
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 type { ProjectDetails } from '@/domain/Project/ProjectDetails';
import { ProjectDetailsStub } from './ProjectDetailsStub';

View File

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

View File

@@ -1,5 +1,5 @@
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 type { CollectionData } from '@/application/collections/';
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 type { Script } from '@/domain/Executables/Script/Script';
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 { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
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 { Script } from '@/domain/Executables/Script/Script';
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 type { ExecutableId } from '@/domain/Executables/Identifiable';
import { ScriptStub } from './ScriptStub';
import { ScriptingDefinitionStub } from './ScriptingDefinitionStub';
import { CategoryStub } from './CategoryStub';
@@ -22,9 +23,9 @@ export class CategoryCollectionStub implements ICategoryCollection {
public readonly actions = new Array<Category>();
public withSomeActions(): this {
this.withAction(new CategoryStub(1));
this.withAction(new CategoryStub(2));
this.withAction(new CategoryStub(3));
this.withAction(new CategoryStub(`[${CategoryCollectionStub}]-action-1`));
this.withAction(new CategoryStub(`[${CategoryCollectionStub}]-action-2`));
this.withAction(new CategoryStub(`[${CategoryCollectionStub}]-action-3`));
return this;
}
@@ -60,9 +61,9 @@ export class CategoryCollectionStub implements ICategoryCollection {
return this;
}
public getCategory(categoryId: number): Category {
public getCategory(categoryId: ExecutableId): Category {
return this.getAllCategories()
.find((category) => category.id === categoryId)
.find((category) => category.executableId === categoryId)
?? new CategoryStub(categoryId);
}
@@ -73,7 +74,7 @@ export class CategoryCollectionStub implements ICategoryCollection {
public getScript(scriptId: string): Script {
return this.getAllScripts()
.find((script) => scriptId === script.id)
.find((script) => scriptId === script.executableId)
?? new ScriptStub(scriptId);
}
@@ -89,7 +90,7 @@ export class CategoryCollectionStub implements ICategoryCollection {
}
function getSubCategoriesRecursively(category: Category): ReadonlyArray<Category> {
return (category.subCategories || []).flatMap(
return (category.subcategories || []).flatMap(
(subCategory) => [subCategory, ...getSubCategoriesRecursively(subCategory)],
);
}
@@ -97,7 +98,7 @@ function getSubCategoriesRecursively(category: Category): ReadonlyArray<Category
function getScriptsRecursively(category: Category): ReadonlyArray<Script> {
return [
...(category.scripts || []),
...(category.subCategories || []).flatMap(
...(category.subcategories || []).flatMap(
(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 { CategoryFactory, CategoryInitParameters } from '@/domain/Executables/Category/CategoryFactory';
import { CategoryStub } from './CategoryStub';
export function createCategoryFactorySpy(): {
@@ -10,7 +9,7 @@ export function createCategoryFactorySpy(): {
const createdCategories = new Map<Category, CategoryInitParameters>();
return {
categoryFactorySpy: (parameters) => {
const category = new CategoryStub(55);
const category = new CategoryStub('category-from-factory-stub');
createdCategories.set(category, parameters);
return category;
},

View File

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

View File

@@ -1,6 +1,6 @@
import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult';
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 { 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 { ScriptInitParameters } from '@/domain/Executables/Script/CollectionScript';
import type { ScriptFactory, ScriptInitParameters } from '@/domain/Executables/Script/ScriptFactory';
import { ScriptStub } from './ScriptStub';
export function createScriptFactorySpy(): {

View File

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

View File

@@ -1,17 +1,18 @@
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';
export class SelectedScriptStub implements SelectedScript {
public readonly script: Script;
public readonly id: string;
public readonly id: RepositoryEntityId;
public revert: boolean;
constructor(
script: Script,
) {
this.id = script.id;
this.id = script.executableId;
this.script = script;
}