| 1 | $ErrorActionPreference = 'Stop' |
| 2 | Set-StrictMode -Version Latest |
| 3 | |
| 4 | . "$PSScriptRoot\update-user-path.ps1" |
| 5 | |
| 6 | function Assert-Equal { |
| 7 | param( |
| 8 | [AllowNull()] |
| 9 | $Expected, |
| 10 | |
| 11 | [AllowNull()] |
| 12 | $Actual, |
| 13 | |
| 14 | [Parameter(Mandatory = $true)] |
| 15 | [string] $Because |
| 16 | ) |
| 17 | |
| 18 | if (-not [object]::Equals($Expected, $Actual)) { |
| 19 | throw "$Because`nExpected: <$Expected>`nActual: <$Actual>" |
| 20 | } |
| 21 | } |
| 22 | |
| 23 | $entry = 'C:\CodeWhalePathTest\bin' |
| 24 | $longPath = ((1..80) | ForEach-Object { 'C:\CWCanary\DevelopmentTool{0:D4}\bin' -f $_ }) -join ';' |
| 25 | if ($longPath.Length -le 1800) { |
| 26 | throw "The long-PATH fixture is too short: $($longPath.Length)." |
| 27 | } |
| 28 | |
| 29 | $addedToEmpty = Get-UpdatedUserPath ` |
| 30 | -Current '' ` |
| 31 | -RequestedOperation Add ` |
| 32 | -RequestedEntry $entry |
| 33 | Assert-Equal $entry $addedToEmpty 'Adding to a missing or empty PATH must create only the requested entry.' |
| 34 | |
| 35 | $added = Get-UpdatedUserPath ` |
| 36 | -Current $longPath ` |
| 37 | -RequestedOperation Add ` |
| 38 | -RequestedEntry $entry |
| 39 | Assert-Equal "$longPath;$entry" $added 'Adding to a long PATH must preserve every existing character.' |
| 40 | |
| 41 | $expanded = '%USERPROFILE%\bin;C:\Tools' |
| 42 | $expandedAdded = Get-UpdatedUserPath ` |
| 43 | -Current $expanded ` |
| 44 | -RequestedOperation Add ` |
| 45 | -RequestedEntry $entry |
| 46 | Assert-Equal "$expanded;$entry" $expandedAdded 'Expandable variable references must remain unexpanded.' |
| 47 | |
| 48 | $alreadyPresent = "$longPath;$($entry.ToUpperInvariant())\" |
| 49 | $unchanged = Get-UpdatedUserPath ` |
| 50 | -Current $alreadyPresent ` |
| 51 | -RequestedOperation Add ` |
| 52 | -RequestedEntry $entry |
| 53 | Assert-Equal $alreadyPresent $unchanged 'Adding must be idempotent across case and a trailing slash.' |
| 54 | |
| 55 | $substringPath = 'C:\CodeWhalePathTest\bin-tools;C:\Other' |
| 56 | $substringAdded = Get-UpdatedUserPath ` |
| 57 | -Current $substringPath ` |
| 58 | -RequestedOperation Add ` |
| 59 | -RequestedEntry $entry |
| 60 | Assert-Equal "$substringPath;$entry" $substringAdded 'A substring must not count as an exact PATH entry.' |
| 61 | |
| 62 | $trailingDelimiter = 'C:\One;C:\Two;' |
| 63 | $trailingAdded = Get-UpdatedUserPath ` |
| 64 | -Current $trailingDelimiter ` |
| 65 | -RequestedOperation Add ` |
| 66 | -RequestedEntry $entry |
| 67 | Assert-Equal "$trailingDelimiter$entry" $trailingAdded 'Adding after a trailing delimiter must not create a blank entry.' |
| 68 | |
| 69 | $removalSource = "C:\Before;$entry;C:\After" |
| 70 | $removed = Get-UpdatedUserPath ` |
| 71 | -Current $removalSource ` |
| 72 | -RequestedOperation Remove ` |
| 73 | -RequestedEntry $entry |
| 74 | Assert-Equal 'C:\Before;C:\After' $removed 'Removing must delete only the exact CodeWhale entry.' |
| 75 | |
| 76 | $duplicateSource = "$entry;C:\Keep;$($entry.ToUpperInvariant())\" |
| 77 | $duplicatesRemoved = Get-UpdatedUserPath ` |
| 78 | -Current $duplicateSource ` |
| 79 | -RequestedOperation Remove ` |
| 80 | -RequestedEntry $entry |
| 81 | Assert-Equal 'C:\Keep' $duplicatesRemoved 'Uninstall must remove all equivalent CodeWhale entries.' |
| 82 | |
| 83 | $unrelated = 'C:\One;C:\CodeWhalePathTest\bin-tools;C:\Two' |
| 84 | $unrelatedResult = Get-UpdatedUserPath ` |
| 85 | -Current $unrelated ` |
| 86 | -RequestedOperation Remove ` |
| 87 | -RequestedEntry $entry |
| 88 | Assert-Equal $unrelated $unrelatedResult 'Removing an absent entry must leave the PATH byte-for-byte unchanged.' |
| 89 | |
| 90 | if ([System.Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT) { |
| 91 | $testKeyPath = "Software\CodeWhale\Tests\UserPath-$PID-$([guid]::NewGuid().ToString('N'))" |
| 92 | $testKey = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey($testKeyPath) |
| 93 | if ($null -eq $testKey) { |
| 94 | throw 'Could not create the isolated registry test key.' |
| 95 | } |
| 96 | |
| 97 | try { |
| 98 | $testKey.SetValue('Path', $longPath, [Microsoft.Win32.RegistryValueKind]::ExpandString) |
| 99 | $changed = Set-RegistryPathEntry ` |
| 100 | -RegistryKey $testKey ` |
| 101 | -RequestedOperation Add ` |
| 102 | -RequestedEntry $entry |
| 103 | Assert-Equal $true $changed 'The registry helper must report a long PATH update.' |
| 104 | |
| 105 | $raw = [string] $testKey.GetValue( |
| 106 | 'Path', |
| 107 | $null, |
| 108 | [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames |
| 109 | ) |
| 110 | Assert-Equal "$longPath;$entry" $raw 'The registry helper must not truncate a long PATH.' |
| 111 | Assert-Equal ` |
| 112 | ([Microsoft.Win32.RegistryValueKind]::ExpandString) ` |
| 113 | ($testKey.GetValueKind('Path')) ` |
| 114 | 'The registry helper must preserve REG_EXPAND_SZ.' |
| 115 | |
| 116 | $changedAgain = Set-RegistryPathEntry ` |
| 117 | -RegistryKey $testKey ` |
| 118 | -RequestedOperation Add ` |
| 119 | -RequestedEntry $entry |
| 120 | Assert-Equal $false $changedAgain 'A repeated install must not rewrite the registry value.' |
| 121 | |
| 122 | $removedFromRegistry = Set-RegistryPathEntry ` |
| 123 | -RegistryKey $testKey ` |
| 124 | -RequestedOperation Remove ` |
| 125 | -RequestedEntry $entry |
| 126 | Assert-Equal $true $removedFromRegistry 'The registry helper must remove the installed entry.' |
| 127 | |
| 128 | $restored = [string] $testKey.GetValue( |
| 129 | 'Path', |
| 130 | $null, |
| 131 | [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames |
| 132 | ) |
| 133 | Assert-Equal $longPath $restored 'Install followed by uninstall must restore the long PATH exactly.' |
| 134 | |
| 135 | $testKey.SetValue('Path', 'C:\Plain', [Microsoft.Win32.RegistryValueKind]::String) |
| 136 | [void] (Set-RegistryPathEntry ` |
| 137 | -RegistryKey $testKey ` |
| 138 | -RequestedOperation Add ` |
| 139 | -RequestedEntry $entry) |
| 140 | Assert-Equal ` |
| 141 | ([Microsoft.Win32.RegistryValueKind]::String) ` |
| 142 | ($testKey.GetValueKind('Path')) ` |
| 143 | 'The registry helper must preserve REG_SZ.' |
| 144 | |
| 145 | $testKey.DeleteValue('Path') |
| 146 | $missingRemoval = Set-RegistryPathEntry ` |
| 147 | -RegistryKey $testKey ` |
| 148 | -RequestedOperation Remove ` |
| 149 | -RequestedEntry $entry |
| 150 | Assert-Equal $false $missingRemoval 'Removing from a missing PATH must be a no-op.' |
| 151 | Assert-Equal ` |
| 152 | $false ` |
| 153 | (@($testKey.GetValueNames()) -contains 'Path') ` |
| 154 | 'A no-op uninstall must not create a missing PATH value.' |
| 155 | } |
| 156 | finally { |
| 157 | $testKey.Close() |
| 158 | [Microsoft.Win32.Registry]::CurrentUser.DeleteSubKeyTree($testKeyPath, $false) |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | Write-Host 'Windows installer PATH helper tests passed.' |
| 163 |