2026-07-25 20:05:49 +02:00
|
|
|
BeforeAll {
|
|
|
|
|
function Invoke-NonBlocking { param($ScriptBlock, $ArgumentList) }
|
|
|
|
|
function Enable-WindowsOptionalFeature {
|
|
|
|
|
param([switch]$Online, $FeatureName, [switch]$All, [switch]$NoRestart)
|
|
|
|
|
[void]$global:OptionalFeatureCalls.Add([PSCustomObject]@{ Action = 'Enable'; Online = $Online; FeatureName = $FeatureName; All = $All; NoRestart = $NoRestart })
|
|
|
|
|
}
|
|
|
|
|
function Disable-WindowsOptionalFeature {
|
|
|
|
|
param([switch]$Online, $FeatureName, [switch]$NoRestart)
|
|
|
|
|
[void]$global:OptionalFeatureCalls.Add([PSCustomObject]@{ Action = 'Disable'; Online = $Online; FeatureName = $FeatureName; All = $false; NoRestart = $NoRestart })
|
|
|
|
|
}
|
|
|
|
|
function Get-WindowsOptionalFeature {
|
|
|
|
|
param([switch]$Online, $FeatureName, $ErrorAction)
|
|
|
|
|
[void]$global:OptionalFeatureQueryCalls.Add([PSCustomObject]@{ Online = $Online; FeatureName = $FeatureName; ErrorAction = $ErrorAction })
|
|
|
|
|
if ($global:OptionalFeatureQueryThrows) {
|
|
|
|
|
throw 'feature service unavailable'
|
|
|
|
|
}
|
|
|
|
|
return [PSCustomObject]@{ State = $global:OptionalFeatureQueryState }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
. (Join-Path $PSScriptRoot '..\Scripts\Features\Windows-OptionalFeatures.ps1')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Enable-WindowsFeature' {
|
|
|
|
|
BeforeEach {
|
|
|
|
|
$script:Params = @{}
|
2026-08-15 17:08:00 +02:00
|
|
|
Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $true; Output = $null; Error = $null } }
|
2026-07-25 20:05:49 +02:00
|
|
|
Mock Write-Host {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'schedules the requested feature with the non-blocking runner' {
|
2026-08-15 17:08:00 +02:00
|
|
|
Enable-WindowsFeature -FeatureName 'Feature.One' | Should -BeTrue
|
2026-07-25 20:05:49 +02:00
|
|
|
|
|
|
|
|
Should -Invoke Invoke-NonBlocking -Times 1 -Exactly -ParameterFilter { $ArgumentList -eq 'Feature.One' }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'calls Enable-WindowsOptionalFeature with the expected arguments' {
|
|
|
|
|
$global:OptionalFeatureCalls = [System.Collections.Generic.List[object]]::new()
|
|
|
|
|
Mock Invoke-NonBlocking {
|
|
|
|
|
param($ScriptBlock, $ArgumentList)
|
|
|
|
|
$script:optionalFeatureBlock = $ScriptBlock
|
|
|
|
|
$script:optionalFeatureArguments = $ArgumentList
|
|
|
|
|
}
|
2026-08-15 17:08:00 +02:00
|
|
|
Enable-WindowsFeature -FeatureName 'Feature.One' | Should -BeFalse
|
2026-07-25 20:05:49 +02:00
|
|
|
& $script:optionalFeatureBlock $script:optionalFeatureArguments
|
|
|
|
|
|
|
|
|
|
$global:OptionalFeatureCalls | Should -HaveCount 1
|
|
|
|
|
$global:OptionalFeatureCalls[0].Action | Should -Be 'Enable'
|
|
|
|
|
$global:OptionalFeatureCalls[0].Online | Should -BeTrue
|
|
|
|
|
$global:OptionalFeatureCalls[0].FeatureName | Should -Be 'Feature.One'
|
|
|
|
|
$global:OptionalFeatureCalls[0].All | Should -BeTrue
|
|
|
|
|
$global:OptionalFeatureCalls[0].NoRestart | Should -BeTrue
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-15 17:08:00 +02:00
|
|
|
It 'writes optional-feature output while returning true' {
|
|
|
|
|
Mock Invoke-NonBlocking {
|
|
|
|
|
param($ScriptBlock, $ArgumentList)
|
|
|
|
|
& $ScriptBlock $ArgumentList
|
|
|
|
|
}
|
|
|
|
|
Mock Enable-WindowsOptionalFeature { [PSCustomObject]@{ State = 'Enabled'; RestartNeeded = $false } }
|
|
|
|
|
|
|
|
|
|
Enable-WindowsFeature -FeatureName 'Feature.One' | Should -BeTrue
|
|
|
|
|
|
|
|
|
|
Should -Invoke Write-Host -Times 1 -Exactly -ParameterFilter { $Object -match 'Enabled' }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'reports the worker error and returns false' {
|
|
|
|
|
Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $false; Output = $null; Error = 'feature servicing failed' } }
|
|
|
|
|
Mock Write-Warning {}
|
|
|
|
|
|
|
|
|
|
Enable-WindowsFeature -FeatureName 'Feature.One' | Should -BeFalse
|
|
|
|
|
|
|
|
|
|
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match 'feature servicing failed' }
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 20:05:49 +02:00
|
|
|
It 'does not schedule changes in WhatIf mode' {
|
|
|
|
|
$script:Params = @{ WhatIf = $true }
|
|
|
|
|
|
|
|
|
|
Enable-WindowsFeature -FeatureName 'Feature.One'
|
|
|
|
|
|
|
|
|
|
Should -Invoke Invoke-NonBlocking -Times 0 -Exactly
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'surfaces an optional-feature enable failure from the scheduled script block' {
|
|
|
|
|
Mock Invoke-NonBlocking {
|
|
|
|
|
param($ScriptBlock, $ArgumentList)
|
|
|
|
|
$script:optionalFeatureBlock = $ScriptBlock
|
|
|
|
|
$script:optionalFeatureArguments = $ArgumentList
|
|
|
|
|
}
|
|
|
|
|
Mock Enable-WindowsOptionalFeature { throw 'feature servicing failed' }
|
|
|
|
|
|
|
|
|
|
Enable-WindowsFeature -FeatureName 'Feature.One'
|
|
|
|
|
|
2026-08-15 17:08:00 +02:00
|
|
|
(& $script:optionalFeatureBlock $script:optionalFeatureArguments).Success | Should -BeFalse
|
2026-07-25 20:05:49 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Disable-WindowsFeature' {
|
|
|
|
|
BeforeEach {
|
|
|
|
|
$script:Params = @{}
|
2026-08-15 17:08:00 +02:00
|
|
|
Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $true; Output = $null; Error = $null } }
|
2026-07-25 20:05:49 +02:00
|
|
|
Mock Write-Host {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'schedules the requested feature with the non-blocking runner' {
|
2026-08-15 17:08:00 +02:00
|
|
|
Disable-WindowsFeature -FeatureName 'Feature.One' | Should -BeTrue
|
2026-07-25 20:05:49 +02:00
|
|
|
|
|
|
|
|
Should -Invoke Invoke-NonBlocking -Times 1 -Exactly -ParameterFilter { $ArgumentList -eq 'Feature.One' }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'calls Disable-WindowsOptionalFeature with the expected arguments' {
|
|
|
|
|
$global:OptionalFeatureCalls = [System.Collections.Generic.List[object]]::new()
|
|
|
|
|
Mock Invoke-NonBlocking {
|
|
|
|
|
param($ScriptBlock, $ArgumentList)
|
|
|
|
|
$script:optionalFeatureBlock = $ScriptBlock
|
|
|
|
|
$script:optionalFeatureArguments = $ArgumentList
|
|
|
|
|
}
|
2026-08-15 17:08:00 +02:00
|
|
|
Disable-WindowsFeature -FeatureName 'Feature.One' | Should -BeFalse
|
2026-07-25 20:05:49 +02:00
|
|
|
& $script:optionalFeatureBlock $script:optionalFeatureArguments
|
|
|
|
|
|
|
|
|
|
$global:OptionalFeatureCalls | Should -HaveCount 1
|
|
|
|
|
$global:OptionalFeatureCalls[0].Action | Should -Be 'Disable'
|
|
|
|
|
$global:OptionalFeatureCalls[0].Online | Should -BeTrue
|
|
|
|
|
$global:OptionalFeatureCalls[0].FeatureName | Should -Be 'Feature.One'
|
|
|
|
|
$global:OptionalFeatureCalls[0].All | Should -BeFalse
|
|
|
|
|
$global:OptionalFeatureCalls[0].NoRestart | Should -BeTrue
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-15 17:08:00 +02:00
|
|
|
It 'writes optional-feature output while returning true' {
|
|
|
|
|
Mock Invoke-NonBlocking {
|
|
|
|
|
param($ScriptBlock, $ArgumentList)
|
|
|
|
|
& $ScriptBlock $ArgumentList
|
|
|
|
|
}
|
|
|
|
|
Mock Disable-WindowsOptionalFeature { [PSCustomObject]@{ State = 'Disabled'; RestartNeeded = $false } }
|
|
|
|
|
|
|
|
|
|
Disable-WindowsFeature -FeatureName 'Feature.One' | Should -BeTrue
|
|
|
|
|
|
|
|
|
|
Should -Invoke Write-Host -Times 1 -Exactly -ParameterFilter { $Object -match 'Disabled' }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'reports the worker error and returns false' {
|
|
|
|
|
Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $false; Output = $null; Error = 'feature servicing failed' } }
|
|
|
|
|
Mock Write-Warning {}
|
|
|
|
|
|
|
|
|
|
Disable-WindowsFeature -FeatureName 'Feature.One' | Should -BeFalse
|
|
|
|
|
|
|
|
|
|
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match 'feature servicing failed' }
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 20:05:49 +02:00
|
|
|
It 'does not schedule changes in WhatIf mode' {
|
|
|
|
|
$script:Params = @{ WhatIf = $true }
|
|
|
|
|
|
|
|
|
|
Disable-WindowsFeature -FeatureName 'Feature.One'
|
|
|
|
|
|
|
|
|
|
Should -Invoke Invoke-NonBlocking -Times 0 -Exactly
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'surfaces an optional-feature disable failure from the scheduled script block' {
|
|
|
|
|
Mock Invoke-NonBlocking {
|
|
|
|
|
param($ScriptBlock, $ArgumentList)
|
|
|
|
|
$script:optionalFeatureBlock = $ScriptBlock
|
|
|
|
|
$script:optionalFeatureArguments = $ArgumentList
|
|
|
|
|
}
|
|
|
|
|
Mock Disable-WindowsOptionalFeature { throw 'feature servicing failed' }
|
|
|
|
|
|
|
|
|
|
Disable-WindowsFeature -FeatureName 'Feature.One'
|
|
|
|
|
|
2026-08-15 17:08:00 +02:00
|
|
|
(& $script:optionalFeatureBlock $script:optionalFeatureArguments).Success | Should -BeFalse
|
2026-07-25 20:05:49 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Test-WindowsOptionalFeatureEnabled' {
|
|
|
|
|
BeforeEach {
|
|
|
|
|
$global:OptionalFeatureQueryCalls = [System.Collections.Generic.List[object]]::new()
|
|
|
|
|
$global:OptionalFeatureQueryThrows = $false
|
|
|
|
|
$global:OptionalFeatureQueryState = 'Disabled'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'returns true for an enabled optional feature' {
|
|
|
|
|
$global:OptionalFeatureQueryState = 'Enabled'
|
|
|
|
|
|
|
|
|
|
Test-WindowsOptionalFeatureEnabled -FeatureName 'Feature.One' | Should -BeTrue
|
|
|
|
|
|
|
|
|
|
$global:OptionalFeatureQueryCalls | Should -HaveCount 1
|
|
|
|
|
$global:OptionalFeatureQueryCalls[0].Online | Should -BeTrue
|
|
|
|
|
$global:OptionalFeatureQueryCalls[0].FeatureName | Should -Be 'Feature.One'
|
|
|
|
|
$global:OptionalFeatureQueryCalls[0].ErrorAction | Should -Be 'Stop'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'returns false for a non-enabled optional feature' {
|
|
|
|
|
Test-WindowsOptionalFeatureEnabled -FeatureName 'Feature.One' | Should -BeFalse
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'returns false when the optional-feature query fails' {
|
|
|
|
|
$global:OptionalFeatureQueryThrows = $true
|
|
|
|
|
|
|
|
|
|
Test-WindowsOptionalFeatureEnabled -FeatureName 'Feature.One' | Should -BeFalse
|
|
|
|
|
}
|
|
|
|
|
}
|