Files
Win11Debloat/Tests/Registry-BackupValidation.Tests.ps1
T

302 lines
13 KiB
PowerShell

BeforeAll {
$registryPathHelperScriptPath = Join-Path $PSScriptRoot '..\Scripts\Helpers\Registry-PathHelpers.ps1'
$validationScriptPath = Join-Path $PSScriptRoot '..\Scripts\Features\Registry-BackupValidation.ps1'
. $registryPathHelperScriptPath
. $validationScriptPath
}
Describe 'Get-NormalizedSelectedFeatureIdsFromBackup' {
It 'returns distinct feature IDs without changing their first occurrence' {
$backup = [PSCustomObject]@{
SelectedFeatures = @('DisableTelemetry', 'disabletelemetry', 'DisableCopilot')
}
$result = Get-NormalizedSelectedFeatureIdsFromBackup -Backup $backup
$result.Errors | Should -BeNullOrEmpty
$result.SelectedFeatures | Should -Be @('DisableTelemetry', 'DisableCopilot')
}
It 'reports missing SelectedFeatures' {
$result = Get-NormalizedSelectedFeatureIdsFromBackup -Backup ([PSCustomObject]@{})
$result.SelectedFeatures | Should -BeNullOrEmpty
$result.Errors | Should -Contain 'Missing property: SelectedFeatures'
}
It 'reports non-string and empty feature IDs' {
$backup = [PSCustomObject]@{
SelectedFeatures = @('DisableTelemetry', '', 42, $null)
}
$result = Get-NormalizedSelectedFeatureIdsFromBackup -Backup $backup
$result.SelectedFeatures | Should -Be 'DisableTelemetry'
$result.Errors | Should -Contain 'SelectedFeatures must contain non-empty string feature IDs.'
}
}
Describe 'Get-NormalizedSelectedUndoFeatureIdsFromBackup' {
It 'allows backups created before SelectedUndoFeatures was introduced' {
$result = Get-NormalizedSelectedUndoFeatureIdsFromBackup -Backup ([PSCustomObject]@{})
$result.SelectedUndoFeatures | Should -BeNullOrEmpty
$result.Errors | Should -BeNullOrEmpty
}
It 'deduplicates undo feature IDs case-insensitively' {
$backup = [PSCustomObject]@{
SelectedUndoFeatures = @('EnableTelemetry', 'enabletelemetry')
}
$result = Get-NormalizedSelectedUndoFeatureIdsFromBackup -Backup $backup
$result.Errors | Should -BeNullOrEmpty
$result.SelectedUndoFeatures | Should -Be 'EnableTelemetry'
}
}
Describe 'Get-SelectedRegistryFeaturesForBackupValidation' {
BeforeEach {
$script:Features = @{
ApplyFeature = [PSCustomObject]@{ Id = 'ApplyFeature'; RegistryKey = 'ApplyFeature.reg' }
UndoFeature = [PSCustomObject]@{ Id = 'UndoFeature'; RegistryKey = 'UndoFeature.reg'; RegistryUndoKey = 'UndoFeature.undo.reg' }
FallbackUndoFeature = [PSCustomObject]@{ Id = 'FallbackUndoFeature'; RegistryKey = 'FallbackUndoFeature.reg'; RegistryUndoKey = '' }
CustomFeature = [PSCustomObject]@{ Id = 'CustomFeature'; RegistryKey = '' }
}
}
It 'creates a case-insensitive registry value-name set' {
$valueNames = ConvertTo-RegistryValueNameSet -ValueNames @('Enabled', 'enabled', 'Mode')
$valueNames.Count | Should -Be 2
$valueNames.Contains('ENABLED') | Should -BeTrue
$valueNames.Contains('mode') | Should -BeTrue
}
It 'selects registry-backed apply features and reports unknown IDs' {
$errors = New-Object System.Collections.Generic.List[string]
$result = @(Get-SelectedRegistryFeaturesForBackupValidation -SelectedFeatureIds @('ApplyFeature', 'CustomFeature', 'MissingFeature') -IsUndoFeature:$false -Errors $errors)
$result.Id | Should -Be 'ApplyFeature'
$errors | Should -Contain "Selected feature 'MissingFeature' was not found in the current feature catalog."
}
It 'uses undo registry keys and falls back to apply keys when needed' {
$errors = New-Object System.Collections.Generic.List[string]
$result = @(Get-SelectedRegistryFeaturesForBackupValidation -SelectedFeatureIds @('UndoFeature', 'FallbackUndoFeature') -IsUndoFeature:$true -Errors $errors)
$result.Id | Should -Be @('UndoFeature', 'FallbackUndoFeature')
$errors | Should -BeNullOrEmpty
}
}
Describe 'Test-RegistryValueKindNameSupported' {
It 'accepts supported registry value kind <KindName>' -ForEach @(
@{ KindName = 'dword' }
@{ KindName = 'QWord' }
@{ KindName = 'MultiString' }
) {
Test-RegistryValueKindNameSupported -KindName $KindName | Should -BeTrue
}
It 'rejects <Case>' -ForEach @(
@{ Case = 'a null kind name'; KindName = $null }
@{ Case = 'an empty kind name'; KindName = '' }
@{ Case = 'the Unknown registry kind'; KindName = 'Unknown' }
@{ Case = 'the unsupported None registry kind'; KindName = 'None' }
@{ Case = 'an invalid registry kind name'; KindName = 'NotARegistryValueKind' }
) {
Test-RegistryValueKindNameSupported -KindName $KindName | Should -BeFalse
}
}
Describe 'Test-RegistryValueDataMatchesKind' {
It 'accepts <Case>' -ForEach @(
@{ Case = 'the largest DWord'; Kind = 'DWord'; Data = [uint32]::MaxValue }
@{ Case = 'the largest QWord'; Kind = 'QWord'; Data = [uint64]::MaxValue }
@{ Case = 'non-empty Binary bytes'; Kind = 'Binary'; Data = @(0, 255) }
@{ Case = 'empty Binary bytes'; Kind = 'Binary'; Data = [byte[]]::new(0) }
@{ Case = 'a string array'; Kind = 'MultiString'; Data = @('one', 'two') }
) {
Test-RegistryValueDataMatchesKind -KindName $Kind -Data $Data | Should -BeTrue
}
It 'rejects <Case>' -ForEach @(
@{ Case = 'an overflowing DWord'; Kind = 'DWord'; Data = '4294967296' }
@{ Case = 'a negative QWord'; Kind = 'QWord'; Data = -1 }
@{ Case = 'null Binary data'; Kind = 'Binary'; Data = $null }
@{ Case = 'an invalid binary byte'; Kind = 'Binary'; Data = @(0, 256) }
@{ Case = 'an object in a MultiString'; Kind = 'MultiString'; Data = @('one', [PSCustomObject]@{}) }
) {
Test-RegistryValueDataMatchesKind -KindName $Kind -Data $Data | Should -BeFalse
}
}
Describe 'Get-NormalizedRegistryValueName' {
It 'normalizes a null value name to the default registry value' {
Get-NormalizedRegistryValueName -ValueName $null | Should -Be ''
Get-RegistryValueReferenceForError -SnapshotPath 'HKEY_CURRENT_USER\Software\Example' -ValueName '' |
Should -Be 'HKEY_CURRENT_USER\Software\Example\\(Default)'
}
It 'keeps a named registry value in the error reference' {
Get-RegistryValueReferenceForError -SnapshotPath 'HKEY_CURRENT_USER\Software\Example' -ValueName 'Setting' |
Should -Be 'HKEY_CURRENT_USER\Software\Example\\Setting'
}
}
Describe 'Normalize-RegistryKeySnapshot' {
It 'normalizes nested snapshots and supplies value defaults' {
$snapshot = [PSCustomObject]@{
Path = 'HKEY_CURRENT_USER\Software\Example'
Exists = $true
Values = @(
[PSCustomObject]@{ Name = 'Enabled'; Kind = 'DWord'; Data = 1 }
[PSCustomObject]@{ Name = 'Removed'; Exists = $false }
)
SubKeys = @(
[PSCustomObject]@{
Path = 'HKEY_CURRENT_USER\Software\Example\Child'
Exists = $true
}
)
}
$result = Normalize-RegistryKeySnapshot -Snapshot $snapshot
$result.Path | Should -Be 'HKEY_CURRENT_USER\Software\Example'
$result.Exists | Should -BeTrue
$result.Values.Count | Should -Be 2
$result.Values[0].Exists | Should -BeTrue
$result.Values[0].Kind | Should -Be 'DWord'
$result.Values[1].Exists | Should -BeFalse
$result.Values[1].Kind | Should -BeNullOrEmpty
$result.Values[1].Data | Should -BeNullOrEmpty
$result.SubKeys[0].Path | Should -Be 'HKEY_CURRENT_USER\Software\Example\Child'
$result.SubKeys[0].Values | Should -BeNullOrEmpty
}
It 'rejects a snapshot without a registry path' {
{ Normalize-RegistryKeySnapshot -Snapshot ([PSCustomObject]@{ Exists = $true }) } |
Should -Throw 'Backup validation failed: Registry key snapshot is missing Path.'
}
}
Describe 'Test-RegistrySnapshotAgainstAllowList' {
It 'normalizes path separators and hive casing' {
Get-NormalizedRegistryPathKey -Path 'hkey_current_user/Software/Example/' |
Should -Match '^HKEY_CURRENT_USER\\+Software\\Example$'
}
It 'matches configured value names case-insensitively' {
$planMap = New-RegistryBackupAllowListPlanMap -CapturePlans @(
[PSCustomObject]@{
Path = 'HKEY_CURRENT_USER\Software\Example'
IncludeSubKeys = $false
CaptureAllValues = $false
ValueNames = @('Enabled')
}
)
$planMatch = Find-RegistryAllowListPlanMatch -NormalizedPath (Get-NormalizedRegistryPathKey -Path 'HKEY_CURRENT_USER\Software\Example') -PlanMap $planMap
Test-RegistryValueAllowedByPlan -PlanMatch $planMatch -ValueName 'enabled' | Should -BeTrue
Test-RegistryValueAllowedByPlan -PlanMatch $planMatch -ValueName 'Unexpected' | Should -BeFalse
}
It 'allows configured values at an exact path' {
$planMap = New-RegistryBackupAllowListPlanMap -CapturePlans @(
[PSCustomObject]@{
Path = 'HKEY_CURRENT_USER\Software\Example'
IncludeSubKeys = $false
CaptureAllValues = $false
ValueNames = @('Enabled')
}
)
$errors = New-Object 'System.Collections.Generic.List[string]'
$snapshot = [PSCustomObject]@{
Path = 'HKEY_CURRENT_USER\Software\Example'
Values = @([PSCustomObject]@{ Name = 'Enabled'; Exists = $true; Kind = 'DWord'; Data = 1 })
SubKeys = @()
}
Test-RegistrySnapshotAgainstAllowList -Snapshot $snapshot -PlanMap $planMap -Errors $errors
$errors | Should -BeNullOrEmpty
}
It 'rejects corrupt value data even when its path and kind are allowed' {
$planMap = New-RegistryBackupAllowListPlanMap -CapturePlans @(
[PSCustomObject]@{
Path = 'HKEY_CURRENT_USER\Software\Example'
IncludeSubKeys = $false
CaptureAllValues = $false
ValueNames = @('Bytes')
}
)
$errors = New-Object 'System.Collections.Generic.List[string]'
$snapshot = [PSCustomObject]@{
Path = 'HKEY_CURRENT_USER\Software\Example'
Values = @([PSCustomObject]@{ Name = 'Bytes'; Exists = $true; Kind = 'Binary'; Data = @(1, 256) })
SubKeys = @()
}
Test-RegistrySnapshotAgainstAllowList -Snapshot $snapshot -PlanMap $planMap -Errors $errors
$errors | Should -HaveCount 1
$errors[0] | Should -BeLike "Backup contains invalid registry data for kind 'Binary'*"
}
It 'allows all values in descendants of a recursive plan' {
$planMap = New-RegistryBackupAllowListPlanMap -CapturePlans @(
[PSCustomObject]@{
Path = 'HKEY_CURRENT_USER\Software\Example'
IncludeSubKeys = $true
CaptureAllValues = $true
ValueNames = @()
}
)
$errors = New-Object 'System.Collections.Generic.List[string]'
$snapshot = [PSCustomObject]@{
Path = 'HKEY_CURRENT_USER\Software\Example\Child'
Values = @([PSCustomObject]@{ Name = 'Unlisted'; Exists = $true; Kind = 'String'; Data = 'value' })
SubKeys = @()
}
Test-RegistrySnapshotAgainstAllowList -Snapshot $snapshot -PlanMap $planMap -Errors $errors
$errors | Should -BeNullOrEmpty
}
It 'reports unexpected paths and values' {
$planMap = New-RegistryBackupAllowListPlanMap -CapturePlans @(
[PSCustomObject]@{
Path = 'HKEY_CURRENT_USER\Software\Example'
IncludeSubKeys = $false
CaptureAllValues = $false
ValueNames = @('Enabled')
}
)
$errors = New-Object 'System.Collections.Generic.List[string]'
$unexpectedPathSnapshot = [PSCustomObject]@{
Path = 'HKEY_CURRENT_USER\Software\Unexpected'
Values = @()
SubKeys = @()
}
$unexpectedValueSnapshot = [PSCustomObject]@{
Path = 'HKEY_CURRENT_USER\Software\Example'
Values = @([PSCustomObject]@{ Name = 'Unexpected'; Exists = $true; Kind = 'String'; Data = 'value' })
SubKeys = @()
}
Test-RegistrySnapshotAgainstAllowList -Snapshot $unexpectedPathSnapshot -PlanMap $planMap -Errors $errors
Test-RegistrySnapshotAgainstAllowList -Snapshot $unexpectedValueSnapshot -PlanMap $planMap -Errors $errors
$errors | Should -Contain "Backup contains unexpected registry path 'HKEY_CURRENT_USER\Software\Unexpected' that is not allowed by SelectedFeatures."
$errors | Should -Contain "Backup contains unexpected value 'Unexpected' under 'HKEY_CURRENT_USER\Software\Example'."
}
}