126 lines
7.9 KiB
PowerShell
126 lines
7.9 KiB
PowerShell
#Requires -Version 5.1
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference='Stop'
|
|
Import-Module (Join-Path $PSScriptRoot '../src/Wind.Apps.psm1') -Force
|
|
Import-Module (Join-Path $PSScriptRoot '../src/Wind.Tui.psm1') -Force
|
|
$script:passed=0
|
|
function Assert-True { param([bool]$Condition,[string]$Message) if (-not $Condition) { throw $Message } }
|
|
function Assert-Throws { param([scriptblock]$Action) $threw=$false; try { & $Action | Out-Null } catch { $threw=$true }; Assert-True $threw 'Expected an exception.' }
|
|
function Run-Test { param([string]$Name,[scriptblock]$Action) & $Action; $script:passed++; Write-Output "PASS $Name" }
|
|
function New-App {
|
|
param([string]$Name='SpotifyAB.SpotifyMusic',[string]$Kind='Appx',[string]$Id='app-1')
|
|
[pscustomobject]@{Id=$Id;Name=$Name;Version='1.0';Publisher='Example';Kind=$Kind;Scope='user';Target=$Id;Protected=$false}
|
|
}
|
|
function New-Harness {
|
|
param([object[]]$Apps=@((New-App)))
|
|
$state=[pscustomobject]@{Apps=$Apps;Saves=0;Saved=$null;FailSaveAt=0;Browser=$false;InstallCode=0;RemoveCode=0;RetainApp=$false;Events=@()}
|
|
$backend=@{
|
|
Inventory={@($state.Apps)}.GetNewClosure()
|
|
BrowserPresent={param($b) $state.Events+='check-browser'; $state.Browser}.GetNewClosure()
|
|
InstallBrowser={param($b) $state.Events+='install-browser'; if ($state.InstallCode -eq 0) {$state.Browser=$true}; $state.InstallCode}.GetNewClosure()
|
|
Remove={param($a) $state.Events+='remove-'+$a.Id; if ($state.RemoveCode -eq 0 -and -not $state.RetainApp) {$state.Apps=@($state.Apps | Where-Object Id -ne $a.Id)}; $state.RemoveCode}.GetNewClosure()
|
|
}
|
|
$save={param($r) $state.Saves++; if ($state.Saves -eq $state.FailSaveAt) {throw 'Storage failure'}; $state.Saved=$r | ConvertTo-Json -Depth 8 | ConvertFrom-Json}.GetNewClosure()
|
|
[pscustomobject]@{State=$state;Backend=$backend;Save=$save;Plan=@(Get-WindAppPlan -Inventory $Apps)}
|
|
}
|
|
function Run-Changes {param($h) Invoke-WindAppChanges -Plan $h.Plan -Backend $h.Backend -Save $h.Save -Confirm:$false}
|
|
|
|
Run-Test 'Default selects curated apps, OneDrive and Edge; keeps personal apps' {
|
|
$apps=@((New-App),(New-App 'PersonalEditor' 'Desktop' 'editor'),(New-App 'Microsoft OneDrive' 'Desktop' 'onedrive'),(New-App 'Microsoft Edge' 'Desktop' 'edge'))
|
|
$plan=@(Get-WindAppPlan -Inventory $apps)
|
|
Assert-True (@($plan | Where-Object Status -eq 'Remove').Count -eq 3) 'Wrong defaults.'
|
|
Assert-True (($plan | Where-Object Id -eq 'editor').Status -eq 'Keep') 'Personal app removed by default.'
|
|
}
|
|
Run-Test 'Personal app opt-in and bundled exclusions' {
|
|
$apps=@((New-App),(New-App 'PersonalEditor' 'Desktop' 'editor'))
|
|
$plan=@(Get-WindAppPlan -Inventory $apps -ExtraId editor -ExcludeId app-1)
|
|
Assert-True (($plan | Where-Object Id -eq 'editor').Status -eq 'Remove') 'Personal selection ignored.'
|
|
Assert-True (($plan | Where-Object Id -eq 'app-1').Status -eq 'Keep') 'Exclusion ignored.'
|
|
}
|
|
Run-Test 'Protected dependencies cannot be opted into removal' {
|
|
foreach ($name in @('Microsoft Edge WebView2 Runtime','Microsoft.DesktopAppInstaller','Microsoft.WindowsStore','Microsoft.VCLibs.140.00','Bitdefender','Microsoft.GamingServices','NVIDIA Display Driver','Mullvad VPN','VeraCrypt')) {
|
|
$app=New-App $name
|
|
Assert-True ((Get-WindAppPlan -Inventory @($app) -ExtraId $app.Id).Status -eq 'Keep') "Protected app selectable: $name"
|
|
}
|
|
}
|
|
Run-Test 'OS nonremovable metadata overrides curated selection' {
|
|
$app=New-App; $app.Protected=$true
|
|
Assert-True ((Get-WindAppPlan -Inventory @($app)).Status -eq 'Keep') 'OS protection ignored.'
|
|
}
|
|
Run-Test 'Stale selection fails instead of broadening removal' {
|
|
Assert-Throws {Get-WindAppPlan -Inventory @((New-App)) -ExtraId missing}
|
|
}
|
|
Run-Test 'Ambiguous desktop registrations block execution' {
|
|
$h=New-Harness @((New-App 'Microsoft OneDrive' 'Desktop' 'a'),(New-App 'Microsoft OneDrive' 'Desktop' 'b'))
|
|
Assert-Throws {Run-Changes $h}
|
|
Assert-True ($h.State.Events.Count -eq 0) 'Blocked plan executed.'
|
|
}
|
|
Run-Test 'WhatIf does not call backend or write reports' {
|
|
$h=New-Harness
|
|
$result=Invoke-WindAppChanges -Plan $h.Plan -Backend $h.Backend -Save $h.Save -WhatIf
|
|
Assert-True ($result.Status -eq 'Preview' -and $h.State.Saves -eq 0 -and $h.State.Events.Count -eq 0) 'WhatIf touched backend.'
|
|
}
|
|
Run-Test 'Browser installed and verified before app removal' {
|
|
$h=New-Harness; $r=Run-Changes $h
|
|
Assert-True (($h.State.Events -join ',') -eq 'check-browser,install-browser,check-browser,remove-app-1') 'Unsafe operation order.'
|
|
Assert-True ($r.Status -eq 'Completed' -and $r.Entries[-1].Status -eq 'Removed') 'Removal not verified.'
|
|
}
|
|
Run-Test 'Failed browser install prevents every removal' {
|
|
$h=New-Harness; $h.State.InstallCode=1603
|
|
Assert-Throws {Run-Changes $h}
|
|
Assert-True (@($h.State.Events | Where-Object {$_ -like 'remove-*'}).Count -eq 0) 'Browser failure removed app.'
|
|
Assert-True ($h.State.Saved.Status -eq 'Incomplete') 'False completion.'
|
|
}
|
|
Run-Test 'Browser verification failure prevents removal' {
|
|
$h=New-Harness; $h.Backend.BrowserPresent={param($b) $false}
|
|
Assert-Throws {Run-Changes $h}
|
|
Assert-True ($h.State.Apps.Count -eq 1) 'Unverified browser allowed removal.'
|
|
}
|
|
Run-Test 'Re-running after removal is idempotent' {
|
|
$h=New-Harness; Run-Changes $h | Out-Null; $r=Run-Changes $h
|
|
Assert-True ($r.Entries[-1].Status -eq 'AlreadyAbsent') 'Repeated removal attempted.'
|
|
Assert-True (@($h.State.Events | Where-Object {$_ -like 'remove-*'}).Count -eq 1) 'Duplicate uninstall.'
|
|
}
|
|
Run-Test 'App update between preview and apply stops removal' {
|
|
$h=New-Harness; $h.State.Apps[0].Version='2.0'
|
|
Assert-Throws {Run-Changes $h}
|
|
Assert-True ($h.State.Apps.Count -eq 1 -and $h.State.Saved.Entries[-1].Status -eq 'Conflict') 'Stale app removed.'
|
|
}
|
|
Run-Test 'Uninstaller nonzero and reboot code stop later operations' {
|
|
foreach ($code in @(1603,3010,-1)) {
|
|
$h=New-Harness @((New-App),(New-App 'Microsoft OneDrive' 'Desktop' 'b')); $h.State.RemoveCode=$code
|
|
Assert-Throws {Run-Changes $h}
|
|
Assert-True (@($h.State.Events | Where-Object {$_ -like 'remove-*'}).Count -eq 1) 'Continued after uncertain uninstall.'
|
|
Assert-True ($h.State.Saved.Entries[-1].Status -eq 'Failed') 'False success.'
|
|
}
|
|
}
|
|
Run-Test 'Uninstaller success without disappearance is unverified' {
|
|
$h=New-Harness; $h.State.RetainApp=$true
|
|
Assert-Throws {Run-Changes $h}
|
|
Assert-True ($h.State.Saved.Entries[-1].Status -eq 'Unverified') 'Trusted exit code without readback.'
|
|
}
|
|
Run-Test 'Edge runs last and requires selected browser' {
|
|
$h=New-Harness @((New-App 'Microsoft Edge' 'Desktop' 'edge'),(New-App 'Microsoft OneDrive' 'Desktop' 'onedrive'))
|
|
Assert-Throws {Invoke-WindAppChanges -Plan $h.Plan -Browser @() -Backend $h.Backend -Save $h.Save}
|
|
$r=Run-Changes $h
|
|
Assert-True ($r.Entries[-1].Name -eq 'Microsoft Edge') 'Edge did not run last.'
|
|
}
|
|
Run-Test 'Write-ahead report failure prevents app removal' {
|
|
$h=New-Harness; $h.State.Browser=$true; $h.State.FailSaveAt=5
|
|
Assert-Throws {Run-Changes $h}
|
|
Assert-True ($h.State.Apps.Count -eq 1) 'Removal ran without saved intent.'
|
|
}
|
|
Run-Test 'Forged protected plan is rejected before any action' {
|
|
$h=New-Harness @((New-App 'Microsoft Edge WebView2 Runtime' 'Desktop'))
|
|
$h.Plan[0].Status='Remove'; $h.Plan[0].Protected=$false
|
|
Assert-Throws {Run-Changes $h}
|
|
Assert-True ($h.State.Events.Count -eq 0) 'Forged plan executed.'
|
|
}
|
|
Run-Test 'Menu selection rejects invalid input and supports combinations' {
|
|
Assert-True ((@(ConvertFrom-WindChoice '1,3,1' 3) -join ',') -eq '0,2') 'Multi-selection incorrect.'
|
|
Assert-True (@(ConvertFrom-WindChoice 'none' 3).Count -eq 0) 'None incorrect.'
|
|
Assert-True (@(ConvertFrom-WindChoice 'all' 3).Count -eq 3) 'All incorrect.'
|
|
foreach ($bad in @('0','4','-1','1;whoami','1.5')) {Assert-Throws {ConvertFrom-WindChoice $bad 3}}
|
|
}
|
|
Write-Output "$script:passed app/menu tests passed. Only synthetic inventories and backends were used."
|