Refactor feature management scripts to improve error handling

This commit is contained in:
Jeffrey
2026-08-16 21:21:05 +02:00
parent 1a26934499
commit 492a374f5c
18 changed files with 870 additions and 317 deletions
+15 -11
View File
@@ -9,27 +9,26 @@ BeforeAll {
Describe 'Import-RegistryFile' {
BeforeEach {
$script:Params = @{}
$script:RegistryImportFailures = 0
$script:regPath = Join-Path $TestDrive 'feature.reg'
'' | Set-Content -LiteralPath $script:regPath
Mock Get-RegistryFilePathForFeature { $script:regPath }
Mock Invoke-RegistryOperationsFromRegFile {}
Mock Invoke-RegistryOperationsFromRegFile { $true }
Mock Invoke-WithTargetUserHive {}
Mock Invoke-NonBlocking { [PSCustomObject]@{ Output = @(); ExitCode = 0; Error = $null } }
Mock Write-Host {}
Mock Write-Warning {}
}
It 'throws and increments the failure count when the registry file is missing' {
It 'returns false when the registry file is missing' {
Mock Get-RegistryFilePathForFeature { Join-Path $TestDrive 'missing.reg' }
{ Import-RegistryFile -message 'Apply' -path 'missing.reg' } | Should -Throw 'Unable to find registry file:*'
$script:RegistryImportFailures | Should -Be 1
Import-RegistryFile -message 'Apply' -path 'missing.reg' | Should -BeFalse
Should -Invoke Invoke-NonBlocking -Times 0 -Exactly
}
It 'uses the PowerShell writer only in WhatIf mode' {
$script:Params = @{ WhatIf = $true }
Import-RegistryFile -message 'Apply' -path 'feature.reg'
Import-RegistryFile -message 'Apply' -path 'feature.reg' | Should -BeTrue
Should -Invoke Invoke-RegistryOperationsFromRegFile -Times 1 -Exactly -ParameterFilter { $RegFilePath -eq $script:regPath }
Should -Invoke Invoke-NonBlocking -Times 0 -Exactly
}
@@ -41,7 +40,7 @@ Describe 'Import-RegistryFile' {
& $ScriptBlock $ArgumentObject ([PSCustomObject]@{ WasAlreadyLoaded = $true })
}
Import-RegistryFile -message 'Apply' -path 'feature.reg'
Import-RegistryFile -message 'Apply' -path 'feature.reg' | Should -BeTrue
Should -Invoke Invoke-WithTargetUserHive -Times 1 -Exactly -ParameterFilter { $TargetUserName -eq 'Alice' -and $PassHiveContext }
Should -Invoke Invoke-RegistryOperationsFromRegFile -Times 1 -Exactly
@@ -51,17 +50,22 @@ Describe 'Import-RegistryFile' {
It 'falls back to the PowerShell writer when reg import fails' {
Mock Invoke-NonBlocking { [PSCustomObject]@{ Output = @('denied'); ExitCode = 5; Error = 'access denied' } }
Import-RegistryFile -message 'Apply' -path 'feature.reg'
Import-RegistryFile -message 'Apply' -path 'feature.reg' | Should -BeTrue
Should -Invoke Invoke-RegistryOperationsFromRegFile -Times 1 -Exactly
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -like "reg import failed*" }
$script:RegistryImportFailures | Should -Be 0
}
It 'returns false when the fallback cannot apply every registry operation' {
Mock Invoke-NonBlocking { [PSCustomObject]@{ Output = @('denied'); ExitCode = 5; Error = 'access denied' } }
Mock Invoke-RegistryOperationsFromRegFile { $false }
Import-RegistryFile -message 'Apply' -path 'feature.reg' | Should -BeFalse
}
It 'does not invoke the fallback after a successful reg import' {
Import-RegistryFile -message 'Apply' -path 'feature.reg'
Import-RegistryFile -message 'Apply' -path 'feature.reg' | Should -BeTrue
Should -Invoke Invoke-NonBlocking -Times 1 -Exactly
Should -Invoke Invoke-RegistryOperationsFromRegFile -Times 0 -Exactly
$script:RegistryImportFailures | Should -Be 0
}
}
+110 -37
View File
@@ -1,24 +1,24 @@
BeforeAll {
function Import-RegistryFile { param($Message, $path) }
function Remove-SelectedApps { param($Apps) }
function Invoke-ForceRemoveEdge {}
function Disable-TelemetryScheduledTasks {}
function Enable-TelemetryScheduledTasks {}
function Remove-SelectedApps { param($Apps) $true }
function Invoke-ForceRemoveEdge { $true }
function Disable-TelemetryScheduledTasks { $true }
function Enable-TelemetryScheduledTasks { $true }
function Generate-AppsList { @() }
function Get-FriendlyTargetUserName { 'current user' }
function Set-StoreSearchSuggestionsEnabledForAllUsers {}
function Set-StoreSearchSuggestionsEnabled { param($StoreAppsDatabase) }
function Set-StoreSearchSuggestionsEnabledForAllUsers { $true }
function Set-StoreSearchSuggestionsEnabled { param($StoreAppsDatabase) $true }
function Get-StoreAppsDatabasePathForUser { param($UserName) 'store.db' }
function Get-UserName { 'Alice' }
function Disable-WindowsFeature { param($FeatureName) }
function Disable-WindowsFeature { param($FeatureName) $true }
function New-RegistrySettingsBackup { param($ActionableKeys, $ExtraFeatures) }
function Invoke-SystemRestorePoint {}
function Enable-WindowsFeature { param($FeatureName) }
function Enable-WindowsFeature { param($FeatureName) $true }
function Get-StartMenuBinPathForUser { param($UserName) 'start.bin' }
function Replace-StartMenu { param($startMenuBinFile, $startMenuTemplate) }
function Replace-StartMenuForAllUsers { param($startMenuTemplate) }
function Set-StoreSearchSuggestionsDisabledForAllUsers {}
function Set-StoreSearchSuggestionsDisabled { param($StoreAppsDatabase) }
function Replace-StartMenu { param($startMenuBinFile, $startMenuTemplate) $true }
function Replace-StartMenuForAllUsers { param($startMenuTemplate) $true }
function Set-StoreSearchSuggestionsDisabledForAllUsers { $true }
function Set-StoreSearchSuggestionsDisabled { param($StoreAppsDatabase) $true }
. (Join-Path $PSScriptRoot '..\Scripts\Features\Invoke-Changes.ps1')
}
@@ -62,19 +62,19 @@ Describe 'Invoke-FeatureApply' {
ReplaceStartAllUsers = [PSCustomObject]@{ ApplyText = 'Replace Start all users'; RegistryKey = '' }
DisableStoreSearchSuggestions = [PSCustomObject]@{ ApplyText = 'Disable Store suggestions'; RegistryKey = '' }
}
Mock Import-RegistryFile {}
Mock Remove-SelectedApps {}
Mock Invoke-ForceRemoveEdge {}
Mock Disable-TelemetryScheduledTasks {}
Mock Import-RegistryFile { $true }
Mock Remove-SelectedApps { $true }
Mock Invoke-ForceRemoveEdge { $true }
Mock Disable-TelemetryScheduledTasks { $true }
Mock Generate-AppsList { @() }
Mock Get-FriendlyTargetUserName { 'current user' }
Mock Enable-WindowsFeature {}
Mock Enable-WindowsFeature { $true }
Mock Get-StartMenuBinPathForUser { 'start.bin' }
Mock Get-UserName { 'Alice' }
Mock Replace-StartMenu {}
Mock Replace-StartMenuForAllUsers {}
Mock Set-StoreSearchSuggestionsDisabledForAllUsers {}
Mock Set-StoreSearchSuggestionsDisabled {}
Mock Replace-StartMenu { $true }
Mock Replace-StartMenuForAllUsers { $true }
Mock Set-StoreSearchSuggestionsDisabledForAllUsers { $true }
Mock Set-StoreSearchSuggestionsDisabled { $true }
Mock Get-StoreAppsDatabasePathForUser { 'store.db' }
Mock Get-Process { @() }
Mock Stop-Process { param($InputObject) }
@@ -95,6 +95,14 @@ Describe 'Invoke-FeatureApply' {
Should -Invoke Disable-TelemetryScheduledTasks -Times 1 -Exactly
}
It 'returns false without side effects when a registry import fails' {
Mock Import-RegistryFile { $false }
Invoke-FeatureApply -FeatureId 'DisableTelemetry' | Should -BeFalse
Should -Invoke Disable-TelemetryScheduledTasks -Times 0 -Exactly
}
It 'does not call app removal when the generated selection is empty' {
Invoke-FeatureApply -FeatureId 'RemoveApps'
@@ -127,6 +135,23 @@ Describe 'Invoke-FeatureApply' {
Should -Invoke Remove-SelectedApps -Times 0 -Exactly
}
It 'returns false when applying a feature throws' {
Mock Invoke-ForceRemoveEdge { throw 'access denied' }
Mock Write-Warning {}
Invoke-FeatureApply -FeatureId 'ForceRemoveEdge' | Should -BeFalse
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match "Failed to apply 'Force remove Edge'.*access denied" }
}
It 'returns false for an unknown feature' {
Mock Write-Warning {}
Invoke-FeatureApply -FeatureId 'Unknown' | Should -BeFalse
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match "Unknown feature 'Unknown'.*could not be applied" }
}
It 'uses the expected static app list for <FeatureId>' -ForEach @(
@{ FeatureId = 'RemoveGamingApps'; MinimumCount = 3; ExpectedApp = 'Microsoft.GamingApp' }
@{ FeatureId = 'RemoveHPApps'; MinimumCount = 10; ExpectedApp = 'AD2F1837.myHP' }
@@ -215,7 +240,7 @@ Describe 'Invoke-ApplyFeatures' {
}
$script:progressCalls = New-Object System.Collections.Generic.List[object]
$script:ApplyProgressCallback = { param($Step, $Total, $Text) $script:progressCalls.Add(@($Step, $Total, $Text)) }
Mock Invoke-FeatureApply {}
Mock Invoke-FeatureApply { $true }
}
It 'reports progress and applies each feature in order' {
@@ -235,6 +260,21 @@ Describe 'Invoke-ApplyFeatures' {
Should -Invoke Invoke-FeatureApply -Times 0 -Exactly
$script:progressCalls | Should -HaveCount 0
}
It 'counts a failed feature application and continues with later features' {
$script:FeatureFailures = 0
$script:ApplyFeatureFailures = 0
Mock Invoke-FeatureApply {
param($FeatureId)
return ($FeatureId -ne 'One')
}
Invoke-ApplyFeatures -FeatureIds @('One', 'Two') -StartStep 1 -TotalSteps 2
$script:FeatureFailures | Should -Be 1
$script:ApplyFeatureFailures | Should -Be 1
Should -Invoke Invoke-FeatureApply -Times 2 -Exactly
}
}
Describe 'Invoke-UndoFeatures' {
@@ -246,14 +286,13 @@ Describe 'Invoke-UndoFeatures' {
CustomUndo = [PSCustomObject]@{ UndoLabel = 'Undo custom'; ApplyUndoText = ''; RegistryUndoKey = '' }
}
Mock Resolve-UndoRegFilePath { param($FileName) "Undo\$FileName" }
Mock Import-RegistryFile {}
Mock Invoke-FeatureUndo {}
Mock Import-RegistryFile { $true }
Mock Invoke-FeatureUndo { $true }
}
It 'imports registry undo data and still invokes custom undo side effects' {
It 'delegates registry-backed undo work to the feature undo handler' {
Invoke-UndoFeatures -FeatureIds @('RegistryUndo') -StartStep 1 -TotalSteps 1
Should -Invoke Import-RegistryFile -Times 1 -Exactly -ParameterFilter { $path -eq 'Undo\undo.reg' }
Should -Invoke Invoke-FeatureUndo -Times 1 -Exactly -ParameterFilter { $FeatureId -eq 'RegistryUndo' }
}
@@ -264,6 +303,18 @@ Describe 'Invoke-UndoFeatures' {
Should -Invoke Invoke-FeatureUndo -Times 2 -Exactly
}
It 'counts one failure when a feature undo fails' {
$script:FeatureFailures = 0
$script:UndoFeatureFailures = 0
Mock Invoke-FeatureUndo { $false }
Invoke-UndoFeatures -FeatureIds @('RegistryUndo') -StartStep 1 -TotalSteps 1
$script:FeatureFailures | Should -Be 1
$script:UndoFeatureFailures | Should -Be 1
Should -Invoke Invoke-FeatureUndo -Times 1 -Exactly
}
It 'stops before undoing when cancellation is requested' {
$script:CancelRequested = $true
@@ -283,12 +334,14 @@ Describe 'Invoke-FeatureUndo' {
DisableTelemetry = [PSCustomObject]@{}
DisableStoreSearchSuggestions = [PSCustomObject]@{}
}
Mock Set-StoreSearchSuggestionsEnabledForAllUsers {}
Mock Set-StoreSearchSuggestionsEnabled {}
Mock Set-StoreSearchSuggestionsEnabledForAllUsers { $true }
Mock Set-StoreSearchSuggestionsEnabled { $true }
Mock Get-StoreAppsDatabasePathForUser { 'store.db' }
Mock Get-UserName { 'Alice' }
Mock Disable-WindowsFeature {}
Mock Enable-TelemetryScheduledTasks {}
Mock Disable-WindowsFeature { $true }
Mock Enable-TelemetryScheduledTasks { $true }
Mock Import-RegistryFile { $true }
Mock Resolve-UndoRegFilePath { param($FileName) "Undo\$FileName" }
Mock Write-Host {}
}
@@ -304,16 +357,35 @@ Describe 'Invoke-FeatureUndo' {
It 'disables both WSL optional features in dependency-safe order' {
$script:disabledFeatures = [System.Collections.Generic.List[string]]::new()
Mock Disable-WindowsFeature { param($FeatureName) $script:disabledFeatures.Add($FeatureName) }
Mock Disable-WindowsFeature { param($FeatureName) $script:disabledFeatures.Add($FeatureName); $true }
Invoke-FeatureUndo -FeatureId 'EnableWindowsSubsystemForLinux'
$script:disabledFeatures | Should -Be @('Microsoft-Windows-Subsystem-Linux', 'VirtualMachinePlatform')
}
It 'disables Sandbox and re-enables telemetry tasks' {
$script:Features.DisableTelemetry = [PSCustomObject]@{ ApplyUndoText = 'Enable telemetry'; RegistryUndoKey = 'enable-telemetry.reg' }
Invoke-FeatureUndo -FeatureId 'EnableWindowsSandbox'
Invoke-FeatureUndo -FeatureId 'DisableTelemetry'
Should -Invoke Disable-WindowsFeature -Times 1 -Exactly -ParameterFilter { $FeatureName -eq 'Containers-DisposableClientVM' }
Should -Invoke Enable-TelemetryScheduledTasks -Times 1 -Exactly
Should -Invoke Import-RegistryFile -Times 1 -Exactly -ParameterFilter { $path -eq 'Undo\enable-telemetry.reg' }
}
It 'returns false without side effects when a registry undo import fails' {
$script:Features.DisableTelemetry = [PSCustomObject]@{ ApplyUndoText = 'Enable telemetry'; RegistryUndoKey = 'enable-telemetry.reg' }
Mock Import-RegistryFile { $false }
Invoke-FeatureUndo -FeatureId 'DisableTelemetry' | Should -BeFalse
Should -Invoke Enable-TelemetryScheduledTasks -Times 0 -Exactly
}
It 'warns and returns false for an unknown feature' {
Mock Write-Warning {}
Invoke-FeatureUndo -FeatureId 'Unknown' | Should -BeFalse
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match "Unknown feature 'Unknown'.*could not be undone" }
}
}
@@ -332,7 +404,7 @@ Describe 'Invoke-AllChanges' {
Mock Test-RunningAsSystem { $false }
Mock Resolve-UndoRegFilePath { param($FileName) "Undo\$FileName" }
Mock New-RegistrySettingsBackup {}
Mock Invoke-SystemRestorePoint {}
Mock Invoke-SystemRestorePoint { $true }
Mock Invoke-ApplyFeatures {}
Mock Invoke-UndoFeatures {}
Mock Write-Host {}
@@ -406,20 +478,21 @@ Describe 'Invoke-AllChanges' {
$script:Params = @{ CreateRestorePoint = $true; CustomApply = $true }
$script:UndoParams = @{}
$script:order = [System.Collections.Generic.List[string]]::new()
Mock Invoke-SystemRestorePoint { $script:order.Add('restore-point') }
Mock Invoke-SystemRestorePoint { $script:order.Add('restore-point'); $true }
Mock Invoke-ApplyFeatures { $script:order.Add('apply') }
Invoke-AllChanges
$script:order | Should -Be @('restore-point', 'apply')
}
It 'reports registry import failures after all requested work completes' {
$script:Params = @{ CustomApply = $true }
It 'reports a restore point failure when the user chooses to continue' {
$script:Params = @{ CreateRestorePoint = $true; CustomApply = $true }
$script:UndoParams = @{}
Mock Invoke-ApplyFeatures { $script:RegistryImportFailures = 2 }
Mock Invoke-SystemRestorePoint { $false }
Invoke-AllChanges
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match '2 registry import change' }
$script:PrerequisiteFailures | Should -Be 1
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match 'requested prerequisite' }
}
It 'reports app removal failures after all requested work completes' {
+20
View File
@@ -27,6 +27,26 @@ Describe 'Invoke-SystemRestorePoint' {
$script:CancelRequested | Should -BeFalse
}
It 'returns false through the continuation flow when the System Restore state cannot be read' {
Mock Get-ItemProperty { throw 'registry access denied' }
Mock Read-Host { 'y' }
Invoke-SystemRestorePoint | Should -BeFalse
$script:CancelRequested | Should -BeFalse
Should -Invoke Invoke-NonBlocking -Times 0 -Exactly
}
It 'returns false without prompting when the System Restore state cannot be read in silent mode' {
$script:Silent = $true
Mock Get-ItemProperty { throw 'registry access denied' }
Invoke-SystemRestorePoint | Should -BeFalse
$script:CancelRequested | Should -BeFalse
Should -Invoke Read-Host -Times 0 -Exactly
}
It 'is loaded by the main entry point' {
$entryPoint = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\Win11Debloat.ps1') -Raw
$expectedImport = [regex]::Escape('Scripts/Features/Invoke-SystemRestorePoint.ps1')
+95 -2
View File
@@ -11,6 +11,7 @@ BeforeAll {
function Resolve-UserProfileContext { param($UserName) $null }
. (Join-Path $PSScriptRoot '..\Scripts\AppRemoval\Remove-SelectedApps.ps1')
. (Join-Path $PSScriptRoot '..\Scripts\AppRemoval\Invoke-ForceRemoveEdge.ps1')
}
Describe 'Remove-SelectedApps' {
@@ -71,10 +72,23 @@ Describe 'Remove-SelectedApps' {
It 'counts a failed WinGet removal' {
Mock Get-AppRemovalMethod { 'WinGet' }
Mock Remove-WinGetApp { $false }
Mock Test-AppInWingetList { $true }
Mock Write-Host {}
Remove-SelectedApps -appsList @('One.App')
$script:AppRemovalFailures | Should -Be 1
Should -Invoke Write-Host -Times 1 -Exactly -ParameterFilter { $Object -eq 'Unable to uninstall One.App via WinGet' -and $ForegroundColor -eq 'Red' }
}
It 'does not count a non-zero WinGet command when the app is absent after verification' {
Mock Get-AppRemovalMethod { 'WinGet' }
Mock Remove-WinGetApp { $false }
Mock Test-AppInWingetList { $false }
Remove-SelectedApps -appsList @('One.App') | Should -BeTrue
$script:AppRemovalFailures | Should -Be 0
}
It 'counts a WinGet removal that remains installed after a successful command' {
@@ -132,7 +146,7 @@ Describe 'Remove-WinGetApp' {
BeforeEach {
$script:Params = @{}
$script:WingetInstalled = $true
Mock Invoke-NonBlocking { $true }
Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $true; ExitCode = 0; Output = @() } }
Mock Set-RunOnceWingetTask { $true }
Mock Get-UserName { 'Alice' }
Mock Write-Host {}
@@ -174,14 +188,93 @@ Describe 'Remove-WinGetApp' {
It 'reports a timed-out winget uninstall and continues' {
$script:Params = @{ User = 'Alice' }
Mock Invoke-NonBlocking { throw 'Operation timed out after 120 seconds' }
Mock Write-Verbose {}
{ Remove-WinGetApp -app 'One.App' } | Should -Not -Throw
Should -Invoke Set-RunOnceWingetTask -Times 1 -Exactly
Should -Invoke Write-Error -Times 1 -Exactly -ParameterFilter {
Should -Invoke Write-Verbose -Times 1 -Exactly -ParameterFilter {
$Message -like '*did not complete within 120 seconds*'
}
}
It 'returns false when winget exits unsuccessfully' {
Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $false; ExitCode = 1; Output = @('failure') } }
Mock Write-Verbose {}
Remove-WinGetApp -app 'One.App' | Should -BeFalse
Should -Invoke Write-Verbose -Times 1 -Exactly -ParameterFilter { $Message -match 'exit code 1' }
}
It 'returns false for a non-zero WinGet exit code without writing an error record' {
Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $false; ExitCode = -1978335212; Output = @('No installed package found matching input criteria.') } }
Mock Write-Verbose {}
Remove-WinGetApp -app 'One.App' | Should -BeFalse
Should -Invoke Write-Verbose -Times 1 -Exactly -ParameterFilter { $Message -match 'post-removal inventory check' }
Should -Invoke Write-Verbose -Times 1 -Exactly -ParameterFilter { $Message -match 'No installed package found' }
}
It 'writes captured winget output to the verbose stream before returning success' {
Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $true; ExitCode = 0; Output = @('Successfully uninstalled One.App') } }
Mock Write-Verbose {}
Remove-WinGetApp -app 'One.App' | Should -BeTrue
Should -Invoke Write-Verbose -Times 1 -Exactly -ParameterFilter { $Message -eq 'Successfully uninstalled One.App' }
}
It 'writes captured winget diagnostics to the verbose stream when winget fails' {
Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $false; ExitCode = 1; Output = @('Package was not found') } }
Mock Write-Verbose {}
Remove-WinGetApp -app 'One.App' | Should -BeFalse
Should -Invoke Write-Verbose -Times 1 -Exactly -ParameterFilter { $Message -eq 'Package was not found' }
}
}
Describe 'Remove-EdgeAutostartValue' {
BeforeEach {
Mock Write-Warning {}
}
It 'treats a missing value as already cleaned up' {
Mock Get-ItemProperty { [PSCustomObject]@{} }
Mock Remove-ItemProperty {}
Remove-EdgeAutostartValue -Path 'HKCU:\Software\Example' -Name 'Microsoft Edge Update' | Should -BeTrue
Should -Invoke Remove-ItemProperty -Times 0 -Exactly
}
It 'removes an existing value' {
Mock Get-ItemProperty { [PSCustomObject]@{ 'Microsoft Edge Update' = 'enabled' } }
Mock Remove-ItemProperty {}
Remove-EdgeAutostartValue -Path 'HKCU:\Software\Example' -Name 'Microsoft Edge Update' | Should -BeTrue
Should -Invoke Remove-ItemProperty -Times 1 -Exactly -ParameterFilter { $Path -eq 'HKCU:\Software\Example' -and $Name -eq 'Microsoft Edge Update' }
}
It 'returns false when an existing value cannot be removed' {
Mock Get-ItemProperty { [PSCustomObject]@{ 'Microsoft Edge Update' = 'enabled' } }
Mock Remove-ItemProperty { throw 'access denied' }
Remove-EdgeAutostartValue -Path 'HKCU:\Software\Example' -Name 'Microsoft Edge Update' | Should -BeFalse
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match 'access denied' }
}
It 'returns false when the registry key cannot be inspected' {
Mock Get-ItemProperty { throw 'access denied' }
Remove-EdgeAutostartValue -Path 'HKCU:\Software\Example' -Name 'Microsoft Edge Update' | Should -BeFalse
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match 'access denied' }
}
}
Describe 'Remove-AppxApp' {
+32 -16
View File
@@ -37,20 +37,20 @@ Describe 'Store-search suggestion all-user operations' {
)
}
Mock Get-StoreAppsDatabasePathForUser { 'C:\Users\Default\AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db' }
Mock Set-StoreSearchSuggestionsDisabled {}
Mock Set-StoreSearchSuggestionsEnabled {}
Mock Set-StoreSearchSuggestionsDisabled { $true }
Mock Set-StoreSearchSuggestionsEnabled { $true }
Mock Write-Warning {}
}
It 'disables suggestions for every discovered and Default profile' {
Set-StoreSearchSuggestionsDisabledForAllUsers
Set-StoreSearchSuggestionsDisabledForAllUsers | Should -BeTrue
Should -Invoke Set-StoreSearchSuggestionsDisabled -Times 3 -Exactly
Should -Invoke Set-StoreSearchSuggestionsDisabled -Times 1 -Exactly -ParameterFilter { $StoreAppsDatabase -match 'Users\\Default\\' }
}
It 'enables suggestions for every discovered and Default profile' {
Set-StoreSearchSuggestionsEnabledForAllUsers
Set-StoreSearchSuggestionsEnabledForAllUsers | Should -BeTrue
Should -Invoke Set-StoreSearchSuggestionsEnabled -Times 3 -Exactly
Should -Invoke Set-StoreSearchSuggestionsEnabled -Times 1 -Exactly -ParameterFilter { $StoreAppsDatabase -match 'Users\\Default\\' }
@@ -72,6 +72,22 @@ Describe 'Store-search suggestion all-user operations' {
Should -Invoke Set-StoreSearchSuggestionsEnabled -Times 2 -Exactly
}
It 'returns false when any profile cannot be updated' {
Mock Set-StoreSearchSuggestionsDisabled { $false } -ParameterFilter { $StoreAppsDatabase -match 'Users\\Bob\\' }
Set-StoreSearchSuggestionsDisabledForAllUsers | Should -BeFalse
Should -Invoke Set-StoreSearchSuggestionsDisabled -Times 3 -Exactly
}
It 'returns false when no target profile can be resolved' {
Mock Get-ChildItem { @() }
Mock Get-StoreAppsDatabasePathForUser { $null }
Mock Write-Warning {}
Set-StoreSearchSuggestionsDisabledForAllUsers | Should -BeFalse
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match 'no target user profiles' }
}
}
Describe 'Set-StoreSearchSuggestionsDisabled' {
@@ -84,7 +100,7 @@ Describe 'Set-StoreSearchSuggestionsDisabled' {
}
It 'does not touch the filesystem in WhatIf mode' {
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db'
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' | Should -BeTrue
Should -Invoke Test-Path -Times 0 -Exactly
Should -Invoke Get-Acl -Times 0 -Exactly
@@ -99,7 +115,7 @@ Describe 'Set-StoreSearchSuggestionsDisabled' {
Mock Get-Acl { $acl }
Mock Set-Acl {}
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db'
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' | Should -BeTrue
Should -Invoke New-Item -Times 2 -Exactly
Should -Invoke New-Item -Times 1 -Exactly -ParameterFilter { $ItemType -eq 'Directory' }
@@ -118,7 +134,7 @@ Describe 'Set-StoreSearchSuggestionsDisabled' {
Mock Get-Acl { $acl }
Mock Set-Acl {}
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db'
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' | Should -BeTrue
Should -Invoke New-Item -Times 0 -Exactly
Should -Invoke Get-Acl -Times 1 -Exactly
@@ -133,7 +149,7 @@ Describe 'Set-StoreSearchSuggestionsDisabled' {
Mock Set-Acl { throw 'ACL must not be written after a read failure.' }
Mock Write-Warning {}
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db'
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' | Should -BeFalse
Should -Invoke Write-Warning -Times 1 -Exactly
Should -Invoke Write-Host -Times 0 -Exactly -ParameterFilter { $Object -like 'Disabled Microsoft Store search suggestions*' }
@@ -165,7 +181,7 @@ Describe 'Set-StoreSearchSuggestionsEnabled' {
}
It 'does nothing when the Store database does not exist' {
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db'
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' | Should -BeTrue
Should -Invoke Get-Acl -Times 0 -Exactly
Should -Invoke Remove-Item -Times 0 -Exactly
@@ -177,7 +193,7 @@ Describe 'Set-StoreSearchSuggestionsEnabled' {
Mock takeown { throw 'WhatIf should not take ownership.' }
Mock icacls { throw 'WhatIf should not change ACLs.' }
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db'
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' | Should -BeTrue
Should -Invoke Test-Path -Times 0 -Exactly
Should -Invoke takeown -Times 0 -Exactly
@@ -193,7 +209,7 @@ Describe 'Set-StoreSearchSuggestionsEnabled' {
Mock Set-Acl {}
Mock Remove-Item {}
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db'
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' | Should -BeTrue
Should -Invoke takeown -Times 1 -Exactly
Should -Invoke icacls -Times 1 -Exactly
@@ -211,14 +227,14 @@ Describe 'Set-StoreSearchSuggestionsEnabled' {
Mock Remove-Item {}
Mock Write-Warning {}
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db'
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' | Should -BeTrue
Should -Invoke Write-Warning -Times 1 -Exactly
Should -Invoke Set-Acl -Times 0 -Exactly
Should -Invoke Remove-Item -Times 1 -Exactly
}
It 'throws a contextual error when the database cannot be removed' {
It 'returns false when the database cannot be removed' {
$acl = New-TestStoreDatabaseAcl
Mock Test-Path { $true }
Mock takeown {}
@@ -226,9 +242,9 @@ Describe 'Set-StoreSearchSuggestionsEnabled' {
Mock Get-Acl { $acl }
Mock Set-Acl {}
Mock Remove-Item { throw 'database is locked' }
Mock Write-Warning {}
{
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db'
} | Should -Throw '*Failed to remove*database is locked*'
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase 'C:\Users\Alice\AppData\Local\Packages\store.db' | Should -BeFalse
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match 'Failed to remove.*database is locked' }
}
}
+18
View File
@@ -108,6 +108,15 @@ Describe 'Disable-TelemetryScheduledTasks' {
Should -Invoke Write-Host -Times 1 -Exactly -ParameterFilter { $Object -like "*$Expected*" }
}
It 'returns false for an unknown scheduler result' {
Mock Get-TelemetryScheduledTasks { @(@{ Path = '\Microsoft\Windows\Test\'; Name = 'Telemetry' }) }
Mock Invoke-NonBlocking { $null }
Mock Write-Warning {}
Disable-TelemetryScheduledTasks | Should -BeFalse
Should -Invoke Write-Warning -Times 1 -Exactly
}
}
Describe 'Enable-TelemetryScheduledTasks' {
@@ -179,4 +188,13 @@ Describe 'Enable-TelemetryScheduledTasks' {
Should -Invoke Write-Host -Times 1 -Exactly -ParameterFilter { $Object -like "*$Expected*" }
}
It 'returns false when the scheduler throws' {
Mock Get-TelemetryScheduledTasks { @(@{ Path = '\Microsoft\Windows\Test\'; Name = 'Telemetry' }) }
Mock Invoke-NonBlocking { throw 'scheduler unavailable' }
Mock Write-Warning {}
Enable-TelemetryScheduledTasks | Should -BeFalse
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -match 'scheduler unavailable' }
}
}
+50 -8
View File
@@ -23,12 +23,12 @@ BeforeAll {
Describe 'Enable-WindowsFeature' {
BeforeEach {
$script:Params = @{}
Mock Invoke-NonBlocking { @() }
Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $true; Output = $null; Error = $null } }
Mock Write-Host {}
}
It 'schedules the requested feature with the non-blocking runner' {
Enable-WindowsFeature -FeatureName 'Feature.One'
Enable-WindowsFeature -FeatureName 'Feature.One' | Should -BeTrue
Should -Invoke Invoke-NonBlocking -Times 1 -Exactly -ParameterFilter { $ArgumentList -eq 'Feature.One' }
}
@@ -40,7 +40,7 @@ Describe 'Enable-WindowsFeature' {
$script:optionalFeatureBlock = $ScriptBlock
$script:optionalFeatureArguments = $ArgumentList
}
Enable-WindowsFeature -FeatureName 'Feature.One'
Enable-WindowsFeature -FeatureName 'Feature.One' | Should -BeFalse
& $script:optionalFeatureBlock $script:optionalFeatureArguments
$global:OptionalFeatureCalls | Should -HaveCount 1
@@ -51,6 +51,27 @@ Describe 'Enable-WindowsFeature' {
$global:OptionalFeatureCalls[0].NoRestart | Should -BeTrue
}
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' }
}
It 'does not schedule changes in WhatIf mode' {
$script:Params = @{ WhatIf = $true }
@@ -69,19 +90,19 @@ Describe 'Enable-WindowsFeature' {
Enable-WindowsFeature -FeatureName 'Feature.One'
{ & $script:optionalFeatureBlock $script:optionalFeatureArguments } | Should -Throw 'feature servicing failed'
(& $script:optionalFeatureBlock $script:optionalFeatureArguments).Success | Should -BeFalse
}
}
Describe 'Disable-WindowsFeature' {
BeforeEach {
$script:Params = @{}
Mock Invoke-NonBlocking { @() }
Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $true; Output = $null; Error = $null } }
Mock Write-Host {}
}
It 'schedules the requested feature with the non-blocking runner' {
Disable-WindowsFeature -FeatureName 'Feature.One'
Disable-WindowsFeature -FeatureName 'Feature.One' | Should -BeTrue
Should -Invoke Invoke-NonBlocking -Times 1 -Exactly -ParameterFilter { $ArgumentList -eq 'Feature.One' }
}
@@ -93,7 +114,7 @@ Describe 'Disable-WindowsFeature' {
$script:optionalFeatureBlock = $ScriptBlock
$script:optionalFeatureArguments = $ArgumentList
}
Disable-WindowsFeature -FeatureName 'Feature.One'
Disable-WindowsFeature -FeatureName 'Feature.One' | Should -BeFalse
& $script:optionalFeatureBlock $script:optionalFeatureArguments
$global:OptionalFeatureCalls | Should -HaveCount 1
@@ -104,6 +125,27 @@ Describe 'Disable-WindowsFeature' {
$global:OptionalFeatureCalls[0].NoRestart | Should -BeTrue
}
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' }
}
It 'does not schedule changes in WhatIf mode' {
$script:Params = @{ WhatIf = $true }
@@ -122,7 +164,7 @@ Describe 'Disable-WindowsFeature' {
Disable-WindowsFeature -FeatureName 'Feature.One'
{ & $script:optionalFeatureBlock $script:optionalFeatureArguments } | Should -Throw 'feature servicing failed'
(& $script:optionalFeatureBlock $script:optionalFeatureArguments).Success | Should -BeFalse
}
}