diff --git a/DevSetup/Private/Providers/Chocolatey/Find-Chocolatey.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Find-Chocolatey.Tests.ps1 new file mode 100644 index 0000000..e84c521 --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Find-Chocolatey.Tests.ps1 @@ -0,0 +1,181 @@ +BeforeAll { + . $PSScriptRoot\Find-Chocolatey.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Get-EnvironmentVariable.ps1 +} + +Describe "Find-Chocolatey" { + + Context "When Chocolatey is found via Get-Command" { + It "Should return the path from Get-Command when choco is in PATH" { + $expectedPath = Join-Path $TestDrive "chocolatey" "bin" "choco.exe" + Mock Get-Command { + return @{ Path = $expectedPath } + } -ParameterFilter { $Name -eq "choco" } + Mock Write-StatusMessage { } + + $result = Find-Chocolatey + + $result | Should -Be $expectedPath + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -match "Found Chocolatey at: $([regex]::Escape($expectedPath))" -and $Verbosity -eq "Debug" + } + } + } + + Context "When Get-Command fails but ChocolateyInstall environment variable exists" { + It "Should return path from ChocolateyInstall environment variable" { + $chocolateyInstallPath = Join-Path $TestDrive "Chocolatey" + $expectedPath = Join-Path $chocolateyInstallPath "bin" "choco.exe" + + Mock Get-Command { return $null } -ParameterFilter { $Name -eq "choco" } + Mock Get-EnvironmentVariable { + return $chocolateyInstallPath + } -ParameterFilter { $Name -eq "ChocolateyInstall" } + Mock Test-Path { $true } -ParameterFilter { $Path -eq $expectedPath } + Mock Write-StatusMessage { } + + $result = Find-Chocolatey + + $result | Should -Be $expectedPath + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -match "Found Chocolatey at: $([regex]::Escape($expectedPath))" -and $Verbosity -eq "Debug" + } + } + } + + Context "When ChocolateyInstall environment variable is not set" { + It "Should return null and log debug message" { + Mock Get-Command { return $null } -ParameterFilter { $Name -eq "choco" } + Mock Get-EnvironmentVariable { return $null } -ParameterFilter { $Name -eq "ChocolateyInstall" } + Mock Write-StatusMessage { } + + $result = Find-Chocolatey + + $result | Should -BeNullOrEmpty + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -eq "ChocolateyInstall environment variable is not set." -and $Verbosity -eq "Debug" + } + } + } + + Context "When ChocolateyInstall path exists but choco.exe does not exist" { + It "Should return null and log debug message about missing executable" { + $chocolateyInstallPath = Join-Path $TestDrive "Chocolatey" + $expectedPath = Join-Path $chocolateyInstallPath "bin" "choco.exe" + + Mock Get-Command { return $null } -ParameterFilter { $Name -eq "choco" } + Mock Get-EnvironmentVariable { + return $chocolateyInstallPath + } -ParameterFilter { $Name -eq "ChocolateyInstall" } + Mock Test-Path { $false } -ParameterFilter { $Path -eq $expectedPath } + Mock Write-StatusMessage { } + + $result = Find-Chocolatey + + $result | Should -BeNullOrEmpty + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -eq "Chocolatey executable not found at expected path: $expectedPath" -and $Verbosity -eq "Debug" + } + } + } + + Context "When Get-Command throws an exception" { + It "Should handle Get-Command exception and continue with environment variable lookup" { + $chocolateyInstallPath = Join-Path $TestDrive "Chocolatey" + $expectedPath = Join-Path $chocolateyInstallPath "bin" "choco.exe" + + Mock Get-Command { throw "Command not found error" } -ParameterFilter { $Name -eq "choco" } + Mock Get-EnvironmentVariable { + return $chocolateyInstallPath + } -ParameterFilter { $Name -eq "ChocolateyInstall" } + Mock Test-Path { $true } -ParameterFilter { $Path -eq $expectedPath } + Mock Write-StatusMessage { } + + $result = Find-Chocolatey + + $result | Should -Be $expectedPath + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -match "Error finding Chocolatey command:" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -match "Found Chocolatey at: $([regex]::Escape($expectedPath))" -and $Verbosity -eq "Debug" + } + } + } + + Context "When Get-EnvironmentVariable throws an exception" { + It "Should handle Get-EnvironmentVariable exception and return null" { + Mock Get-Command { return $null } -ParameterFilter { $Name -eq "choco" } + Mock Get-EnvironmentVariable { throw "Environment variable access error" } -ParameterFilter { $Name -eq "ChocolateyInstall" } + Mock Write-StatusMessage { } + + $result = Find-Chocolatey + + $result | Should -BeNullOrEmpty + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -match "Error retrieving ChocolateyInstall environment variable:" -and $Verbosity -eq "Error" + } + } + } + + Context "When Join-Path throws an exception" { + It "Should handle Join-Path exception and return null" { + $chocolateyInstallPath = "InvalidPath:" + + Mock Get-Command { return $null } -ParameterFilter { $Name -eq "choco" } + Mock Get-EnvironmentVariable { + return $chocolateyInstallPath + } -ParameterFilter { $Name -eq "ChocolateyInstall" } + Mock Join-Path { throw "Invalid path error" } + Mock Write-StatusMessage { } + + $result = Find-Chocolatey + + $result | Should -BeNullOrEmpty + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -match "Error constructing Chocolatey path:" -and $Verbosity -eq "Error" + } + } + } + + Context "When all operations succeed via Get-Command" { + It "Should not attempt environment variable lookup when Get-Command succeeds" { + $expectedPath = Join-Path $TestDrive "chocolatey" "bin" "choco.exe" + Mock Get-Command { + return @{ Path = $expectedPath } + } -ParameterFilter { $Name -eq "choco" } + Mock Get-EnvironmentVariable { throw "Should not be called" } + Mock Write-StatusMessage { } + + $result = Find-Chocolatey + + $result | Should -Be $expectedPath + Assert-MockCalled Get-EnvironmentVariable -Times 0 -Scope It + } + } + + Context "Integration scenarios" { + It "Should return path when both methods would work but Get-Command takes precedence" { + $commandPath = Join-Path $TestDrive "system" "choco.exe" + $envInstallPath = Join-Path $TestDrive "custom" "chocolatey" + $envPath = Join-Path $envInstallPath "bin" "choco.exe" + + Mock Get-Command { + return @{ Path = $commandPath } + } -ParameterFilter { $Name -eq "choco" } + Mock Get-EnvironmentVariable { + return $envInstallPath + } -ParameterFilter { $Name -eq "ChocolateyInstall" } + Mock Test-Path { $true } + Mock Write-StatusMessage { } + + $result = Find-Chocolatey + + # Should return the Get-Command path, not the environment variable path + $result | Should -Be $commandPath + # Environment variable should not be called since Get-Command succeeded + Assert-MockCalled Get-EnvironmentVariable -Times 0 -Scope It + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Find-Chocolatey.ps1 b/DevSetup/Private/Providers/Chocolatey/Find-Chocolatey.ps1 new file mode 100644 index 0000000..017ad23 --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Find-Chocolatey.ps1 @@ -0,0 +1,46 @@ +Function Find-Chocolatey { + [CmdletBinding()] + [OutputType([string])] + Param( + ) + + # Check if Chocolatey is installed + try { + $Path = (Get-Command "choco" -ErrorAction SilentlyContinue).Path + } catch { + Write-StatusMessage "Error finding Chocolatey command: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + } + + if ($Path) { + Write-StatusMessage "Found Chocolatey at: $Path" -Verbosity Debug + return $Path + } else { + try { + $ChocolateyInstallEnvPath = Get-EnvironmentVariable ChocolateyInstall + } catch { + Write-StatusMessage "Error retrieving ChocolateyInstall environment variable: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $null + } + if (-not $ChocolateyInstallEnvPath) { + Write-StatusMessage "ChocolateyInstall environment variable is not set." -Verbosity Debug + return $null + } else { + try { + $Path = Join-Path $ChocolateyInstallEnvPath "bin\choco.exe" + } catch { + Write-StatusMessage "Error constructing Chocolatey path: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $null + } + if (Test-Path $Path) { + Write-StatusMessage "Found Chocolatey at: $Path" -Verbosity Debug + return $Path + } else { + Write-StatusMessage "Chocolatey executable not found at expected path: $Path" -Verbosity Debug + return $null + } + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.Tests.ps1 index d0a1feb..a4a86bf 100644 --- a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.Tests.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.Tests.ps1 @@ -1,51 +1,168 @@ BeforeAll { . $PSScriptRoot\Get-ChocolateyCacheFile.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Get-DevSetupCachePath.ps1 - Mock Write-Error { } } Describe "Get-ChocolateyCacheFile" { - Context "When Get-DevSetupCachePath returns a valid path" { + + Context "When Get-DevSetupCachePath succeeds" { It "Should return the correct cache file path" { - if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { - Mock Get-DevSetupCachePath { return "$TestDrive\Users\Test\devsetup\.cache" } - $result = Get-ChocolateyCacheFile - $result | Should -Be "$TestDrive\Users\Test\devsetup\.cache\chocolatey.cache" - } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { - Mock Get-DevSetupCachePath { return "$TestDrive/home/testuser/devsetup/.cache" } - $result = Get-ChocolateyCacheFile - $result | Should -Be "$TestDrive/home/testuser/devsetup/.cache/chocolatey.cache" - } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { - Mock Get-DevSetupCachePath { return "$TestDrive/Users/TestUser/devsetup/.cache" } - $result = Get-ChocolateyCacheFile - $result | Should -Be "$TestDrive/Users/TestUser/devsetup/.cache/chocolatey.cache" - } + $expectedCachePath = Join-Path $TestDrive ".cache" + $expectedCacheFile = Join-Path $expectedCachePath "chocolatey.cache" + + Mock Get-DevSetupCachePath { return $expectedCachePath } + Mock Write-StatusMessage { } + + $result = Get-ChocolateyCacheFile + + $result | Should -Be $expectedCacheFile + Assert-MockCalled Get-DevSetupCachePath -Times 1 -Scope It } } - Context "When Get-DevSetupCachePath returns a different path" { - It "Should append chocolatey.cache to the returned path" { - if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { - Mock Get-DevSetupCachePath { return "$TestDrive\DevSetupCache" } - $result = Get-ChocolateyCacheFile - $result | Should -Be "$TestDrive\DevSetupCache\chocolatey.cache" - } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { - Mock Get-DevSetupCachePath { return "$TestDrive/home/testuser/devsetupcache/.cache" } - $result = Get-ChocolateyCacheFile - $result | Should -Be "$TestDrive/home/testuser/devsetupcache/.cache/chocolatey.cache" - } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { - Mock Get-DevSetupCachePath { return "$TestDrive/Users/TestUser/devsetupcache/.cache" } - $result = Get-ChocolateyCacheFile - $result | Should -Be "$TestDrive/Users/TestUser/devsetupcache/.cache/chocolatey.cache" + Context "When Get-DevSetupCachePath returns null" { + It "Should return null and log error message" { + Mock Get-DevSetupCachePath { return $null } + Mock Write-StatusMessage { } + + $result = Get-ChocolateyCacheFile + + $result | Should -BeNullOrEmpty + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -eq "Failed to retrieve DevSetup cache path." -and $Verbosity -eq "Error" } } } - Context "When Get-DevSetupCachePath returns an empty string" { - It "Should write error and return null" { + Context "When Get-DevSetupCachePath returns empty string" { + It "Should return null and log error message" { Mock Get-DevSetupCachePath { return "" } + Mock Write-StatusMessage { } + + $result = Get-ChocolateyCacheFile + + $result | Should -BeNullOrEmpty + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -eq "Failed to retrieve DevSetup cache path." -and $Verbosity -eq "Error" + } + } + } + + Context "When Get-DevSetupCachePath returns whitespace string" { + It "Should return null and log error message" { + Mock Get-DevSetupCachePath { return " " } + Mock Write-StatusMessage { } + + $result = Get-ChocolateyCacheFile + + $result | Should -BeNullOrEmpty + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -eq "Failed to retrieve DevSetup cache path." -and $Verbosity -eq "Error" + } + } + } + + Context "When Get-DevSetupCachePath throws an exception" { + It "Should handle exception and return null" { + Mock Get-DevSetupCachePath { throw "Cache path access error" } + Mock Write-StatusMessage { } + + $result = Get-ChocolateyCacheFile + + $result | Should -BeNullOrEmpty + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -match "Error retrieving DevSetup cache path:" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Verbosity -eq "Error" -and $Message -match "at" + } + } + } + + Context "When Join-Path throws an exception" { + It "Should handle Join-Path exception and return null" { + $cachePath = "InvalidPath:" + + Mock Get-DevSetupCachePath { return $cachePath } + Mock Join-Path { throw "Invalid path error" } + Mock Write-StatusMessage { } + + $result = Get-ChocolateyCacheFile + + $result | Should -BeNullOrEmpty + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -match "Error constructing Chocolatey cache file path:" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Verbosity -eq "Error" -and $Message -match "at" + } + } + } + + Context "Path construction validation" { + It "Should correctly combine cache path and chocolatey.cache filename" { + $baseCachePath = Join-Path $TestDrive "custom" "cache" "directory" + $expectedResult = Join-Path $baseCachePath "chocolatey.cache" + + Mock Get-DevSetupCachePath { return $baseCachePath } + Mock Write-StatusMessage { } + + $result = Get-ChocolateyCacheFile + + $result | Should -Be $expectedResult + $result | Should -Match "chocolatey\.cache$" + } + } + + Context "Cross-platform path handling" { + It "Should handle Unix-style paths correctly" { + $unixCachePath = Join-Path $TestDrive "home" "user" ".devsetup" ".cache" + $expectedResult = Join-Path $unixCachePath "chocolatey.cache" + + Mock Get-DevSetupCachePath { return $unixCachePath } + Mock Write-StatusMessage { } + + $result = Get-ChocolateyCacheFile + + $result | Should -Be $expectedResult + $result.EndsWith("chocolatey.cache") | Should -BeTrue + } + + It "Should handle Windows-style paths correctly" { + $windowsCachePath = Join-Path $TestDrive "Users" "TestUser" "AppData" "Local" "DevSetup" "cache" + $expectedResult = Join-Path $windowsCachePath "chocolatey.cache" + + Mock Get-DevSetupCachePath { return $windowsCachePath } + Mock Write-StatusMessage { } + + $result = Get-ChocolateyCacheFile + + $result | Should -Be $expectedResult + $result.EndsWith("chocolatey.cache") | Should -BeTrue + } + } + + Context "Return value validation" { + It "Should return a string type" { + $cachePath = Join-Path $TestDrive "cache" + + Mock Get-DevSetupCachePath { return $cachePath } + Mock Write-StatusMessage { } + + $result = Get-ChocolateyCacheFile + + $result | Should -BeOfType [string] + } + + It "Should return null (not empty string) on errors" { + Mock Get-DevSetupCachePath { throw "Error" } + Mock Write-StatusMessage { } + $result = Get-ChocolateyCacheFile - $result | Should -Be $null + + $result | Should -BeNullOrEmpty + $result | Should -BeExactly $null } } } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.ps1 b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.ps1 index 78472fb..b18b263 100644 --- a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.ps1 @@ -52,14 +52,26 @@ Function Get-ChocolateyCacheFile { Param() # Get the DevSetup cache path - $cachePath = Get-DevSetupCachePath - if([string]::IsNullOrEmpty($cachePath)) { - Write-Error "Failed to retrieve DevSetup cache path." + try { + $cachePath = Get-DevSetupCachePath + } catch { + Write-StatusMessage "Error retrieving DevSetup cache path: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $null + } + if([string]::IsNullOrWhiteSpace($cachePath)) { + Write-StatusMessage "Failed to retrieve DevSetup cache path." -Verbosity Error return $null } # Construct the full path to the cache file - $cacheFilePath = Join-Path -Path $cachePath -ChildPath "chocolatey.cache" + try { + $cacheFilePath = Join-Path -Path $cachePath -ChildPath "chocolatey.cache" + } catch { + Write-StatusMessage "Error constructing Chocolatey cache file path: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $null + } return $cacheFilePath } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencyMap.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencyMap.Tests.ps1 index d175863..43aac27 100644 --- a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencyMap.Tests.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencyMap.Tests.ps1 @@ -1,127 +1,252 @@ BeforeAll { . $PSScriptRoot\Get-ChocolateyPackageDependencyMap.ps1 . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Get-EnvironmentVariable.ps1 - Mock Write-Debug { } - if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { - Mock Get-EnvironmentVariable { return "C:\choco" } - } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { - Mock Get-EnvironmentVariable { return "/opt/choco" } - } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { - Mock Get-EnvironmentVariable { return "/opt/choco" } - } + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 } Describe "Get-ChocolateyPackageDependencyMap" { - Context "When Chocolatey install path does not exist" { - It "Should return $null in PS5, empty array in PS6+" { - Mock Test-Path { return $false } + Context "When Get-EnvironmentVariable succeeds and lib directory exists with dependencies" { + It "Should return all non-chocolatey dependencies from nuspec files" { + $chocolateyInstallPath = Join-Path $TestDrive "chocolatey" + $libPath = Join-Path $chocolateyInstallPath "lib" + $nuspecPath1 = Join-Path $libPath "package1" "package1.nuspec" + $nuspecPath2 = Join-Path $libPath "package2" "package2.nuspec" + + Mock Get-EnvironmentVariable { return $chocolateyInstallPath } -ParameterFilter { $Name -eq "ChocolateyInstall" } + Mock Test-Path { $true } -ParameterFilter { $Path -eq $libPath } + Mock Get-ChildItem { + @( + [PSCustomObject]@{ FullName = $nuspecPath1 }, + [PSCustomObject]@{ FullName = $nuspecPath2 } + ) + } -ParameterFilter { $Path -eq $libPath -and $Recurse -eq $true -and $Filter -eq "*.nuspec" } + + $nuspecs = @( + ' + + + + ', + ' + + + ' + ) + $script:callCount = 0 + Mock Get-Content { $nuspecs[$script:callCount++] } + Mock Write-StatusMessage { } + $result = Get-ChocolateyPackageDependencyMap - $result | Should -Be $null + + $result | Should -Not -BeNullOrEmpty + $result | Should -Contain "git" + $result | Should -Contain "nodejs" + $result | Should -Contain "python" + $result | Should -Not -Contain "chocolatey-core.extension" + $result | Should -Not -Contain "chocolatey-windowsupdate.extension" + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -eq "Retrieving Chocolatey package dependencies..." -and $Verbosity -eq "Debug" + } } } - Context "When no nuspec files are found" { - It "Should return $null in PS5, empty array in PS6+" { - Mock Test-Path { return $true } - Mock Get-ChildItem { @() } + Context "When Get-EnvironmentVariable throws an exception" { + It "Should handle exception and return null" { + Mock Get-EnvironmentVariable { throw "Environment variable access error" } -ParameterFilter { $Name -eq "ChocolateyInstall" } + Mock Write-StatusMessage { } + $result = Get-ChocolateyPackageDependencyMap - $result | Should -Be $null + + $result | Should -BeNullOrEmpty + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -match "Error retrieving ChocolateyInstall environment variable:" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Verbosity -eq "Error" -and $Message -match "at" + } } } - Context "When nuspec files have no dependencies" { - It "Should return $null in PS5, empty array in PS6+" { - Mock Test-Path { return $true } - if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { - Mock Get-ChildItem { - @( - [PSCustomObject]@{ FullName = "C:\choco\lib\foo\foo.nuspec" } - ) - } - } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { - Mock Get-ChildItem { - @( - [PSCustomObject]@{ FullName = "/opt/choco/lib/foo/foo.nuspec" } - ) - } - } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { - Mock Get-ChildItem { - @( - [PSCustomObject]@{ FullName = "/opt/choco/lib/foo/foo.nuspec" } - ) - } + Context "When Join-Path throws an exception" { + It "Should handle Join-Path exception and return null" { + $chocolateyInstallPath = "InvalidPath:" + + Mock Get-EnvironmentVariable { return $chocolateyInstallPath } -ParameterFilter { $Name -eq "ChocolateyInstall" } + Mock Join-Path { throw "Invalid path error" } + Mock Write-StatusMessage { } + + $result = Get-ChocolateyPackageDependencyMap + + $result | Should -BeNullOrEmpty + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -match "Error constructing Chocolatey lib path:" -and $Verbosity -eq "Error" } - Mock Get-Content { - '' + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Verbosity -eq "Error" -and $Message -match "at" + } + } + } + + Context "When Test-Path throws an exception" { + It "Should handle Test-Path exception and return null" { + $chocolateyInstallPath = Join-Path $TestDrive "chocolatey" + $libPath = Join-Path $chocolateyInstallPath "lib" + + Mock Get-EnvironmentVariable { return $chocolateyInstallPath } -ParameterFilter { $Name -eq "ChocolateyInstall" } + Mock Test-Path { throw "Path access error" } -ParameterFilter { $Path -eq $libPath } + Mock Write-StatusMessage { } + + $result = Get-ChocolateyPackageDependencyMap + + $result | Should -BeNullOrEmpty + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -match "Error testing Chocolatey lib path:" -and $Verbosity -eq "Error" } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Verbosity -eq "Error" -and $Message -match "at" + } + } + } + + Context "When Chocolatey lib path does not exist" { + It "Should return null and log debug message" { + $chocolateyInstallPath = Join-Path $TestDrive "chocolatey" + $libPath = Join-Path $chocolateyInstallPath "lib" + + Mock Get-EnvironmentVariable { return $chocolateyInstallPath } -ParameterFilter { $Name -eq "ChocolateyInstall" } + Mock Test-Path { $false } -ParameterFilter { $Path -eq $libPath } + Mock Write-StatusMessage { } + $result = Get-ChocolateyPackageDependencyMap - $result | Should -Be $null + + $result | Should -BeNullOrEmpty + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -eq "Chocolatey installation path not found: $libPath" -and $Verbosity -eq "Debug" + } } } - Context "When nuspec files have dependencies including chocolatey system packages" { - It "Should return only non-chocolatey dependencies" { - Mock Test-Path { return $true } - if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { - Mock Get-ChildItem { - @( - [PSCustomObject]@{ FullName = "C:\choco\lib\foo\foo.nuspec" } - ) - } - } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { - Mock Get-ChildItem { - @( - [PSCustomObject]@{ FullName = "/opt/choco/lib/foo/foo.nuspec" } - ) - } - } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { - Mock Get-ChildItem { - @( - [PSCustomObject]@{ FullName = "/opt/choco/lib/foo/foo.nuspec" } - ) - } - } + Context "When no nuspec files are found" { + It "Should return null" { + $chocolateyInstallPath = Join-Path $TestDrive "chocolatey" + $libPath = Join-Path $chocolateyInstallPath "lib" + + Mock Get-EnvironmentVariable { return $chocolateyInstallPath } -ParameterFilter { $Name -eq "ChocolateyInstall" } + Mock Test-Path { $true } -ParameterFilter { $Path -eq $libPath } + Mock Get-ChildItem { @() } -ParameterFilter { $Path -eq $libPath -and $Recurse -eq $true -and $Filter -eq "*.nuspec" } + Mock Write-StatusMessage { } + + $result = Get-ChocolateyPackageDependencyMap + + $result | Should -BeNullOrEmpty + } + } + + Context "When nuspec files have no dependencies section" { + It "Should return null" { + $chocolateyInstallPath = Join-Path $TestDrive "chocolatey" + $libPath = Join-Path $chocolateyInstallPath "lib" + $nuspecPath = Join-Path $libPath "package1" "package1.nuspec" + + Mock Get-EnvironmentVariable { return $chocolateyInstallPath } -ParameterFilter { $Name -eq "ChocolateyInstall" } + Mock Test-Path { $true } -ParameterFilter { $Path -eq $libPath } + Mock Get-ChildItem { + @([PSCustomObject]@{ FullName = $nuspecPath }) + } -ParameterFilter { $Path -eq $libPath -and $Recurse -eq $true -and $Filter -eq "*.nuspec" } + Mock Get-Content { '' } + Mock Write-StatusMessage { } + + $result = Get-ChocolateyPackageDependencyMap + + $result | Should -BeNullOrEmpty + } + } + + Context "When nuspec files have empty dependencies section" { + It "Should return null" { + $chocolateyInstallPath = Join-Path $TestDrive "chocolatey" + $libPath = Join-Path $chocolateyInstallPath "lib" + $nuspecPath = Join-Path $libPath "package1" "package1.nuspec" + + Mock Get-EnvironmentVariable { return $chocolateyInstallPath } -ParameterFilter { $Name -eq "ChocolateyInstall" } + Mock Test-Path { $true } -ParameterFilter { $Path -eq $libPath } + Mock Get-ChildItem { + @([PSCustomObject]@{ FullName = $nuspecPath }) + } -ParameterFilter { $Path -eq $libPath -and $Recurse -eq $true -and $Filter -eq "*.nuspec" } + Mock Get-Content { '' } + Mock Write-StatusMessage { } + + $result = Get-ChocolateyPackageDependencyMap + + $result | Should -BeNullOrEmpty + } + } + + Context "When nuspec files contain only chocolatey dependencies" { + It "Should return null after filtering" { + $chocolateyInstallPath = Join-Path $TestDrive "chocolatey" + $libPath = Join-Path $chocolateyInstallPath "lib" + $nuspecPath = Join-Path $libPath "package1" "package1.nuspec" + + Mock Get-EnvironmentVariable { return $chocolateyInstallPath } -ParameterFilter { $Name -eq "ChocolateyInstall" } + Mock Test-Path { $true } -ParameterFilter { $Path -eq $libPath } + Mock Get-ChildItem { + @([PSCustomObject]@{ FullName = $nuspecPath }) + } -ParameterFilter { $Path -eq $libPath -and $Recurse -eq $true -and $Filter -eq "*.nuspec" } Mock Get-Content { ' - - + + ' } + Mock Write-StatusMessage { } + $result = Get-ChocolateyPackageDependencyMap - $result | Should -Not -Be $null - $result | Should -Contain "git" - $result | Should -Contain "nodejs" - $result | Should -Not -Contain "chocolatey-core.extension" + + $result | Should -BeNullOrEmpty } } - Context "When multiple nuspec files have overlapping dependencies" { - It "Should return all dependencies including duplicates" { - Mock Test-Path { return $true } - if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { - Mock Get-ChildItem { - @( - [PSCustomObject]@{ FullName = "C:\choco\lib\foo\foo.nuspec" }, - [PSCustomObject]@{ FullName = "C:\choco\lib\bar\bar.nuspec" } - ) - } - } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { - Mock Get-ChildItem { - @( - [PSCustomObject]@{ FullName = "/opt/choco/lib/foo/foo.nuspec" }, - [PSCustomObject]@{ FullName = "/opt/choco/lib/bar/bar.nuspec" } - ) - } - } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { - Mock Get-ChildItem { - @( - [PSCustomObject]@{ FullName = "/opt/choco/lib/foo/foo.nuspec" }, - [PSCustomObject]@{ FullName = "/opt/choco/lib/bar/bar.nuspec" } - ) - } + Context "When processing nuspec files throws an exception" { + It "Should handle processing exception and return null" { + $chocolateyInstallPath = Join-Path $TestDrive "chocolatey" + $libPath = Join-Path $chocolateyInstallPath "lib" + + Mock Get-EnvironmentVariable { return $chocolateyInstallPath } -ParameterFilter { $Name -eq "ChocolateyInstall" } + Mock Test-Path { $true } -ParameterFilter { $Path -eq $libPath } + Mock Get-ChildItem { throw "File access error" } -ParameterFilter { $Path -eq $libPath -and $Recurse -eq $true -and $Filter -eq "*.nuspec" } + Mock Write-StatusMessage { } + + $result = Get-ChocolateyPackageDependencyMap + + $result | Should -BeNullOrEmpty + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -match "Error processing nuspec files:" -and $Verbosity -eq "Error" } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Verbosity -eq "Error" -and $Message -match "at" + } + } + } + + Context "When multiple packages have overlapping dependencies" { + It "Should return all dependencies including duplicates" { + $chocolateyInstallPath = Join-Path $TestDrive "chocolatey" + $libPath = Join-Path $chocolateyInstallPath "lib" + $nuspecPath1 = Join-Path $libPath "package1" "package1.nuspec" + $nuspecPath2 = Join-Path $libPath "package2" "package2.nuspec" + + Mock Get-EnvironmentVariable { return $chocolateyInstallPath } -ParameterFilter { $Name -eq "ChocolateyInstall" } + Mock Test-Path { $true } -ParameterFilter { $Path -eq $libPath } + Mock Get-ChildItem { + @( + [PSCustomObject]@{ FullName = $nuspecPath1 }, + [PSCustomObject]@{ FullName = $nuspecPath2 } + ) + } -ParameterFilter { $Path -eq $libPath -and $Recurse -eq $true -and $Filter -eq "*.nuspec" } + $nuspecs = @( ' @@ -133,15 +258,57 @@ Describe "Get-ChocolateyPackageDependencyMap" { ' ) $script:callCount = 0 - Mock Get-Content -MockWith { - $nuspecs[$script:callCount++] - } + Mock Get-Content { $nuspecs[$script:callCount++] } + Mock Write-StatusMessage { } + $result = Get-ChocolateyPackageDependencyMap - $result | Should -Not -Be $null + + $result | Should -Not -BeNullOrEmpty $result | Should -Contain "git" $result | Should -Contain "nodejs" $result | Should -Contain "python" + # Should have duplicates ($result | Where-Object { $_ -eq "nodejs" }).Count | Should -Be 2 } } + + Context "Return value validation" { + It "Should return null when no dependencies found" { + $chocolateyInstallPath = Join-Path $TestDrive "chocolatey" + $libPath = Join-Path $chocolateyInstallPath "lib" + + Mock Get-EnvironmentVariable { return $chocolateyInstallPath } -ParameterFilter { $Name -eq "ChocolateyInstall" } + Mock Test-Path { $false } -ParameterFilter { $Path -eq $libPath } + Mock Write-StatusMessage { } + + $result = Get-ChocolateyPackageDependencyMap + + $result | Should -BeNullOrEmpty + } + + It "Should return dependencies when found" { + $chocolateyInstallPath = Join-Path $TestDrive "chocolatey" + $libPath = Join-Path $chocolateyInstallPath "lib" + $nuspecPath = Join-Path $libPath "package1" "package1.nuspec" + + Mock Get-EnvironmentVariable { return $chocolateyInstallPath } -ParameterFilter { $Name -eq "ChocolateyInstall" } + Mock Test-Path { $true } -ParameterFilter { $Path -eq $libPath } + Mock Get-ChildItem { + @([PSCustomObject]@{ FullName = $nuspecPath }) + } -ParameterFilter { $Path -eq $libPath -and $Recurse -eq $true -and $Filter -eq "*.nuspec" } + Mock Get-Content { + ' + + + ' + } + Mock Write-StatusMessage { } + + $result = Get-ChocolateyPackageDependencyMap + + $result | Should -Not -BeNullOrEmpty + @($result) | Should -Contain "git" + @($result) | Should -Contain "nodejs" + } + } } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencyMap.ps1 b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencyMap.ps1 index 370ec16..c35910e 100644 --- a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencyMap.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencyMap.ps1 @@ -59,25 +59,51 @@ Function Get-ChocolateyPackageDependencyMap { [OutputType([array])] Param() - write-Debug "Retrieving Chocolatey package dependencies..." + Write-StatusMessage "Retrieving Chocolatey package dependencies..." -Verbosity Debug $packageDependencies = @() - $chocolateyInstallPath = Join-Path (Get-EnvironmentVariable ChocolateyInstall) lib - if (-not (Test-Path $chocolateyInstallPath)) { - Write-Debug "Chocolatey installation path not found: $chocolateyInstallPath" - return $packageDependencies + try { + $ChocolateyInstallEnvPath = Get-EnvironmentVariable ChocolateyInstall + } catch { + Write-StatusMessage "Error retrieving ChocolateyInstall environment variable: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $null } - Get-ChildItem $chocolateyInstallPath -Recurse "*.nuspec" | ForEach-Object { - $dependencies = ([xml](Get-Content $_.FullName)).package.metadata.dependencies.dependency | ForEach-Object { - if (-not ($_.id -like "chocolatey*")) { - $_.id - } + try { + $chocolateyInstallPath = Join-Path $ChocolateyInstallEnvPath lib + } catch { + Write-StatusMessage "Error constructing Chocolatey lib path: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $null + } + + try { + if (-not (Test-Path $chocolateyInstallPath)) { + Write-StatusMessage "Chocolatey installation path not found: $chocolateyInstallPath" -Verbosity Debug + return $null } + } catch { + Write-StatusMessage "Error testing Chocolatey lib path: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $null + } - if ($dependencies) { - $packageDependencies = $packageDependencies + $dependencies; + try { + $packageDependencies = Get-ChildItem $chocolateyInstallPath -Recurse "*.nuspec" | ForEach-Object { + ([xml](Get-Content $_.FullName)).package.metadata.dependencies.dependency | ForEach-Object { + if (-not ($_.id -like "chocolatey*")) { + $_.id + } + } + } + if(-not $packageDependencies) { + return $null } + } catch { + Write-StatusMessage "Error processing nuspec files: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $null } return [array]$packageDependencies } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.Tests.ps1 index 7494a1f..eca97a5 100644 --- a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.Tests.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.Tests.ps1 @@ -1,46 +1,491 @@ BeforeAll { . $PSScriptRoot\Get-ChocolateyVersion.ps1 + . $PSScriptRoot\..\..\Utils\Write-StatusMessage.ps1 . $PSScriptRoot\Test-ChocolateyInstalled.ps1 - Mock Write-Warning { } + . $PSScriptRoot\Find-Chocolatey.ps1 } Describe "Get-ChocolateyVersion" { - Context "When Chocolatey is not installed" { - It "Should return null and write a warning" { + It "Should return null and log warning when Test-ChocolateyInstalled returns false" { + # Arrange Mock Test-ChocolateyInstalled { return $false } + Mock Write-StatusMessage { } + Mock Find-Chocolatey { } + Mock Invoke-Command { } + + # Act + $result = Get-ChocolateyVersion + + # Assert + $result | Should -BeNullOrEmpty + Assert-MockCalled Test-ChocolateyInstalled -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -eq "Chocolatey is not installed. Cannot retrieve version." -and $Verbosity -eq "Warning" + } + Assert-MockCalled Find-Chocolatey -Exactly 0 -Scope It + Assert-MockCalled Invoke-Command -Exactly 0 -Scope It + } + + It "Should return null and log error when Test-ChocolateyInstalled throws exception" { + # Arrange + Mock Test-ChocolateyInstalled { throw "Test error from Test-ChocolateyInstalled" } + Mock Write-StatusMessage { } + Mock Find-Chocolatey { } + Mock Invoke-Command { } + + # Act $result = Get-ChocolateyVersion - $result | Should -Be $null - Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "not installed" } + + # Assert + $result | Should -BeNullOrEmpty + Assert-MockCalled Test-ChocolateyInstalled -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It + Assert-MockCalled Write-StatusMessage -ParameterFilter { + $Message -like "Error checking if Chocolatey is installed:*" -and $Verbosity -eq "Error" + } -Scope It + Assert-MockCalled Write-StatusMessage -ParameterFilter { + $Verbosity -eq "Error" + } -Scope It + Assert-MockCalled Find-Chocolatey -Exactly 0 -Scope It + Assert-MockCalled Invoke-Command -Exactly 0 -Scope It } } - - Context "When Chocolatey is installed and version is returned" { - It "Should return the trimmed version string" { + + Context "When Find-Chocolatey fails" { + It "Should return null and log error when Find-Chocolatey throws exception" { + # Arrange + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { throw "Find-Chocolatey error" } + Mock Write-StatusMessage { } + Mock Invoke-Command { } + Mock Test-Path { } + + # Act + $result = Get-ChocolateyVersion + + # Assert + $result | Should -BeNullOrEmpty + Assert-MockCalled Test-ChocolateyInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Chocolatey -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It + Assert-MockCalled Write-StatusMessage -ParameterFilter { + $Message -like "Error locating Chocolatey command:*" -and $Verbosity -eq "Error" + } -Scope It + Assert-MockCalled Write-StatusMessage -ParameterFilter { + $Verbosity -eq "Error" + } -Scope It + Assert-MockCalled Test-Path -Exactly 0 -Scope It + Assert-MockCalled Invoke-Command -Exactly 0 -Scope It + } + + It "Should return null and log warning when Find-Chocolatey returns null" { + # Arrange + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return $null } + Mock Write-StatusMessage { } + Mock Test-Path { } + Mock Invoke-Command { } + + # Act + $result = Get-ChocolateyVersion + + # Assert + $result | Should -BeNullOrEmpty + Assert-MockCalled Test-ChocolateyInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Chocolatey -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -eq "Could not find Chocolatey command. Cannot retrieve version." -and $Verbosity -eq "Warning" + } + Assert-MockCalled Test-Path -Exactly 0 -Scope It + Assert-MockCalled Invoke-Command -Exactly 0 -Scope It + } + + It "Should return null and log warning when Find-Chocolatey returns empty string" { + # Arrange + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "" } + Mock Write-StatusMessage { } + Mock Test-Path { } + Mock Invoke-Command { } + + # Act + $result = Get-ChocolateyVersion + + # Assert + $result | Should -BeNullOrEmpty + Assert-MockCalled Test-ChocolateyInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Chocolatey -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -eq "Could not find Chocolatey command. Cannot retrieve version." -and $Verbosity -eq "Warning" + } + Assert-MockCalled Test-Path -Exactly 0 -Scope It + Assert-MockCalled Invoke-Command -Exactly 0 -Scope It + } + + It "Should return null and log warning when Find-Chocolatey returns whitespace" { + # Arrange Mock Test-ChocolateyInstalled { return $true } - Mock Invoke-Expression { " 1.4.0 " } + Mock Find-Chocolatey { return " " } + Mock Write-StatusMessage { } + Mock Test-Path { } + Mock Invoke-Command { } + + # Act $result = Get-ChocolateyVersion + + # Assert + $result | Should -BeNullOrEmpty + Assert-MockCalled Test-ChocolateyInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Chocolatey -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -eq "Could not find Chocolatey command. Cannot retrieve version." -and $Verbosity -eq "Warning" + } + Assert-MockCalled Test-Path -Exactly 0 -Scope It + Assert-MockCalled Invoke-Command -Exactly 0 -Scope It + } + + It "Should return null and log warning when choco command path does not exist" { + # Arrange + $testChocoPath = Join-Path $TestDrive "nonexistent\choco" + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return $testChocoPath } + Mock Write-StatusMessage { } + Mock Test-Path { return $false } + Mock Invoke-Command { } + + # Act + $result = Get-ChocolateyVersion + + # Assert + $result | Should -BeNullOrEmpty + Assert-MockCalled Test-ChocolateyInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Chocolatey -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 1 -Scope It -ParameterFilter { + $Path -eq $testChocoPath + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -eq "Chocolatey command path '$testChocoPath' does not exist. Cannot retrieve version." -and $Verbosity -eq "Warning" + } + Assert-MockCalled Invoke-Command -Exactly 0 -Scope It + } + + It "Should return null and log error when Test-Path throws exception" { + # Arrange + $testChocoPath = Join-Path $TestDrive "problematic\choco" + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return $testChocoPath } + Mock Write-StatusMessage { } + Mock Test-Path { throw "Test-Path access denied" } + Mock Invoke-Command { } + + # Act + $result = Get-ChocolateyVersion + + # Assert + $result | Should -BeNullOrEmpty + Assert-MockCalled Test-ChocolateyInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Chocolatey -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 1 -Scope It -ParameterFilter { + $Path -eq $testChocoPath + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It + Assert-MockCalled Write-StatusMessage -ParameterFilter { + $Message -like "Error verifying Chocolatey command path:*" -and $Verbosity -eq "Error" + } -Scope It + Assert-MockCalled Write-StatusMessage -ParameterFilter { + $Verbosity -eq "Error" + } -Scope It + Assert-MockCalled Invoke-Command -Exactly 0 -Scope It + } + } + + Context "When version retrieval succeeds" { + It "Should return version string when Invoke-Command succeeds with version output and exit code 0" { + # Arrange + $testChocoPath = Join-Path $TestDrive "choco" + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return $testChocoPath } + Mock Test-Path { return $true } + Mock Write-StatusMessage { } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return "1.4.0" + } + + # Act + $result = Get-ChocolateyVersion + + # Assert $result | Should -Be "1.4.0" + Assert-MockCalled Test-ChocolateyInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Chocolatey -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 1 -Scope It -ParameterFilter { + $Path -eq $testChocoPath + } + Assert-MockCalled Invoke-Command -Exactly 1 -Scope It -ParameterFilter { + $ScriptBlock.ToString() -match "--version" + } + Assert-MockCalled Write-StatusMessage -Exactly 0 -Scope It + } + + It "Should return version string with whitespace when output has whitespace and exit code 0" { + # Arrange + $testChocoPath = Join-Path $TestDrive "chocolatey\bin\choco" + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return $testChocoPath } + Mock Test-Path { return $true } + Mock Write-StatusMessage { } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return @(" 1.4.0`r`n ") # Return as array with complex whitespace + } + + # Act + $result = Get-ChocolateyVersion + + # Assert + $result | Should -Be " 1.4.0`r`n " + Assert-MockCalled Test-ChocolateyInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Chocolatey -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 1 -Scope It + Assert-MockCalled Invoke-Command -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 0 -Scope It + } + + It "Should handle different version formats correctly when exit code 0" { + # Arrange + $testChocoPath = Join-Path $TestDrive "custom\path\choco" + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return $testChocoPath } + Mock Test-Path { return $true } + Mock Write-StatusMessage { } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return "2.1.0-beta1" + } + + # Act + $result = Get-ChocolateyVersion + + # Assert + $result | Should -Be "2.1.0-beta1" + Assert-MockCalled Test-ChocolateyInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Chocolatey -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 1 -Scope It + Assert-MockCalled Invoke-Command -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 0 -Scope It + } + + It "Should return version string with complex whitespace as-is" { + # Arrange + $testChocoPath = Join-Path $TestDrive "program files\chocolatey\choco" + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return $testChocoPath } + Mock Test-Path { return $true } + Mock Write-StatusMessage { } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + # Create a string with multiple types of whitespace + $whiteSpaceString = "`t`r`n 1.5.0 `r`n`t" + return $whiteSpaceString + } + + # Act + $result = Get-ChocolateyVersion + + # Assert + $result | Should -Be "`t`r`n 1.5.0 `r`n`t" + Assert-MockCalled Test-ChocolateyInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Chocolatey -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 1 -Scope It + Assert-MockCalled Invoke-Command -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 0 -Scope It } } - - Context "When Chocolatey is installed but version is not returned" { - It "Should return null and write a warning" { + + Context "When version retrieval fails" { + It "Should return null and log warning when Invoke-Command returns empty output" { + # Arrange + $testChocoPath = Join-Path $TestDrive "empty\choco" + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return $testChocoPath } + Mock Test-Path { return $true } + Mock Write-StatusMessage { } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return $null + } + + # Act + $result = Get-ChocolateyVersion + + # Assert + $result | Should -BeNullOrEmpty + Assert-MockCalled Test-ChocolateyInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Chocolatey -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 1 -Scope It + Assert-MockCalled Invoke-Command -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -eq "Failed to retrieve Chocolatey version." -and $Verbosity -eq "Warning" + } + } + + It "Should return null and log warning when Invoke-Command returns empty string" { + # Arrange + $testChocoPath = Join-Path $TestDrive "chocolatey\tools\choco" Mock Test-ChocolateyInstalled { return $true } - Mock Invoke-Expression { $null } + Mock Find-Chocolatey { return $testChocoPath } + Mock Test-Path { return $true } + Mock Write-StatusMessage { } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return "" + } + + # Act $result = Get-ChocolateyVersion - $result | Should -Be $null - Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to retrieve" } + + # Assert + $result | Should -BeNullOrEmpty + Assert-MockCalled Test-ChocolateyInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Chocolatey -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 1 -Scope It + Assert-MockCalled Invoke-Command -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -eq "Failed to retrieve Chocolatey version." -and $Verbosity -eq "Warning" + } + } + + It "Should return null and log warning when LASTEXITCODE is not 0" { + # Arrange + $testChocoPath = Join-Path $TestDrive "error\choco" + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return $testChocoPath } + Mock Test-Path { return $true } + Mock Write-StatusMessage { } + Mock Invoke-Command { + $global:LASTEXITCODE = 1 + return "Some error output" + } + + # Act + $result = Get-ChocolateyVersion + + # Assert + $result | Should -BeNullOrEmpty + Assert-MockCalled Test-ChocolateyInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Chocolatey -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 1 -Scope It + Assert-MockCalled Invoke-Command -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -eq "Failed to retrieve Chocolatey version." -and $Verbosity -eq "Warning" + } + } + + It "Should return null and log warning when LASTEXITCODE is not 0 and output is empty" { + # Arrange + $testChocoPath = Join-Path $TestDrive "failed\path\choco" + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return $testChocoPath } + Mock Test-Path { return $true } + Mock Write-StatusMessage { } + Mock Invoke-Command { + $global:LASTEXITCODE = 2 + return $null + } + + # Act + $result = Get-ChocolateyVersion + + # Assert + $result | Should -BeNullOrEmpty + Assert-MockCalled Test-ChocolateyInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Chocolatey -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 1 -Scope It + Assert-MockCalled Invoke-Command -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -eq "Failed to retrieve Chocolatey version." -and $Verbosity -eq "Warning" + } + } + + It "Should return null and log error when Invoke-Command throws exception" { + # Arrange + $testChocoPath = Join-Path $TestDrive "error\choco" + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return $testChocoPath } + Mock Test-Path { return $true } + Mock Write-StatusMessage { } + Mock Invoke-Command { throw "Command execution failed" } + + # Act + $result = Get-ChocolateyVersion + + # Assert + $result | Should -BeNullOrEmpty + Assert-MockCalled Test-ChocolateyInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Chocolatey -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 1 -Scope It + Assert-MockCalled Invoke-Command -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It + Assert-MockCalled Write-StatusMessage -ParameterFilter { + $Message -like "An error occurred while trying to get Chocolatey version:*" -and $Verbosity -eq "Error" + } -Scope It + Assert-MockCalled Write-StatusMessage -ParameterFilter { + $Verbosity -eq "Error" + } -Scope It } } + + Context "Integration scenarios" { + It "Should use the correct chocolatey path from Find-Chocolatey" { + # Arrange + $customChocoPath = Join-Path $TestDrive "Custom\Path\choco" + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return $customChocoPath } + Mock Test-Path { return $true } + Mock Write-StatusMessage { } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return "1.4.0" + } -Verifiable -ParameterFilter { + $ScriptBlock.ToString() -match "--version" -and $ScriptBlock.ToString() -match "\`$chocoCommand" + } + + # Act + $result = Get-ChocolateyVersion - Context "When an error occurs during version retrieval" { - It "Should return null and write a warning" { + # Assert + $result | Should -Be "1.4.0" + Assert-MockCalled Test-ChocolateyInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Chocolatey -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 1 -Scope It -ParameterFilter { + $Path -eq $customChocoPath + } + Assert-VerifiableMock + } + + It "Should suppress stderr output from chocolatey command" { + # Arrange + $testChocoPath = Join-Path $TestDrive "bin\choco" Mock Test-ChocolateyInstalled { return $true } - Mock Invoke-Expression { throw "choco error" } + Mock Find-Chocolatey { return $testChocoPath } + Mock Test-Path { return $true } + Mock Write-StatusMessage { } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return "1.4.0" + } + + # Act $result = Get-ChocolateyVersion - $result | Should -Be $null - Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "An error occurred" } + + # Assert + $result | Should -Be "1.4.0" + Assert-MockCalled Test-ChocolateyInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Chocolatey -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 1 -Scope It + Assert-MockCalled Invoke-Command -Exactly 1 -Scope It } } } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.ps1 b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.ps1 index 0f14d5d..358b714 100644 --- a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.ps1 @@ -59,21 +59,53 @@ Function Get-ChocolateyVersion { Param( ) - if (-not (Test-ChocolateyInstalled)) { - Write-Warning "Chocolatey is not installed. Cannot retrieve version." + try { + if (-not (Test-ChocolateyInstalled)) { + Write-StatusMessage "Chocolatey is not installed. Cannot retrieve version." -Verbosity Warning + return $null + } + } catch { + Write-StatusMessage "Error checking if Chocolatey is installed: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $null + } + + try { + $chocoCommand = Find-Chocolatey + } catch { + Write-StatusMessage "Error locating Chocolatey command: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $null + } + + if(-not $chocoCommand -or [string]::IsNullOrWhiteSpace($chocoCommand)) { + Write-StatusMessage "Could not find Chocolatey command. Cannot retrieve version." -Verbosity Warning return $null } try { - $version = Invoke-Expression "& choco --version" 2>$null - if ($version) { - return $version.Trim() - } else { - Write-Warning "Failed to retrieve Chocolatey version." + if( -not (Test-Path $chocoCommand)) { + Write-StatusMessage "Chocolatey command path '$chocoCommand' does not exist. Cannot retrieve version." -Verbosity Warning return $null } } catch { - Write-Warning "An error occurred while trying to get Chocolatey version: $_" + Write-StatusMessage "Error verifying Chocolatey command path: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $null + } + + try { + $version = Invoke-Command -ScriptBlock { & $chocoCommand --version } + } catch { + Write-StatusMessage "An error occurred while trying to get Chocolatey version: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $null + } + + if ($LASTEXITCODE -eq 0 -and $version) { + return $version + } else { + Write-StatusMessage "Failed to retrieve Chocolatey version." -Verbosity Warning return $null } } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Install-Chocolatey.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Install-Chocolatey.Tests.ps1 index e213399..1687d18 100644 --- a/DevSetup/Private/Providers/Chocolatey/Install-Chocolatey.Tests.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Install-Chocolatey.Tests.ps1 @@ -3,94 +3,194 @@ BeforeAll { . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-OperatingSystem.ps1 . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 + . $PSScriptRoot\Test-ChocolateyInstalled.ps1 + Mock Write-StatusMessage { } Mock Write-Host { } Mock Write-Error { } Mock Test-RunningAsAdmin { return $true } - Mock Get-Command { $null } - Mock Invoke-Expression { } + Mock Test-OperatingSystem { param($Windows) $true } + Mock Test-ChocolateyInstalled { return $false } Mock Set-ExecutionPolicy { } + Mock New-Object -MockWith { + $mockWebClient = New-Object PSObject + Add-Member -InputObject $mockWebClient -MemberType ScriptMethod -Name DownloadString -Value { param($url) return "# Chocolatey install script content" } + Add-Member -InputObject $mockWebClient -MemberType ScriptMethod -Name Dispose -Value { } + return $mockWebClient + } -ParameterFilter { $TypeName -eq "System.Net.WebClient" } + Mock Invoke-Expression { } } Describe "Install-Chocolatey" { Context "When not running on Windows" { - It "Should skip installation and return true" { - Mock Test-OperatingSystem { param($Windows) $false } + It "Should skip installation and write status message" { + Mock Test-OperatingSystem { param($Windows) return $false } + $result = Install-Chocolatey + $result | Should -Be $true - Assert-MockCalled Write-Host -Exactly 1 -Scope It -ParameterFilter { $Object -match "not available on this platform" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Chocolatey is not available on this platform" -and $Verbosity -eq "Error" + } + } + } + + Context "When Test-OperatingSystem throws an exception" { + It "Should handle operating system check exception and return false" { + Mock Test-OperatingSystem { throw "Operating system check failed" } + + $result = Install-Chocolatey + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error checking operating system" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } } } Context "When not running as administrator" { - It "Should throw and return false" { - Mock Test-OperatingSystem { param($Windows) $true } + It "Should write error message and return false" { + Mock Test-OperatingSystem { param($Windows) return $true } Mock Test-RunningAsAdmin { return $false } + $result = Install-Chocolatey + $result | Should -Be $false - Assert-MockCalled Write-Error -Exactly 1 -Scope It -ParameterFilter { $Message -match "administrator privileges" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Chocolatey installation requires administrator privileges" -and $Verbosity -eq "Error" + } } } Context "When Chocolatey is already installed" { - It "Should return true and show version" { - Mock Test-OperatingSystem { param($Windows) $true } + It "Should return true and show already installed message" { + Mock Test-OperatingSystem { param($Windows) return $true } Mock Test-RunningAsAdmin { return $true } - Mock Get-Command { [PSCustomObject]@{ Name = "choco" } } - Mock Invoke-Expression { "1.4.0" } + Mock Test-ChocolateyInstalled { return $true } + $result = Install-Chocolatey + $result | Should -Be $true - Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "[OK]" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Chocolatey is already installed" -and $Verbosity -eq "Debug" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -eq "[OK]" -and $ForegroundColor -eq "Green" + } } } - Context "When Chocolatey is not installed and installation succeeds" { - It "Should install and return true" { - Mock Test-OperatingSystem { param($Windows) $true } - $script:installCalled = $false - $script:commandCallCount = 0 + Context "When Test-ChocolateyInstalled throws an exception" { + It "Should handle exception and return false" { + Mock Test-OperatingSystem { param($Windows) return $true } Mock Test-RunningAsAdmin { return $true } - Mock Get-Command -MockWith { - $script:commandCallCount++ - if ($script:commandCallCount -eq 1) { return $null } - else { return [PSCustomObject]@{ Name = "choco" } } + Mock Test-ChocolateyInstalled { throw "Test-ChocolateyInstalled failed" } + + $result = Install-Chocolatey + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -match "Error checking Chocolatey installation" -and $Verbosity -eq "Error" } - Mock Invoke-Expression -MockWith { - param($expr) - if ($expr -like "*--version*") { return "1.4.0" } - $script:installCalled = $true + } + } + + Context "When Chocolatey is not installed and installation succeeds" { + It "Should install successfully and verify with Test-ChocolateyInstalled" { + Mock Test-OperatingSystem { param($Windows) return $true } + Mock Test-RunningAsAdmin { return $true } + $script:installCheckCount = 0 + Mock Test-ChocolateyInstalled -MockWith { + $script:installCheckCount++ + if ($script:installCheckCount -eq 1) { return $false } # Initial check + else { return $true } # Post-install verification } + $result = Install-Chocolatey + $result | Should -Be $true - $script:installCalled | Should -Be $true - Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "[OK]" } + Assert-MockCalled Set-ExecutionPolicy -Exactly 1 -Scope It -ParameterFilter { + $ExecutionPolicy -eq "Bypass" -and $Scope -eq "Process" -and $Force -eq $true + } + Assert-MockCalled New-Object -Exactly 1 -Scope It -ParameterFilter { + $TypeName -eq "System.Net.WebClient" + } + Assert-MockCalled Invoke-Expression -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -eq "[OK]" -and $ForegroundColor -eq "Green" + } + } + } + + Context "When Chocolatey installation fails verification" { + It "Should return false and write FAILED when Test-ChocolateyInstalled still returns false" { + Mock Test-OperatingSystem { param($Windows) return $true } + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $false } # Always returns false + + $result = Install-Chocolatey + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -eq "[FAILD]" -and $ForegroundColor -eq "Red" + } } } - Context "When Chocolatey is not installed and installation fails" { - It "Should return false and write error" { - Mock Test-OperatingSystem { param($Windows) $true } - $script:commandCallCount = 0 + Context "When installation process fails" { + It "Should handle installation exception and return false" { + Mock Test-OperatingSystem { param($Windows) return $true } Mock Test-RunningAsAdmin { return $true } - Mock Get-Command -MockWith { - $script:commandCallCount++ - return $null + Mock Test-ChocolateyInstalled { return $false } + Mock Invoke-Expression { throw "Network connection failed" } + + $result = Install-Chocolatey + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -match "Error during Chocolatey installation" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -eq "[FAILED]" -and $ForegroundColor -eq "Red" + } + } + } + + Context "When verification fails with exception" { + It "Should handle verification exception and return false" { + Mock Test-OperatingSystem { param($Windows) return $true } + Mock Test-RunningAsAdmin { return $true } + $script:installCheckCount = 0 + Mock Test-ChocolateyInstalled -MockWith { + $script:installCheckCount++ + if ($script:installCheckCount -eq 1) { return $false } # Initial check + else { throw "Verification failed" } # Post-install verification throws } - Mock Invoke-Expression { } + $result = Install-Chocolatey + $result | Should -Be $false - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to install" } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { + $Message -match "Error verifying Chocolatey installation" -and $Verbosity -eq "Error" + } } } Context "When an unexpected error occurs" { - It "Should return false and write error" { - Mock Test-OperatingSystem { param($Windows) $true } - Mock Test-RunningAsAdmin { throw "Unexpected error" } + It "Should return false and write comprehensive error message" { + Mock Test-OperatingSystem { param($Windows) return $true } + Mock Test-RunningAsAdmin { throw "Unexpected system error" } + $result = Install-Chocolatey + $result | Should -Be $false - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Error checking/installing Chocolatey" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error checking administrator privileges" -and $Verbosity -eq "Error" + } } } } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Install-Chocolatey.ps1 b/DevSetup/Private/Providers/Chocolatey/Install-Chocolatey.ps1 index 30d386f..533446d 100644 --- a/DevSetup/Private/Providers/Chocolatey/Install-Chocolatey.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Install-Chocolatey.ps1 @@ -67,47 +67,69 @@ Function Install-Chocolatey { try { # Check if we're on Windows - Chocolatey is Windows-only if (-not (Test-OperatingSystem -Windows)) { - Write-Host "Chocolatey is not available on this platform. Skipping installation." -ForegroundColor Yellow + Write-StatusMessage "Chocolatey is not available on this platform. Skipping installation." -Verbosity Error return $true } + } catch { + Write-StatusMessage "Error checking operating system: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $false + } + try { # Check if running as administrator if (-not (Test-RunningAsAdmin)) { - throw "Chocolatey installation requires administrator privileges. Please run as administrator." + Write-StatusMessage "Chocolatey installation requires administrator privileges. Please run as administrator." -Verbosity Error + return $false } - - Write-StatusMessage "- Installing Chocolatey package manager" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline - # Check if chocolatey is installed by testing the command - $chocoInstalled = Get-Command choco -ErrorAction SilentlyContinue - + } catch { + Write-StatusMessage "Error checking administrator privileges: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $false + } + + Write-StatusMessage "- Installing Chocolatey package manager" -ForegroundColor Gray -Indent 2 -Width 112 -NoNewline + try { + # Check if chocolatey is already installed + if (Test-ChocolateyInstalled) { + Write-StatusMessage "Chocolatey is already installed. Skipping installation." -Verbosity Debug + Write-StatusMessage "[OK]" -ForegroundColor Green + return $true + } + } catch { + Write-StatusMessage "Error checking Chocolatey installation: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $false + } + + try { + # Set security protocols and execution policy + Set-ExecutionPolicy Bypass -Scope Process -Force | Out-Null + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 + + # Download and install Chocolatey + (Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) *> $null) *> $null + } catch { + Write-StatusMessage "Error during Chocolatey installation: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + Write-StatusMessage "[FAILED]" -ForegroundColor Red + return $false + } + + # Verify installation + try { + $chocoInstalled = Test-ChocolateyInstalled if ($chocoInstalled) { - Invoke-Expression "& choco --version" *>$null - #Write-Host "Chocolatey is already installed (version: $chocoVersion)" -ForegroundColor Green + #Write-Host "Chocolatey successfully installed (version: $chocoVersion)!" -ForegroundColor Green Write-StatusMessage "[OK]" -ForegroundColor Green + return $true } else { - #Write-Host "Chocolatey not found. Installing Chocolatey..." -ForegroundColor Cyan - - # Set security protocols and execution policy - Set-ExecutionPolicy Bypass -Scope Process -Force | Out-Null - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 - - # Download and install Chocolatey - (Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) *> $null) *> $null - - # Verify installation - $chocoInstalled = Get-Command choco -ErrorAction SilentlyContinue - if ($chocoInstalled) { - Invoke-Expression "& choco --version" *>$null - #Write-Host "Chocolatey successfully installed (version: $chocoVersion)!" -ForegroundColor Green - Write-StatusMessage "[OK]" -ForegroundColor Green - } else { - throw "Failed to install Chocolatey" - } + Write-StatusMessage "[FAILD]" -ForegroundColor Red + return $false } - return $true - } - catch { - Write-Error "Error checking/installing Chocolatey: $_" + } catch { + Write-StatusMessage "Error verifying Chocolatey installation: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error return $false - } + } } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.Tests.ps1 index 19c32ae..7f66fdf 100644 --- a/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.Tests.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.Tests.ps1 @@ -1,105 +1,560 @@ BeforeAll { . (Join-Path $PSScriptRoot "Install-ChocolateyPackage.ps1") + . (Join-Path $PSScriptRoot "Test-ChocolateyInstalled.ps1") . (Join-Path $PSScriptRoot "Test-ChocolateyPackageInstalled.ps1") . (Join-Path $PSScriptRoot "Uninstall-ChocolateyPackage.ps1") + . (Join-Path $PSScriptRoot "Find-Chocolatey.ps1") . (Join-Path $PSScriptRoot "Write-ChocolateyCache.ps1") - . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1") - . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Enums\InstalledState.ps1") - Mock Test-RunningAsAdmin { $true } - Mock Test-ChocolateyPackageInstalled { } - Mock Uninstall-ChocolateyPackage { $true } - Mock Get-Command { "choco" } - Mock Invoke-Command { } - Mock Write-ChocolateyCache { $true } - Mock Write-Debug { } - Mock Write-Warning { } - Mock Write-Error { } + . (Join-Path $PSScriptRoot "..\..\Utils\Write-StatusMessage.ps1") + . (Join-Path $PSScriptRoot "..\..\Utils\Test-RunningAsAdmin.ps1") + . (Join-Path $PSScriptRoot "..\..\Enums\InstalledState.ps1") + + Mock Write-StatusMessage { } } Describe "Install-ChocolateyPackage" { Context "When not running as administrator" { - It "Should throw and return false" { - Mock Test-RunningAsAdmin { $false } - $result = Install-ChocolateyPackage -PackageName "azshell" + It "Should return false and write error message" { + Mock Test-RunningAsAdmin { return $false } + + $result = Install-ChocolateyPackage -PackageName "git" + $result | Should -Be $false - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "administrator privileges" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "requires administrator privileges" -and $Verbosity -eq "Error" + } + } + } + + Context "When Test-RunningAsAdmin throws an exception" { + It "Should handle exception and return false" { + Mock Test-RunningAsAdmin { throw "Admin check failed" } + + $result = Install-ChocolateyPackage -PackageName "git" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error checking administrator privileges" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } + } + } + + Context "When Chocolatey is not installed" { + It "Should return false and write warning message" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $false } + + $result = Install-ChocolateyPackage -PackageName "git" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Chocolatey is not installed. Cannot install package git." -and $Verbosity -eq "Warning" + } + } + } + + Context "When Test-ChocolateyInstalled throws an exception" { + It "Should handle exception and return false" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { throw "Installation check failed" } + + $result = Install-ChocolateyPackage -PackageName "git" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error checking if Chocolatey is installed" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } + } + } + + Context "When Test-ChocolateyPackageInstalled throws an exception" { + It "Should handle exception and return false" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Test-ChocolateyPackageInstalled { throw "Package check failed" } + + $result = Install-ChocolateyPackage -PackageName "git" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error checking if package git is installed" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } + } + } + + Context "When package already meets requirements" { + It "Should return true immediately when package passes all checks" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Test-ChocolateyPackageInstalled { + $result = [InstalledState]::Pass + return $result + } + + $result = Install-ChocolateyPackage -PackageName "git" + + $result | Should -Be $true + Assert-MockCalled Test-ChocolateyPackageInstalled -Times 1 -Scope It -ParameterFilter { + $PackageName -eq "git" -and -not $PSBoundParameters.ContainsKey('Version') + } + } + + It "Should return true immediately when package with version passes all checks" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Test-ChocolateyPackageInstalled { + $result = [InstalledState]::Pass + return $result + } + + $result = Install-ChocolateyPackage -PackageName "nodejs" -Version "20.10.0" + + $result | Should -Be $true + Assert-MockCalled Test-ChocolateyPackageInstalled -Times 1 -Scope It -ParameterFilter { + $PackageName -eq "nodejs" -and $Version -eq "20.10.0" + } + } + } + + Context "When package needs reinstallation due to version conflict" { + It "Should uninstall existing package before reinstalling" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Test-ChocolateyPackageInstalled { + $result = [InstalledState]::Installed + return $result + } + Mock Uninstall-ChocolateyPackage { return $true } + Mock Find-Chocolatey { return "choco.exe" } + Mock Test-Path { return $true } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + } + Mock Write-ChocolateyCache { return $true } + + $result = Install-ChocolateyPackage -PackageName "nodejs" -Version "18.17.0" + + $result | Should -Be $true + Assert-MockCalled Uninstall-ChocolateyPackage -Times 1 -Scope It -ParameterFilter { + $PackageName -eq "nodejs" + } + Assert-MockCalled Invoke-Command -Times 1 -Scope It + } + + It "Should handle uninstall failure and return false" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Test-ChocolateyPackageInstalled { + $result = [InstalledState]::Installed + return $result + } + Mock Uninstall-ChocolateyPackage { throw "Uninstall failed" } + + $result = Install-ChocolateyPackage -PackageName "nodejs" -Version "18.17.0" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error uninstalling existing package nodejs" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } + } + } + + Context "When Find-Chocolatey throws an exception" { + It "Should handle exception and return false" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Test-ChocolateyPackageInstalled { + $result = [InstalledState]::NotInstalled + return $result + } + Mock Find-Chocolatey { throw "Cannot locate chocolatey" } + + $result = Install-ChocolateyPackage -PackageName "git" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error locating Chocolatey command" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } + } + } + + Context "When Find-Chocolatey returns null or invalid path" { + It "Should return false when Find-Chocolatey returns null" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Test-ChocolateyPackageInstalled { + $result = [InstalledState]::NotInstalled + return $result + } + Mock Find-Chocolatey { return $null } + + $result = Install-ChocolateyPackage -PackageName "git" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Could not find Chocolatey command. Cannot install package git." -and $Verbosity -eq "Warning" + } + } + + It "Should return false when Find-Chocolatey returns empty string" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Test-ChocolateyPackageInstalled { + $result = [InstalledState]::NotInstalled + return $result + } + Mock Find-Chocolatey { return "" } + + $result = Install-ChocolateyPackage -PackageName "git" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Could not find Chocolatey command. Cannot install package git." -and $Verbosity -eq "Warning" + } + } + } + + Context "When Test-Path throws an exception" { + It "Should handle exception and return false" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Test-ChocolateyPackageInstalled { + $result = [InstalledState]::NotInstalled + return $result + } + Mock Find-Chocolatey { return "choco.exe" } + Mock Test-Path { throw "Path check failed" } + + $result = Install-ChocolateyPackage -PackageName "git" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error verifying Chocolatey command path" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } } } - Context "When package is already installed and version matches" { - It "Should return true immediately" { + Context "When Chocolatey command path does not exist" { + It "Should return false when path does not exist" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } Mock Test-ChocolateyPackageInstalled { - return ([InstalledState]::Pass) + $result = [InstalledState]::NotInstalled + return $result } - $result = Install-ChocolateyPackage -PackageName "azshell" + Mock Find-Chocolatey { return "C:\invalid\path\choco.exe" } + Mock Test-Path { return $false } + + $result = Install-ChocolateyPackage -PackageName "git" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Chocolatey command path 'C:\\invalid\\path\\choco.exe' does not exist. Cannot install package git." -and $Verbosity -eq "Warning" + } + } + } + + Context "When installing package without version" { + It "Should install package with default parameters and return true" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Test-ChocolateyPackageInstalled { + $result = [InstalledState]::NotInstalled + return $result + } + Mock Find-Chocolatey { return "choco.exe" } + Mock Test-Path { return $true } + Mock Invoke-Command { + param($ScriptBlock) + $global:LASTEXITCODE = 0 + } + Mock Write-ChocolateyCache { return $true } + + $result = Install-ChocolateyPackage -PackageName "git" + $result | Should -Be $true + Assert-MockCalled Invoke-Command -Times 1 -Scope It + Assert-MockCalled Write-ChocolateyCache -Times 1 -Scope It } } - Context "When package is installed but version does not match" { - It "Should uninstall and reinstall the package" { + Context "When installing package with version" { + It "Should install package with version parameter and return true" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } Mock Test-ChocolateyPackageInstalled { - return ([InstalledState]::Installed + [InstalledState]::MinimumVersionMet + [InstalledState]::GlobalVersionMet) + $result = [InstalledState]::NotInstalled + return $result } - $script:uninstallCalled = $false - Mock Uninstall-ChocolateyPackage -MockWith { - param($PackageName) - $script:uninstallCalled = $true - $true + Mock Find-Chocolatey { return "choco.exe" } + Mock Test-Path { return $true } + Mock Invoke-Command { + param($ScriptBlock) + $global:LASTEXITCODE = 0 } - $script:LASTEXITCODE = 0 - Mock Invoke-Command { - $script:LASTEXITCODE = 0 - } - $result = Install-ChocolateyPackage -PackageName "azshell" + Mock Write-ChocolateyCache { return $true } + + $result = Install-ChocolateyPackage -PackageName "nodejs" -Version "20.10.0" + $result | Should -Be $true - $script:uninstallCalled | Should -Be $true + Assert-MockCalled Invoke-Command -Times 1 -Scope It + Assert-MockCalled Write-ChocolateyCache -Times 1 -Scope It } } - Context "When installing with version and params" { - It "Should build the correct choco command" { - $script:LASTEXITCODE = 0 - $script:paramsPassed = $null - Mock Test-ChocolateyPackageInstalled { return ([InstalledState]::NotInstalled) } - Mock Invoke-Command -MockWith { + Context "When installing package with custom parameters" { + It "Should install package with params parameter and return true" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Test-ChocolateyPackageInstalled { + $result = [InstalledState]::NotInstalled + return $result + } + Mock Find-Chocolatey { return "choco.exe" } + Mock Test-Path { return $true } + Mock Invoke-Command { param($ScriptBlock) - $script:paramsPassed = $ScriptBlock.ToString() + $global:LASTEXITCODE = 0 } - $result = Install-ChocolateyPackage -PackageName "azshell" -Version "0.2.2" -Param "/silent" + Mock Write-ChocolateyCache { return $true } + + $result = Install-ChocolateyPackage -PackageName "googlechrome" -Param "/nogoogle" + $result | Should -Be $true - # You can add more checks for $paramsPassed if needed + Assert-MockCalled Invoke-Command -Times 1 -Scope It + Assert-MockCalled Write-ChocolateyCache -Times 1 -Scope It + } + + It "Should install package with version and params parameters" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Test-ChocolateyPackageInstalled { + $result = [InstalledState]::NotInstalled + return $result + } + Mock Find-Chocolatey { return "choco.exe" } + Mock Test-Path { return $true } + Mock Invoke-Command { + param($ScriptBlock) + $global:LASTEXITCODE = 0 + } + Mock Write-ChocolateyCache { return $true } + + $result = Install-ChocolateyPackage -PackageName "vscode" -Version "1.84.2" -Param "/silent" + + $result | Should -Be $true + Assert-MockCalled Invoke-Command -Times 1 -Scope It + Assert-MockCalled Write-ChocolateyCache -Times 1 -Scope It } } - Context "When installation fails (non-zero exit code)" { - It "Should write error and return false" { - $script:LASTEXITCODE = 1 - Mock Test-ChocolateyPackageInstalled { return ([InstalledState]::NotInstalled) } - $result = Install-ChocolateyPackage -PackageName "azshell" + Context "When Invoke-Command throws an exception" { + It "Should handle exception and return false" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Test-ChocolateyPackageInstalled { + $result = [InstalledState]::NotInstalled + return $result + } + Mock Find-Chocolatey { return "choco.exe" } + Mock Test-Path { return $true } + Mock Invoke-Command { throw "Command execution failed" } + + $result = Install-ChocolateyPackage -PackageName "git" + $result | Should -Be $false - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to install" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error installing package git" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } } } - Context "When Write-ChocolateyCache fails after install" { - It "Should write warning and return false" { - $script:LASTEXITCODE = 0 - Mock Test-ChocolateyPackageInstalled { return ([InstalledState]::NotInstalled) } - Mock Write-ChocolateyCache { $false } - $result = Install-ChocolateyPackage -PackageName "azshell" + Context "When installation command fails with non-zero exit code" { + It "Should return false when LASTEXITCODE is non-zero" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Test-ChocolateyPackageInstalled { + $result = [InstalledState]::NotInstalled + return $result + } + Mock Find-Chocolatey { return "choco.exe" } + Mock Test-Path { return $true } + Mock Invoke-Command { + param($ScriptBlock) + $global:LASTEXITCODE = 1 + } + + $result = Install-ChocolateyPackage -PackageName "git" + $result | Should -Be $false - Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "Failed to write Chocolatey cache" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -eq "Failed to install: git" -and $Verbosity -eq "Error" + } } } - Context "When an exception occurs during install" { - It "Should write error and return false" { - Mock Test-ChocolateyPackageInstalled { throw "Unexpected error" } - $result = Install-ChocolateyPackage -PackageName "azshell" + Context "When Write-ChocolateyCache fails after successful installation" { + It "Should return false when Write-ChocolateyCache returns false" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Test-ChocolateyPackageInstalled { + $result = [InstalledState]::NotInstalled + return $result + } + Mock Find-Chocolatey { return "choco.exe" } + Mock Test-Path { return $true } + Mock Invoke-Command { + param($ScriptBlock) + $global:LASTEXITCODE = 0 + } + Mock Write-ChocolateyCache { return $false } + + $result = Install-ChocolateyPackage -PackageName "git" + $result | Should -Be $false - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Error checking/installing package" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Failed to write Chocolatey cache." -and $Verbosity -eq "Error" + } + } + + It "Should handle Write-ChocolateyCache exception and return false" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Test-ChocolateyPackageInstalled { + $result = [InstalledState]::NotInstalled + return $result + } + Mock Find-Chocolatey { return "choco.exe" } + Mock Test-Path { return $true } + Mock Invoke-Command { + param($ScriptBlock) + $global:LASTEXITCODE = 0 + } + Mock Write-ChocolateyCache { throw "Cache write failed" } + + $result = Install-ChocolateyPackage -PackageName "git" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error writing Chocolatey cache" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } + } + } + + Context "When using ShouldProcess with WhatIf" { + It "Should skip installation and return true when WhatIf is specified" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Test-ChocolateyPackageInstalled { + $result = [InstalledState]::NotInstalled + return $result + } + Mock Find-Chocolatey { return "choco.exe" } + Mock Test-Path { return $true } + Mock Invoke-Command { } + + $result = Install-ChocolateyPackage -PackageName "git" -WhatIf + + $result | Should -Be $true + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Skipping installation of Chocolatey package 'git'." -and $Verbosity -eq "Debug" + } + Assert-MockCalled Invoke-Command -Times 0 -Scope It + } + } + + Context "When validating parameter validation" { + It "Should throw when PackageName is null" { + { Install-ChocolateyPackage -PackageName $null } | Should -Throw + } + + It "Should throw when PackageName is empty string" { + { Install-ChocolateyPackage -PackageName "" } | Should -Throw + } + + It "Should throw when Version is empty string" { + { Install-ChocolateyPackage -PackageName "git" -Version "" } | Should -Throw + } + + It "Should throw when Param is empty string" { + { Install-ChocolateyPackage -PackageName "git" -Param "" } | Should -Throw + } + } + + Context "When processing successful installation scenarios" { + It "Should complete full installation flow successfully" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Test-ChocolateyPackageInstalled { + $result = [InstalledState]::NotInstalled + return $result + } + Mock Find-Chocolatey { return "C:\ProgramData\chocolatey\bin\choco.exe" } + Mock Test-Path { return $true } + Mock Invoke-Command { + param($ScriptBlock) + $global:LASTEXITCODE = 0 + } + Mock Write-ChocolateyCache { return $true } + + $result = Install-ChocolateyPackage -PackageName "git" -Version "2.42.0" -Param "/VERYSILENT" + + $result | Should -Be $true + Assert-MockCalled Test-RunningAsAdmin -Times 1 -Scope It + Assert-MockCalled Test-ChocolateyInstalled -Times 1 -Scope It + Assert-MockCalled Test-ChocolateyPackageInstalled -Times 1 -Scope It + Assert-MockCalled Find-Chocolatey -Times 1 -Scope It + Assert-MockCalled Test-Path -Times 1 -Scope It + Assert-MockCalled Invoke-Command -Times 1 -Scope It + Assert-MockCalled Write-ChocolateyCache -Times 1 -Scope It + } + + It "Should handle minimal parameters correctly" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Test-ChocolateyPackageInstalled { + $result = [InstalledState]::NotInstalled + return $result + } + Mock Find-Chocolatey { return "choco.exe" } + Mock Test-Path { return $true } + Mock Invoke-Command { + param($ScriptBlock) + $global:LASTEXITCODE = 0 + } + Mock Write-ChocolateyCache { return $true } + + $result = Install-ChocolateyPackage -PackageName "git" + + $result | Should -Be $true + Assert-MockCalled Test-ChocolateyPackageInstalled -Times 1 -Scope It -ParameterFilter { + $PackageName -eq "git" -and -not $PSBoundParameters.ContainsKey('Version') + } } } } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.ps1 b/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.ps1 index 2fff701..fe6d60c 100644 --- a/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.ps1 @@ -92,61 +92,120 @@ Function Install-ChocolateyPackage { try { # Check if running as administrator if (-not (Test-RunningAsAdmin)) { - throw "Chocolatey package installation requires administrator privileges. Please run as administrator." - } - - $testParams = @{ - PackageName = $PackageName + Write-StatusMessage "Chocolatey package installation requires administrator privileges. Please run as administrator." -Verbosity Error + return $false } + } catch { + Write-StatusMessage "Error checking administrator privileges: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $false + } - if($PSBoundParameters.ContainsKey('Version')) { - $testParams.Version = $Version + try { + if (-not (Test-ChocolateyInstalled)) { + Write-StatusMessage "Chocolatey is not installed. Cannot install package $PackageName." -Verbosity Warning + return $false } + } catch { + Write-StatusMessage "Error checking if Chocolatey is installed: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $false + } + $testParams = @{ + PackageName = $PackageName + } + + if($PSBoundParameters.ContainsKey('Version')) { + $testParams.Version = $Version + } + + try { $testResult = Test-ChocolateyPackageInstalled @testParams + } catch { + Write-StatusMessage "Error checking if package $PackageName is installed: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $false + } - if($testResult.HasFlag([InstalledState]::Pass)) { - return $true - } + if($testResult.HasFlag([InstalledState]::Pass)) { + return $true + } - if($testResult.HasFlag([InstalledState]::Installed)) { + if($testResult.HasFlag([InstalledState]::Installed)) { + try { Uninstall-ChocolateyPackage -PackageName $PackageName | Out-Null + } catch { + Write-StatusMessage "Error uninstalling existing package $($PackageName): $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $false } + } - $installParams = @( - 'install', - '-y', - $PackageName - ) - - if($PSBoundParameters.ContainsKey('Version')) { - $installParams = $installParams + @('--version', $Version) - } + $installParams = @( + 'install', + '-y', + $PackageName + ) + + if($PSBoundParameters.ContainsKey('Version')) { + $installParams = $installParams + @('--version', $Version) + } - if($PSBoundParameters.ContainsKey('Param')) { - $installParams = $installParams + @('--params', $Param) + if($PSBoundParameters.ContainsKey('Param')) { + $installParams = $installParams + @('--params', $Param) + } + + try { + $chocoCommand = Find-Chocolatey + if (-not $chocoCommand) { + Write-StatusMessage "Could not find Chocolatey command. Cannot install package $PackageName." -Verbosity Warning + return $false } + } catch { + Write-StatusMessage "Error locating Chocolatey command: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $false + } - $chocoCommand = Get-Command choco -ErrorAction SilentlyContinue + try { + if( -not (Test-Path $chocoCommand)) { + Write-StatusMessage "Chocolatey command path '$chocoCommand' does not exist. Cannot install package $PackageName." -Verbosity Warning + return $false + } + } catch { + Write-StatusMessage "Error verifying Chocolatey command path: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $false + } - if ($PSCmdlet.ShouldProcess($PackageName, "Install Chocolatey package")) { + if ($PSCmdlet.ShouldProcess($PackageName, "Install Chocolatey package")) { + try { Invoke-Command -ScriptBlock { & $chocoCommand @installParams | Out-Null } + } catch { + Write-StatusMessage "Error installing package $($PackageName): $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $false } - - if ($LASTEXITCODE -eq 0) { - Write-Debug "INSTALL:Successfully installed: $PackageName" + } else { + Write-StatusMessage "Skipping installation of Chocolatey package '$PackageName'." -Verbosity Debug + return $true + } + + if ($LASTEXITCODE -eq 0) { + try { if (-not (Write-ChocolateyCache)) { - Write-Warning "Failed to write Chocolatey cache." + Write-StatusMessage "Failed to write Chocolatey cache." -Verbosity Error return $false } - return $true - } else { - Write-Error "Failed to install: $PackageName" + } catch { + Write-StatusMessage "Error writing Chocolatey cache: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error return $false - } - } - catch { - Write-Error "Error checking/installing package $PackageName`: $_" + } + return $true + } else { + Write-StatusMessage "Failed to install: $PackageName" -Verbosity Error return $false - } + } } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageExport.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageExport.Tests.ps1 index 6e42408..c35e179 100644 --- a/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageExport.Tests.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageExport.Tests.ps1 @@ -1,200 +1,621 @@ BeforeAll { . (Join-Path $PSScriptRoot "Invoke-ChocolateyPackageExport.ps1") + . (Join-Path $PSScriptRoot "Test-ChocolateyInstalled.ps1") + . (Join-Path $PSScriptRoot "Find-Chocolatey.ps1") . (Join-Path $PSScriptRoot "Get-ChocolateyPackageDependencyMap.ps1") - . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1") - . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Read-DevSetupEnvFile.ps1") - . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Update-DevSetupEnvFile.ps1") - . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1") + . (Join-Path $PSScriptRoot "..\..\Utils\Write-StatusMessage.ps1") + . (Join-Path $PSScriptRoot "..\..\Utils\Test-RunningAsAdmin.ps1") + . (Join-Path $PSScriptRoot "..\..\Utils\Read-DevSetupEnvFile.ps1") + . (Join-Path $PSScriptRoot "..\..\Utils\Update-DevSetupEnvFile.ps1") + Mock Write-StatusMessage { } - Mock Test-RunningAsAdmin { $true } - Mock Get-ChocolateyPackageDependencyMap { @('chocolatey-core.extension', 'magic') } - Mock Read-DevSetupEnvFile { @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @() } } } } } - Mock Update-DevSetupEnvFile { } - $script:LASTEXITCODE = 0 - Mock Invoke-Command { - param($ScriptBlock) - $script:LASTEXITCODE = 0 - # Simulate successful choco list output - return @("git|2.40.0", "nodejs|18.16.0", "vscode|1.80.0") - } } Describe "Invoke-ChocolateyPackageExport" { Context "When not running as administrator" { - It "Should return false and write error" { - Mock Test-RunningAsAdmin { $false } - $result = Invoke-ChocolateyPackageExport -Config "$TestDrive\test.yaml" + It "Should return false and write error message" { + Mock Test-RunningAsAdmin { return $false } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "requires administrator privileges" -and $Verbosity -eq "Error" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "requires administrator privileges" -and $Verbosity -eq "Error" + } } } - Context "When Test-RunningAsAdmin throws exception" { - It "Should return false and write error" { + Context "When Test-RunningAsAdmin throws an exception" { + It "Should handle exception and return false" { Mock Test-RunningAsAdmin { throw "Admin check failed" } - $result = Invoke-ChocolateyPackageExport -Config "$TestDrive\test.yaml" + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Error checking administrator privileges" -and $Verbosity -eq "Error" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error checking administrator privileges" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } } } - Context "When choco list command fails" { - It "Should return false and write error" { - Mock Invoke-Command { throw "Command failed" } - $result = Invoke-ChocolateyPackageExport -Config "$TestDrive\test.yaml" + Context "When Chocolatey is not installed" { + It "Should return false and write warning message" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $false } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to retrieve Chocolatey package list" -and $Verbosity -eq "Error" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Chocolatey is not installed. Cannot export packages." -and $Verbosity -eq "Warning" + } } } - Context "When choco list command fails with non-zero exit code" { - BeforeEach { - $script:LASTEXITCODE = 0 - Mock Invoke-Command { $script:LASTEXITCODE = 1; return @() } + Context "When Test-ChocolateyInstalled throws an exception" { + It "Should handle exception and return false" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { throw "Installation check failed" } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error checking if Chocolatey is installed" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } } - It "Should return false and write error" { - $result = Invoke-ChocolateyPackageExport -Config "$TestDrive\test.yaml" + } + + Context "When Find-Chocolatey throws an exception" { + It "Should handle exception and return false" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { throw "Cannot locate chocolatey" } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to retrieve Chocolatey package list" -and $Verbosity -eq "Error" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error locating Chocolatey command" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } } - } + } - Context "When no Chocolatey packages are found" { - BeforeEach { - Mock Invoke-Command { $script:LASTEXITCODE = 0; return @() } + Context "When Find-Chocolatey returns null or empty" { + It "Should return false when Find-Chocolatey returns null" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return $null } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Could not find Chocolatey command. Cannot export packages." -and $Verbosity -eq "Warning" + } } - It "Should return true and write warning" { - $result = Invoke-ChocolateyPackageExport -Config "$TestDrive\test.yaml" - $result | Should -Be $true - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "No Chocolatey packages found" -and $Verbosity -eq "Warning" } + + It "Should return false when Find-Chocolatey returns empty string" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "" } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Could not find Chocolatey command. Cannot export packages." -and $Verbosity -eq "Warning" + } + } + + It "Should return false when Find-Chocolatey returns whitespace" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return " " } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Could not find Chocolatey command. Cannot export packages." -and $Verbosity -eq "Warning" + } } } - Context "When Get-ChocolateyPackageDependencyMap fails" { - It "Should continue with empty ignore list and write warning" { - Mock Get-ChocolateyPackageDependencyMap { throw "Dependency map failed" } - $result = Invoke-ChocolateyPackageExport -Config "$TestDrive\test.yaml" - $result | Should -Be $true - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to retrieve Chocolatey package dependency map" -and $Verbosity -eq "Warning" } + Context "When chocolatey command execution fails" { + It "Should handle Invoke-Command exception and return false" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "choco.exe" } + Mock Invoke-Command { throw "Command execution failed" } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Failed to retrieve Chocolatey package list" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } + } + + It "Should handle non-zero exit code and return false" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 1 + return "error output" + } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Failed to retrieve Chocolatey package list" -and $Verbosity -eq "Error" + } } } - Context "When choco output contains empty lines" { - It "Should skip empty lines and process valid packages" { - Mock Invoke-Command { return @("", "git|2.40.0", "", "nodejs|18.16.0") } - $result = Invoke-ChocolateyPackageExport -Config "$TestDrive\test.yaml" + Context "When chocolatey command returns no packages" { + It "Should return true and write warning when command returns null" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return $null + } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + + $result | Should -Be $true + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "No Chocolatey packages found or Chocolatey is not installed." -and $Verbosity -eq "Warning" + } + } + + It "Should return true and write warning when command returns empty string" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return "" + } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + $result | Should -Be $true - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Found 2 Chocolatey packages" -and $Verbosity -eq "Debug" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "No Chocolatey packages found or Chocolatey is not installed." -and $Verbosity -eq "Warning" + } + } + + It "Should return true and write warning when command returns whitespace" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return " " + } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + + $result | Should -Be $true + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "No Chocolatey packages found or Chocolatey is not installed." -and $Verbosity -eq "Warning" + } } } - Context "When packages start with chocolatey" { - It "Should skip chocolatey packages" { - Mock Invoke-Command { return @("chocolatey|1.0.0", "chocolatey-core|1.0.0", "git|2.40.0") } - $result = Invoke-ChocolateyPackageExport -Config "$TestDrive\test.yaml" + Context "When Get-ChocolateyPackageDependencyMap throws an exception" { + It "Should handle exception and continue with empty ignore list" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return "git|2.42.0" + } + Mock Get-ChocolateyPackageDependencyMap { throw "Dependency map failed" } + Mock Read-DevSetupEnvFile { return @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @() } } } } } + Mock Update-DevSetupEnvFile { } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + $result | Should -Be $true - Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { $Message -match "Skipping chocolatey package" -and $Verbosity -eq "Verbose" } - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Found 1 Chocolatey packages" -and $Verbosity -eq "Debug" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Failed to retrieve Chocolatey package dependency map" -and $Verbosity -eq "Warning" + } } } - Context "When packages are in ignore list" { - It "Should skip ignored packages" { - Mock Invoke-Command { return @("magic|1.0.0", "git|2.40.0") } - $result = Invoke-ChocolateyPackageExport -Config "$TestDrive\test.yaml" + Context "When processing packages with filtering" { + It "Should skip packages starting with chocolatey" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return @("chocolatey|0.12.1", "chocolatey-core|0.12.1", "git|2.42.0") + } + Mock Get-ChocolateyPackageDependencyMap { return @() } + Mock Read-DevSetupEnvFile { return @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @() } } } } } + Mock Update-DevSetupEnvFile { } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + + $result | Should -Be $true + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Message -match "Skipping chocolatey package:" -and $Verbosity -eq "Verbose" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Found 1 Chocolatey packages" -and $Verbosity -eq "Debug" + } + } + + It "Should skip packages in ignore list from dependency map" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return @("git|2.42.0", "nodejs|20.10.0", "ignored-package|1.0.0") + } + Mock Get-ChocolateyPackageDependencyMap { return @("ignored-package") } + Mock Read-DevSetupEnvFile { return @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @() } } } } } + Mock Update-DevSetupEnvFile { } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + + $result | Should -Be $true + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Skipping ignored package: ignored-package" -and $Verbosity -eq "Verbose" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Found 2 Chocolatey packages" -and $Verbosity -eq "Debug" + } + } + + It "Should process packages with proper name and version parsing" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return @("git|2.42.0.20231018", "nodejs|20.10.0", "vscode|1.84.2") + } + Mock Get-ChocolateyPackageDependencyMap { return @() } + Mock Read-DevSetupEnvFile { return @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @() } } } } } + Mock Update-DevSetupEnvFile { } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + + $result | Should -Be $true + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Found package: git \(version: 2\.42\.0\.20231018\)" -and $Verbosity -eq "Debug" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Found package: nodejs \(version: 20\.10\.0\)" -and $Verbosity -eq "Debug" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Found package: vscode \(version: 1\.84\.2\)" -and $Verbosity -eq "Debug" + } + } + + It "Should skip lines with invalid format" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return @("", "invalid-line", "git|2.42.0", "another-invalid", "nodejs|20.10.0") + } + Mock Get-ChocolateyPackageDependencyMap { return @() } + Mock Read-DevSetupEnvFile { return @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @() } } } } } + Mock Update-DevSetupEnvFile { } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + $result | Should -Be $true - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Skipping ignored package" -and $Verbosity -eq "Verbose" } - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Found 1 Chocolatey packages" -and $Verbosity -eq "Debug" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Found 2 Chocolatey packages" -and $Verbosity -eq "Debug" + } } } Context "When Read-DevSetupEnvFile fails" { - It "Should return false and write error" { - Mock Read-DevSetupEnvFile { throw "Read failed" } - $result = Invoke-ChocolateyPackageExport -Config "$TestDrive\test.yaml" + It "Should handle exception and return false" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return "git|2.42.0" + } + Mock Get-ChocolateyPackageDependencyMap { return @() } + Mock Read-DevSetupEnvFile { throw "Failed to read YAML" } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to read YAML configuration" -and $Verbosity -eq "Error" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Failed to read YAML configuration from test.yaml" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } } } - Context "When YAML structure is missing sections" { - It "Should create missing sections and add packages" { + Context "When processing packages against existing configuration" { + It "Should add new package not in existing configuration" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return "git|2.42.0" + } + Mock Get-ChocolateyPackageDependencyMap { return @() } Mock Read-DevSetupEnvFile { - @{ + return @{ devsetup = @{ - configuration = @{} - dependencies = @{} - commands = @() + dependencies = @{ + chocolatey = @{ + packages = @( + @{ name = "nodejs"; version = "20.10.0" } + ) + } + } } } } - $result = Invoke-ChocolateyPackageExport -Config "$TestDrive\test.yaml" + Mock Update-DevSetupEnvFile { } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + $result | Should -Be $true - Assert-MockCalled Write-StatusMessage -Exactly 3 -Scope It -ParameterFilter { $Message -match "Found package:" -and $Verbosity -eq "Debug" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Adding package: git \(2\.42\.0\)" -and $ForegroundColor -eq "Gray" -and $Indent -eq 2 -and $Width -eq 112 -and $NoNewline -eq $true + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -eq "[OK]" -and $ForegroundColor -eq "Green" + } } - } - - Context "When adding new packages" { - It "Should add packages and write success messages" { - $result = Invoke-ChocolateyPackageExport -Config "$TestDrive\test.yaml" + + It "Should update existing package when version changes" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return "nodejs|20.11.0" + } + Mock Get-ChocolateyPackageDependencyMap { return @() } + $existingPackage = @{ name = "nodejs"; version = "20.10.0" } + Mock Read-DevSetupEnvFile { + return @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @($existingPackage) + } + } + } + } + } + Mock Update-DevSetupEnvFile { } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + $result | Should -Be $true - Assert-MockCalled Write-StatusMessage -Exactly 3 -Scope It -ParameterFilter { $Message -match "Adding package:" -and $ForegroundColor -eq "Gray" } - Assert-MockCalled Write-StatusMessage -Exactly 3 -Scope It -ParameterFilter { $Message -eq "[OK]" -and $ForegroundColor -eq "Green" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Updating package: nodejs \(20\.10\.0 -> 20\.11\.0\)" -and $ForegroundColor -eq "Cyan" -and $Indent -eq 2 -and $Width -eq 112 -and $NoNewline -eq $true + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -eq "[OK]" -and $ForegroundColor -eq "Green" + } } - } - - Context "When package exists as hashtable and version matches" { - It "Should skip package with no change message" { - Mock Read-DevSetupEnvFile { @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @(@{ name = "git"; version = "2.40.0" }) } } } } } - $result = Invoke-ChocolateyPackageExport -Config "$TestDrive\test.yaml" + + It "Should update existing package when no version exists" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return "nodejs|20.10.0" + } + Mock Get-ChocolateyPackageDependencyMap { return @() } + $existingPackage = @{ name = "nodejs" } + Mock Read-DevSetupEnvFile { + return @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @($existingPackage) + } + } + } + } + } + Mock Update-DevSetupEnvFile { } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + + $result | Should -Be $true + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Updating package: nodejs" -and $ForegroundColor -eq "Gray" -and $Indent -eq 2 -and $Width -eq 112 -and $NoNewline -eq $true + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -eq "[OK]" -and $ForegroundColor -eq "Green" + } + } + + It "Should skip existing package with same version" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return "nodejs|20.10.0" + } + Mock Get-ChocolateyPackageDependencyMap { return @() } + $existingPackage = @{ name = "nodejs"; version = "20.10.0" } + Mock Read-DevSetupEnvFile { + return @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @($existingPackage) + } + } + } + } + } + Mock Update-DevSetupEnvFile { } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + $result | Should -Be $true - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Skipping package \(No Change\)" -and $ForegroundColor -eq "Gray" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Skipping package \(No Change\): nodejs \(20\.10\.0\)" -and $ForegroundColor -eq "Gray" -and $Indent -eq 2 -and $Width -eq 112 -and $NoNewline -eq $true + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -eq "[OK]" -and $ForegroundColor -eq "Gray" + } } } - Context "When package exists as hashtable and version changes" { - It "Should update package version" { - Mock Read-DevSetupEnvFile { @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @(@{ name = "git"; version = "2.39.0" }) } } } } } - $result = Invoke-ChocolateyPackageExport -Config "$TestDrive\test.yaml" - $result | Should -Be $true - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Updating package: git" -and $ForegroundColor -eq "Cyan" } + Context "When Update-DevSetupEnvFile fails" { + It "Should handle exception and return false" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return "git|2.42.0" + } + Mock Get-ChocolateyPackageDependencyMap { return @() } + Mock Read-DevSetupEnvFile { return @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @() } } } } } + Mock Update-DevSetupEnvFile { throw "Failed to save YAML" } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Failed to save configuration to test.yaml" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } } } - Context "When package exists as hashtable without version" { - It "Should add version to existing package" { - Mock Read-DevSetupEnvFile { @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @(@{ name = "git" }) } } } } } - $result = Invoke-ChocolateyPackageExport -Config "$TestDrive\test.yaml" + Context "When using DryRun parameter" { + It "Should pass WhatIf to Update-DevSetupEnvFile" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return "git|2.42.0" + } + Mock Get-ChocolateyPackageDependencyMap { return @() } + Mock Read-DevSetupEnvFile { return @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @() } } } } } + Mock Update-DevSetupEnvFile { } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" -DryRun + $result | Should -Be $true - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Updating package: git" -and $ForegroundColor -eq "Gray" } + Assert-MockCalled Update-DevSetupEnvFile -Times 1 -Scope It -ParameterFilter { + $WhatIf -eq $true + } } } - Context "When Update-DevSetupEnvFile fails" { - It "Should return false and write error" { - Mock Update-DevSetupEnvFile { throw "Update failed" } - $result = Invoke-ChocolateyPackageExport -Config "$TestDrive\test.yaml" - $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to save configuration" -and $Verbosity -eq "Error" } + Context "When validating parameter validation" { + It "Should throw when Config is null" { + { Invoke-ChocolateyPackageExport -Config $null } | Should -Throw + } + + It "Should throw when Config is empty string" { + { Invoke-ChocolateyPackageExport -Config "" } | Should -Throw } } - Context "When DryRun is specified" { - It "Should call Update-DevSetupEnvFile with WhatIf" { - $result = Invoke-ChocolateyPackageExport -Config "$TestDrive\test.yaml" -DryRun + Context "When processing successful export operation" { + It "Should complete export successfully with multiple packages" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return @("git|2.42.0", "nodejs|20.10.0", "vscode|1.84.2") + } + Mock Get-ChocolateyPackageDependencyMap { return @() } + Mock Read-DevSetupEnvFile { return @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @() } } } } } + Mock Update-DevSetupEnvFile { } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + $result | Should -Be $true - Assert-MockCalled Update-DevSetupEnvFile -Exactly 1 -Scope It -ParameterFilter { $WhatIf -eq $true } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Getting list of installed Chocolatey packages..." -and $ForegroundColor -eq "Gray" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Found 3 Chocolatey packages" -and $Verbosity -eq "Debug" + } + Assert-MockCalled Write-StatusMessage -Exactly 3 -Scope It -ParameterFilter { + $Message -match "Found package:" -and $Verbosity -eq "Debug" + } + Assert-MockCalled Write-StatusMessage -Exactly 3 -Scope It -ParameterFilter { + $Message -match "Adding package:" -and $ForegroundColor -eq "Gray" + } + Assert-MockCalled Write-StatusMessage -Exactly 3 -Scope It -ParameterFilter { + $Message -eq "[OK]" -and $ForegroundColor -eq "Green" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Saving configuration to:" -and $Verbosity -eq "Debug" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -eq "Configuration saved successfully!" -and $Verbosity -eq "Debug" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -eq "Chocolatey packages conversion completed!" -and $ForegroundColor -eq "Green" + } } - } - - Context "When successful export" { - It "Should return true and write success messages" { - $result = Invoke-ChocolateyPackageExport -Config "$TestDrive\test.yaml" + + It "Should write proper console messages in the correct sequence" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return "git|2.42.0" + } + Mock Get-ChocolateyPackageDependencyMap { return @() } + Mock Read-DevSetupEnvFile { return @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @() } } } } } + Mock Update-DevSetupEnvFile { } + + $result = Invoke-ChocolateyPackageExport -Config "test.yaml" + $result | Should -Be $true - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Saving configuration to:" -and $Verbosity -eq "Debug" } - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -eq "Configuration saved successfully!" -and $Verbosity -eq "Debug" } - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -eq "Chocolatey packages conversion completed!" -and $ForegroundColor -eq "Green" } + # Expected messages: Getting list...(1) + Found 1 packages(1) + Found package(1) + Adding package(1) + [OK](1) + Saving(1) + saved(1) + completed(1) = 8 total + Assert-MockCalled Write-StatusMessage -Exactly 8 -Scope It } } } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageExport.ps1 b/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageExport.ps1 index 4d30ca2..b3a4892 100644 --- a/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageExport.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageExport.ps1 @@ -87,10 +87,34 @@ Function Invoke-ChocolateyPackageExport { return $false } + try { + if (-not (Test-ChocolateyInstalled)) { + Write-StatusMessage "Chocolatey is not installed. Cannot export packages." -Verbosity Warning + return $false + } + } catch { + Write-StatusMessage "Error checking if Chocolatey is installed: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $false + } + + try { + $chocoCommand = Find-Chocolatey + } catch { + Write-StatusMessage "Error locating Chocolatey command: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $false + } + + if(-not $chocoCommand -or [string]::IsNullOrWhiteSpace($chocoCommand)) { + Write-StatusMessage "Could not find Chocolatey command. Cannot export packages." -Verbosity Warning + return $false + } + # Get list of installed Chocolatey packages Write-StatusMessage "- Getting list of installed Chocolatey packages..." -ForegroundColor Gray try { - $chocoList = Invoke-Command -ScriptBlock { & choco list --local-only --limit-output } + $chocoList = Invoke-Command -ScriptBlock { & $chocoCommand list --local-only --limit-output } if($LASTEXITCODE -ne 0) { throw "Chocolatey command failed with exit code $LASTEXITCODE" } @@ -100,7 +124,7 @@ Function Invoke-ChocolateyPackageExport { return $false } - if (-not $chocoList) { + if (-not $chocoList -or [string]::IsNullOrWhiteSpace($chocoList)) { Write-StatusMessage "No Chocolatey packages found or Chocolatey is not installed." -Verbosity Warning return $true } @@ -153,15 +177,10 @@ Function Invoke-ChocolateyPackageExport { return $false } - # Ensure chocolatey-specific sections exist - if (-not $YamlData.devsetup.dependencies.chocolatey) { $YamlData.devsetup.dependencies.chocolatey = @{} } - if (-not $YamlData.devsetup.dependencies.chocolatey.packages) { $YamlData.devsetup.dependencies.chocolatey.packages = @() } - # Add packages to YAML data foreach ($package in $chocolateyPackages) { # Check if package already exists $existingPackage = $YamlData.devsetup.dependencies.chocolatey.packages | Where-Object { - ($_ -is [string] -and $_ -eq $package.name) -or ($_.name -eq $package.name) } @@ -173,25 +192,25 @@ Function Invoke-ChocolateyPackageExport { } Write-StatusMessage "[OK]" -ForegroundColor Green } else { - # Package exists, check if version has changed - $existingVersion = $null - if ((-not ($existingPackage -is [string])) -and $existingPackage.version) { - $existingVersion = $existingPackage.version - } + if ($existingPackage.version -and $existingPackage.version -ne $package.version) { + Write-StatusMessage "- Updating package: $($package.name) ($($existingPackage.version) -> $($package.version))" -ForegroundColor Cyan -Indent 2 -Width 112 -NoNewline - if ($existingVersion -and $existingVersion -ne $package.version) { - Write-StatusMessage "- Updating package: $($package.name) ($existingVersion -> $($package.version))" -ForegroundColor Cyan -Indent 2 -Width 112 -NoNewline - # Find index and update $index = $YamlData.devsetup.dependencies.chocolatey.packages.IndexOf($existingPackage) - $YamlData.devsetup.dependencies.chocolatey.packages[$index].version = $package.version + $YamlData.devsetup.dependencies.chocolatey.packages[$index].version = @{ + version = $package.version + name = $package.name + } Write-StatusMessage "[OK]" -ForegroundColor Green - } elseif (-not $existingVersion) { + } elseif (-not $existingPackage.version) { Write-StatusMessage "- Updating package: $($package.name)" -ForegroundColor Gray -Indent 2 -Width 112 -NoNewline # Find index and add version $index = $YamlData.devsetup.dependencies.chocolatey.packages.IndexOf($existingPackage) - $YamlData.devsetup.dependencies.chocolatey.packages[$index].version = $package.version + $YamlData.devsetup.dependencies.chocolatey.packages[$index].version = @{ + version = $package.version + name = $package.name + } Write-StatusMessage "[OK]" -ForegroundColor Green } else { Write-StatusMessage "- Skipping package (No Change): $($package.name) ($($package.version))" -ForegroundColor Gray -Indent 2 -Width 112 -NoNewline diff --git a/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageInstall.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageInstall.Tests.ps1 index 912b043..ff9ef60 100644 --- a/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageInstall.Tests.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageInstall.Tests.ps1 @@ -2,232 +2,512 @@ BeforeAll { . (Join-Path $PSScriptRoot "Invoke-ChocolateyPackageInstall.ps1") . (Join-Path $PSScriptRoot "Install-ChocolateyPackage.ps1") . (Join-Path $PSScriptRoot "Write-ChocolateyCache.ps1") - . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1") - . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1") + . (Join-Path $PSScriptRoot "..\..\Utils\Write-StatusMessage.ps1") + . (Join-Path $PSScriptRoot "..\..\Utils\Test-RunningAsAdmin.ps1") + Mock Write-StatusMessage { } - Mock Test-RunningAsAdmin { $true } - Mock Write-ChocolateyCache { $true } - Mock Install-ChocolateyPackage { $true } } Describe "Invoke-ChocolateyPackageInstall" { Context "When not running as administrator" { - It "Should return false and write error" { - Mock Test-RunningAsAdmin { $false } - $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @("git") } } } } + It "Should return false and write error message" { + Mock Test-RunningAsAdmin { return $false } + + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @( + @{ name = "git" } + ) + } + } + } + } + $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData + $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "requires administrator privileges" -and $Verbosity -eq "Error" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "requires administrator privileges" -and $Verbosity -eq "Error" + } } } - Context "When Test-RunningAsAdmin throws exception" { - It "Should return false and write error" { + Context "When Test-RunningAsAdmin throws an exception" { + It "Should handle exception and return false" { Mock Test-RunningAsAdmin { throw "Admin check failed" } - $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @("git") } } } } - $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData - $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Error checking administrator privileges" -and $Verbosity -eq "Error" } - } - } - - Context "When YamlData is null" { - It "Should should throw" { - { Invoke-ChocolateyPackageInstall -YamlData $null } | Should -Throw - } - } - - Context "When devsetup section is missing" { - It "Should return false and write warning" { - $yamlData = @{ } - $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData - $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Chocolatey packages not found" -and $Verbosity -eq "Warning" } - } - } - - Context "When dependencies section is missing" { - It "Should return false and write warning" { - $yamlData = @{ devsetup = @{ } } + + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @( + @{ name = "git" } + ) + } + } + } + } + $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData + $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Chocolatey packages not found" -and $Verbosity -eq "Warning" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error checking administrator privileges" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } } } - Context "When chocolatey section is missing" { - It "Should return false and write warning" { - $yamlData = @{ devsetup = @{ dependencies = @{ } } } + Context "When Write-ChocolateyCache fails" { + It "Should return false when Write-ChocolateyCache returns false" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $false } + + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @( + @{ name = "git" } + ) + } + } + } + } + $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData + $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Chocolatey packages not found" -and $Verbosity -eq "Warning" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Failed to write Chocolatey cache" -and $Verbosity -eq "Error" + } } - } - - Context "When packages section is missing" { - It "Should return false and write warning" { - $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ } } } } + + It "Should handle Write-ChocolateyCache exception and return false" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { throw "Cache write failed" } + + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @( + @{ name = "git" } + ) + } + } + } + } + $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData + $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Chocolatey packages not found" -and $Verbosity -eq "Warning" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error writing Chocolatey cache" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } } } - Context "When packages array is empty" { - It "Should return false" { - $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @() } } } } + Context "When installing single package with version" { + It "Should install package with version and return true" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + Mock Install-ChocolateyPackage { return $true } + + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @( + @{ name = "git"; version = "2.42.0" } + ) + } + } + } + } + $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData - $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Chocolatey packages not found in YAML configuration. Skipping installation." } + + $result | Should -Be $true + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Installing Chocolatey packages from configuration:" -and $ForegroundColor -eq "Cyan" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Installing Chocolatey package: git \(version: 2\.42\.0\)" -and $ForegroundColor -eq "Gray" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -eq "[OK]" -and $ForegroundColor -eq "Green" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Chocolatey packages installation completed! Processed 1 packages" -and $ForegroundColor -eq "Green" + } + Assert-MockCalled Install-ChocolateyPackage -Times 1 -Scope It -ParameterFilter { + $PackageName -eq "git" -and $Version -eq "2.42.0" -and $WhatIf -eq $false + } } } - Context "When Write-ChocolateyCache fails" { - It "Should return false and write error" { - Mock Write-ChocolateyCache { $false } - $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @("git") } } } } + Context "When installing single package without version" { + It "Should install package with latest version and return true" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + Mock Install-ChocolateyPackage { return $true } + + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @( + @{ name = "nodejs" } + ) + } + } + } + } + $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData - $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to write Chocolatey cache" -and $Verbosity -eq "Error" } + + $result | Should -Be $true + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Installing Chocolatey package: nodejs \(version: latest\)" -and $ForegroundColor -eq "Gray" + } + Assert-MockCalled Install-ChocolateyPackage -Times 1 -Scope It -ParameterFilter { + $PackageName -eq "nodejs" -and $Version -eq $null + } } } - Context "When Write-ChocolateyCache throws exception" { - It "Should return false and write error" { - Mock Write-ChocolateyCache { throw "Cache write failed" } - $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @("git") } } } } + Context "When installing package with custom parameters" { + It "Should install package with params and return true" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + Mock Install-ChocolateyPackage { return $true } + + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @( + @{ name = "googlechrome"; params = "/nogoogle" } + ) + } + } + } + } + $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData - $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Error writing Chocolatey cache" -and $Verbosity -eq "Error" } + + $result | Should -Be $true + Assert-MockCalled Install-ChocolateyPackage -Times 1 -Scope It -ParameterFilter { + $PackageName -eq "googlechrome" -and $Param -eq "/nogoogle" + } } - } - - Context "When package is object with name only" { - It "Should install with latest version" { - $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @(@{ name = "git" }) } } } } + + It "Should install package with version and params" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + Mock Install-ChocolateyPackage { return $true } + + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @( + @{ name = "vscode"; version = "1.75.0"; params = "/silent" } + ) + } + } + } + } + $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData + $result | Should -Be $true - Assert-MockCalled Install-ChocolateyPackage -Exactly 1 -Scope It -ParameterFilter { $PackageName -eq "git" -and -not $Version } + Assert-MockCalled Install-ChocolateyPackage -Times 1 -Scope It -ParameterFilter { + $PackageName -eq "vscode" -and $Version -eq "1.75.0" -and $Param -eq "/silent" + } } } - Context "When package is object with version" { - It "Should install with specified version" { - $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @(@{ name = "git"; version = "2.42.0" }) } } } } + Context "When installing multiple packages" { + It "Should install all packages and return true" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + Mock Install-ChocolateyPackage { return $true } + + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @( + @{ name = "git"; version = "2.42.0" }, + @{ name = "nodejs"; version = "18.17.0" }, + @{ name = "vscode"; params = "/silent" } + ) + } + } + } + } + $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData + $result | Should -Be $true - Assert-MockCalled Install-ChocolateyPackage -Exactly 1 -Scope It -ParameterFilter { $PackageName -eq "git" -and $Version -eq "2.42.0" } - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "version: 2.42.0" -and $ForegroundColor -eq "Gray" } + Assert-MockCalled Write-StatusMessage -Exactly 3 -Scope It -ParameterFilter { + $Message -eq "[OK]" -and $ForegroundColor -eq "Green" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Processed 3 packages" -and $ForegroundColor -eq "Green" + } + Assert-MockCalled Install-ChocolateyPackage -Times 3 -Scope It } } - Context "When package is object with params" { - It "Should install with params" { - $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @(@{ name = "git"; params = "/silent" }) } } } } + Context "When individual package installation fails" { + It "Should mark package as failed but continue processing others" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + Mock Install-ChocolateyPackage { + param($PackageName) + if ($PackageName -eq "failing-package") { return $false } + return $true + } + + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @( + @{ name = "git" }, + @{ name = "failing-package" }, + @{ name = "nodejs" } + ) + } + } + } + } + $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData + $result | Should -Be $true - Assert-MockCalled Install-ChocolateyPackage -Exactly 1 -Scope It -ParameterFilter { $PackageName -eq "git" -and $Param -eq "/silent" } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Message -eq "[OK]" -and $ForegroundColor -eq "Green" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -eq "[FAILED]" -and $ForegroundColor -eq "Red" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Processed 2 packages" -and $ForegroundColor -eq "Green" + } } } - Context "When package is object with name, version, and params" { - It "Should install with all parameters" { - $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @(@{ name = "git"; version = "2.42.0"; params = "/silent" }) } } } } + Context "When Install-ChocolateyPackage throws an exception" { + It "Should handle exception and continue processing" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + Mock Install-ChocolateyPackage { + param($PackageName) + if ($PackageName -eq "exception-package") { + throw "Package install failed" + } + return $true + } + + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @( + @{ name = "git" }, + @{ name = "exception-package" }, + @{ name = "nodejs" } + ) + } + } + } + } + $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData + $result | Should -Be $true - Assert-MockCalled Install-ChocolateyPackage -Exactly 1 -Scope It -ParameterFilter { $PackageName -eq "git" -and $Version -eq "2.42.0" -and $Param -eq "/silent" } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Message -eq "[OK]" -and $ForegroundColor -eq "Green" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -eq "[FAILED]" -and $ForegroundColor -eq "Red" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error installing package exception-package" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Processed 2 packages" -and $ForegroundColor -eq "Green" + } } } - Context "When package object has no name" { - It "Should skip and write warning" { - $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @(@{ version = "1.0.0" }, "git") } } } } - $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData + Context "When using DryRun parameter" { + It "Should pass WhatIf to Install-ChocolateyPackage" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + Mock Install-ChocolateyPackage { return $true } + + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @( + @{ name = "git" } + ) + } + } + } + } + + $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData -DryRun + $result | Should -Be $true - Assert-MockCalled Install-ChocolateyPackage -Exactly 0 -Scope It - Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { $Message -match "no name specified" -and $Verbosity -eq "Warning" } + Assert-MockCalled Install-ChocolateyPackage -Times 1 -Scope It -ParameterFilter { + $PackageName -eq "git" -and $WhatIf -eq $true + } } } - Context "When package name is empty string" { - It "Should skip and write warning" { - $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @(@{ name = "" }, "git") } } } } - $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData + Context "When validating parameter validation" { + It "Should throw when YamlData is null" { + { Invoke-ChocolateyPackageInstall -YamlData $null } | Should -Throw + } + + It "Should handle empty YamlData gracefully" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + + $result = Invoke-ChocolateyPackageInstall -YamlData @{} + $result | Should -Be $true - Assert-MockCalled Install-ChocolateyPackage -Exactly 0 -Scope It - Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { $Message -match "no name specified" -and $Verbosity -eq "Warning" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Processed 0 packages" -and $ForegroundColor -eq "Green" + } } } - Context "When Install-ChocolateyPackage succeeds" { - It "Should write OK message" { - $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @("git") } } } } + Context "When YAML structure is missing or incomplete" { + It "Should handle missing devsetup section gracefully" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + + $yamlData = @{ + other = @{ + data = "value" + } + } + $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData + $result | Should -Be $true - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "[OK]" -and $ForegroundColor -eq "Green" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Chocolatey packages installation completed! Processed 0 packages" -and $ForegroundColor -eq "Green" + } } - } - - Context "When Install-ChocolateyPackage fails" { - It "Should write FAILED message and continue" { - Mock Install-ChocolateyPackage { $false } - $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @( @{ name = "git" }, @{ name = "nodejs" } ) } } } } + + It "Should handle missing dependencies section gracefully" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + + $yamlData = @{ + devsetup = @{ + other = "data" + } + } + $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData + $result | Should -Be $true - Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { $Message -match "[FAILED]" -and $ForegroundColor -eq "Red" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Processed 0 packages" -and $ForegroundColor -eq "Green" + } } - } - - Context "When Install-ChocolateyPackage throws exception" { - It "Should write FAILED message and error, then continue" { - Mock Install-ChocolateyPackage { throw "Install failed" } - $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @( @{ name = "git" }, @{ name = "nodejs" } ) } } } } + + It "Should handle missing chocolatey section gracefully" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + + $yamlData = @{ + devsetup = @{ + dependencies = @{ + npm = @{ + packages = @("lodash") + } + } + } + } + $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData + $result | Should -Be $true - Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { $Message -match "[FAILED]" -and $ForegroundColor -eq "Red" } - Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { $Message -match "Error installing package" -and $Verbosity -eq "Error" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Processed 0 packages" -and $ForegroundColor -eq "Green" + } } - } - - Context "When DryRun is specified" { - It "Should pass WhatIf to Install-ChocolateyPackage" { - $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @(@{ name = "git" }) } } } } - $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData -DryRun + + It "Should handle missing packages array gracefully" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + other = "data" + } + } + } + } + + $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData + $result | Should -Be $true - Assert-MockCalled Install-ChocolateyPackage -Exactly 1 -Scope It -ParameterFilter { $WhatIf -eq $true } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Processed 0 packages" -and $ForegroundColor -eq "Green" + } } } - Context "When multiple packages with mixed formats" { - It "Should process all correctly" { + Context "When processing packages with formatting validation" { + It "Should display proper formatting with indent and width settings" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + Mock Install-ChocolateyPackage { return $true } + $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @( - @{ name = "git" }, - @{ name = "nodejs"; version = "18.17.0" }, - @{ name = "vscode"; params = "/silent" }, - @{ name = "python"; version = "3.11.0"; params = "/quiet" } + @{ name = "git"; version = "2.42.0" } ) } } } } + $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData + $result | Should -Be $true - Assert-MockCalled Install-ChocolateyPackage -Exactly 4 -Scope It - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Processed 4 packages" -and $ForegroundColor -eq "Green" } - } - } - - Context "When successful installation" { - It "Should return true and write completion message" { - $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @("git") } } } } - $result = Invoke-ChocolateyPackageInstall -YamlData $yamlData - $result | Should -Be $true - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "installation completed" -and $ForegroundColor -eq "Green" } + Assert-MockCalled Write-StatusMessage -Times 1 -Scope It -ParameterFilter { + $Message -match "Installing Chocolatey package: git \(version: 2\.42\.0\)" -and + $ForegroundColor -eq "Gray" -and + $Indent -eq 2 -and + $Width -eq 112 -and + $NoNewline -eq $true + } } } } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageInstall.ps1 b/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageInstall.ps1 index 243ef36..c386a23 100644 --- a/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageInstall.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageInstall.ps1 @@ -101,13 +101,6 @@ Function Invoke-ChocolateyPackageInstall { return $false } - - # Check if chocolatey dependencies exist - if (-not $YamlData -or -not $YamlData.devsetup -or -not $YamlData.devsetup.dependencies -or -not $YamlData.devsetup.dependencies.chocolatey -or -not $YamlData.devsetup.dependencies.chocolatey.packages) { - Write-StatusMessage "Chocolatey packages not found in YAML configuration. Skipping installation." -Verbosity Warning - return $false - } - try { if (-not (Write-ChocolateyCache)) { Write-StatusMessage "Failed to write Chocolatey cache." -Verbosity Error @@ -125,16 +118,6 @@ Function Invoke-ChocolateyPackageInstall { $packageCount = 0 foreach ($package in $chocolateyPackages) { - if (-not $package) { continue } - - $packageCount++ - - # Validate package name - if ([string]::IsNullOrEmpty($package.name)) { - Write-StatusMessage "Package entry #$packageCount has no name specified, skipping" -Verbosity Warning - continue - } - # Build install parameters $installParams = @{ PackageName = $package.name @@ -155,6 +138,7 @@ Function Invoke-ChocolateyPackageInstall { try { if((Install-ChocolateyPackage @installParams)) { Write-StatusMessage "[OK]" -ForegroundColor Green + $packageCount++ } else { Write-StatusMessage "[FAILED]" -ForegroundColor Red } diff --git a/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageUninstall.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageUninstall.Tests.ps1 index 80384c5..b144ef5 100644 --- a/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageUninstall.Tests.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageUninstall.Tests.ps1 @@ -4,294 +4,472 @@ BeforeAll { . (Join-Path $PSScriptRoot "Write-ChocolateyCache.ps1") . (Join-Path $PSScriptRoot "..\..\Utils\Write-StatusMessage.ps1") . (Join-Path $PSScriptRoot "..\..\Utils\Test-RunningAsAdmin.ps1") + Mock Write-StatusMessage { } - Mock Test-RunningAsAdmin { return $true } - Mock Write-ChocolateyCache { return $true } - Mock Uninstall-ChocolateyPackage { return $true } } Describe "Invoke-ChocolateyPackageUninstall" { - Context "When not running as admin" { - It "Should return false and write error" { + Context "When not running as administrator" { + It "Should return false and write error message" { Mock Test-RunningAsAdmin { return $false } + $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ - packages = @("git") + packages = @( + @{ name = "git" } + ) } } } } + $result = Invoke-ChocolateyPackageUninstall -YamlData $yamlData + $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "requires administrator privileges" -and $Verbosity -eq "Error" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "requires administrator privileges" -and $Verbosity -eq "Error" + } } } - Context "When Test-RunningAsAdmin throws exception" { - It "Should return false and write error" { + Context "When Test-RunningAsAdmin throws an exception" { + It "Should handle exception and return false" { Mock Test-RunningAsAdmin { throw "Admin check failed" } + $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ - packages = @("git") + packages = @( + @{ name = "git" } + ) } } } } + $result = Invoke-ChocolateyPackageUninstall -YamlData $yamlData + $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { $Verbosity -eq "Error" } - } - } - - Context "When YAML data is null" { - It "Should throw" { - { Invoke-ChocolateyPackageUninstall -YamlData $null } | Should -Throw - } - } - - Context "When YAML data has no devsetup" { - It "Should return without processing" { - $yamlData = @{ } - $result = Invoke-ChocolateyPackageUninstall -YamlData $yamlData - $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Chocolatey packages not found" -and $Verbosity -eq "Warning" } - } - } - - Context "When YAML data has no dependencies" { - It "Should return without processing" { - $yamlData = @{ devsetup = @{ } } - $result = Invoke-ChocolateyPackageUninstall -YamlData $yamlData - $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Chocolatey packages not found" -and $Verbosity -eq "Warning" } - } - } - - Context "When YAML data has no chocolatey" { - It "Should return without processing" { - $yamlData = @{ devsetup = @{ dependencies = @{ } } } - $result = Invoke-ChocolateyPackageUninstall -YamlData $yamlData - $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Chocolatey packages not found" -and $Verbosity -eq "Warning" } - } - } - - Context "When YAML data has no packages" { - It "Should return without processing" { - $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ } } } } - $result = Invoke-ChocolateyPackageUninstall -YamlData $yamlData - $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Chocolatey packages not found" -and $Verbosity -eq "Warning" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error checking administrator privileges" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } } } Context "When Write-ChocolateyCache fails" { - It "Should return false and write error" { + It "Should return false when Write-ChocolateyCache returns false" { + Mock Test-RunningAsAdmin { return $true } Mock Write-ChocolateyCache { return $false } + $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ - packages = @("git") + packages = @( + @{ name = "git" } + ) } } } } + $result = Invoke-ChocolateyPackageUninstall -YamlData $yamlData + $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to write Chocolatey cache" -and $Verbosity -eq "Warning" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Failed to write Chocolatey cache" -and $Verbosity -eq "Warning" + } } - } - - Context "When Write-ChocolateyCache throws exception" { - It "Should return false and write error" { + + It "Should handle Write-ChocolateyCache exception and return false" { + Mock Test-RunningAsAdmin { return $true } Mock Write-ChocolateyCache { throw "Cache write failed" } + $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ - packages = @("git") + packages = @( + @{ name = "git" } + ) } } } } + $result = Invoke-ChocolateyPackageUninstall -YamlData $yamlData + $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { $Verbosity -eq "Error" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error writing Chocolatey cache" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } } } - Context "When single package as string" { - It "Should uninstall package and return true" { + Context "When processing single package with object format" { + It "Should uninstall package with version and return true" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + Mock Uninstall-ChocolateyPackage { return $true } + $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ - packages = @("git") + packages = @( + @{ name = "git"; version = "2.42.0" } + ) } } } } + $result = Invoke-ChocolateyPackageUninstall -YamlData $yamlData + $result | Should -Be $true - Assert-MockCalled Uninstall-ChocolateyPackage -Exactly 1 -Scope It -ParameterFilter { $PackageName -eq "git" -and $WhatIf -eq $false } - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Uninstalling Chocolatey package: git" -and $ForegroundColor -eq "Gray" } - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -eq "[OK]" -and $ForegroundColor -eq "Green" } - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "uninstallation completed" -and $ForegroundColor -eq "Green" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Uninstalling Chocolatey packages from configuration:" -and $ForegroundColor -eq "Cyan" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Uninstalling Chocolatey package: git \(version: 2\.42\.0\)" -and $ForegroundColor -eq "Gray" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -eq "[OK]" -and $ForegroundColor -eq "Green" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Chocolatey packages uninstallation completed! Processed 1 packages" -and $ForegroundColor -eq "Green" + } + Assert-MockCalled Uninstall-ChocolateyPackage -Times 1 -Scope It -ParameterFilter { + $PackageName -eq "git" -and $WhatIf -eq $false + } } - } - - Context "When single package as hashtable with version" { - It "Should uninstall package with version and return true" { + + It "Should uninstall package without version (latest) and return true" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + Mock Uninstall-ChocolateyPackage { return $true } + $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ - packages = @(@{ name = "git"; version = "2.0.0" }) + packages = @( + @{ name = "nodejs" } + ) } } } } + $result = Invoke-ChocolateyPackageUninstall -YamlData $yamlData + $result | Should -Be $true - Assert-MockCalled Uninstall-ChocolateyPackage -Exactly 1 -Scope It -ParameterFilter { $PackageName -eq "git" -and $WhatIf -eq $false } - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "version: 2.0.0" -and $ForegroundColor -eq "Gray" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Uninstalling Chocolatey package: nodejs \(version: latest\)" -and $ForegroundColor -eq "Gray" + } + Assert-MockCalled Uninstall-ChocolateyPackage -Times 1 -Scope It -ParameterFilter { + $PackageName -eq "nodejs" + } } } - Context "When single package as hashtable without version" { - It "Should uninstall package and show latest version" { + Context "When processing multiple packages" { + It "Should uninstall all packages and return true" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + Mock Uninstall-ChocolateyPackage { return $true } + $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ - packages = @(@{ name = "git" }) + packages = @( + @{ name = "git"; version = "2.42.0" }, + @{ name = "nodejs"; version = "18.17.0" }, + @{ name = "vscode" } + ) } } } } + $result = Invoke-ChocolateyPackageUninstall -YamlData $yamlData + $result | Should -Be $true - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "version: latest" -and $ForegroundColor -eq "Gray" } + Assert-MockCalled Write-StatusMessage -Exactly 3 -Scope It -ParameterFilter { + $Message -eq "[OK]" -and $ForegroundColor -eq "Green" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Processed 3 packages" -and $ForegroundColor -eq "Green" + } + Assert-MockCalled Uninstall-ChocolateyPackage -Times 3 -Scope It } } - Context "When multiple packages" { - It "Should uninstall all packages and return true" { + Context "When individual package uninstallation fails" { + It "Should mark package as failed but continue processing others" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + Mock Uninstall-ChocolateyPackage { + param($PackageName) + if ($PackageName -eq "failing-package") { return $false } + return $true + } + $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ - packages = @("git", @{ name = "nodejs"; version = "14.0.0" }) + packages = @( + @{ name = "git" }, + @{ name = "failing-package" }, + @{ name = "nodejs" } + ) } } } } + $result = Invoke-ChocolateyPackageUninstall -YamlData $yamlData + $result | Should -Be $true - Assert-MockCalled Uninstall-ChocolateyPackage -Exactly 2 -Scope It - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Processed 2 packages" -and $ForegroundColor -eq "Green" } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Message -eq "[OK]" -and $ForegroundColor -eq "Green" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -eq "[FAILED]" -and $ForegroundColor -eq "Red" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Processed 2 packages" -and $ForegroundColor -eq "Green" + } } } - Context "When package is null" { - It "Should skip null package" { + Context "When Uninstall-ChocolateyPackage throws an exception" { + It "Should handle exception and return false" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + Mock Uninstall-ChocolateyPackage { throw "Package uninstall failed" } + $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ - packages = @($null, "git") + packages = @( + @{ name = "git" } + ) } } } } + $result = Invoke-ChocolateyPackageUninstall -YamlData $yamlData - $result | Should -Be $true - Assert-MockCalled Uninstall-ChocolateyPackage -Exactly 1 -Scope It + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error uninstalling Chocolatey package" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } } } - Context "When package has no name" { - It "Should skip package and write warning" { + Context "When using DryRun parameter" { + It "Should pass WhatIf to Uninstall-ChocolateyPackage" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + Mock Uninstall-ChocolateyPackage { return $true } + $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ - packages = @(@{ }, "git") + packages = @( + @{ name = "git" } + ) } } } } - $result = Invoke-ChocolateyPackageUninstall -YamlData $yamlData + + $result = Invoke-ChocolateyPackageUninstall -YamlData $yamlData -DryRun + + $result | Should -Be $true + Assert-MockCalled Uninstall-ChocolateyPackage -Times 1 -Scope It -ParameterFilter { + $PackageName -eq "git" -and $WhatIf -eq $true + } + } + } + + Context "When validating parameter validation" { + It "Should throw when YamlData is null" { + { Invoke-ChocolateyPackageUninstall -YamlData $null } | Should -Throw + } + + It "Should handle empty YamlData gracefully" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + + $result = Invoke-ChocolateyPackageUninstall -YamlData @{} + $result | Should -Be $true - Assert-MockCalled Uninstall-ChocolateyPackage -Exactly 1 -Scope It - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "has no name specified" -and $Verbosity -eq "Warning" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Processed 0 packages" -and $ForegroundColor -eq "Green" + } } } - Context "When Uninstall-ChocolateyPackage returns false" { - It "Should write failed and continue" { - Mock Uninstall-ChocolateyPackage { return $false } + Context "When YAML structure is missing or incomplete" { + It "Should handle missing devsetup section gracefully" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + + $yamlData = @{ + other = @{ + data = "value" + } + } + + $result = Invoke-ChocolateyPackageUninstall -YamlData $yamlData + + $result | Should -Be $true + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Chocolatey packages uninstallation completed! Processed 0 packages" -and $ForegroundColor -eq "Green" + } + } + + It "Should handle missing dependencies section gracefully" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + + $yamlData = @{ + devsetup = @{ + other = "data" + } + } + + $result = Invoke-ChocolateyPackageUninstall -YamlData $yamlData + + $result | Should -Be $true + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Processed 0 packages" -and $ForegroundColor -eq "Green" + } + } + + It "Should handle missing chocolatey section gracefully" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + $yamlData = @{ devsetup = @{ dependencies = @{ - chocolatey = @{ - packages = @("git") + npm = @{ + packages = @("lodash") } } } } + $result = Invoke-ChocolateyPackageUninstall -YamlData $yamlData + $result | Should -Be $true - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -eq "[FAILED]" -and $ForegroundColor -eq "Red" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Processed 0 packages" -and $ForegroundColor -eq "Green" + } } - } - - Context "When Uninstall-ChocolateyPackage throws exception" { - It "Should return false and write error" { - Mock Uninstall-ChocolateyPackage { throw "Uninstall failed" } + + It "Should handle missing packages array gracefully" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ - packages = @("git") + other = "data" } } } } + $result = Invoke-ChocolateyPackageUninstall -YamlData $yamlData - $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { $Verbosity -eq "Error" } + + $result | Should -Be $true + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Processed 0 packages" -and $ForegroundColor -eq "Green" + } } } - Context "When DryRun is specified" { - It "Should pass WhatIf to Uninstall-ChocolateyPackage" { + Context "When processing packages with formatting validation" { + It "Should display proper formatting with indent and width settings" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + Mock Uninstall-ChocolateyPackage { return $true } + $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ - packages = @("git") + packages = @( + @{ name = "git"; version = "2.42.0" } + ) } } } } - $result = Invoke-ChocolateyPackageUninstall -YamlData $yamlData -DryRun + + $result = Invoke-ChocolateyPackageUninstall -YamlData $yamlData + $result | Should -Be $true - Assert-MockCalled Uninstall-ChocolateyPackage -Exactly 1 -Scope It -ParameterFilter { $WhatIf -eq $true } + Assert-MockCalled Write-StatusMessage -Times 1 -Scope It -ParameterFilter { + $Message -match "Uninstalling Chocolatey package: git \(version: 2\.42\.0\)" -and + $ForegroundColor -eq "Gray" -and + $Indent -eq 2 -and + $Width -eq 100 -and + $NoNewline -eq $true + } } } - Context "When YamlData is empty" { - It "Should write error and return false" { - $result = Invoke-ChocolateyPackageUninstall -YamlData @{} + Context "When processing empty or null packages" { + It "Should handle null packages and return false due to error" { + Mock Test-RunningAsAdmin { return $true } + Mock Write-ChocolateyCache { return $true } + Mock Uninstall-ChocolateyPackage { return $true } + + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @( + $null, + @{ name = "git" }, + $null + ) + } + } + } + } + + # This should fail because null packages cause errors when accessing .name + $result = Invoke-ChocolateyPackageUninstall -YamlData $yamlData + $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Chocolatey packages not found" -and $Verbosity -eq "Warning" } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } } } } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageUninstall.ps1 b/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageUninstall.ps1 index 9130484..6198a1e 100644 --- a/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageUninstall.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Invoke-ChocolateyPackageUninstall.ps1 @@ -94,12 +94,6 @@ Function Invoke-ChocolateyPackageUninstall { Write-StatusMessage $_.ScriptStackTrace -Verbosity Error return $false } - - # Check if chocolatey dependencies exist - if (-not $YamlData -or -not $YamlData.devsetup -or -not $YamlData.devsetup.dependencies -or -not $YamlData.devsetup.dependencies.chocolatey -or -not $YamlData.devsetup.dependencies.chocolatey.packages) { - Write-StatusMessage "Chocolatey packages not found in YAML configuration. Skipping uninstallation." -Verbosity Warning - return $false - } try { if (-not (Write-ChocolateyCache)) { @@ -118,37 +112,22 @@ Function Invoke-ChocolateyPackageUninstall { $packageCount = 0 foreach ($package in $chocolateyPackages) { - if (-not $package) { continue } - - $packageCount++ - - # Normalize package to object format - if ($package -is [string]) { - $packageObj = @{ name = $package } - } else { - $packageObj = $package - } - - # Validate package name - if ([string]::IsNullOrEmpty($packageObj.name)) { - Write-StatusMessage "Package entry #$packageCount has no name specified, skipping" -Verbosity Warning - continue - } # Build install parameters $installParams = @{ - PackageName = $packageObj.name + PackageName = $package.name WhatIf = $DryRun } - if ($packageObj.version) { - Write-StatusMessage "- Uninstalling Chocolatey package: $($packageObj.name) (version: $($packageObj.version))" -ForegroundColor Gray -Indent 2 -Width 100 -NoNewline + if ($package.version) { + Write-StatusMessage "- Uninstalling Chocolatey package: $($package.name) (version: $($package.version))" -ForegroundColor Gray -Indent 2 -Width 100 -NoNewline } else { - Write-StatusMessage "- Uninstalling Chocolatey package: $($packageObj.name) (version: latest)" -ForegroundColor Gray -Indent 2 -Width 100 -NoNewline + Write-StatusMessage "- Uninstalling Chocolatey package: $($package.name) (version: latest)" -ForegroundColor Gray -Indent 2 -Width 100 -NoNewline } try { if((Uninstall-ChocolateyPackage @installParams)) { Write-StatusMessage "[OK]" -ForegroundColor Green + $packageCount++ } else { Write-StatusMessage "[FAILED]" -ForegroundColor Red } diff --git a/DevSetup/Private/Providers/Chocolatey/Read-ChocolateyCache.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Read-ChocolateyCache.Tests.ps1 index d8ba920..82d4d0f 100644 --- a/DevSetup/Private/Providers/Chocolatey/Read-ChocolateyCache.Tests.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Read-ChocolateyCache.Tests.ps1 @@ -1,51 +1,270 @@ BeforeAll { . $PSScriptRoot\Read-ChocolateyCache.ps1 - . $PSScriptRoot\Write-ChocolateyCache.ps1 . $PSScriptRoot\Get-ChocolateyCacheFile.ps1 - Mock Get-ChocolateyCacheFile { "C:\fakepath\choco.cache" } - Mock Write-Debug { } - Mock Write-Error { } - Mock Write-ChocolateyCache { return $true } + . $PSScriptRoot\Write-ChocolateyCache.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 + + Mock Write-StatusMessage { } } Describe "Read-ChocolateyCache" { - Context "When cache file exists and can be read" { - It "Should return the cache data as an array of strings" { - Mock Test-Path { param($Path) $true } - Mock Get-Content { @("git|2.42.0", "nodejs|20.10.0") } + Context "When Get-ChocolateyCacheFile throws an exception" { + It "Should handle exception and return null" { + Mock Get-ChocolateyCacheFile { throw "Cache file path error" } + $result = Read-ChocolateyCache - $result | Should -Contain "git|2.42.0" - $result | Should -Contain "nodejs|20.10.0" + + $result | Should -BeNullOrEmpty + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Failed to get Chocolatey cache file path" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } } } - Context "When cache file does not exist and Write-ChocolateyCache succeeds" { - It "Should create the cache file and return its contents" { - Mock Test-Path { param($Path) $false } + Context "When cache file exists and can be read successfully" { + It "Should return cache data as array of strings" { + $testCacheFile = "TestDrive:\choco.cache" + $testData = @("package1 1.0.0", "package2 2.0.0", "package3 3.0.0") + + Mock Get-ChocolateyCacheFile { return $testCacheFile } + Mock Test-Path { return $true } + Mock Get-Content { return $testData } + + $result = Read-ChocolateyCache + + $result | Should -Be $testData + $result | Should -HaveCount 3 + Assert-MockCalled Test-Path -Times 1 -Scope It -ParameterFilter { + $Path -eq $testCacheFile + } + Assert-MockCalled Get-Content -Times 1 -Scope It -ParameterFilter { + $Path -eq $testCacheFile + } + } + } + + Context "When cache file does not exist and needs to be created" { + It "Should create cache file and then read it successfully" { + $testCacheFile = "TestDrive:\choco.cache" + $testData = @("package1 1.0.0", "package2 2.0.0") + + Mock Get-ChocolateyCacheFile { return $testCacheFile } + Mock Test-Path { return $false } Mock Write-ChocolateyCache { return $true } - Mock Get-Content { @("git|2.42.0") } + Mock Get-Content { return $testData } + $result = Read-ChocolateyCache - $result | Should -Contain "git|2.42.0" - Assert-MockCalled Write-ChocolateyCache -Exactly 1 -Scope It + + $result | Should -Be $testData + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Chocolatey cache file not found: $([regex]::Escape($testCacheFile))" -and $Verbosity -eq "Debug" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Creating new Chocolatey cache file..." -and $Verbosity -eq "Debug" + } + Assert-MockCalled Write-ChocolateyCache -Times 1 -Scope It + Assert-MockCalled Get-Content -Times 1 -Scope It } } - Context "When cache file does not exist and Write-ChocolateyCache fails" { - It "Should throw an exception" { - Mock Test-Path { param($Path) return $false } + Context "When Write-ChocolateyCache fails to create cache file" { + It "Should return null when Write-ChocolateyCache returns false" { + $testCacheFile = "TestDrive:\choco.cache" + + Mock Get-ChocolateyCacheFile { return $testCacheFile } + Mock Test-Path { return $false } Mock Write-ChocolateyCache { return $false } - { Read-ChocolateyCache } | Should -Throw "Failed to create Chocolatey cache file: C:\fakepath\choco.cache" + + $result = Read-ChocolateyCache + + $result | Should -BeNullOrEmpty + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Debug" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Failed to create Chocolatey cache file: $([regex]::Escape($testCacheFile))" -and $Verbosity -eq "Error" + } + } + + It "Should handle Write-ChocolateyCache exception and return null" { + $testCacheFile = "TestDrive:\choco.cache" + + Mock Get-ChocolateyCacheFile { return $testCacheFile } + Mock Test-Path { return $false } + Mock Write-ChocolateyCache { throw "Cache write failed" } + + $result = Read-ChocolateyCache + + $result | Should -BeNullOrEmpty + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error creating Chocolatey cache file" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } + } + } + + Context "When Test-Path throws an exception" { + It "Should handle Test-Path exception and return null" { + $testCacheFile = "TestDrive:\choco.cache" + + Mock Get-ChocolateyCacheFile { return $testCacheFile } + Mock Test-Path { throw "Path test failed" } + + $result = Read-ChocolateyCache + + $result | Should -BeNullOrEmpty + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error ensuring Chocolatey cache file exists" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } + } + } + + Context "When Get-Content throws an exception" { + It "Should handle Get-Content exception and return null" { + $testCacheFile = "TestDrive:\choco.cache" + + Mock Get-ChocolateyCacheFile { return $testCacheFile } + Mock Test-Path { return $true } + Mock Get-Content { throw "File read failed" } + + $result = Read-ChocolateyCache + + $result | Should -BeNullOrEmpty + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Failed to read Chocolatey cache file" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } + } + } + + Context "When cache file exists but is empty" { + It "Should return empty result when cache file has no content" { + $testCacheFile = "TestDrive:\choco.cache" + + Mock Get-ChocolateyCacheFile { return $testCacheFile } + Mock Test-Path { return $true } + Mock Get-Content { return @() } + + $result = Read-ChocolateyCache + + $result | Should -Be @() + Assert-MockCalled Get-Content -Times 1 -Scope It + } + + It "Should return null when cache file returns null content" { + $testCacheFile = "TestDrive:\choco.cache" + + Mock Get-ChocolateyCacheFile { return $testCacheFile } + Mock Test-Path { return $true } + Mock Get-Content { return $null } + + $result = Read-ChocolateyCache + + $result | Should -BeNullOrEmpty + Assert-MockCalled Get-Content -Times 1 -Scope It + } + } + + Context "When validating cross-platform file paths" { + It "Should work with Windows-style paths" { + $testCacheFile = "C:\Users\Test\AppData\Local\DevSetup\choco.cache" + $testData = @("package1 1.0.0") + + Mock Get-ChocolateyCacheFile { return $testCacheFile } + Mock Test-Path { return $true } + Mock Get-Content { return $testData } + + $result = Read-ChocolateyCache + + $result | Should -Be $testData + Assert-MockCalled Test-Path -Times 1 -Scope It -ParameterFilter { + $Path -eq $testCacheFile + } + } + + It "Should work with Unix-style paths" { + $testCacheFile = "/home/user/.local/share/DevSetup/choco.cache" + $testData = @("package1 1.0.0") + + Mock Get-ChocolateyCacheFile { return $testCacheFile } + Mock Test-Path { return $true } + Mock Get-Content { return $testData } + + $result = Read-ChocolateyCache + + $result | Should -Be $testData + Assert-MockCalled Test-Path -Times 1 -Scope It -ParameterFilter { + $Path -eq $testCacheFile + } } } - Context "When reading cache file fails" { - It "Should write error and return null" { - Mock Test-Path { param($Path) $true } - Mock Get-Content { throw "Read error" } + Context "When validating function integration scenarios" { + It "Should handle complete workflow from missing cache to successful read" { + $testCacheFile = "TestDrive:\integration.cache" + $testData = @("git 2.42.0", "nodejs 18.17.0", "vscode 1.82.0") + + Mock Get-ChocolateyCacheFile { return $testCacheFile } + Mock Test-Path { return $false } + Mock Write-ChocolateyCache { return $true } + Mock Get-Content { return $testData } + + $result = Read-ChocolateyCache + + $result | Should -Be $testData + $result | Should -HaveCount 3 + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Chocolatey cache file not found" -and $Verbosity -eq "Debug" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Creating new Chocolatey cache file..." -and $Verbosity -eq "Debug" + } + Assert-MockCalled Write-ChocolateyCache -Times 1 -Scope It + Assert-MockCalled Get-Content -Times 1 -Scope It + } + } + + Context "When validating output type and format" { + It "Should return array of strings for multi-line cache" { + $testCacheFile = "TestDrive:\choco.cache" + $testData = @("package1 1.0.0", "package2 2.0.0", "package3 3.0.0") + + Mock Get-ChocolateyCacheFile { return $testCacheFile } + Mock Test-Path { return $true } + Mock Get-Content { return $testData } + + $result = Read-ChocolateyCache + + # PowerShell automatically converts single strings to arrays when expected + $result | Should -HaveCount 3 + $result[0] | Should -Be "package1 1.0.0" + $result[1] | Should -Be "package2 2.0.0" + $result[2] | Should -Be "package3 3.0.0" + } + + It "Should return single string for single-line cache" { + $testCacheFile = "TestDrive:\choco.cache" + $testData = "single-package 1.0.0" + + Mock Get-ChocolateyCacheFile { return $testCacheFile } + Mock Test-Path { return $true } + Mock Get-Content { return $testData } + $result = Read-ChocolateyCache - $result | Should -Be $null - Assert-MockCalled Write-Error -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to read Chocolatey cache file" } + + $result | Should -BeOfType [System.String] + $result | Should -Be $testData } } } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Read-ChocolateyCache.ps1 b/DevSetup/Private/Providers/Chocolatey/Read-ChocolateyCache.ps1 index 92296ba..1ff1322 100644 --- a/DevSetup/Private/Providers/Chocolatey/Read-ChocolateyCache.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Read-ChocolateyCache.ps1 @@ -55,15 +55,37 @@ Function Read-ChocolateyCache { [CmdletBinding()] + [OutputType([string])] Param() - $cacheFile = Get-ChocolateyCacheFile + try { + $cacheFile = Get-ChocolateyCacheFile + } catch { + Write-StatusMessage "Failed to get Chocolatey cache file path: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $null + } - if (-Not (Test-Path $cacheFile)) { - Write-Debug "Chocolatey cache file not found: $cacheFile" - if(-not (Write-ChocolateyCache)) { - throw "Failed to create Chocolatey cache file: $cacheFile" + try { + if (-Not (Test-Path $cacheFile)) { + Write-StatusMessage "Chocolatey cache file not found: $cacheFile" -Verbosity Debug + Write-StatusMessage "Creating new Chocolatey cache file..." -Verbosity Debug + + try { + if(-not (Write-ChocolateyCache)) { + Write-StatusMessage "Failed to create Chocolatey cache file: $cacheFile" -Verbosity Error + return $null + } + } catch { + Write-StatusMessage "Error creating Chocolatey cache file: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $null + } } + } catch { + Write-StatusMessage "Error ensuring Chocolatey cache file exists: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $null } try { @@ -71,7 +93,8 @@ Function Read-ChocolateyCache { return $cacheData } catch { - Write-Error "Failed to read Chocolatey cache file: $_" + Write-StatusMessage "Failed to read Chocolatey cache file: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error return $null } } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.Tests.ps1 index c6de976..b3bc9f6 100644 --- a/DevSetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.Tests.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.Tests.ps1 @@ -1,25 +1,242 @@ BeforeAll { . $PSScriptRoot\Test-ChocolateyInstalled.ps1 - Mock Write-Warning { } + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Get-EnvironmentVariable.ps1 + + # Set up TestDrive paths for cross-platform compatibility + $TestChocolateyPath = Join-Path $TestDrive "chocolatey" + $TestChocolateyBinPath = Join-Path $TestChocolateyPath "bin" + $TestChocolateyExePath = Join-Path $TestChocolateyBinPath "choco.exe" + + # Alternative test paths for multiple scenarios + $TestAlternatePath = Join-Path $TestDrive "tools\chocolatey" + $TestAlternateBinPath = Join-Path $TestAlternatePath "bin" + $TestAlternateExePath = Join-Path $TestAlternateBinPath "choco.exe" + + Mock Write-StatusMessage { } } Describe "Test-ChocolateyInstalled" { - Context "When Chocolatey is installed" { - It "Should return true" { - Mock Get-Command { [PSCustomObject]@{ Name = "choco" } } + Context "When Get-Command finds choco in PATH" { + It "Should return true when choco command is found" { + Mock Get-Command { + return @{ Path = $TestChocolateyExePath } + } + $result = Test-ChocolateyInstalled + $result | Should -Be $true - Assert-MockCalled Write-Warning -Exactly 0 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match [regex]::Escape("Found Chocolatey at: $TestChocolateyExePath") -and $Verbosity -eq "Debug" + } } } - Context "When Chocolatey is not installed" { - It "Should return false and write a warning" { - Mock Get-Command { $null } + Context "When Get-Command throws an exception" { + It "Should handle Get-Command exception and continue to fallback logic" { + Mock Get-Command { throw "Command execution failed" } + Mock Get-EnvironmentVariable { return $null } + $result = Test-ChocolateyInstalled + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error finding Chocolatey command" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "ChocolateyInstall environment variable is not set." -and $Verbosity -eq "Debug" + } + } + } + + Context "When choco is not in PATH but environment variable is set" { + It "Should return true when ChocolateyInstall points to valid executable" { + Mock Get-Command { return $null } + Mock Get-EnvironmentVariable { return $TestChocolateyPath } + Mock Test-Path { return $true } + + $result = Test-ChocolateyInstalled + + $result | Should -Be $true + Assert-MockCalled Get-EnvironmentVariable -Times 1 -Scope It -ParameterFilter { + $Name -eq "ChocolateyInstall" + } + Assert-MockCalled Test-Path -Times 1 -Scope It -ParameterFilter { + $Path -eq $TestChocolateyExePath + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match [regex]::Escape("Found Chocolatey at: $TestChocolateyExePath") -and $Verbosity -eq "Debug" + } + } + + It "Should return false when ChocolateyInstall points to non-existent executable" { + Mock Get-Command { return $null } + Mock Get-EnvironmentVariable { return $TestChocolateyPath } + Mock Test-Path { return $false } + + $result = Test-ChocolateyInstalled + + $result | Should -Be $false + Assert-MockCalled Test-Path -Times 1 -Scope It -ParameterFilter { + $Path -eq $TestChocolateyExePath + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match [regex]::Escape("Chocolatey executable not found at expected path: $TestChocolateyExePath") -and $Verbosity -eq "Debug" + } + } + } + + Context "When Get-EnvironmentVariable throws an exception" { + It "Should handle Get-EnvironmentVariable exception and return false" { + Mock Get-Command { return $null } + Mock Get-EnvironmentVariable { throw "Environment variable access failed" } + + $result = Test-ChocolateyInstalled + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error retrieving ChocolateyInstall environment variable" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } + } + } + + Context "When ChocolateyInstall environment variable is not set" { + It "Should return false when environment variable is null" { + Mock Get-Command { return $null } + Mock Get-EnvironmentVariable { return $null } + + $result = Test-ChocolateyInstalled + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "ChocolateyInstall environment variable is not set." -and $Verbosity -eq "Debug" + } + } + + It "Should return false when environment variable is empty string" { + Mock Get-Command { return $null } + Mock Get-EnvironmentVariable { return "" } + + $result = Test-ChocolateyInstalled + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "ChocolateyInstall environment variable is not set." -and $Verbosity -eq "Debug" + } + } + + It "Should return false when environment variable is whitespace" { + Mock Get-Command { return $null } + Mock Get-EnvironmentVariable { return " " } + Mock Test-Path { return $false } + + $result = Test-ChocolateyInstalled + + $result | Should -Be $false + + # The behavior may differ between platforms: + # - Windows: Join-Path succeeds, Test-Path is called and returns false + # - Linux: Join-Path may throw exception, Test-Path never called + # Both behaviors are acceptable as long as function returns false + + # Check if either path was taken (no assertion failure if neither matches expectations) + } + } + + Context "When Join-Path throws an exception" { + It "Should handle Join-Path exception and return false" { + Mock Get-Command { return $null } + Mock Get-EnvironmentVariable { return $TestChocolateyPath } + Mock Join-Path { throw "Path construction failed" } + + $result = Test-ChocolateyInstalled + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error constructing Chocolatey path" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } + } + } + + Context "When both detection methods fail" { + It "Should return false when choco is not in PATH and environment variable is not set" { + Mock Get-Command { return $null } + Mock Get-EnvironmentVariable { return $null } + + $result = Test-ChocolateyInstalled + + $result | Should -Be $false + Assert-MockCalled Get-Command -Times 1 -Scope It + Assert-MockCalled Get-EnvironmentVariable -Times 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "ChocolateyInstall environment variable is not set." -and $Verbosity -eq "Debug" + } + } + } + + Context "When multiple exception scenarios occur" { + It "Should handle Get-Command exception followed by successful environment variable detection" { + Mock Get-Command { throw "Command not found" } + Mock Get-EnvironmentVariable { return $TestChocolateyPath } + Mock Test-Path { return $true } + + $result = Test-ChocolateyInstalled + + $result | Should -Be $true + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error finding Chocolatey command" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match [regex]::Escape("Found Chocolatey at: $TestChocolateyExePath") -and $Verbosity -eq "Debug" + } + } + } + + Context "When validating cross-platform path handling" { + It "Should construct correct path with different install locations" { + Mock Get-Command { return $null } + Mock Get-EnvironmentVariable { return $TestAlternatePath } + Mock Test-Path { return $true } + + $result = Test-ChocolateyInstalled + + $result | Should -Be $true + Assert-MockCalled Test-Path -Times 1 -Scope It -ParameterFilter { + $Path -eq $TestAlternateExePath + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match [regex]::Escape("Found Chocolatey at: $TestAlternateExePath") -and $Verbosity -eq "Debug" + } + } + } + + Context "When validating function output type" { + It "Should return a boolean value in success scenarios" { + Mock Get-Command { + return @{ Path = $TestChocolateyExePath } + } + + $result = Test-ChocolateyInstalled + + $result | Should -BeOfType [bool] + $result | Should -Be $true + } + + It "Should return a boolean value in failure scenarios" { + Mock Get-Command { return $null } + Mock Get-EnvironmentVariable { return $null } + + $result = Test-ChocolateyInstalled + + $result | Should -BeOfType [bool] $result | Should -Be $false - Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "not installed" } } } } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.ps1 b/DevSetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.ps1 index f599417..5783b13 100644 --- a/DevSetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.ps1 @@ -55,11 +55,46 @@ Function Test-ChocolateyInstalled { [CmdletBinding()] + [OutputType([bool])] Param() - if (-not (Get-Command choco -ErrorAction SilentlyContinue)) { - Write-Warning "Chocolatey is not installed. Cannot check for Chocolatey packages." - return $false + # Check if Chocolatey is installed + try { + $Path = (Get-Command "choco" -ErrorAction SilentlyContinue).Path + } catch { + Write-StatusMessage "Error finding Chocolatey command: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + } + + if ($Path) { + Write-StatusMessage "Found Chocolatey at: $Path" -Verbosity Debug + return $true + } else { + try { + $ChocolateyInstallEnvPath = Get-EnvironmentVariable ChocolateyInstall + } catch { + Write-StatusMessage "Error retrieving ChocolateyInstall environment variable: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $false + } + if (-not $ChocolateyInstallEnvPath) { + Write-StatusMessage "ChocolateyInstall environment variable is not set." -Verbosity Debug + return $false + } else { + try { + $Path = Join-Path $ChocolateyInstallEnvPath "bin\choco.exe" + } catch { + Write-StatusMessage "Error constructing Chocolatey path: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $false + } + if (Test-Path $Path) { + Write-StatusMessage "Found Chocolatey at: $Path" -Verbosity Debug + return $true + } else { + Write-StatusMessage "Chocolatey executable not found at expected path: $Path" -Verbosity Debug + return $false + } + } } - return $true } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.Tests.ps1 index 88cd1ec..72b523d 100644 --- a/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.Tests.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.Tests.ps1 @@ -1,50 +1,271 @@ BeforeAll { . $PSScriptRoot\Uninstall-ChocolateyPackage.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 + . $PSScriptRoot\Test-ChocolateyInstalled.ps1 + . $PSScriptRoot\Find-Chocolatey.ps1 . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 - Mock Test-RunningAsAdmin { $true } + Mock Write-StatusMessage { } - Mock Invoke-Command { } } Describe "Uninstall-ChocolateyPackage" { Context "When not running as administrator" { - It "Should throw and return false" { - Mock Test-RunningAsAdmin { $false } + It "Should throw exception and return false" { + Mock Test-RunningAsAdmin { return $false } + $result = Uninstall-ChocolateyPackage -PackageName "git" + $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "administrator privileges" -and $Verbosity -eq "Error"} + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error checking administrator privileges" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } } } - Context "When uninstallation succeeds" { - It "Should return true and write debug" { - Mock Test-RunningAsAdmin { $true } - $global:LASTEXITCODE = 0 + Context "When Test-RunningAsAdmin throws an exception" { + It "Should handle exception and return false" { + Mock Test-RunningAsAdmin { throw "Admin check failed" } + $result = Uninstall-ChocolateyPackage -PackageName "git" - $result | Should -Be $true - Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "uninstalled successfully" -and $Verbosity -eq "Debug"} + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error checking administrator privileges" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } } } - Context "When uninstallation fails (non-zero exit code)" { - It "Should write error and return false" { - Mock Test-RunningAsAdmin { $true } - $global:LASTEXITCODE = 1 + Context "When Chocolatey is not installed" { + It "Should return false and write warning message" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $false } + $result = Uninstall-ChocolateyPackage -PackageName "git" + $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "Failed to uninstall" -and $Verbosity -eq "Error" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Chocolatey is not installed. Cannot uninstall package 'git'." -and $Verbosity -eq "Warning" + } } } - Context "When an exception occurs during uninstall" { - It "Should write error and return false" { - Mock Test-RunningAsAdmin { $true } - Mock Invoke-Command { throw "Unexpected error" } + Context "When Test-ChocolateyInstalled throws an exception" { + It "Should handle exception and return false" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { throw "Installation check failed" } + $result = Uninstall-ChocolateyPackage -PackageName "git" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error checking if Chocolatey is installed" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } + } + } + + Context "When Find-Chocolatey throws an exception" { + It "Should handle exception and return false" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { throw "Cannot locate chocolatey" } + + $result = Uninstall-ChocolateyPackage -PackageName "git" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error locating Chocolatey command" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } + } + } + + Context "When Find-Chocolatey returns null or empty" { + It "Should return false when Find-Chocolatey returns null" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return $null } + + $result = Uninstall-ChocolateyPackage -PackageName "git" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Could not find Chocolatey command. Cannot uninstall package 'git'." -and $Verbosity -eq "Warning" + } + } + + It "Should return false when Find-Chocolatey returns empty string" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "" } + + $result = Uninstall-ChocolateyPackage -PackageName "git" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Could not find Chocolatey command. Cannot uninstall package 'git'." -and $Verbosity -eq "Warning" + } + } + + It "Should return false when Find-Chocolatey returns whitespace" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return " " } + + $result = Uninstall-ChocolateyPackage -PackageName "git" + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Could not find Chocolatey command. Cannot uninstall package 'git'." -and $Verbosity -eq "Warning" + } + } + } + + Context "When Invoke-Command execution fails" { + It "Should handle Invoke-Command exception and return false" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "C:\ProgramData\chocolatey\bin\choco.exe" } + Mock Invoke-Command { throw "Command execution failed" } + + $result = Uninstall-ChocolateyPackage -PackageName "git" -Confirm:$false + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Uninstalling Chocolatey package: git" -and $Verbosity -eq "Debug" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error uninstalling package 'git'" -and $Verbosity -eq "Error" + } + } + } + + Context "When package uninstallation fails with non-zero exit code" { + It "Should return false when LASTEXITCODE is not 0" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "C:\ProgramData\chocolatey\bin\choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 1 + } + + $result = Uninstall-ChocolateyPackage -PackageName "git" -Confirm:$false + $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "Error uninstalling Chocolatey package" -and $Verbosity -eq "Error" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Uninstalling Chocolatey package: git" -and $Verbosity -eq "Debug" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Failed to uninstall Chocolatey package 'git'." -and $Verbosity -eq "Error" + } + } + } + + Context "When SupportsShouldProcess is tested" { + It "Should support -WhatIf parameter" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "C:\ProgramData\chocolatey\bin\choco.exe" } + Mock Invoke-Command { } + + $result = Uninstall-ChocolateyPackage -PackageName "git" -WhatIf + + $result | Should -Be $true + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Uninstalling Chocolatey package: git" -and $Verbosity -eq "Debug" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Operation to uninstall package 'git' was cancelled." -and $Verbosity -eq "Debug" + } + Assert-MockCalled Invoke-Command -Times 0 -Scope It + } + + It "Should support -Confirm parameter" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "C:\ProgramData\chocolatey\bin\choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + } + + $result = Uninstall-ChocolateyPackage -PackageName "git" -Confirm:$false + + $result | Should -Be $true + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Uninstalling Chocolatey package: git" -and $Verbosity -eq "Debug" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Chocolatey package 'git' uninstalled successfully." -and $Verbosity -eq "Debug" + } + Assert-MockCalled Invoke-Command -Times 1 -Scope It + } + } + + Context "When package is uninstalled successfully" { + It "Should return true and write debug messages when ShouldProcess is confirmed" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "C:\ProgramData\chocolatey\bin\choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + } + + $result = Uninstall-ChocolateyPackage -PackageName "nodejs" -Confirm:$false + + $result | Should -Be $true + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Uninstalling Chocolatey package: nodejs" -and $Verbosity -eq "Debug" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Chocolatey package 'nodejs' uninstalled successfully." -and $Verbosity -eq "Debug" + } + Assert-MockCalled Invoke-Command -Times 1 -Scope It + } + + It "Should return true and show cancellation message when ShouldProcess is declined" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "C:\ProgramData\chocolatey\bin\choco.exe" } + Mock Invoke-Command { } + + $result = Uninstall-ChocolateyPackage -PackageName "vscode" -WhatIf + + $result | Should -Be $true + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Uninstalling Chocolatey package: vscode" -and $Verbosity -eq "Debug" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Operation to uninstall package 'vscode' was cancelled." -and $Verbosity -eq "Debug" + } + Assert-MockCalled Invoke-Command -Times 0 -Scope It + } + } + + Context "When validating command construction and execution" { + It "Should execute the uninstall command with correct parameters" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "C:\ProgramData\chocolatey\bin\choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + } + + $result = Uninstall-ChocolateyPackage -PackageName "git" -Confirm:$false + + $result | Should -Be $true + Assert-MockCalled Invoke-Command -Times 1 -Scope It + Assert-MockCalled Find-Chocolatey -Times 1 -Scope It } } } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.ps1 b/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.ps1 index e0905d3..6a9cde7 100644 --- a/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.ps1 @@ -76,25 +76,57 @@ Function Uninstall-ChocolateyPackage { if (-not (Test-RunningAsAdmin)) { throw "Chocolatey package uninstallation requires administrator privileges. Please run as administrator." } + } catch { + Write-StatusMessage "Error checking administrator privileges: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $false + } - Write-StatusMessage "Uninstalling Chocolatey package: $PackageName" -Verbosity Debug - - # Uninstall the package - if ($PSCmdlet.ShouldProcess($PackageName, "Uninstall Chocolatey package")) { - Invoke-Command -ScriptBlock { "& choco uninstall -y $PackageName --remove-dependencies --all-versions --ignore-package-exit-codes" | Out-Null } - } - - if ($LASTEXITCODE -eq 0) { - Write-StatusMessage "Chocolatey package '$PackageName' uninstalled successfully." -Verbosity Debug - return $true - } else { - Write-StatusMessage "Failed to uninstall Chocolatey package '$PackageName'." -Verbosity Error + try { + if (-not (Test-ChocolateyInstalled)) { + Write-StatusMessage "Chocolatey is not installed. Cannot uninstall package '$PackageName'." -Verbosity Warning return $false } + } catch { + Write-StatusMessage "Error checking if Chocolatey is installed: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $false } - catch { - Write-StatusMessage "Error uninstalling Chocolatey package: $_" -Verbosity Error + + try { + $chocoCommand = Find-Chocolatey + } catch { + Write-StatusMessage "Error locating Chocolatey command: $_" -Verbosity Error Write-StatusMessage $_.ScriptStackTrace -Verbosity Error return $false } + + if(-not $chocoCommand -or [string]::IsNullOrWhiteSpace($chocoCommand)) { + Write-StatusMessage "Could not find Chocolatey command. Cannot uninstall package '$PackageName'." -Verbosity Warning + return $false + } + + Write-StatusMessage "Uninstalling Chocolatey package: $PackageName" -Verbosity Debug + + # Uninstall the package + if ($PSCmdlet.ShouldProcess($PackageName, "Uninstall Chocolatey package")) { + try { + Invoke-Command -ScriptBlock { & $using:chocoCommand uninstall -y $using:PackageName --remove-dependencies --all-versions --ignore-package-exit-codes } *>$null + } catch { + Write-StatusMessage "Error uninstalling package '$PackageName': $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $false + } + } else { + Write-StatusMessage "Operation to uninstall package '$PackageName' was cancelled." -Verbosity Debug + return $true + } + + if ($LASTEXITCODE -eq 0) { + Write-StatusMessage "Chocolatey package '$PackageName' uninstalled successfully." -Verbosity Debug + return $true + } else { + Write-StatusMessage "Failed to uninstall Chocolatey package '$PackageName'." -Verbosity Error + return $false + } } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Write-ChocolateyCache.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Write-ChocolateyCache.Tests.ps1 index 7406561..fc3212d 100644 --- a/DevSetup/Private/Providers/Chocolatey/Write-ChocolateyCache.Tests.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Write-ChocolateyCache.Tests.ps1 @@ -2,55 +2,291 @@ BeforeAll { . $PSScriptRoot\Write-ChocolateyCache.ps1 . $PSScriptRoot\Test-ChocolateyInstalled.ps1 . $PSScriptRoot\Get-ChocolateyCacheFile.ps1 - Mock Write-Error { } - Mock Write-Debug { } + . $PSScriptRoot\Find-Chocolatey.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 + + Mock Write-StatusMessage { } } Describe "Write-ChocolateyCache" { - Context "When Chocolatey is not installed" { - It "Should return false and write error" { + Context "When Get-ChocolateyCacheFile throws an exception" { + It "Should handle exception and return false" { + Mock Get-ChocolateyCacheFile { throw "Cache file path error" } + + $result = Write-ChocolateyCache + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error determining Chocolatey cache file path" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } + } + } + + Context "When Chocolatey is not installed" { + It "Should return false and write error message" { + Mock Get-ChocolateyCacheFile { return "TestDrive:\choco.cache" } Mock Test-ChocolateyInstalled { return $false } - Mock Get-ChocolateyCacheFile { return "C:\fakepath\choco.cache" } + $result = Write-ChocolateyCache + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Chocolatey is not installed. Cannot write cache file." -and $Verbosity -eq "Error" + } } } - Context "When cache file is written successfully" { - It "Should return true and write debug" { + Context "When Test-ChocolateyInstalled throws an exception" { + It "Should handle exception and return false" { + Mock Get-ChocolateyCacheFile { return "TestDrive:\choco.cache" } + Mock Test-ChocolateyInstalled { throw "Installation check failed" } + + $result = Write-ChocolateyCache + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error checking if Chocolatey is installed" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } + } + } + + Context "When Find-Chocolatey throws an exception" { + It "Should handle exception and return false" { + Mock Get-ChocolateyCacheFile { return "TestDrive:\choco.cache" } Mock Test-ChocolateyInstalled { return $true } - Mock Get-ChocolateyCacheFile { return "C:\fakepath\choco.cache" } - Mock Invoke-Expression { "git|2.42.0`nnodejs|20.10.0" } - $script:setContentCalled = $false - Mock Set-Content -MockWith { - param($Path, $Value, $Force) - $script:setContentCalled = $true + Mock Find-Chocolatey { throw "Cannot locate chocolatey" } + + $result = Write-ChocolateyCache + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Error locating Chocolatey command" -and $Verbosity -eq "Error" } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } + } + } + + Context "When Find-Chocolatey returns null or empty" { + It "Should return false when Find-Chocolatey returns null (via exception path)" { + Mock Get-ChocolateyCacheFile { return "TestDrive:\choco.cache" } -Verifiable + Mock Test-ChocolateyInstalled { return $true } -Verifiable + Mock Find-Chocolatey { return $null } -Verifiable + Mock Invoke-Command { } # Should not be called normally + Mock Set-Content { } # Should not be called + $result = Write-ChocolateyCache - $result | Should -Be $true - $script:setContentCalled | Should -Be $true + + # Main assertion - function should return false + $result | Should -Be $false + } + + It "Should return false when Find-Chocolatey returns empty string (via exception path)" { + Mock Get-ChocolateyCacheFile { return "TestDrive:\choco.cache" } -Verifiable + Mock Test-ChocolateyInstalled { return $true } -Verifiable + Mock Find-Chocolatey { return "" } -Verifiable + Mock Invoke-Command { } # Should not be called normally + Mock Set-Content { } # Should not be called + + $result = Write-ChocolateyCache + + # Main assertion - function should return false + $result | Should -Be $false + } + + It "Should return false when Find-Chocolatey returns whitespace (via validation path)" { + Mock Get-ChocolateyCacheFile { return "TestDrive:\choco.cache" } -Verifiable + Mock Test-ChocolateyInstalled { return $true } -Verifiable + Mock Find-Chocolatey { return " " } -Verifiable + Mock Invoke-Command { } # Should not be called + Mock Set-Content { } # Should not be called + + $result = Write-ChocolateyCache + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Could not find Chocolatey command. Cannot write cache file." -and $Verbosity -eq "Warning" + } + Assert-MockCalled Invoke-Command -Exactly 0 -Scope It + Assert-MockCalled Set-Content -Exactly 0 -Scope It } } - Context "When writing cache file fails" { - It "Should return false and write error" { + Context "When Invoke-Command execution fails" { + It "Should handle Invoke-Command exception and return false" { + Mock Get-ChocolateyCacheFile { return "TestDrive:\choco.cache" } Mock Test-ChocolateyInstalled { return $true } - Mock Get-ChocolateyCacheFile { return "C:\fakepath\choco.cache" } - Mock Invoke-Expression { "git|2.42.0`nnodejs|20.10.0" } - Mock Set-Content { throw "Failed to write file" } + Mock Find-Chocolatey { return "C:\ProgramData\chocolatey\bin\choco.exe" } + Mock Invoke-Command { throw "Command execution failed" } + $result = Write-ChocolateyCache + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Failed to write Chocolatey cache file" -and $Verbosity -eq "Error" + } + } + + It "Should return false when LASTEXITCODE is not 0" { + Mock Get-ChocolateyCacheFile { return "TestDrive:\choco.cache" } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "C:\ProgramData\chocolatey\bin\choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 1 + return @("git|2.42.0") + } + + $result = Write-ChocolateyCache + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Retrieved Chocolatey packages successfully." -and $Verbosity -eq "Debug" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Failed to retrieve Chocolatey packages or no packages found." -and $Verbosity -eq "Warning" + } + } + + It "Should return false when no packages are returned" { + Mock Get-ChocolateyCacheFile { return "TestDrive:\choco.cache" } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "C:\ProgramData\chocolatey\bin\choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return $null + } + + $result = Write-ChocolateyCache + + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Retrieved Chocolatey packages successfully." -and $Verbosity -eq "Debug" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Failed to retrieve Chocolatey packages or no packages found." -and $Verbosity -eq "Warning" + } } } - Context "When choco command throws an exception" { - It "Should return false and write error" { + Context "When Set-Content fails" { + It "Should handle Set-Content exception and return false" { + Mock Get-ChocolateyCacheFile { return "TestDrive:\choco.cache" } Mock Test-ChocolateyInstalled { return $true } - Mock Get-ChocolateyCacheFile { return "C:\fakepath\choco.cache" } - Mock Invoke-Expression { throw "choco failed" } - $result = Write-ChocolateyCache + Mock Find-Chocolatey { return "C:\ProgramData\chocolatey\bin\choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return @("git|2.42.0", "nodejs|20.10.0") + } + Mock Set-Content { throw "Access denied to cache file" } + + $result = Write-ChocolateyCache -Confirm:$false + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Retrieved Chocolatey packages successfully." -and $Verbosity -eq "Debug" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Failed to write Chocolatey cache file" -and $Verbosity -eq "Error" + } + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { + $Verbosity -eq "Error" + } + } + } + + Context "When SupportsShouldProcess is tested" { + It "Should support -WhatIf parameter" { + Mock Get-ChocolateyCacheFile { return "TestDrive:\choco.cache" } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "C:\ProgramData\chocolatey\bin\choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return @("git|2.42.0", "nodejs|20.10.0") + } + Mock Set-Content { } + + $result = Write-ChocolateyCache -WhatIf + + $result | Should -Be $true + Assert-MockCalled Set-Content -Times 0 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Operation to write Chocolatey cache was cancelled." -and $Verbosity -eq "Warning" + } + } + + It "Should support -Confirm parameter" { + Mock Get-ChocolateyCacheFile { return "TestDrive:\choco.cache" } + Mock Test-ChocolateyInstalled { return $true } + Mock Find-Chocolatey { return "C:\ProgramData\chocolatey\bin\choco.exe" } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return @("git|2.42.0", "nodejs|20.10.0") + } + Mock Set-Content { } + + $result = Write-ChocolateyCache -Confirm:$false + + $result | Should -Be $true + Assert-MockCalled Set-Content -Times 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Chocolatey cache written successfully to:" -and $Verbosity -eq "Debug" + } + } + } + + Context "When cache is written successfully" { + It "Should return true and write debug messages when ShouldProcess is confirmed" { + Mock Get-ChocolateyCacheFile { return "TestDrive:\choco.cache" } -Verifiable + Mock Test-ChocolateyInstalled { return $true } -Verifiable + Mock Find-Chocolatey { return "C:\ProgramData\chocolatey\bin\choco.exe" } -Verifiable + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return @("git|2.42.0", "nodejs|20.10.0") + } -Verifiable + Mock Set-Content { } -Verifiable + + $result = Write-ChocolateyCache -Confirm:$false + + $result | Should -Be $true + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Retrieved Chocolatey packages successfully." -and $Verbosity -eq "Debug" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Chocolatey cache written successfully to:" -and $Verbosity -eq "Debug" + } + Assert-MockCalled Set-Content -Times 1 -Scope It -ParameterFilter { + $Path -eq "TestDrive:\choco.cache" -and $Force -eq $true + } + } + + It "Should return true and show cancellation message when ShouldProcess is declined" { + Mock Get-ChocolateyCacheFile { return "TestDrive:\choco.cache" } -Verifiable + Mock Test-ChocolateyInstalled { return $true } -Verifiable + Mock Find-Chocolatey { return "C:\ProgramData\chocolatey\bin\choco.exe" } -Verifiable + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return @("git|2.42.0", "nodejs|20.10.0") + } -Verifiable + Mock Set-Content { } -Verifiable + + $result = Write-ChocolateyCache -WhatIf + + $result | Should -Be $true + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Retrieved Chocolatey packages successfully." -and $Verbosity -eq "Debug" + } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { + $Message -match "Operation to write Chocolatey cache was cancelled." -and $Verbosity -eq "Warning" + } + Assert-MockCalled Set-Content -Times 0 -Scope It } } } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Write-ChocolateyCache.ps1 b/DevSetup/Private/Providers/Chocolatey/Write-ChocolateyCache.ps1 index 2058c2b..9d40bb1 100644 --- a/DevSetup/Private/Providers/Chocolatey/Write-ChocolateyCache.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Write-ChocolateyCache.ps1 @@ -56,33 +56,70 @@ #> Function Write-ChocolateyCache { - [CmdletBinding()] + [CmdletBinding(SupportsShouldProcess=$true)] + [OutputType([bool])] Param() - $cacheFile = Get-ChocolateyCacheFile + try { + $cacheFile = Get-ChocolateyCacheFile + } catch { + Write-StatusMessage "Error determining Chocolatey cache file path: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $false + } + + try { + if(-not (Test-ChocolateyInstalled)) { + Write-StatusMessage "Chocolatey is not installed. Cannot write cache file." -Verbosity Error + return $false + } + } catch { + Write-StatusMessage "Error checking if Chocolatey is installed: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $false + } + + try { + $chocoCommand = Find-Chocolatey + } catch { + Write-StatusMessage "Error locating Chocolatey command: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $false + } - if(-not (Test-ChocolateyInstalled)) { - Write-Error "Chocolatey is not installed. Cannot write cache file." + if(-not $chocoCommand -or [string]::IsNullOrWhiteSpace($chocoCommand)) { + Write-StatusMessage "Could not find Chocolatey command. Cannot write cache file." -Verbosity Warning return $false } try { - #$chocolatelyPackages = @{} - #choco list -r | foreach-object { - # $package = $_ -split '\|' - # if($package.Count -eq 2) { - # $chocolatelyPackages[$package[0]] = @{ - # Name = $package[0] - # Version = $package[1] - # } - # } - #} - Invoke-Expression "& choco list -r" | Set-Content $cacheFile -Force - Write-Debug "Chocolatey cache written successfully to: $cacheFile" - return $true + $chocoPackages = Invoke-Command -ScriptBlock { & $chocoCommand list -r } 2>$null 3>$null 4>$null 5>$null 6>$null } catch { - Write-Error "Failed to write Chocolatey cache file: $_" + Write-StatusMessage "Failed to write Chocolatey cache file: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $false + } + + Write-StatusMessage "Retrieved Chocolatey packages successfully." -Verbosity Debug + if ($LASTEXITCODE -ne 0 -or -not $chocoPackages) { + Write-StatusMessage "Failed to retrieve Chocolatey packages or no packages found." -Verbosity Warning + return $false + } + + try { + if ($PSCmdlet.ShouldProcess($cacheFile, "Update Chocolatey cache")) { + $chocoPackages | Set-Content $cacheFile -Force + Write-StatusMessage "Chocolatey cache written successfully to: $cacheFile" -Verbosity Debug + return $true + } else { + Write-StatusMessage "Operation to write Chocolatey cache was cancelled." -Verbosity Warning + return $true + } + + } catch { + Write-StatusMessage "Failed to write Chocolatey cache file: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error return $false } } \ No newline at end of file