diff --git a/.github/workflows/run-lint.yml b/.github/workflows/run-lint.yml new file mode 100644 index 0000000..c8c2dd3 --- /dev/null +++ b/.github/workflows/run-lint.yml @@ -0,0 +1,30 @@ +name: Run PSScriptAnalyzer Tests +on: + pull_request: + branches: + - develop + - main + workflow_dispatch: + +jobs: + psscriptanalyzer: + name: PSScriptAnalyzer + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + checks: write + pull-requests: write + security-events: write + steps: + - uses: actions/checkout@v4 + - name: Lint with PSScriptAnalyzer + shell: pwsh + run: | + .\runSecurity.ps1 + + - name: Upload SARIF results file + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif + token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml new file mode 100644 index 0000000..732a8a0 --- /dev/null +++ b/.github/workflows/run-unit-tests.yml @@ -0,0 +1,80 @@ +name: Run Pester Tests +on: + pull_request: + branches: + - develop + - main + workflow_dispatch: + +jobs: + pester-test-linux: + name: Pester test (On Linux) + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: read + issues: read + checks: write + pull-requests: write + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - name: Execute runTests.ps1 + shell: pwsh + run: | + .\runTests.ps1 + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action/linux@v2 + if: (!cancelled()) + with: + check_name: Pester test (On Linux) Results + files: testResults.xml + + pester-test-windows: + name: Pester test (On Windows) + runs-on: windows-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: read + issues: read + checks: write + pull-requests: write + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - name: Execute runTests.ps1 + shell: pwsh + run: | + .\runTests.ps1 + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action/windows@v2 + if: (!cancelled()) + with: + check_name: Pester test (On Windows) Results + files: testResults.xml + + pester-test-macos: + name: Pester test (On macOS) + runs-on: macos-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: read + issues: read + checks: write + pull-requests: write + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - name: Execute runTests.ps1 + shell: pwsh + run: | + .\runTests.ps1 + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action/macos@v2 + if: (!cancelled()) + with: + check_name: Pester test (On macOS) Results + files: testResults.xml \ No newline at end of file diff --git a/.github/workflows/update-module-version.yaml b/.github/workflows/update-module-version.yaml index 7e92541..afb9148 100644 --- a/.github/workflows/update-module-version.yaml +++ b/.github/workflows/update-module-version.yaml @@ -4,14 +4,19 @@ on: push: branches: - develop # Trigger on pushes to the main branch + workflow_dispatch: jobs: update_module_version: + name: Update Module Version runs-on: ubuntu-latest env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} permissions: - contents: write + contents: write + issues: read + checks: write + pull-requests: write steps: - name: checkout @@ -29,8 +34,16 @@ jobs: run: | perl -pi -e 's/[0-9]\.[0-9]\.[0-9]/${{ steps.version_tracker.outputs.version }}/' DevSetup/DevSetup.psd1 - - name: Commit and push changes - uses: stefanzweifel/git-auto-commit-action@v5 +# - name: Commit and push changes +# uses: stefanzweifel/git-auto-commit-action@v5 +# with: +# commit_message: "Automated Release Tagging for ${{ steps.version_tracker.outputs.version }} in DevSetup.psd1" +# branch: + - name: Create Pull Request + uses: peter-evans/create-pull-request@v7 with: - commit_message: "Automated Release Tagging for ${{ steps.version_tracker.outputs.version }} in DevSetup.psd1" - branch: develop \ No newline at end of file + commit-message: Automated Release Tagging for ${{ steps.version_tracker.outputs.version }} in DevSetup.psd1 + title: Automated Release Tagging for ${{ steps.version_tracker.outputs.version }} in DevSetup.psd1 + body: Changing version to ${{ steps.version_tracker.outputs.version }} + branch: update-release-version-to-${{ steps.version_tracker.outputs.version }} + token: ${{ secrets.WORKFLOW_TOKEN }} \ No newline at end of file diff --git a/DevSetup/DevSetup.psd1 b/DevSetup/DevSetup.psd1 index 5b547d5..6602e1f 100644 --- a/DevSetup/DevSetup.psd1 +++ b/DevSetup/DevSetup.psd1 @@ -12,7 +12,7 @@ RootModule = 'DevSetup.psm1' # Version number of this module. -ModuleVersion = '1.0.8' +ModuleVersion = '1.0.9' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/DevSetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.Tests.ps1 b/DevSetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.Tests.ps1 index e1383d0..c1e13c8 100644 --- a/DevSetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.Tests.ps1 +++ b/DevSetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.Tests.ps1 @@ -2,10 +2,12 @@ BeforeAll { . $PSScriptRoot\ConvertFrom-3rdPartyInstall.ps1 . $PSScriptRoot\..\..\..\DevSetup\Private\3rdParty\VisualStudio\ConvertFrom-VisualStudioInstall.ps1 . $PSScriptRoot\..\..\..\DevSetup\Private\3rdParty\VisualStudioCode\ConvertFrom-VisualStudioCodeInstall.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Test-OperatingSystem.ps1 Mock Write-Host { } Mock Write-Warning { } Mock ConvertFrom-VisualStudioInstall { $true } Mock ConvertFrom-VisualStudioCodeInstall { $true } + Mock Test-OperatingSystem { $true } } Describe "ConvertFrom-3rdPartyInstall" { diff --git a/DevSetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.ps1 b/DevSetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.ps1 index 540a0f6..8425ad3 100644 --- a/DevSetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.ps1 +++ b/DevSetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.ps1 @@ -3,15 +3,17 @@ Function ConvertFrom-3rdPartyInstall { [string]$Config ) - # Convert from Visual Studio installations - Write-Host "`nScanning Visual Studio installations..." -ForegroundColor Cyan - if (-not (ConvertFrom-VisualStudioInstall -Config $Config)) { - Write-Warning "Failed to convert Visual Studio installations, but continuing..." - } + if((Test-OperatingSystem -Windows)) { + # Convert from Visual Studio installations + Write-Host "`nScanning Visual Studio installations..." -ForegroundColor Cyan + if (-not (ConvertFrom-VisualStudioInstall -Config $Config)) { + Write-Warning "Failed to convert Visual Studio installations, but continuing..." + } - # Convert from Visual Studio Code installations - Write-Host "`nScanning Visual Studio Code installation..." -ForegroundColor Cyan - if (-not (ConvertFrom-VisualStudioCodeInstall -Config $Config)) { - Write-Warning "Failed to convert Visual Studio Code installation, but continuing..." + # Convert from Visual Studio Code installations + Write-Host "`nScanning Visual Studio Code installation..." -ForegroundColor Cyan + if (-not (ConvertFrom-VisualStudioCodeInstall -Config $Config)) { + Write-Warning "Failed to convert Visual Studio Code installation, but continuing..." + } } } \ No newline at end of file diff --git a/DevSetup/Private/Commands/Export-DevSetupEnv.Tests.ps1 b/DevSetup/Private/Commands/Export-DevSetupEnv.Tests.ps1 index a494767..80b8c04 100644 --- a/DevSetup/Private/Commands/Export-DevSetupEnv.Tests.ps1 +++ b/DevSetup/Private/Commands/Export-DevSetupEnv.Tests.ps1 @@ -1,15 +1,35 @@ BeforeAll { - . $PSScriptRoot\Export-DevSetupEnv.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupEnvPath.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupLocalEnvPath.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupCommunityEnvPath.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Write-NewConfig.ps1 - Mock Get-DevSetupEnvPath { "TestDrive:\DevSetupEnvs" } - Mock Get-DevSetupLocalEnvPath { "TestDrive:\DevSetupEnvs\local"} - Mock Get-DevSetupCommunityEnvPath { "TestDrive:\DevSetupEnvs\community"} + . (Join-Path $PSScriptRoot "Export-DevSetupEnv.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Get-DevSetupEnvPath.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Get-DevSetupLocalEnvPath.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Get-DevSetupCommunityEnvPath.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Write-NewConfig.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1") + if ($PSVersionTable.PSVersion.Major -eq 5) { + Mock Get-DevSetupEnvPath { "$TestDrive\DevSetup\DevSetupEnvs" } + Mock Get-DevSetupLocalEnvPath { "$TestDrive\DevSetup\DevSetupEnvs\local" } + Mock Get-DevSetupCommunityEnvPath { "$TestDrive\DevSetup\DevSetupEnvs\community" } + } elseif ($PSVersionTable.PSVersion.Major -ge 6) { + if ($IsWindows) { + Mock Get-DevSetupEnvPath { "$TestDrive\DevSetup\DevSetupEnvs" } + Mock Get-DevSetupLocalEnvPath { "$TestDrive\DevSetup\DevSetupEnvs\local" } + Mock Get-DevSetupCommunityEnvPath { "$TestDrive\DevSetup\DevSetupEnvs\community" } + } + if ($IsLinux) { + Mock Get-DevSetupEnvPath { "$TestDrive/home/testuser/DevSetup/DevSetupEnvs" } + Mock Get-DevSetupLocalEnvPath { "$TestDrive/home/testuser/DevSetup/DevSetupEnvs/local" } + Mock Get-DevSetupCommunityEnvPath { "$TestDrive/home/testuser/DevSetup/DevSetupEnvs/community" } + } + if ($IsMacOS) { + Mock Get-DevSetupEnvPath { "$TestDrive/Users/TestUser/DevSetup/DevSetupEnvs" } + Mock Get-DevSetupLocalEnvPath { "$TestDrive/Users/TestUser/DevSetup/DevSetupEnvs/local" } + Mock Get-DevSetupCommunityEnvPath { "$TestDrive/Users/TestUser/DevSetup/DevSetupEnvs/community" } + } + } Mock Write-NewConfig { param($OutFile) $OutFile } Mock Write-Host { } Mock Write-Error { } + Mock Write-StatusMessage { } } Describe "Export-DevSetupEnv" { @@ -17,34 +37,66 @@ Describe "Export-DevSetupEnv" { Context "When called with a valid name" { It "Should create the config file and return its path" { $result = Export-DevSetupEnv -Name "MyEnv" - $result | Should -Be "TestDrive:\DevSetupEnvs\local\MyEnv.devsetup" - Assert-MockCalled Write-NewConfig -Exactly 1 -Scope It -ParameterFilter { $OutFile -eq "TestDrive:\DevSetupEnvs\local\MyEnv.devsetup" } - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "exported to" -and $ForegroundColor -eq "Green" } + if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { + $expectedPath = "$TestDrive\DevSetup\DevSetupEnvs\local\MyEnv.devsetup" + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { + $expectedPath = "$TestDrive/home/testuser/DevSetup/DevSetupEnvs/local/MyEnv.devsetup" + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { + $expectedPath = "$TestDrive/Users/TestUser/DevSetup/DevSetupEnvs/local/MyEnv.devsetup" + } + $result | Should -Be $expectedPath + Assert-MockCalled Write-NewConfig -Exactly 1 -Scope It -ParameterFilter { $OutFile -eq $expectedPath } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "exported to" -and $ForegroundColor -eq "Green" } } } Context "When called with a valid path" { It "Should create the config file and return its path" { - $result = Export-DevSetupEnv -Path "TestDrive:\MyCustomPath\MyEnv.devsetup" - $result | Should -Be "TestDrive:\MyCustomPath\MyEnv.devsetup" - Assert-MockCalled Write-NewConfig -Exactly 1 -Scope It -ParameterFilter { $OutFile -eq "TestDrive:\MyCustomPath\MyEnv.devsetup" } - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "exported to" -and $ForegroundColor -eq "Green" } + if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { + $result = Export-DevSetupEnv -Path "$TestDrive\MyCustomPath\MyEnv.devsetup" + $expectedPath = "$TestDrive\MyCustomPath\MyEnv.devsetup" + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { + $result = Export-DevSetupEnv -Path "$TestDrive/MyCustomPath/MyEnv.devsetup" + $expectedPath = "$TestDrive/MyCustomPath/MyEnv.devsetup" + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { + $result = Export-DevSetupEnv -Path "$TestDrive/MyCustomPath/MyEnv.devsetup" + $expectedPath = "$TestDrive/MyCustomPath/MyEnv.devsetup" + } + $result | Should -Be $expectedPath + Assert-MockCalled Write-NewConfig -Exactly 1 -Scope It -ParameterFilter { $OutFile -eq $expectedPath } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "exported to" -and $ForegroundColor -eq "Green" } } } Context "When called with a name that needs sanitization" { It "Should sanitize the name and warn" { $result = Export-DevSetupEnv -Name "Data Science Environment!" - $result | Should -Be "TestDrive:\DevSetupEnvs\local\DataScienceEnvironment.devsetup" - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "sanitized" -and $ForegroundColor -eq "Yellow" } + if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { + $expectedPath = "$TestDrive\DevSetup\DevSetupEnvs\local\DataScienceEnvironment.devsetup" + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { + $expectedPath = "$TestDrive/home/testuser/DevSetup/DevSetupEnvs/local/DataScienceEnvironment.devsetup" + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { + $expectedPath = "$TestDrive/Users/TestUser/DevSetup/DevSetupEnvs/local/DataScienceEnvironment.devsetup" + } + $result | Should -Be $expectedPath + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "sanitized" -and $ForegroundColor -eq "Yellow" } } } Context "When called with a path that needs sanitization" { It "Should sanitize the path and warn" { - $result = Export-DevSetupEnv -Path "TestDrive:\MyCustomPath\MyEnv!.devsetup" - $result | Should -Be "TestDrive:\MyCustomPath\MyEnv.devsetup" - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "sanitized" -and $ForegroundColor -eq "Yellow" } + if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { + $result = Export-DevSetupEnv -Path "$TestDrive\MyCustomPath\MyEnv!.devsetup" + $expectedPath = "$TestDrive\MyCustomPath\MyEnv.devsetup" + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { + $result = Export-DevSetupEnv -Path "$TestDrive/MyCustomPath/MyEnv!.devsetup" + $expectedPath = "$TestDrive/MyCustomPath/MyEnv.devsetup" + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { + $result = Export-DevSetupEnv -Path "$TestDrive/MyCustomPath/MyEnv!.devsetup" + $expectedPath = "$TestDrive/MyCustomPath/MyEnv.devsetup" + } + $result | Should -Be $expectedPath + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "sanitized" -and $ForegroundColor -eq "Yellow" } } } @@ -53,7 +105,7 @@ Describe "Export-DevSetupEnv" { Mock Write-NewConfig { param($OutFile) $null } $result = Export-DevSetupEnv -Name "FailEnv" $result | Should -Be $null - Assert-MockCalled Write-Error -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to create configuration file" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to create configuration file" -and $Verbosity -eq "Error" } } } } diff --git a/DevSetup/Private/Commands/Export-DevSetupEnv.ps1 b/DevSetup/Private/Commands/Export-DevSetupEnv.ps1 index b30fa3b..286ca53 100644 --- a/DevSetup/Private/Commands/Export-DevSetupEnv.ps1 +++ b/DevSetup/Private/Commands/Export-DevSetupEnv.ps1 @@ -21,7 +21,7 @@ .EXAMPLE Export-DevSetupEnv -Name "MyCurrentSetup" - + Exports the current system state to a configuration file named "MyCurrentSetup.yaml". .EXAMPLE @@ -31,12 +31,12 @@ } else { Write-Host "Export failed" } - + Demonstrates capturing the return value to verify export success. .EXAMPLE Export-DevSetupEnv -Name "Data Science Environment!" - + The exclamation mark will be removed, resulting in "DataScienceEnvironment.yaml". A warning message will indicate the sanitization that occurred. @@ -65,7 +65,9 @@ Function Export-DevSetupEnv { [Parameter(Mandatory=$true, ParameterSetName="Export")] [string]$Name, [Parameter(Mandatory=$true, ParameterSetName="ExportPath")] - [string]$Path + [string]$Path, + [Parameter(Mandatory=$false)] + [switch]$DryRun ) $OutFile = $null if($PSBoundParameters.ContainsKey('Name')) { @@ -78,7 +80,7 @@ Function Export-DevSetupEnv { } $SanitizedEnvName = $Name -replace '[^a-zA-Z0-9\-\.]', '' if ($SanitizedEnvName -ne $Name) { - Write-Host "EnvName sanitized from '$Name' to '$SanitizedEnvName' (removed non-alphanumeric characters)" -ForegroundColor Yellow + Write-StatusMessage "EnvName sanitized from '$Name' to '$SanitizedEnvName' (removed non-alphanumeric characters)" -ForegroundColor Yellow } $BasePath = Join-Path -Path (Get-DevSetupEnvPath) -ChildPath $Provider @@ -96,25 +98,25 @@ Function Export-DevSetupEnv { if($Name -notlike "*.devsetup") { $Name = "$Name.devsetup" } - + $SanitizedEnvName = $Name -replace '[^a-zA-Z0-9\-\.]', '' if ($SanitizedEnvName -ne $Name) { - Write-Host "EnvName sanitized from '$Name' to '$SanitizedEnvName' (removed non-alphanumeric characters)" -ForegroundColor Yellow + Write-StatusMessage "EnvName sanitized from '$Name' to '$SanitizedEnvName' (removed non-alphanumeric characters)" -ForegroundColor Yellow } $OutFile = Join-Path -Path $BasePath -ChildPath $SanitizedEnvName } if(-not $OutFile) { - Write-Error "Failed to determine output file path" + Write-StatusMessage "Failed to determine output file path" -Verbosity Error return $null } - $config = Write-NewConfig -OutFile $OutFile + $config = Write-NewConfig -OutFile $OutFile -DryRun:$DryRun if (-not $config) { - Write-Error "Failed to create configuration file" + Write-StatusMessage "Failed to create configuration file" -Verbosity Error return $null - } - Write-Host "Configuration file exported to: $OutFile" -ForegroundColor Green - return $OutFile + } + Write-StatusMessage "Configuration file exported to: $OutFile" -ForegroundColor Green + return $OutFile } \ No newline at end of file diff --git a/DevSetup/Private/Commands/Initialize-DevSetup.Tests.ps1 b/DevSetup/Private/Commands/Initialize-DevSetup.Tests.ps1 index 2906d50..e8d3038 100644 --- a/DevSetup/Private/Commands/Initialize-DevSetup.Tests.ps1 +++ b/DevSetup/Private/Commands/Initialize-DevSetup.Tests.ps1 @@ -1,11 +1,14 @@ BeforeAll { - . $PSScriptRoot\Initialize-DevSetup.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupPath.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupEnvPath.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupLocalEnvPath.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupCommunityEnvPath.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Providers\Core\Install-CoreDependencies.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Initialize-DevSetupEnvs.ps1 + . (Join-Path $PSScriptRoot "Initialize-DevSetup.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Get-DevSetupPath.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Get-DevSetupEnvPath.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Get-DevSetupLocalEnvPath.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Get-DevSetupCommunityEnvPath.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Providers\Core\Install-CoreDependencies.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Initialize-DevSetupEnvs.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Get-DevSetupCachePath.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Get-DevSetupLogPath.ps1") Mock Write-Host { } Mock Write-Error { } Mock Write-Verbose { } @@ -17,6 +20,9 @@ BeforeAll { Mock Test-Path { $false } Mock New-Item { } Mock Initialize-DevSetupEnvs { "TestDrive:\Users\Test\devsetup\envs" } + Mock Write-StatusMessage { } + Mock Get-DevSetupCachePath { "TestDrive:\Users\Test\devsetup\cache" } + Mock Get-DevSetupLogPath { "TestDrive:\Users\Test\devsetup\logs" } } Describe "Initialize-DevSetup" { @@ -37,7 +43,7 @@ Describe "Initialize-DevSetup" { Mock Install-CoreDependencies { $false } $result = Initialize-DevSetup $result | Should -Be $null - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to install core dependencies" } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "Failed to install core dependencies" -and $Verbosity -eq "Error" } } } @@ -47,7 +53,7 @@ Describe "Initialize-DevSetup" { $result = Initialize-DevSetup $result | Should -Be $true Assert-MockCalled New-Item -Exactly 0 -Scope It - Assert-MockCalled Write-Verbose -Scope It -ParameterFilter { $Message -match "already exists" } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "already exists" -and $Verbosity -eq "Verbose" } } } @@ -56,7 +62,7 @@ Describe "Initialize-DevSetup" { Mock Initialize-DevSetupEnvs { $null } $result = Initialize-DevSetup $result | Should -Be $false - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to initialize DevSetup environment path" } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "Failed to initialize DevSetup environment path" -and $Verbosity -eq "Error" } } } @@ -65,7 +71,7 @@ Describe "Initialize-DevSetup" { Mock Install-CoreDependencies { throw "Unexpected error" } $result = Initialize-DevSetup $result | Should -Be $false - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to initialize DevSetup environment" } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "Failed to initialize DevSetup environment" -and $Verbosity -eq "Error" } } } } \ No newline at end of file diff --git a/DevSetup/Private/Commands/Initialize-DevSetup.ps1 b/DevSetup/Private/Commands/Initialize-DevSetup.ps1 index bd50b5f..2ce6751 100644 --- a/DevSetup/Private/Commands/Initialize-DevSetup.ps1 +++ b/DevSetup/Private/Commands/Initialize-DevSetup.ps1 @@ -15,7 +15,7 @@ .EXAMPLE Initialize-DevSetup - + Initializes the complete DevSetup environment with default settings. .EXAMPLE @@ -26,7 +26,7 @@ Write-Host "DevSetup initialization failed" # Handle initialization failure } - + Demonstrates conditional logic based on initialization success. .EXAMPLE @@ -34,7 +34,7 @@ if ($setupReady) { Use-DevSetup -List } - + Shows using the function result to proceed with DevSetup operations. .NOTES @@ -62,53 +62,55 @@ #> Function Initialize-DevSetup { - try { + try { # Install core dependencies first - Write-Host "- Installing core dependencies..." -ForegroundColor Cyan + Write-StatusMessage "- Installing core dependencies..." -ForegroundColor Cyan if (-not (Install-CoreDependencies)) { - Write-Error "Failed to install core dependencies" + Write-StatusMessage "Failed to install core dependencies" -Verbosity Error return } - Write-Host "- Core dependencies installed successfully" -ForegroundColor Green - + Write-StatusMessage "- Core dependencies installed successfully" -ForegroundColor Green + # Define .devsetup folder path $devSetupPath = Get-DevSetupPath - + # Check if .devsetup folder exists if (-not (Test-Path -Path $devSetupPath)) { #Write-Host "Creating .devsetup directory at: $devSetupPath" -ForegroundColor Cyan New-Item -Path $devSetupPath -ItemType Directory -Force | Out-Null #Write-Host ".devsetup directory created successfully" -ForegroundColor Green } else { - Write-Verbose ".devsetup directory already exists at: $devSetupPath" + Write-StatusMessage ".devsetup directory already exists at: $devSetupPath" -Verbosity Verbose } - Write-Host "" - Write-Host "- Installing community environments..." -ForegroundColor Cyan + Write-StatusMessage "`n- Installing community environments..." -ForegroundColor Cyan # Initialize DevSetup environments path $envSetupPath = Initialize-DevSetupEnvs if (-not $envSetupPath) { - Write-Error "Failed to initialize DevSetup environment path" + Write-StatusMessage "Failed to initialize DevSetup environment path" -Verbosity Error return $false } else { - Write-Host "- Community environments installed successfully" -ForegroundColor Green + Write-StatusMessage "- Community environments installed successfully" -ForegroundColor Green } - Write-Host "" - Write-Host "Path Information: " -ForegroundColor Yellow - Write-Host "- DevSetup:" -ForegroundColor Cyan - Write-Host " - $devSetupPath" -ForegroundColor Gray - Write-Host "- Local Environments: " -ForegroundColor Cyan - Write-Host " - $($envSetupPath.Local)" -ForegroundColor Gray - Write-Host "- Community Environments: " -ForegroundColor Cyan - Write-Host " - $($envSetupPath.Community)" -ForegroundColor Gray - Write-Host "" + Write-StatusMessage "`nPath Information: " -ForegroundColor Yellow + Write-StatusMessage "- DevSetup:" -ForegroundColor Cyan + Write-StatusMessage " - $devSetupPath" -ForegroundColor Gray + Write-StatusMessage "- Local Environments: " -ForegroundColor Cyan + Write-StatusMessage " - $($envSetupPath.Local)" -ForegroundColor Gray + Write-StatusMessage "- Community Environments: " -ForegroundColor Cyan + Write-StatusMessage " - $($envSetupPath.Community)" -ForegroundColor Gray + Write-StatusMessage "- Logs:" -ForegroundColor Cyan + Write-StatusMessage " - $(Get-DevSetupLogPath)" -ForegroundColor Gray + Write-StatusMessage "- Cache:" -ForegroundColor Cyan + Write-StatusMessage " - $(Get-DevSetupCachePath)`n" -ForegroundColor Gray # Return the path for use by other functions return $true } catch { - Write-Error "Failed to initialize DevSetup environment: $_" + Write-StatusMessage "Failed to initialize DevSetup environment: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error return $false } } \ No newline at end of file diff --git a/DevSetup/Private/Commands/Install-DevSetupEnv.Tests.ps1 b/DevSetup/Private/Commands/Install-DevSetupEnv.Tests.ps1 index 05a5b21..a1fe22c 100644 --- a/DevSetup/Private/Commands/Install-DevSetupEnv.Tests.ps1 +++ b/DevSetup/Private/Commands/Install-DevSetupEnv.Tests.ps1 @@ -1,11 +1,25 @@ BeforeAll { - . $PSScriptRoot\Install-DevSetupEnv.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Providers\PowerShell\Install-PowershellModules.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Providers\Chocolatey\Install-ChocolateyPackages.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Providers\Scoop\Install-ScoopComponents.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Read-ConfigurationFile.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupEnvPath.ps1 - Mock Get-DevSetupEnvPath { "C:\DevSetupEnvs" } + . (Join-Path $PSScriptRoot "Install-DevSetupEnv.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Providers\Powershell\Install-PowershellModules.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Providers\Chocolatey\Install-ChocolateyPackages.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Providers\Scoop\Install-ScoopComponents.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Read-ConfigurationFile.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Get-DevSetupEnvPath.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Test-OperatingSystem.ps1") + if ($PSVersionTable.PSVersion.Major -eq 5) { + Mock Get-DevSetupEnvPath { "C:\DevSetup" } + } elseif ($PSVersionTable.PSVersion.Major -ge 6) { + if ($IsWindows) { + Mock Get-DevSetupEnvPath { "C:\DevSetup" } + } + if ($IsLinux) { + Mock Get-DevSetupEnvPath { "/home/testuser/devsetup" } + } + if ($IsMacOS) { + Mock Get-DevSetupEnvPath { "/Users/TestUser/devsetup" } + } + } Mock Test-Path { $true } Mock Read-ConfigurationFile { } Mock Install-PowershellModules { } @@ -16,6 +30,8 @@ BeforeAll { Mock Write-Warning { } Mock Invoke-Command { } Mock Invoke-Expression { } + Mock Write-StatusMessage { } + Mock Test-OperatingSystem { $true } } Describe "Install-DevSetupEnv" { @@ -25,7 +41,7 @@ Describe "Install-DevSetupEnv" { Mock Test-Path { $false } $result = Install-DevSetupEnv -Name "missing-env" $result | Should -Be $null - Assert-MockCalled Write-Error -Exactly 1 -Scope It -ParameterFilter { $Message -match "Environment file not found" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Environment file not found" -and $Verbosity -eq "Error" } } } @@ -35,7 +51,7 @@ Describe "Install-DevSetupEnv" { Mock Read-ConfigurationFile { $null } $result = Install-DevSetupEnv -Name "bad-yaml" $result | Should -Be $null - Assert-MockCalled Write-Error -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to parse YAML" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to parse YAML" -and $Verbosity -eq "Error" } } } @@ -48,7 +64,7 @@ Describe "Install-DevSetupEnv" { Assert-MockCalled Install-PowershellModules -Exactly 1 -Scope It Assert-MockCalled Install-ChocolateyPackages -Exactly 1 -Scope It Assert-MockCalled Install-ScoopComponents -Exactly 1 -Scope It - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "No commands found" } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "No commands found" } } } @@ -63,8 +79,8 @@ Describe "Install-DevSetupEnv" { $result = Install-DevSetupEnv -Name "cmd-env" $result | Should -Be $null Assert-MockCalled Invoke-Expression -Exactly 2 -Scope It - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Executing command for: git" } - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Executing command for: nodejs" } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "Executing command for: git" } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "Executing command for: nodejs" } } } @@ -78,7 +94,7 @@ Describe "Install-DevSetupEnv" { Mock Read-ConfigurationFile { @{ devsetup = @{ commands = $commands } } } $result = Install-DevSetupEnv -Name "missing-cmd" $result | Should -Be $null - Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "missing command property" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "missing command property" -and $Verbosity -eq "Warning" } Assert-MockCalled Invoke-Expression -Exactly 1 -Scope It } } diff --git a/DevSetup/Private/Commands/Install-DevSetupEnv.ps1 b/DevSetup/Private/Commands/Install-DevSetupEnv.ps1 index 5382ed1..d281f5b 100644 --- a/DevSetup/Private/Commands/Install-DevSetupEnv.ps1 +++ b/DevSetup/Private/Commands/Install-DevSetupEnv.ps1 @@ -18,17 +18,17 @@ .EXAMPLE Install-DevSetupEnv -Name "development" - + Installs the development environment from the "development.yaml" configuration file. .EXAMPLE Install-DevSetupEnv "web-dev" - + Installs the web development environment using positional parameter syntax. .EXAMPLE Install-DevSetupEnv -Name "my-environment" - + Demonstrates the PSCustomObject structure that would be parsed from the YAML file. .NOTES @@ -62,7 +62,9 @@ Function Install-DevSetupEnv { [Parameter(Mandatory=$true, Position=0, ParameterSetName = "InstallPath")] [string]$Path, [Parameter(Mandatory=$true, Position=0, ParameterSetName = "InstallUrl")] - [string]$Url + [string]$Url, + [Parameter(Mandatory=$false)] + [switch]$DryRun = $false ) $YamlFile = $null @@ -79,20 +81,20 @@ Function Install-DevSetupEnv { $YamlFile = Join-Path -Path (Join-Path -Path (Get-DevSetupEnvPath) -ChildPath $Provider) -ChildPath "$Name.devsetup" } elseif($PSBoundParameters.ContainsKey('Path')) { if(-not (Test-Path -Path $Path)) { - Write-Error "Invalid Path provided" + Write-StatusMessage "Invalid Path provided" -Verbosity Error return } $YamlFile = $Path } elseif($PSBoundParameters.ContainsKey('Url')) { $FileName = Split-Path $Url -Leaf - Write-Host "Downloading DevSetup environment from:" -ForegroundColor Cyan - Write-Host "- $Url" -ForegroundColor Gray + Write-StatusMessage "Downloading DevSetup environment from:" -ForegroundColor Cyan + Write-StatusMessage "- $Url" -Indent 2 -ForegroundColor Gray $YamlFile = Join-Path -Path (Get-DevSetupLocalEnvPath) -ChildPath $FileName - Write-Host "Saving Devsetup environment file to:" -ForegroundColor Cyan - Write-Host "- $YamlFile" -ForegroundColor Gray + Write-StatusMessage "Saving Devsetup environment file to:" -ForegroundColor Cyan + Write-StatusMessage "- $YamlFile" -Indent 2 -ForegroundColor Gray if((Test-Path -Path $YamlFile)) { Write-Warning "File $YamlFile already exists" - do { + do { if(($sAnswer = Read-Host "Overwrite existing file and continue? [Y/N]") -eq '') { $sAnswer = 'N' } } until ($sAnswer.ToUpper()[0] -match '[yYnN]') if(-not ($sAnswer.ToUpper()[0] -match '[Y]')) { @@ -102,51 +104,54 @@ Function Install-DevSetupEnv { try { Invoke-WebRequest -Uri $Url -OutFile $YamlFile | Out-Null } catch { - Write-Error "Failed to download devsetup env file" + Write-StatusMessage "Failed to download devsetup env file" -Verbosity Error return } } if (-not (Test-Path $YamlFile)) { - Write-Error "Environment file not found: $YamlFile" + Write-StatusMessage "Environment file not found: $YamlFile" -Verbosity Error return } - Write-Host "Installing DevSetup environment from:" -ForegroundColor Cyan - Write-Host "- $YamlFile" -ForegroundColor Gray - Write-Host "" + Write-StatusMessage "Installing DevSetup environment from:" -ForegroundColor Cyan + Write-StatusMessage "- $YamlFile`n" -Indent 2 -ForegroundColor Gray # Read the configuration from the YAML file $YamlData = Read-ConfigurationFile -Config $YamlFile - + # Check if YAML data was successfully parsed if ($null -eq $YamlData) { - Write-Error "Failed to parse YAML configuration from: $YamlFile" + Write-StatusMessage "Failed to parse YAML configuration from: $YamlFile" -Verbosity Error return } - + # Install PowerShell module dependencies Install-PowershellModules -YamlData $YamlData | Out-Null - # Install Chocolatey package dependencies - Install-ChocolateyPackages -YamlData $YamlData | Out-Null - - # Install Scoop package dependencies - Install-ScoopComponents -YamlData $YamlData | Out-Null + if ((Test-OperatingSystem -Windows)) { + # Install Chocolatey package dependencies + Install-ChocolateyPackages -YamlData $YamlData | Out-Null + # Install Scoop package dependencies + Install-ScoopComponents -YamlData $YamlData | Out-Null + } else { + # Install Homebrew package dependencies + Invoke-HomebrewComponentsInstall -YamlData $YamlData -DryRun:$DryRun | Out-Null + } # Execute any commands defined in the configuration if ($YamlData.devsetup.commands -and $YamlData.devsetup.commands.Count -gt 0) { - Write-Host "Executing configuration commands..." -ForegroundColor Cyan - + Write-StatusMessage "Executing configuration commands..." -ForegroundColor Cyan + foreach ($commandEntry in $YamlData.devsetup.commands) { if ($commandEntry.command) { - Write-Host " - Executing command for: $($commandEntry.packageName)" -ForegroundColor Gray + Write-StatusMessage "- Executing command for: $($commandEntry.packageName)" -Indent 2 -ForegroundColor Gray Invoke-Expression -Command $commandEntry.command *> $null } else { - Write-Warning "Skipping command entry with missing command property" + Write-StatusMessage "Skipping command entry with missing command property" -Verbosity Warning } } } else { - Write-Host "No commands found in configuration to execute." -ForegroundColor Gray - } + Write-StatusMessage "No commands found in configuration to execute." -ForegroundColor Gray + } } \ No newline at end of file diff --git a/DevSetup/Private/Commands/Show-DevSetupEnvList.Tests.ps1 b/DevSetup/Private/Commands/Show-DevSetupEnvList.Tests.ps1 index 2880232..63eb0e9 100644 --- a/DevSetup/Private/Commands/Show-DevSetupEnvList.Tests.ps1 +++ b/DevSetup/Private/Commands/Show-DevSetupEnvList.Tests.ps1 @@ -1,13 +1,26 @@ BeforeAll { - . $PSScriptRoot\Show-DevSetupEnvList.ps1 - . $PSScriptRoot\Show-DevSetupEnvList.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupEnvPath.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Optimize-DevSetupEnvs.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupPath.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Test-OperatingSystem.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Format-PrettyTable.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 - Mock Get-DevSetupPath { "C:\DevSetup" } + . (Join-Path $PSScriptRoot "Show-DevSetupEnvList.ps1") + . (Join-Path $PSScriptRoot "Show-DevSetupEnvList.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Get-DevSetupEnvPath.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Optimize-DevSetupEnvs.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Get-DevSetupPath.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Test-OperatingSystem.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Format-PrettyTable.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1") + if ($PSVersionTable.PSVersion.Major -eq 5) { + Mock Get-DevSetupPath { "C:\DevSetup" } + } elseif ($PSVersionTable.PSVersion.Major -ge 6) { + if ($IsWindows) { + Mock Get-DevSetupPath { "C:\DevSetup" } + } + if ($IsLinux) { + Mock Get-DevSetupPath { "/home/testuser/devsetup" } + } + if ($IsMacOS) { + Mock Get-DevSetupPath { "/Users/TestUser/devsetup" } + } + } + Mock Optimize-DevSetupEnvs { } Mock Write-Host { } Mock Write-Warning { } diff --git a/DevSetup/Private/Commands/Show-DevSetupEnvList.ps1 b/DevSetup/Private/Commands/Show-DevSetupEnvList.ps1 index df7c7a9..463d3c5 100644 --- a/DevSetup/Private/Commands/Show-DevSetupEnvList.ps1 +++ b/DevSetup/Private/Commands/Show-DevSetupEnvList.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Lists available development environment configurations with platform filtering. @@ -21,22 +21,22 @@ .EXAMPLE Show-DevSetupEnvList - + Lists development environments compatible with the current platform. .EXAMPLE Show-DevSetupEnvList -Platform "all" - + Displays all available development environments regardless of platform. .EXAMPLE Show-DevSetupEnvList -Platform "linux" - + Shows only environments specifically designed for Linux systems. .EXAMPLE Show-DevSetupEnvList -Platform "windows" - + Lists environments compatible with Windows systems. .NOTES @@ -107,7 +107,7 @@ Function Show-DevSetupEnvList { # Get the environments.json file path $devSetupPath = Get-DevSetupPath $environmentsJsonPath = Join-Path -Path $devSetupPath -ChildPath "environments.json" - + if (-not (Test-Path $environmentsJsonPath)) { Write-Host "No environments index found. Running optimization to create it..." -ForegroundColor Cyan Optimize-DevSetupEnvs | Out-Null @@ -122,7 +122,7 @@ Function Show-DevSetupEnvList { Optimize-DevSetupEnvs | Out-Null } } - + if ($Provider) { $environments = $environments | Where-Object { $_.provider -and ($_.provider.ToLower() -eq $Provider.ToLower()) } } @@ -134,7 +134,7 @@ Function Show-DevSetupEnvList { if ($platformFilter -ne "all") { $environments = $environments | Where-Object { ($_.platform -and ($_.platform.ToLower() -eq $platformFilter)) } } - + if ($environments.Count -eq 0) { if ($platformFilter -eq "all") { Write-Host "No development environments found." -ForegroundColor Yellow @@ -144,7 +144,7 @@ Function Show-DevSetupEnvList { } return $true } - + # Create a formatted table $tableData = @() foreach ($env in $environments) { @@ -159,7 +159,7 @@ Function Show-DevSetupEnvList { Color = "DarkGray" } } - + $columnDefinitions = [ordered]@{ Name = @{ Name = "Name"; Width = 32; Alignment = "Left"; Color = "White"; Key = "Name" } Version = @{ Name = "Version"; Width = 10; Alignment = "Center"; Color = "White"; Key = "Version" } diff --git a/DevSetup/Private/Commands/Uninstall-DevSetupEnv.Tests.ps1 b/DevSetup/Private/Commands/Uninstall-DevSetupEnv.Tests.ps1 index 3dbcbbf..478e95c 100644 --- a/DevSetup/Private/Commands/Uninstall-DevSetupEnv.Tests.ps1 +++ b/DevSetup/Private/Commands/Uninstall-DevSetupEnv.Tests.ps1 @@ -1,20 +1,26 @@ BeforeAll { - . $PSScriptRoot\Uninstall-DevSetupEnv.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Read-ConfigurationFile.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupEnvPath.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Test-OperatingSystem.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Providers\Scoop\Uninstall-ScoopComponents.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Providers\Chocolatey\Uninstall-ChocolateyPackages.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Providers\PowerShell\Uninstall-PowershellModules.ps1 - Mock Get-DevSetupEnvPath { "TestDrive:\DevSetupEnvs" } + Function Write-EZLog { } + . (Join-Path $PSScriptRoot "Uninstall-DevSetupEnv.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Read-ConfigurationFile.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Get-DevSetupEnvPath.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Test-OperatingSystem.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Providers\Scoop\Uninstall-ScoopComponents.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Providers\Chocolatey\Uninstall-ChocolateyPackages.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Providers\Powershell\Uninstall-PowershellModules.ps1") + . (Join-Path $PSScriptRoot "..\..\..\DevSetup\Private\Providers\Homebrew\Invoke-HomebrewComponentsUninstall.ps1") + Mock Get-DevSetupEnvPath { "$TestDrive\DevSetup\DevSetupEnvs" } Mock Test-Path { $true } Mock Read-ConfigurationFile { } - Mock Uninstall-PowershellModules { $true } - Mock Uninstall-ChocolateyPackages { $true } - Mock Uninstall-ScoopComponents { $true } + Mock Uninstall-PowershellModules { Param($YamlData, $DryRun) $true } + Mock Uninstall-ChocolateyPackages { Param($YamlData, $DryRun) $true } + Mock Uninstall-ScoopComponents { Param($YamlData, $DryRun) $true } Mock Test-OperatingSystem { $true } Mock Write-Host { } Mock Write-Error { } + Mock Write-StatusMessage { } + Mock Write-EZLog { } + Mock Invoke-HomebrewComponentsUninstall { $true } } Describe "Uninstall-DevSetupEnv" { @@ -22,9 +28,9 @@ Describe "Uninstall-DevSetupEnv" { Context "When environment file does not exist" { It "Should write error and return" { Mock Test-Path { $false } - $result = Uninstall-DevSetupEnv -Name "missing-env" + $result = Uninstall-DevSetupEnv -Name "missing-env" -DryRun:$false $result | Should -Be $null - Assert-MockCalled Write-Error -Exactly 1 -Scope It -ParameterFilter { $Message -match "Environment file not found" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Environment file not found" -and $Verbosity -eq "Error" } } } @@ -34,7 +40,7 @@ Describe "Uninstall-DevSetupEnv" { Mock Read-ConfigurationFile { $null } $result = Uninstall-DevSetupEnv -Name "bad-yaml" $result | Should -Be $null - Assert-MockCalled Write-Error -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to parse YAML" } + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to parse YAML" -and $Verbosity -eq "Error" } } } @@ -42,26 +48,38 @@ Describe "Uninstall-DevSetupEnv" { It "Should call all uninstallers and write status" { Mock Test-Path { $true } Mock Read-ConfigurationFile { @{ devsetup = @{ } } } + Mock Test-OperatingSystem { Param($Windows, $Linux, $MacOS) { return $true } } $result = Uninstall-DevSetupEnv -Name "basic-env" $result | Should -Be $null + Assert-MockCalled Test-OperatingSystem -Exactly 1 -Scope It Assert-MockCalled Uninstall-PowershellModules -Exactly 1 -Scope It Assert-MockCalled Uninstall-ChocolateyPackages -Exactly 1 -Scope It Assert-MockCalled Uninstall-ScoopComponents -Exactly 1 -Scope It - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Uninstalling DevSetup environment from:" } + Assert-MockCalled Invoke-HomebrewComponentsUninstall -Exactly 0 -Scope It + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "Uninstalling DevSetup environment from:" } } } Context "When a component uninstaller fails" { It "Should continue calling other uninstallers" { + Mock Test-OperatingSystem { return $true } $script:callCount = 0 Mock Uninstall-PowershellModules { $script:callCount++; $false } Mock Uninstall-ChocolateyPackages { $script:callCount++; $true } Mock Uninstall-ScoopComponents { $script:callCount++; $true } + Mock Invoke-HomebrewComponentsUninstall { $script:callCount++; $true } Mock Test-Path { $true } Mock Read-ConfigurationFile { @{ devsetup = @{ } } } + $result = Uninstall-DevSetupEnv -Name "partial-fail" + Assert-MockCalled Test-OperatingSystem -Exactly 1 -Scope It -ParameterFilter { $Windows -eq $true } + Assert-MockCalled Uninstall-PowershellModules -Exactly 1 -Scope It + Assert-MockCalled Uninstall-ChocolateyPackages -Exactly 1 -Scope It + Assert-MockCalled Uninstall-ScoopComponents -Exactly 1 -Scope It + Assert-MockCalled Invoke-HomebrewComponentsUninstall -Exactly 0 -Scope It $result | Should -Be $null $script:callCount | Should -Be 3 + } } @@ -71,7 +89,7 @@ Describe "Uninstall-DevSetupEnv" { Mock Read-ConfigurationFile { throw "Unexpected error" } $result = Uninstall-DevSetupEnv -Name "exception-env" $result | Should -Be $null - Assert-MockCalled Write-Error -Scope It + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Verbosity -eq "Error" } } } } \ No newline at end of file diff --git a/DevSetup/Private/Commands/Uninstall-DevSetupEnv.ps1 b/DevSetup/Private/Commands/Uninstall-DevSetupEnv.ps1 index b2b596d..696833b 100644 --- a/DevSetup/Private/Commands/Uninstall-DevSetupEnv.ps1 +++ b/DevSetup/Private/Commands/Uninstall-DevSetupEnv.ps1 @@ -20,18 +20,18 @@ .EXAMPLE Uninstall-DevSetupEnv -Name "WebDev" - + Uninstalls all packages and components from the "WebDev" environment configuration. .EXAMPLE Uninstall-DevSetupEnv "DataScience" - + Removes the complete "DataScience" development environment using positional parameter. .EXAMPLE $envName = "GameDev" Uninstall-DevSetupEnv -Name $envName - + Demonstrates using a variable to specify the environment name for uninstallation. .NOTES @@ -40,7 +40,7 @@ - Validates YAML file existence before attempting to parse configuration - Processes uninstallation in specific order: 1. PowerShell modules via Uninstall-PowershellModules - 2. Chocolatey packages via Uninstall-ChocolateyPackages + 2. Chocolatey packages via Uninstall-ChocolateyPackages 3. Scoop packages via Uninstall-ScoopComponents - Each uninstaller function handles its own error reporting and validation - Does not remove the YAML configuration file itself after uninstallation @@ -58,16 +58,22 @@ Function Uninstall-DevSetupEnv { [CmdletBinding()] + [OutputType([void])] Param( [Parameter(Mandatory=$true, Position=0, ParameterSetName = "Uninstall")] + [Parameter(Mandatory=$true, Position=0, ParameterSetName = "UninstallDry")] [string]$Name, [Parameter(Mandatory=$true, Position=0, ParameterSetName = "UninstallPath")] - [string]$Path + [Parameter(Mandatory=$true, Position=0, ParameterSetName = "UninstallPathDry")] + [string]$Path, + [Parameter(Mandatory=$true, Position=1, ParameterSetName = "UninstallDry")] + [Parameter(Mandatory=$true, Position=1, ParameterSetName = "UninstallPathDry")] + [switch]$DryRun ) try { $YamlFile = $null - + if($PSBoundParameters.ContainsKey('Name')) { $Provider = "local" @@ -83,38 +89,45 @@ Function Uninstall-DevSetupEnv { Write-Error "Invalid Path provided" return } - $YamlFile = $Path + $YamlFile = $Path } #$YamlFile = Join-Path -Path (Get-DevSetupEnvPath) -ChildPath "$Name.yaml" if (-not (Test-Path $YamlFile)) { - Write-Error "Environment file not found: $YamlFile" + Write-StatusMessage "Environment file not found: $YamlFile" -Verbosity Error return } - Write-Host "Uninstalling DevSetup environment from: $YamlFile" -ForegroundColor Cyan + Write-StatusMessage "Uninstalling DevSetup environment from:" -ForegroundColor Cyan + Write-StatusMessage "- $YamlFile`n" -Indent 2 -ForegroundColor Gray # Read the configuration from the YAML file $YamlData = Read-ConfigurationFile -Config $YamlFile - + # Check if YAML data was successfully parsed if ($null -eq $YamlData) { - Write-Error "Failed to parse YAML configuration from: $YamlFile" + Write-StatusMessage "Failed to parse YAML configuration from: $YamlFile" -Verbosity Error return } - - # Install PowerShell module dependencies - $status = Uninstall-PowershellModules -YamlData $YamlData - if ((Test-OperatingSystem -Windows)) { - # Install Chocolatey package dependencies - $status = Uninstall-ChocolateyPackages -YamlData $YamlData + # Uninstall PowerShell module dependencies + Uninstall-PowershellModules -YamlData $YamlData | Out-Null + + $windows = Test-OperatingSystem -Windows + + if ($windows) { + # Uninstall Chocolatey package dependencies + Uninstall-ChocolateyPackages -YamlData $YamlData | Out-Null - # Install Scoop package dependencies - $status = Uninstall-ScoopComponents -YamlData $YamlData + # Uninstall Scoop package dependencies + Uninstall-ScoopComponents -YamlData $YamlData | Out-Null + } else { + # Uninstall Homebrew package dependencies + Invoke-HomebrewComponentsUninstall -YamlData $YamlData -DryRun:$DryRun | Out-Null } } catch { - Write-Error "An error occurred during uninstallation: $_" + Write-StatusMessage "An error occurred during uninstallation: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error return - } + } } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Export-InstalledChocolateyPackages.ps1 b/DevSetup/Private/Providers/Chocolatey/Export-InstalledChocolateyPackages.ps1 index c259a60..55dc2b4 100644 --- a/DevSetup/Private/Providers/Chocolatey/Export-InstalledChocolateyPackages.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Export-InstalledChocolateyPackages.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Exports installed Chocolatey packages to a YAML configuration file. diff --git a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.Tests.ps1 index bf6cb94..d0a1feb 100644 --- a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.Tests.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.Tests.ps1 @@ -5,20 +5,39 @@ BeforeAll { } Describe "Get-ChocolateyCacheFile" { - Context "When Get-DevSetupCachePath returns a valid path" { It "Should return the correct cache file path" { - Mock Get-DevSetupCachePath { return "C:\Users\Test\.devsetup\.cache" } - $result = Get-ChocolateyCacheFile - $result | Should -Be "C:\Users\Test\.devsetup\.cache\chocolatey.cache" + 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" + } } } Context "When Get-DevSetupCachePath returns a different path" { It "Should append chocolatey.cache to the returned path" { - Mock Get-DevSetupCachePath { return "D:\DevSetupCache" } - $result = Get-ChocolateyCacheFile - $result | Should -Be "D:\DevSetupCache\chocolatey.cache" + 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" + } } } diff --git a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.Tests.ps1 index 100e8d1..e5712e4 100644 --- a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.Tests.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.Tests.ps1 @@ -1,7 +1,14 @@ BeforeAll { . $PSScriptRoot\Get-ChocolateyPackageDependencies.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Get-EnvironmentVariable.ps1 Mock Write-Debug { } - $isPS5 = $PSVersionTable.PSVersion.Major -eq 5 + 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" } + } } Describe "Get-ChocolateyPackageDependencies" { @@ -26,10 +33,24 @@ Describe "Get-ChocolateyPackageDependencies" { Context "When nuspec files have no dependencies" { It "Should return $null in PS5, empty array in PS6+" { Mock Test-Path { return $true } - Mock Get-ChildItem { - @( - [PSCustomObject]@{ FullName = "C:\choco\lib\foo\foo.nuspec" } - ) + 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" } + ) + } } Mock Get-Content { '' @@ -42,11 +63,25 @@ Describe "Get-ChocolateyPackageDependencies" { Context "When nuspec files have dependencies including chocolatey system packages" { It "Should return only non-chocolatey dependencies" { Mock Test-Path { return $true } - Mock Get-ChildItem { - @( - [PSCustomObject]@{ FullName = "C:\choco\lib\foo\foo.nuspec" } - ) - } + 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" } + ) + } + } Mock Get-Content { ' @@ -65,11 +100,27 @@ Describe "Get-ChocolateyPackageDependencies" { Context "When multiple nuspec files have overlapping dependencies" { It "Should return all dependencies including duplicates" { Mock Test-Path { return $true } - Mock Get-ChildItem { - @( - [PSCustomObject]@{ FullName = "C:\choco\lib\foo\foo.nuspec" }, - [PSCustomObject]@{ FullName = "C:\choco\lib\bar\bar.nuspec" } - ) + 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" } + ) + } } $nuspecs = @( ' diff --git a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.ps1 b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.ps1 index 0c4e424..f0b8e03 100644 --- a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Retrieves all package dependencies from installed Chocolatey packages. @@ -56,19 +56,20 @@ Function Get-ChocolateyPackageDependencies { [CmdletBinding()] + [OutputType([array])] Param() write-Debug "Retrieving Chocolatey package dependencies..." $packageDependencies = @() - $chocolateyInstallPath = Join-Path $Env:ChocolateyInstall lib + $chocolateyInstallPath = Join-Path (Get-EnvironmentVariable ChocolateyInstall) lib if (-not (Test-Path $chocolateyInstallPath)) { Write-Debug "Chocolatey installation path not found: $chocolateyInstallPath" return $packageDependencies } - Get-ChildItem $chocolateyInstallPath -Recurse "*.nuspec" | % { - $dependencies = ([xml](Get-Content $_.FullName)).package.metadata.dependencies.dependency | Foreach-Object { + Get-ChildItem $chocolateyInstallPath -Recurse "*.nuspec" | ForEach-Object { + $dependencies = ([xml](Get-Content $_.FullName)).package.metadata.dependencies.dependency | ForEach-Object { if (-not ($_.id -like "chocolatey*")) { $_.id } diff --git a/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.Tests.ps1 index 2299ecf..19c32ae 100644 --- a/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.Tests.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.Tests.ps1 @@ -1,9 +1,10 @@ BeforeAll { - . $PSScriptRoot\Install-ChocolateyPackage.ps1 - . $PSScriptRoot\Test-ChocolateyPackageInstalled.ps1 - . $PSScriptRoot\Uninstall-ChocolateyPackage.ps1 - . $PSScriptRoot\Write-ChocolateyCache.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 + . (Join-Path $PSScriptRoot "Install-ChocolateyPackage.ps1") + . (Join-Path $PSScriptRoot "Test-ChocolateyPackageInstalled.ps1") + . (Join-Path $PSScriptRoot "Uninstall-ChocolateyPackage.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 } @@ -20,7 +21,7 @@ Describe "Install-ChocolateyPackage" { Context "When not running as administrator" { It "Should throw and return false" { Mock Test-RunningAsAdmin { $false } - $result = Install-ChocolateyPackage -PackageName "git" + $result = Install-ChocolateyPackage -PackageName "azshell" $result | Should -Be $false Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "administrator privileges" } } @@ -29,25 +30,29 @@ Describe "Install-ChocolateyPackage" { Context "When package is already installed and version matches" { It "Should return true immediately" { Mock Test-ChocolateyPackageInstalled { - [InstalledState]::Pass -bor [InstalledState]::Installed + return ([InstalledState]::Pass) } - $result = Install-ChocolateyPackage -PackageName "git" + $result = Install-ChocolateyPackage -PackageName "azshell" $result | Should -Be $true } } Context "When package is installed but version does not match" { It "Should uninstall and reinstall the package" { - Mock Test-ChocolateyPackageInstalled { [InstalledState]::Installed } + Mock Test-ChocolateyPackageInstalled { + return ([InstalledState]::Installed + [InstalledState]::MinimumVersionMet + [InstalledState]::GlobalVersionMet) + } $script:uninstallCalled = $false Mock Uninstall-ChocolateyPackage -MockWith { param($PackageName) $script:uninstallCalled = $true $true } - $LASTEXITCODE = 0 - Mock Invoke-Command { } - $result = Install-ChocolateyPackage -PackageName "git" + $script:LASTEXITCODE = 0 + Mock Invoke-Command { + $script:LASTEXITCODE = 0 + } + $result = Install-ChocolateyPackage -PackageName "azshell" $result | Should -Be $true $script:uninstallCalled | Should -Be $true } @@ -55,14 +60,14 @@ Describe "Install-ChocolateyPackage" { Context "When installing with version and params" { It "Should build the correct choco command" { - $LASTEXITCODE = 0 + $script:LASTEXITCODE = 0 $script:paramsPassed = $null - Mock Test-ChocolateyPackageInstalled { [InstalledState]::NotInstalled } + Mock Test-ChocolateyPackageInstalled { return ([InstalledState]::NotInstalled) } Mock Invoke-Command -MockWith { param($ScriptBlock) $script:paramsPassed = $ScriptBlock.ToString() } - $result = Install-ChocolateyPackage -PackageName "git" -Version "2.42.0" -Param "/silent" + $result = Install-ChocolateyPackage -PackageName "azshell" -Version "0.2.2" -Param "/silent" $result | Should -Be $true # You can add more checks for $paramsPassed if needed } @@ -70,9 +75,9 @@ Describe "Install-ChocolateyPackage" { Context "When installation fails (non-zero exit code)" { It "Should write error and return false" { - $LASTEXITCODE = 1 - Mock Test-ChocolateyPackageInstalled { [InstalledState]::NotInstalled } - $result = Install-ChocolateyPackage -PackageName "git" + $script:LASTEXITCODE = 1 + Mock Test-ChocolateyPackageInstalled { return ([InstalledState]::NotInstalled) } + $result = Install-ChocolateyPackage -PackageName "azshell" $result | Should -Be $false Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to install" } } @@ -80,10 +85,10 @@ Describe "Install-ChocolateyPackage" { Context "When Write-ChocolateyCache fails after install" { It "Should write warning and return false" { - $LASTEXITCODE = 0 - Mock Test-ChocolateyPackageInstalled { [InstalledState]::NotInstalled } + $script:LASTEXITCODE = 0 + Mock Test-ChocolateyPackageInstalled { return ([InstalledState]::NotInstalled) } Mock Write-ChocolateyCache { $false } - $result = Install-ChocolateyPackage -PackageName "git" + $result = Install-ChocolateyPackage -PackageName "azshell" $result | Should -Be $false Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "Failed to write Chocolatey cache" } } @@ -92,7 +97,7 @@ Describe "Install-ChocolateyPackage" { Context "When an exception occurs during install" { It "Should write error and return false" { Mock Test-ChocolateyPackageInstalled { throw "Unexpected error" } - $result = Install-ChocolateyPackage -PackageName "git" + $result = Install-ChocolateyPackage -PackageName "azshell" $result | Should -Be $false Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Error checking/installing package" } } diff --git a/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackages.ps1 b/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackages.ps1 index 3583f32..d50e744 100644 --- a/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackages.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackages.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Installs Chocolatey packages from YAML configuration data. diff --git a/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackages.ps1 b/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackages.ps1 index 35943c1..5939ac0 100644 --- a/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackages.ps1 +++ b/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackages.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Uninstalls multiple Chocolatey packages from the system based on YAML configuration. diff --git a/DevSetup/Private/Providers/Core/Install-CoreDependencies.Tests.ps1 b/DevSetup/Private/Providers/Core/Install-CoreDependencies.Tests.ps1 index 8673921..51bb937 100644 --- a/DevSetup/Private/Providers/Core/Install-CoreDependencies.Tests.ps1 +++ b/DevSetup/Private/Providers/Core/Install-CoreDependencies.Tests.ps1 @@ -1,15 +1,15 @@ BeforeAll { - . $PSScriptRoot\Install-CoreDependencies.ps1 - . $PSScriptRoot\Install-Nuget.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Get-DevSetupManifest.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Providers\Powershell\Install-PowerShellModule.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Providers\Chocolatey\Install-Chocolatey.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Providers\Chocolatey\Install-ChocolateyPackage.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Providers\Scoop\Install-Scoop.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Providers\Homebrew\Install-Homebrew.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-OperatingSystem.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 + . (Join-Path $PSScriptRoot "Install-CoreDependencies.ps1") + . (Join-Path $PSScriptRoot "Install-Nuget.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Get-DevSetupManifest.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Providers\Powershell\Install-PowershellModule.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Providers\Chocolatey\Install-Chocolatey.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Providers\Chocolatey\Install-ChocolateyPackage.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Providers\Scoop\Install-Scoop.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Providers\Homebrew\Install-Homebrew.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Test-OperatingSystem.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1") Mock Write-StatusMessage { } Mock Write-Host { } Mock Write-Warning { } @@ -23,7 +23,7 @@ Describe "Install-CoreDependencies" { Context "When NuGet installation fails" { It "Should return false and write error" { Mock Install-NuGet { return $false } - Mock Test-OperatingSystem { param($os) return $false } + Mock Test-OperatingSystem { param($os) return $true } $result = Install-CoreDependencies $result | Should -Be $false } @@ -33,7 +33,7 @@ Describe "Install-CoreDependencies" { It "Should return true and write warning" { Mock Install-NuGet { return $true } Mock Get-DevSetupManifest { return $null } - Mock Test-OperatingSystem { param($os) return $false } + Mock Test-OperatingSystem { param($os) return $true } $result = Install-CoreDependencies $result | Should -Be $true @@ -49,7 +49,7 @@ Describe "Install-CoreDependencies" { Mock Get-DevSetupManifest { return @{ RequiredModules = @("posh-git", "PSReadLine") } } Mock Test-OperatingSystem { param($os) return $false } $script:callCount = 0 - Mock Install-PowerShellModule -MockWith { + Mock Install-PowershellModule -MockWith { param($ModuleName, $Force, $AllowClobber, $Scope) $script:callCount++ if ($script:callCount -eq 1) { return $true } @@ -64,7 +64,7 @@ Describe "Install-CoreDependencies" { It "Should skip empty module names and return true" { Mock Install-NuGet { return $true } Mock Get-DevSetupManifest { return @{ RequiredModules = @("posh-git", $null, "PSReadLine") } } - Mock Install-PowerShellModule { return $true } + Mock Install-PowershellModule { return $true } Mock Test-OperatingSystem { param($os) return $false } $result = Install-CoreDependencies $result | Should -Be $true @@ -75,7 +75,7 @@ Describe "Install-CoreDependencies" { It "Should install everything and return true" { Mock Install-NuGet { return $true } Mock Get-DevSetupManifest { return @{ RequiredModules = @("posh-git", "PSReadLine") } } - Mock Install-PowerShellModule { return $true } + Mock Install-PowershellModule { return $true } Mock Install-Chocolatey { return $true } Mock Install-ChocolateyPackage { return $true } Mock Install-Scoop { return $true } @@ -89,7 +89,7 @@ Describe "Install-CoreDependencies" { It "Should return false and write error" { Mock Install-NuGet { return $true } Mock Get-DevSetupManifest { return @{ RequiredModules = @("posh-git") } } - Mock Install-PowerShellModule { return $true } + Mock Install-PowershellModule { return $true } Mock Install-Chocolatey { return $false } Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $true } else { return $false } } $result = Install-CoreDependencies @@ -101,7 +101,7 @@ Describe "Install-CoreDependencies" { It "Should return false and write error" { Mock Install-NuGet { return $true } Mock Get-DevSetupManifest { return @{ RequiredModules = @("posh-git") } } - Mock Install-PowerShellModule { return $true } + Mock Install-PowershellModule { return $true } Mock Install-Chocolatey { return $true } Mock Install-ChocolateyPackage { return $false } Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $true } else { return $false } } @@ -114,7 +114,7 @@ Describe "Install-CoreDependencies" { It "Should return false and write error" { Mock Install-NuGet { return $true } Mock Get-DevSetupManifest { return @{ RequiredModules = @("posh-git") } } - Mock Install-PowerShellModule { return $true } + Mock Install-PowershellModule { return $true } Mock Install-Chocolatey { return $true } Mock Install-ChocolateyPackage { return $true } Mock Install-Scoop { return $false } @@ -128,7 +128,7 @@ Describe "Install-CoreDependencies" { It "Should skip Windows-only installs and return true" { Mock Install-NuGet { return $true } Mock Get-DevSetupManifest { return @{ RequiredModules = @("posh-git") } } - Mock Install-PowerShellModule { return $true } + Mock Install-PowershellModule { return $true } Mock Test-OperatingSystem { param($os) return $false } $result = Install-CoreDependencies $result | Should -Be $true diff --git a/DevSetup/Private/Providers/Core/Install-CoreDependencies.ps1 b/DevSetup/Private/Providers/Core/Install-CoreDependencies.ps1 index 008c6bc..5ef3ceb 100644 --- a/DevSetup/Private/Providers/Core/Install-CoreDependencies.ps1 +++ b/DevSetup/Private/Providers/Core/Install-CoreDependencies.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Installs core dependencies required for the DevSetup module to function properly. @@ -74,7 +74,7 @@ Function Install-CoreDependencies { # Install NuGet PackageProvider if ((Test-OperatingSystem -Windows)) { if (-not (Install-NuGet)) { - Write-Error "Failed to install NuGet PackageProvider" + Write-StatusMessage "Failed to install NuGet PackageProvider" -Verbosity Error return $false } } @@ -82,21 +82,21 @@ Function Install-CoreDependencies { # Get required modules from DevSetup manifest $manifest = Get-DevSetupManifest if (-not $manifest -or -not $manifest.RequiredModules) { - Write-Warning "No required modules found in DevSetup manifest" + Write-StatusMessage "No required modules found in DevSetup manifest" -Verbosity Warning return $true } # Install each required PowerShell module foreach ($moduleName in $manifest.RequiredModules) { if (-not $moduleName -or [string]::IsNullOrEmpty($moduleName)) { - Write-Warning "Skipping empty module name" + Write-StatusMessage "Skipping empty module name" -Verbosity Warning continue } Write-StatusMessage "- Installing powershell module: $moduleName" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline if (-not (Install-PowerShellModule -ModuleName $moduleName -Force -AllowClobber -Scope 'CurrentUser')) { Write-StatusMessage "[FAILED]" -ForegroundColor Red - Write-Error "Failed to install required PowerShell module: $moduleName" + Write-StatusMessage "Failed to install required PowerShell module: $moduleName" -Verbosity Error return $false } Write-StatusMessage "[OK]" -ForegroundColor Green @@ -105,7 +105,7 @@ Function Install-CoreDependencies { if ((Test-OperatingSystem -Windows)) { # Install Chocolatey first if (-not (Install-Chocolatey)) { - Write-Error "Cannot proceed without Chocolatey" + Write-StatusMessage "Cannot proceed without Chocolatey" -Verbosity Error return $false } @@ -113,7 +113,7 @@ Function Install-CoreDependencies { Write-StatusMessage "- Installing Git package via Chocolatey" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline if (-not (Install-ChocolateyPackage -PackageName "git" -Version 2.50.1)) { Write-StatusMessage "[FAILED]" -ForegroundColor Red - Write-Error "Failed to install Git package" + Write-StatusMessage "Failed to install Git package" -Verbosity Error return $false } else { Write-StatusMessage "[OK]" -ForegroundColor Green @@ -123,12 +123,12 @@ Function Install-CoreDependencies { # Install Scoop PackageProvider if (-not (Install-Scoop)) { - Write-Error "Failed to install Scoop PackageProvider" + Write-StatusMessage "Failed to install Scoop PackageProvider" -Verbosity Error return $false } } else { if (-not (Install-Homebrew)) { - Write-Error "Failed to install Homebrew" + Write-StatusMessage "Failed to install Homebrew" -Verbosity Error return $false } } diff --git a/DevSetup/Private/Providers/Core/Install-Nuget.ps1 b/DevSetup/Private/Providers/Core/Install-Nuget.ps1 index e2e52af..68365b4 100644 --- a/DevSetup/Private/Providers/Core/Install-Nuget.ps1 +++ b/DevSetup/Private/Providers/Core/Install-Nuget.ps1 @@ -66,7 +66,7 @@ Function Install-Nuget { try { # Check if we're on Windows - NuGet PackageProvider is Windows-only if (-not (Test-OperatingSystem -Windows)) { - Write-Host "NuGet PackageProvider is not available on this platform. Skipping installation." -ForegroundColor Yellow + Write-StatusMessage "NuGet PackageProvider is not available on this platform. Skipping installation." -ForegroundColor Yellow return $true } @@ -79,7 +79,7 @@ Function Install-Nuget { $nugetProvider = Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue Write-StatusMessage "- Installing NuGet PackageProvider" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline if ($nugetProvider) { - #Write-Host "NuGet PackageProvider is already installed (version: $($nugetProvider.Version))" -ForegroundColor Green + Write-StatusMessage "NuGet PackageProvider is already installed (version: $($nugetProvider.Version))" -ForegroundColor Green -Verbosity Verbose Write-StatusMessage "[OK]" -ForegroundColor Green } else { #Write-Host "Installing NuGet PackageProvider..." -ForegroundColor Cyan @@ -88,7 +88,7 @@ Function Install-Nuget { # Verify installation $nugetProvider = Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue if ($nugetProvider) { - #Write-Host "NuGet PackageProvider successfully installed (version: $($nugetProvider.Version))" -ForegroundColor Green + Write-StatusMessage "NuGet PackageProvider successfully installed (version: $($nugetProvider.Version))" -ForegroundColor Green -Verbosity Verbose Write-StatusMessage "[OK]" -ForegroundColor Green } else { throw "Failed to install NuGet PackageProvider" @@ -112,7 +112,8 @@ Function Install-Nuget { return $true } catch { - Write-Error "Error checking/installing NuGet: $_" + Write-StatusMessage "Error checking/installing NuGet: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error return $false } } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Find-Homebrew.Tests.ps1 b/DevSetup/Private/Providers/Homebrew/Find-Homebrew.Tests.ps1 new file mode 100644 index 0000000..2304f07 --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Find-Homebrew.Tests.ps1 @@ -0,0 +1,114 @@ +BeforeAll { + . $PSScriptRoot\Find-Homebrew.ps1 +} + +Describe "Find-Homebrew" { + Context "When brew is found in PATH" { + It "should return the path from Get-Command" { + Mock Get-Command { [PSCustomObject]@{ Path = "/usr/local/bin/brew" } } + Mock Test-Path { $false } # Not needed since Get-Command succeeds + + $result = Find-Homebrew + $result | Should -Be "/usr/local/bin/brew" + Assert-MockCalled Get-Command -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 0 -Scope It # Test-Path not called if Get-Command succeeds + } + } + + Context "When brew is not in PATH but found in test paths" { + It "should return the first matching test path" { + Mock Get-Command { $null } + Mock Test-Path { + Param($Path) + switch ($Path) { + "/usr/local/bin/brew" { $true } + default { $false } + } + } + + $result = Find-Homebrew + $result | Should -Be "/usr/local/bin/brew" + Assert-MockCalled Get-Command -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 1 -Scope It -ParameterFilter { $Path -eq "/usr/local/bin/brew" } + } + + It "should return the second matching test path if first fails" { + Mock Get-Command { $null } + Mock Test-Path { + Param($Path) + switch ($Path) { + "/usr/local/bin/brew" { $false } + "/opt/homebrew/bin/brew" { $true } + default { $false } + } + } + + $result = Find-Homebrew + $result | Should -Be "/opt/homebrew/bin/brew" + Assert-MockCalled Get-Command -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 2 -Scope It # Checks first two paths + } + + It "should return the third matching test path if first two fail" { + Mock Get-Command { $null } + Mock Test-Path { + Param($Path) + switch ($Path) { + "/usr/local/bin/brew" { $false } + "/opt/homebrew/bin/brew" { $false } + "/home/linuxbrew/.linuxbrew/bin/brew" { $true } + default { $false } + } + } + + $result = Find-Homebrew + $result | Should -Be "/home/linuxbrew/.linuxbrew/bin/brew" + Assert-MockCalled Get-Command -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 3 -Scope It # Checks all three paths + } + } + + Context "When brew is not found anywhere" { + It "should return null" { + Mock Get-Command { $null } + Mock Test-Path { $false } + + $result = Find-Homebrew + $result | Should -Be $null + Assert-MockCalled Get-Command -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 3 -Scope It # Checks all three paths + } + } + + Context "Cross-platform compatibility" { + It "should handle Windows (where brew is unlikely to be found)" { + Mock Get-Command { $null } + Mock Test-Path { $false } + + $result = Find-Homebrew + $result | Should -Be $null + } + + It "should handle Linux paths" { + Mock Get-Command { $null } + Mock Test-Path { + Param($Path) + $Path -eq "/home/linuxbrew/.linuxbrew/bin/brew" + } + + $result = Find-Homebrew + $result | Should -Be "/home/linuxbrew/.linuxbrew/bin/brew" + } + + It "should handle macOS paths" { + Mock Get-Command { $null } + Mock Test-Path { + Param($Path) + $Path -eq "/opt/homebrew/bin/brew" + } + + $result = Find-Homebrew + $result | Should -Be "/opt/homebrew/bin/brew" + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Find-Homebrew.ps1 b/DevSetup/Private/Providers/Homebrew/Find-Homebrew.ps1 new file mode 100644 index 0000000..1fa028a --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Find-Homebrew.ps1 @@ -0,0 +1,25 @@ +Function Find-Homebrew { + [CmdletBinding()] + [OutputType([string])] + Param( + ) + + $TestPaths = @( + '/usr/local/bin/brew', + '/opt/homebrew/bin/brew', + '/home/linuxbrew/.linuxbrew/bin/brew' + ) + + # Check if Homebrew is installed + $Path = (Get-Command "brew" -ErrorAction SilentlyContinue).Path + if ([string]::IsNullOrEmpty($Path)) { + foreach ($p in $TestPaths) { + if (Test-Path $p) { + $Path = $p + break + } + } + } + + return $Path +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Get-HomebrewCacheFile.Tests.ps1 b/DevSetup/Private/Providers/Homebrew/Get-HomebrewCacheFile.Tests.ps1 new file mode 100644 index 0000000..e83d4c2 --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Get-HomebrewCacheFile.Tests.ps1 @@ -0,0 +1,41 @@ +BeforeAll { + . (Join-Path $PSScriptRoot Get-HomebrewCacheFile.ps1) + . (Join-Path $PSScriptRoot ..\..\..\..\DevSetup\Private\Utils\Get-DevSetupCachePath.ps1) + . (Join-Path $PSScriptRoot ..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1) + if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { + Mock Get-DevSetupCachePath { return "$TestDrive\Users\TestUser\devsetup\.cache" } + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { + Mock Get-DevSetupCachePath { return "$TestDrive/home/testuser/devsetup/.cache" } + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { + Mock Get-DevSetupCachePath { return "$TestDrive/Users/testuser/devsetup/.cache" } + } + Mock Write-StatusMessage { } +} + +Describe "Get-HomebrewCacheFile" { + Context "Windows" { + It "should return the correct cache file path on Windows" { + $result = Get-HomebrewCacheFile + if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { + $result | Should -Be "$TestDrive\Users\TestUser\devsetup\.cache\homebrew.cache" + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { + $result | Should -Be "$TestDrive/home/testuser/devsetup/.cache/homebrew.cache" + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { + $result | Should -Be "$TestDrive/Users/testuser/devsetup/.cache/homebrew.cache" + } + + Assert-MockCalled Get-DevSetupCachePath -Exactly 1 -Scope It + } + } + + Context "When Get-DevSetupCachePath returns null" { + It "should return null" { + Mock Get-DevSetupCachePath { return $null } + + $result = Get-HomebrewCacheFile + $result | Should -Be $null + Assert-MockCalled Get-DevSetupCachePath -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It -ParameterFilter { $Verbosity -eq "Error" } + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Get-HomebrewCacheFile.ps1 b/DevSetup/Private/Providers/Homebrew/Get-HomebrewCacheFile.ps1 new file mode 100644 index 0000000..8124e9f --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Get-HomebrewCacheFile.ps1 @@ -0,0 +1,13 @@ +Function Get-HomebrewCacheFile { + [CmdletBinding()] + Param() + + try { + $CacheFile = Join-Path -Path (Get-DevSetupCachePath) -ChildPath "homebrew.cache" + } catch { + Write-StatusMessage "Failed to get Homebrew cache file path: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $null + } + return $CacheFile +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Get-HomebrewVersion.Tests.ps1 b/DevSetup/Private/Providers/Homebrew/Get-HomebrewVersion.Tests.ps1 new file mode 100644 index 0000000..40ead2f --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Get-HomebrewVersion.Tests.ps1 @@ -0,0 +1,111 @@ +BeforeAll { + . (Join-Path $PSScriptRoot Get-HomebrewVersion.ps1) + . (Join-Path $PSScriptRoot ..\..\..\..\DevSetup\Private\Providers\Homebrew\Test-HomebrewInstalled.ps1) + . (Join-Path $PSScriptRoot ..\..\..\..\DevSetup\Private\Providers\Homebrew\Find-Homebrew.ps1) + . (Join-Path $PSScriptRoot ..\..\..\..\DevSetup\Private\Utils\Invoke-ExternalCommand.ps1) + . (Join-Path $PSScriptRoot ..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1) +} + +Describe "Get-HomebrewVersion" { + Context "When Homebrew is not installed" { + It "should return null" { + Mock Test-HomebrewInstalled { $false } + Mock Write-StatusMessage { } + + $result = Get-HomebrewVersion + $result | Should -Be $null + Assert-MockCalled Test-HomebrewInstalled -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Homebrew is not installed" } + } + } + + Context "When Homebrew path is not found" { + It "should return null" { + Mock Test-HomebrewInstalled { $true } + Mock Find-Homebrew { $null } + Mock Write-StatusMessage { } + + $result = Get-HomebrewVersion + $result | Should -Be $null + Assert-MockCalled Test-HomebrewInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Homebrew -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Homebrew installation not found" } + } + } + + Context "When version is successfully retrieved" { + It "should return the version string" { + Mock Test-HomebrewInstalled { $true } + Mock Find-Homebrew { "/usr/local/bin/brew" } + Mock Invoke-ExternalCommand { "Homebrew 3.5.10" } + Mock Write-StatusMessage { } + + $result = Get-HomebrewVersion + $result | Should -Be "3.5.10" + Assert-MockCalled Test-HomebrewInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Homebrew -Exactly 1 -Scope It + Assert-MockCalled Invoke-ExternalCommand -Exactly 1 -Scope It -ParameterFilter { $Command -eq "/usr/local/bin/brew" -and $Arguments -contains "--version" } + } + } + + Context "When output does not contain a version" { + It "should return null" { + Mock Test-HomebrewInstalled { $true } + Mock Find-Homebrew { "/usr/local/bin/brew" } + Mock Invoke-ExternalCommand { "Homebrew version not available" } + Mock Write-StatusMessage { } + + $result = Get-HomebrewVersion + $result | Should -Be $null + Assert-MockCalled Test-HomebrewInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Homebrew -Exactly 1 -Scope It + Assert-MockCalled Invoke-ExternalCommand -Exactly 1 -Scope It + } + } + + Context "When Invoke-ExternalCommand throws an exception" { + It "should return null" { + Mock Test-HomebrewInstalled { $true } + Mock Find-Homebrew { "/usr/local/bin/brew" } + Mock Invoke-ExternalCommand { throw "Command failed" } + Mock Write-StatusMessage { } + + $result = Get-HomebrewVersion + $result | Should -Be $null + Assert-MockCalled Test-HomebrewInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Homebrew -Exactly 1 -Scope It + Assert-MockCalled Invoke-ExternalCommand -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to get Homebrew version" } + } + } + + Context "Cross-platform compatibility" { + It "should work on Windows (where Homebrew is unlikely)" { + Mock Test-HomebrewInstalled { $false } + Mock Write-StatusMessage { } + + $result = Get-HomebrewVersion + $result | Should -Be $null + } + + It "should work on Linux" { + Mock Test-HomebrewInstalled { $true } + Mock Find-Homebrew { "/home/linuxbrew/.linuxbrew/bin/brew" } + Mock Invoke-ExternalCommand { "Homebrew 3.5.10" } + Mock Write-StatusMessage { } + + $result = Get-HomebrewVersion + $result | Should -Be "3.5.10" + } + + It "should work on macOS" { + Mock Test-HomebrewInstalled { $true } + Mock Find-Homebrew { "/opt/homebrew/bin/brew" } + Mock Invoke-ExternalCommand { "Homebrew 3.5.10" } + Mock Write-StatusMessage { } + + $result = Get-HomebrewVersion + $result | Should -Be "3.5.10" + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Get-HomebrewVersion.ps1 b/DevSetup/Private/Providers/Homebrew/Get-HomebrewVersion.ps1 new file mode 100644 index 0000000..6596d9c --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Get-HomebrewVersion.ps1 @@ -0,0 +1,37 @@ +Function Get-HomebrewVersion { + [CmdletBinding()] + [OutputType([string])] + Param( + ) + + try { + # Get Homebrew version + if( -not (Test-HomebrewInstalled) ) { + Write-StatusMessage "Homebrew is not installed" -Verbosity Verbose + return $null + } + + $HomebrewPath = Find-Homebrew + if (-not $HomebrewPath) { + Write-StatusMessage "Homebrew installation not found" -Verbosity Verbose + return $null + } + + $BrewArgs = @{ + Command = $HomebrewPath + Arguments = @("--version") + } + + $HomeBrewVersion = (Invoke-ExternalCommand @BrewArgs) -match "([0-9]+\.[0-9]+\.[0-9]+)" + if ($HomeBrewVersion) { + $version = $matches[1] + return $version + } else { + return $null + } + } catch { + Write-StatusMessage "Failed to get Homebrew version: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $null + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Install-Homebrew.Tests.ps1 b/DevSetup/Private/Providers/Homebrew/Install-Homebrew.Tests.ps1 new file mode 100644 index 0000000..85368e4 --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Install-Homebrew.Tests.ps1 @@ -0,0 +1,202 @@ +BeforeAll { + . (Join-Path $PSScriptRoot "Install-Homebrew.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Test-HasSudoAccess.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Providers\Homebrew\Find-Homebrew.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Invoke-ExternalCommand.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Get-EnvironmentVariable.ps1") +} + +Describe "Install-Homebrew" { + Context "When sudo access is not available" { + It "should return false" { + Mock Test-HasSudoAccess { $false } + Mock Write-StatusMessage { } + + $result = Install-Homebrew + $result | Should -Be $false + Assert-MockCalled Test-HasSudoAccess -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 3 -Scope It # One for checking sudo, one for failure + } + } + + Context "When Homebrew is already installed" { + It "should return true without installing" { + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { "/usr/local/bin/brew" } + Mock Write-StatusMessage { } + Mock Invoke-ExternalCommand { return $true } + + $result = Install-Homebrew + $result | Should -Be $true + Assert-MockCalled Test-HasSudoAccess -Exactly 1 -Scope It + Assert-MockCalled Find-Homebrew -Exactly 1 -Scope It + Assert-MockCalled Invoke-ExternalCommand -Exactly 0 -Scope It # No installation needed + } + } + + Context "When installation succeeds and shell is bash" { + It "should install Homebrew, add to .bashrc, and return true" { + $script:callCount = 0 + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { + $script:callCount++ + if ($script:callCount -eq 1) { return $null } # First call returns null + else { return "/usr/local/bin/brew" } # Second call returns path + } + Mock Invoke-ExternalCommand { $true } + Mock Write-StatusMessage { } + Mock Get-EnvironmentVariable { + Param($Name) + switch($Name) { + "SHELL" { return "/bin/bash" } + "HOME" { return "/home/testuser" } + } + } + Mock Add-Content { } + + $result = Install-Homebrew + $result | Should -Be $true + Assert-MockCalled Test-HasSudoAccess -Exactly 1 -Scope It + Assert-MockCalled Find-Homebrew -Exactly 2 -Scope It # Once before install, once after + Assert-MockCalled Invoke-ExternalCommand -Exactly 1 -Scope It + Assert-MockCalled Add-Content -Exactly 2 -Scope It # Blank line and shellenv line + } + } + + Context "When installation succeeds and shell is zsh" { + It "should install Homebrew, add to .zshrc, and return true" { + $script:callCount = 0 + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { + $script:callCount++ + if ($script:callCount -eq 1) { return $null } + else { return "/usr/local/bin/brew" } + } + Mock Invoke-ExternalCommand { "Installation successful" } + Mock Write-StatusMessage { } + Mock Get-EnvironmentVariable { + Param($Name) + switch($Name) { + "SHELL" { return "/bin/zsh" } + "HOME" { return "/home/testuser" } + } + } + Mock Add-Content { } + + $result = Install-Homebrew + $result | Should -Be $true + Assert-MockCalled Test-HasSudoAccess -Exactly 1 -Scope It + Assert-MockCalled Find-Homebrew -Exactly 2 -Scope It + Assert-MockCalled Invoke-ExternalCommand -Exactly 1 -Scope It + Assert-MockCalled Add-Content -Exactly 2 -Scope It + } + } + + Context "When installation succeeds and shell is unknown" { + It "should install Homebrew, warn about shell, and return true" { + $script:callCount = 0 + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { + $script:callCount++ + if ($script:callCount -eq 1) { return $null } + else { return "/usr/local/bin/brew" } + } + Mock Invoke-ExternalCommand { "Installation successful" } + Mock Write-StatusMessage { } + Mock Get-EnvironmentVariable { + Param($Name) + switch($Name) { + "SHELL" { return "/bin/fish" } + "HOME" { return "/home/testuser" } + } + } + Mock Add-Content { } + + $result = Install-Homebrew + $result | Should -Be $true + Assert-MockCalled Test-HasSudoAccess -Exactly 1 -Scope It + Assert-MockCalled Find-Homebrew -Exactly 2 -Scope It + Assert-MockCalled Invoke-ExternalCommand -Exactly 1 -Scope It + Assert-MockCalled Add-Content -Exactly 0 -Scope It # No shell config added + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "Unknown shell" } + } + } + + Context "When installation fails" { + It "should return false" { + $script:callCount = 0 + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { + $script:callCount++ + return $null # Always return null + } + Mock Invoke-ExternalCommand { "Installation failed" } + Mock Write-StatusMessage { } + + $result = Install-Homebrew + $result | Should -Be $false + Assert-MockCalled Test-HasSudoAccess -Exactly 1 -Scope It + Assert-MockCalled Find-Homebrew -Exactly 2 -Scope It + Assert-MockCalled Invoke-ExternalCommand -Exactly 1 -Scope It + } + } + + Context "Cross-platform compatibility" { + It "should handle Windows (where Homebrew installation is not supported)" { + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { $null } + Mock Invoke-ExternalCommand { throw "Not supported on Windows" } + Mock Write-StatusMessage { } + + $result = Install-Homebrew + $result | Should -Be $false + } + + It "should work on Linux" { + $script:callCount = 0 + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { + $script:callCount++ + if ($script:callCount -eq 1) { return $null } + else { return "/home/linuxbrew/.linuxbrew/bin/brew" } + } + Mock Invoke-ExternalCommand { $true } + Mock Write-StatusMessage { } + Mock Get-EnvironmentVariable { + Param($Name) + switch($Name) { + "SHELL" { return "/bin/bash" } + "HOME" { return "/home/testuser" } + } + } + Mock Add-Content { } + + $result = Install-Homebrew + $result | Should -Be $true + } + + It "should work on macOS" { + $script:callCount = 0 + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { + $script:callCount++ + if ($script:callCount -eq 1) { return $null } + else { return "/opt/homebrew/bin/brew" } + } + Mock Invoke-ExternalCommand { $true } + Mock Write-StatusMessage { } + Mock Get-EnvironmentVariable { + Param($Name) + switch($Name) { + "SHELL" { return "/bin/zsh" } + "HOME" { return "/Users/TestUser" } + } + } + Mock Add-Content { } + + $result = Install-Homebrew + $result | Should -Be $true + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Install-Homebrew.ps1 b/DevSetup/Private/Providers/Homebrew/Install-Homebrew.ps1 index b11664a..11885bb 100644 --- a/DevSetup/Private/Providers/Homebrew/Install-Homebrew.ps1 +++ b/DevSetup/Private/Providers/Homebrew/Install-Homebrew.ps1 @@ -1,23 +1,62 @@ Function Install-Homebrew { [CmdletBinding()] + [OutputType([bool])] Param( ) - # Install Homebrew - Write-StatusMessage "- Installing Homebrew package manager" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline - if (-not (Get-Command "brew" -ErrorAction SilentlyContinue)) { - # Installation command for Homebrew (Linux/Mac) - $installCmd = 'NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' - (bash -c "$installCmd") *> $null - if (-not (Get-Command "brew" -ErrorAction SilentlyContinue)) { + try { + Write-StatusMessage "- Checking for sudo access" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline + if ((Test-HasSudoAccess)) { + Write-StatusMessage "[OK]" -ForegroundColor Green + } else { Write-StatusMessage "[FAILED]" -ForegroundColor Red + Write-StatusMessage "Sudo access is required to install Homebrew. Please run this script with a user that has sudo privileges." -Verbosity Warning return $false + } + + # Install Homebrew + Write-StatusMessage "- Installing Homebrew package manager" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline + if (-not (Find-Homebrew)) { + # Installation command for Homebrew (Linux/Mac) + $installCmd = 'NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' + $BrewArgs = @{ + Command = "bash" + Arguments = @("-c", "$installCmd") + } + Invoke-ExternalCommand @BrewArgs *> $null + $HomebrewPath = Find-Homebrew + if ([string]::IsNullOrEmpty($HomebrewPath)) { + Write-StatusMessage "[FAILED]" -ForegroundColor Red + return $false + } else { + Write-StatusMessage "[OK]" -ForegroundColor Green + switch ((Get-EnvironmentVariable SHELL)) { + { $_ -like "*zsh*" } { + Write-StatusMessage "- Adding Homebrew path to $HOME/.zshrc" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline + Add-Content -Path ([string]::format("{0}/.zshrc", (Get-EnvironmentVariable HOME))) -Value "" + Add-Content -Path ([string]::format("{0}/.zshrc", (Get-EnvironmentVariable HOME))) -Value ([string]::Format('eval "$({0} shellenv)"', $HomebrewPath)) + Write-StatusMessage "[OK]" -ForegroundColor Green + } + { $_ -like "*bash*" } { + Write-StatusMessage "- Adding Homebrew path to $HOME/.bashrc" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline + Add-Content -Path ([string]::format("{0}/.bashrc", (Get-EnvironmentVariable HOME))) -Value "" + Add-Content -Path ([string]::format("{0}/.bashrc", (Get-EnvironmentVariable HOME))) -Value ([string]::Format('eval "$({0} shellenv)"', $HomebrewPath)) + Write-StatusMessage "[OK]" -ForegroundColor Green + } + default { + Write-StatusMessage "Unknown shell: $($env:SHELL). You may need to manually add Homebrew to your PATH." -Verbosity Warning + } + } + + return $true + } } else { Write-StatusMessage "[OK]" -ForegroundColor Green return $true } - } else { - Write-StatusMessage "[OK]" -ForegroundColor Green - return $true + } catch { + Write-StatusMessage "An error occurred while installing Homebrew: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + return $false } } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Install-HomebrewPackage.Tests.ps1 b/DevSetup/Private/Providers/Homebrew/Install-HomebrewPackage.Tests.ps1 new file mode 100644 index 0000000..c956814 --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Install-HomebrewPackage.Tests.ps1 @@ -0,0 +1,141 @@ +BeforeAll { + . (Join-Path $PSScriptRoot "Install-HomebrewPackage.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Test-HasSudoAccess.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Providers\Homebrew\Find-Homebrew.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Providers\Homebrew\Test-HomebrewPackageInstalled.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Invoke-ExternalCommand.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1") +} + +Describe "Install-HomebrewPackage" { + Context "When sudo access is not available" { + It "should return false" { + Mock Test-HasSudoAccess { $false } + Mock Write-StatusMessage { } + + $result = Install-HomebrewPackage -PackageName "git" + $result | Should -Be $false + Assert-MockCalled Test-HasSudoAccess -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 3 -Scope It # One for status, one for failure + } + } + + Context "When Homebrew is not installed" { + It "should return false" { + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { $null } + Mock Write-StatusMessage { } + + $result = Install-HomebrewPackage -PackageName "git" + $result | Should -Be $false + Assert-MockCalled Test-HasSudoAccess -Exactly 1 -Scope It + Assert-MockCalled Find-Homebrew -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 3 -Scope It + } + } + + Context "When package is already installed" { + It "should return true without installing" { + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { "/usr/local/bin/brew" } + Mock Test-HomebrewPackageInstalled { [InstalledState]::Pass } + Mock Write-StatusMessage { } + Mock Invoke-ExternalCommand { $true } + + $result = Install-HomebrewPackage -PackageName "git" + $result | Should -Be $true + Assert-MockCalled Test-HasSudoAccess -Exactly 1 -Scope It + Assert-MockCalled Find-Homebrew -Exactly 1 -Scope It + Assert-MockCalled Test-HomebrewPackageInstalled -Exactly 1 -Scope It + Assert-MockCalled Invoke-ExternalCommand -Exactly 0 -Scope It # No installation needed + } + } + + Context "When installation succeeds" { + It "should install the package and return true" { + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { "/usr/local/bin/brew" } + Mock Test-HomebrewPackageInstalled { [InstalledState]::NotInstalled } + Mock Invoke-ExternalCommand { $true } + Mock Write-StatusMessage { } + $global:LASTEXITCODE = 0 + + $result = Install-HomebrewPackage -PackageName "git" + $result | Should -Be $true + Assert-MockCalled Test-HasSudoAccess -Exactly 1 -Scope It + Assert-MockCalled Find-Homebrew -Exactly 2 -Scope It + Assert-MockCalled Test-HomebrewPackageInstalled -Exactly 1 -Scope It + Assert-MockCalled Invoke-ExternalCommand -Exactly 1 -Scope It + } + } + + Context "When installation fails" { + It "should return false" { + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { "/usr/local/bin/brew" } + Mock Test-HomebrewPackageInstalled { [InstalledState]::NotInstalled } + Mock Invoke-ExternalCommand { $false } + Mock Write-StatusMessage { } + $global:LASTEXITCODE = 1 + + $result = Install-HomebrewPackage -PackageName "git" + $result | Should -Be $false + Assert-MockCalled Test-HasSudoAccess -Exactly 1 -Scope It + Assert-MockCalled Find-Homebrew -Exactly 2 -Scope It + Assert-MockCalled Test-HomebrewPackageInstalled -Exactly 1 -Scope It + Assert-MockCalled Invoke-ExternalCommand -Exactly 1 -Scope It + } + } + + Context "When -WhatIf is used" { + It "should not perform the installation" { + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { "/usr/local/bin/brew" } + Mock Test-HomebrewPackageInstalled { [InstalledState]::Fail } + Mock Write-StatusMessage { } + Mock Invoke-ExternalCommand { $true } + + ($result = Install-HomebrewPackage -PackageName "git" -WhatIf) *> $null + $result | Should -Be $null # ShouldProcess returns null when WhatIf is used + Assert-MockCalled Test-HasSudoAccess -Exactly 0 -Scope It # Should not proceed + Assert-MockCalled Find-Homebrew -Exactly 0 -Scope It + Assert-MockCalled Test-HomebrewPackageInstalled -Exactly 0 -Scope It + Assert-MockCalled Invoke-ExternalCommand -Exactly 0 -Scope It + } + } + + Context "Cross-platform compatibility" { + It "should handle Windows (where Homebrew is unlikely)" { + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { $null } + Mock Write-StatusMessage { } + + $result = Install-HomebrewPackage -PackageName "git" + $result | Should -Be $false + } + + It "should work on Linux" { + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { "/home/linuxbrew/.linuxbrew/bin/brew" } + Mock Test-HomebrewPackageInstalled { [InstalledState]::NotInstalled } + Mock Invoke-ExternalCommand { $true } + Mock Write-StatusMessage { } + $global:LASTEXITCODE = 0 + + $result = Install-HomebrewPackage -PackageName "git" + $result | Should -Be $true + } + + It "should work on macOS" { + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { "/opt/homebrew/bin/brew" } + Mock Test-HomebrewPackageInstalled { [InstalledState]::NotInstalled } + Mock Invoke-ExternalCommand { $true } + Mock Write-StatusMessage { } + $global:LASTEXITCODE = 0 + + $result = Install-HomebrewPackage -PackageName "git" + $result | Should -Be $true + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Install-HomebrewPackage.ps1 b/DevSetup/Private/Providers/Homebrew/Install-HomebrewPackage.ps1 new file mode 100644 index 0000000..03f2f4f --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Install-HomebrewPackage.ps1 @@ -0,0 +1,45 @@ +Function Install-HomebrewPackage { + [CmdletBinding(SupportsShouldProcess=$true)] + [OutputType([bool])] + Param( + [Parameter(Mandatory=$true, Position=0, ParameterSetName="Install")] + [Parameter(Mandatory=$true, Position=0, ParameterSetName="InstallMinimumVersion")] + [string]$PackageName, + [Parameter(Mandatory=$true, Position=1, ParameterSetName="InstallMinimumVersion")] + [string]$MinimumVersion + ) + + if ($PSCmdlet.ShouldProcess($PackageName, "brew install")) { + Write-StatusMessage "- Installing Homebrew package '$PackageName'" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline + + if(-not (Test-HasSudoAccess)) { + Write-StatusMessage "Sudo Access is required to install Homebrew packages." -ForegroundColor Red -Verbosity Verbose + Write-StatusMessage "[FAILED]" -ForegroundColor Red + return $false + } + + if (-not (Find-Homebrew)) { + Write-StatusMessage "Homebrew is not installed. Please install Homebrew first." -ForegroundColor Red -Verbosity Verbose + Write-StatusMessage "[FAILED]" -ForegroundColor Red + return $false + } + + if((Test-HomebrewPackageInstalled -PackageName $PackageName).HasFlag([InstalledState]::Pass)) { + Write-StatusMessage "[OK]" -ForegroundColor Green + return $true + } + # Install Homebrew package + $BrewArgs = @{ + Command = (Find-Homebrew) + Arguments = @("install", $PackageName) + } + Invoke-ExternalCommand @BrewArgs *> $null + if ($LASTEXITCODE -eq 0) { + Write-StatusMessage "[OK]" -ForegroundColor Green + return $true + } else { + Write-StatusMessage "[FAILED]" -ForegroundColor Red + return $false + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Invoke-HomebrewComponentsExport.Tests.ps1 b/DevSetup/Private/Providers/Homebrew/Invoke-HomebrewComponentsExport.Tests.ps1 new file mode 100644 index 0000000..2b0222c --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Invoke-HomebrewComponentsExport.Tests.ps1 @@ -0,0 +1,169 @@ +BeforeAll { + Function ConvertTo-Yaml { } + . (Join-Path $PSScriptRoot "Invoke-HomebrewComponentsExport.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Read-ConfigurationFile.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Providers\Homebrew\Find-Homebrew.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Invoke-ExternalCommand.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1") +} + +Describe "Invoke-HomebrewComponentsExport" { + Context "When Homebrew is not installed" { + It "should return false" { + Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ } } } } + Mock Find-Homebrew { $null } + Mock Write-StatusMessage { } + + $result = Invoke-HomebrewComponentsExport -Config "test.yaml" + $result | Should -Be $false + Assert-MockCalled Find-Homebrew -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It -ParameterFilter { $Message -match "Homebrew is not installed" } + } + } + + Context "When export succeeds" { + It "should update YAML data and save the file" { + Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ homebrew = @() } } } } + Mock Find-Homebrew { "/usr/local/bin/brew" } + Mock Invoke-ExternalCommand { + Param($Command, $Arguments) + if ($Arguments -contains "list --versions") { + return "git 2.30.1`nnode 14.17.0" + } elseif ($Arguments -contains "list --installed-on-request") { + return "git`nnode" + } + } + Mock ConvertTo-Yaml { "mock yaml output" } + Mock Out-File { } + Mock Write-StatusMessage { } + + $result = Invoke-HomebrewComponentsExport -Config "test.yaml" + $result | Should -Be $true + Assert-MockCalled Read-ConfigurationFile -Exactly 1 -Scope It + Assert-MockCalled Find-Homebrew -Exactly 3 -Scope It + Assert-MockCalled Invoke-ExternalCommand -Exactly 2 -Scope It + Assert-MockCalled ConvertTo-Yaml -Exactly 1 -Scope It + Assert-MockCalled Out-File -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "Configuration saved successfully" } + } + } + + Context "When YAML conversion fails" { + It "should fall back to JSON and save" { + Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ homebrew = @() } } } } + Mock Find-Homebrew { "/usr/local/bin/brew" } + Mock Invoke-ExternalCommand { + Param($Command, $Arguments) + if ($Arguments -contains "list --versions") { + return "git 2.30.1" + } elseif ($Arguments -contains "list --installed-on-request") { + return "git" + } + } + Mock ConvertTo-Yaml { throw "YAML conversion failed" } + Mock ConvertTo-Json { "mock json output" } + Mock Out-File { } + Mock Write-StatusMessage { } + + $result = Invoke-HomebrewComponentsExport -Config "test.yaml" + $result | Should -Be $true + Assert-MockCalled ConvertTo-Yaml -Exactly 1 -Scope It + Assert-MockCalled ConvertTo-Json -Exactly 1 -Scope It + Assert-MockCalled Out-File -Exactly 1 -Scope It + } + } + + Context "When saving fails" { + It "should return false" { + Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ homebrew = @() } } } } + Mock Find-Homebrew { "/usr/local/bin/brew" } + Mock Invoke-ExternalCommand { + Param($Command, $Arguments) + if ($Arguments -contains "list --versions") { + return "git 2.30.1" + } elseif ($Arguments -contains "list --installed-on-request") { + return "git" + } + } + Mock ConvertTo-Yaml { "mock yaml output" } + Mock Out-File { throw "Save failed" } + Mock Write-StatusMessage { } + + $result = Invoke-HomebrewComponentsExport -Config "test.yaml" + $result | Should -Be $false + Assert-MockCalled Out-File -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "Failed to save configuration" } + } + } + + Context "When WhatIf is specified" { + It "should not save the file" { + Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ homebrew = @() } } } } + Mock Find-Homebrew { "/usr/local/bin/brew" } + Mock Invoke-ExternalCommand { + Param($Command, $Arguments) + if ($Arguments -contains "list --versions") { + return "git 2.30.1" + } elseif ($Arguments -contains "list --installed-on-request") { + return "git" + } + } + Mock ConvertTo-Yaml { "mock yaml output" } + Mock Out-File { } + Mock Write-StatusMessage { } + + ($result = Invoke-HomebrewComponentsExport -Config "test.yaml" -WhatIf:$true) *> $null + $result | Should -Be $true + Assert-MockCalled Out-File -Exactly 0 -Scope It # Should not save + } + } + + Context "Cross-platform compatibility" { + It "should handle Windows (where Homebrew is unlikely)" { + Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ } } } } + Mock Find-Homebrew { $null } + Mock Write-StatusMessage { } + + $result = Invoke-HomebrewComponentsExport -Config "test.yaml" + $result | Should -Be $false + } + + It "should work on Linux" { + Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ homebrew = @() } } } } + Mock Find-Homebrew { "/home/linuxbrew/.linuxbrew/bin/brew" } + Mock Invoke-ExternalCommand { + Param($Command, $Arguments) + if ($Arguments -contains "list --versions") { + return "git 2.30.1" + } elseif ($Arguments -contains "list --installed-on-request") { + return "git" + } + } + Mock ConvertTo-Yaml { "mock yaml output" } + Mock Out-File { } + Mock Write-StatusMessage { } + + $result = Invoke-HomebrewComponentsExport -Config "test.yaml" + $result | Should -Be $true + } + + It "should work on macOS" { + Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ homebrew = @() } } } } + Mock Find-Homebrew { "/opt/homebrew/bin/brew" } + Mock Invoke-ExternalCommand { + Param($Command, $Arguments) + if ($Arguments -contains "list --versions") { + return "git 2.30.1" + } elseif ($Arguments -contains "list --installed-on-request") { + return "git" + } + } + Mock ConvertTo-Yaml { "mock yaml output" } + Mock Out-File { } + Mock Write-StatusMessage { } + + $result = Invoke-HomebrewComponentsExport -Config "test.yaml" + $result | Should -Be $true + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Invoke-HomebrewComponentsExport.ps1 b/DevSetup/Private/Providers/Homebrew/Invoke-HomebrewComponentsExport.ps1 new file mode 100644 index 0000000..b3579c4 --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Invoke-HomebrewComponentsExport.ps1 @@ -0,0 +1,80 @@ +Function Invoke-HomebrewComponentsExport { + [CmdletBinding(SupportsShouldProcess = $true)] + [OutputType([bool])] + Param( + [Parameter(Mandatory = $true)] + [string]$Config, + [Parameter(Mandatory = $false)] + [string]$OutFile + ) + + $YamlData = Read-ConfigurationFile -Config $Config + + # Ensure scoopPackages and scoopBuckets sections exist + if (-not $YamlData.devsetup) { $YamlData.devsetup = @{} } + if (-not $YamlData.devsetup.dependencies) { $YamlData.devsetup.dependencies = @{} } + if (-not $YamlData.devsetup.dependencies.homebrew) { $YamlData.devsetup.dependencies.homebrew = @() } + + if(-not (Find-Homebrew)) { + Write-StatusMessage "Homebrew is not installed. Please install Homebrew first." -ForegroundColor Red -Verbosity Verbose + return $false + } + + Write-StatusMessage "- Getting list of installed Homebrew packages..." -ForegroundColor Gray + $AvailablePackages = @{ + } + $BrewArgs = @{ + Command = "bash" + Arguments = @("-c", "$(Find-Homebrew) list --versions") + } + (Invoke-ExternalCommand @BrewArgs) | foreach-object { $Parts = $_ -split " "; $AvailablePackages[$Parts[0]] = $Parts[1]} | Out-Null + + $InstalledPackages = @() + $BrewArgs = @{ + Command = "bash" + Arguments = @("-c", "$(Find-Homebrew) list --installed-on-request") + } + (Invoke-ExternalCommand @BrewArgs) | Foreach-Object { $InstalledPackages += $_} | Out-Null + + Foreach($Package in $InstalledPackages) { + $PackageVersion = $AvailablePackages[$Package] + $existing = $YamlData.devsetup.dependencies.homebrew | Where-Object { $_.name -eq $Package } + if ($existing) { + # Update the version if package exists + Write-StatusMessage " - Updating package: $Package" -ForegroundColor Gray + $index = ($YamlData.devsetup.dependencies.homebrew).IndexOf($existing) + $YamlData.devsetup.dependencies.homebrew[$index].minimumVersion = $PackageVersion + } else { + Write-StatusMessage " - Adding package: $Package" -ForegroundColor Gray + # Add new package entry + $YamlData.devsetup.dependencies.homebrew += @{ name = $Package; minimumVersion = $PackageVersion } + } + } + + try { + $yamlOutput = $YamlData | ConvertTo-Yaml + } + catch { + Write-StatusMessage "Could not convert to YAML format. Showing PowerShell object instead:" -Verbosity Warning + $yamlOutput = $YamlData | ConvertTo-Json -Depth 10 + } + + # Determine output file + $outputFile = if ($OutFile) { $OutFile } else { $Config } + + try { + Write-StatusMessage "Saving configuration to: $outputFile" -Verbosity Verbose + if ($PSCmdlet.ShouldProcess($outputFile, "Out-File")) { + $yamlOutput | Out-File -FilePath $outputFile + } + Write-StatusMessage "Configuration saved successfully!" -Verbosity Verbose + } + catch { + Write-StatusMessage "Failed to save configuration to $outputFile`: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace + return $false + } + + Write-StatusMessage "Homebrew packages conversion completed!" -ForegroundColor Green + return $true +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Invoke-HomebrewComponentsInstall.Tests.ps1 b/DevSetup/Private/Providers/Homebrew/Invoke-HomebrewComponentsInstall.Tests.ps1 new file mode 100644 index 0000000..76dcec0 --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Invoke-HomebrewComponentsInstall.Tests.ps1 @@ -0,0 +1,106 @@ +BeforeAll { + . (Join-Path $PSScriptRoot "Invoke-HomebrewComponentsInstall.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Providers\Homebrew\Install-HomebrewPackage.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Providers\Homebrew\Write-HomebrewCache.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1") +} + +Describe "Invoke-HomebrewComponentsInstall" { + Context "When there are no packages in YAML" { + It "should process 0 packages" { + $YamlData = @{ devsetup = @{ dependencies = @{ homebrew = @() } } } + Mock Write-HomebrewCache { } + Mock Write-StatusMessage { } + Mock Install-HomebrewPackage { $false } + + Invoke-HomebrewComponentsInstall -YamlData $YamlData + Assert-MockCalled Write-HomebrewCache -Exactly 1 -Scope It # Initial call + Assert-MockCalled Install-HomebrewPackage -Exactly 0 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It # Start and completion + } + } + + Context "When all packages install successfully" { + It "should increment package count and write cache for each" { + $YamlData = @{ devsetup = @{ dependencies = @{ homebrew = @(@{ name = "git" }, @{ name = "node" }) } } } + Mock Write-HomebrewCache { } + Mock Install-HomebrewPackage { $true } + Mock Write-StatusMessage { } + + Invoke-HomebrewComponentsInstall -YamlData $YamlData + Assert-MockCalled Write-HomebrewCache -Exactly 3 -Scope It # Initial + 2 successful + Assert-MockCalled Install-HomebrewPackage -Exactly 2 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It + } + } + + Context "When some packages fail" { + It "should only increment for successful packages" { + $YamlData = @{ devsetup = @{ dependencies = @{ homebrew = @(@{ name = "git" }, @{ name = "node" }) } } } + Mock Write-HomebrewCache { } + Mock Install-HomebrewPackage { Param($PackageName) if ($PackageName -eq "git") { $true } else { $false } } + Mock Write-StatusMessage { } + + Invoke-HomebrewComponentsInstall -YamlData $YamlData + Assert-MockCalled Write-HomebrewCache -Exactly 2 -Scope It # Initial + 1 successful + Assert-MockCalled Install-HomebrewPackage -Exactly 2 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It + } + } + + Context "When DryRun is specified" { + It "should pass WhatIf to Install-HomebrewPackage" { + $YamlData = @{ devsetup = @{ dependencies = @{ homebrew = @(@{ name = "git" }) } } } + Mock Write-HomebrewCache { } + Mock Install-HomebrewPackage { $true } + Mock Write-StatusMessage { } + + Invoke-HomebrewComponentsInstall -YamlData $YamlData -DryRun + Assert-MockCalled Install-HomebrewPackage -Exactly 1 -Scope It -ParameterFilter { $WhatIf -eq $true } + } + } + + Context "When package has minimumVersion" { + It "should pass MinimumVersion to Install-HomebrewPackage" { + $YamlData = [PSCustomObject]@{ devsetup = [PSCustomObject]@{ dependencies = @{ homebrew = @([PSCustomObject]@{ name = "node"; minimumVersion = "14.0" }) } } } + Mock Write-HomebrewCache { } + Mock Install-HomebrewPackage { $true } + Mock Write-StatusMessage { } + + Invoke-HomebrewComponentsInstall -YamlData $YamlData + Assert-MockCalled Install-HomebrewPackage -Exactly 1 -Scope It -ParameterFilter { $MinimumVersion -eq "14.0" } + } + } + + Context "Cross-platform compatibility" { + It "should work on Windows (no specific Homebrew logic)" { + $YamlData = @{ devsetup = @{ dependencies = @{ homebrew = @(@{ name = "git" }) } } } + Mock Write-HomebrewCache { } + Mock Install-HomebrewPackage { $true } + Mock Write-StatusMessage { } + + Invoke-HomebrewComponentsInstall -YamlData $YamlData + Assert-MockCalled Install-HomebrewPackage -Exactly 1 -Scope It + } + + It "should work on Linux" { + $YamlData = @{ devsetup = @{ dependencies = @{ homebrew = @(@{ name = "git" }) } } } + Mock Write-HomebrewCache { } + Mock Install-HomebrewPackage { $true } + Mock Write-StatusMessage { } + + Invoke-HomebrewComponentsInstall -YamlData $YamlData + Assert-MockCalled Install-HomebrewPackage -Exactly 1 -Scope It + } + + It "should work on macOS" { + $YamlData = @{ devsetup = @{ dependencies = @{ homebrew = @(@{ name = "git" }) } } } + Mock Write-HomebrewCache { } + Mock Install-HomebrewPackage { $true } + Mock Write-StatusMessage { } + + Invoke-HomebrewComponentsInstall -YamlData $YamlData + Assert-MockCalled Install-HomebrewPackage -Exactly 1 -Scope It + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Invoke-HomebrewComponentsInstall.ps1 b/DevSetup/Private/Providers/Homebrew/Invoke-HomebrewComponentsInstall.ps1 new file mode 100644 index 0000000..8445efe --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Invoke-HomebrewComponentsInstall.ps1 @@ -0,0 +1,31 @@ +Function Invoke-HomebrewComponentsInstall { + [CmdletBinding()] + [OutputType([bool])] + Param( + [Parameter(Mandatory=$true)] + [PSCustomObject]$YamlData, + [Parameter(Mandatory=$false)] + [switch]$DryRun = $false + ) + + $packageCount = 0 + Write-HomebrewCache | Out-Null + Write-StatusMessage "- Installing Homebrew packages from configuration:" -ForegroundColor Cyan + foreach($package in $YamlData.devsetup.dependencies.homebrew) { + $Params = @{ + PackageName = $package.name + WhatIf = $DryRun + } + if ($package.PSObject.Properties.Name -contains "minimumVersion") { + $Params.MinimumVersion = $package.minimumVersion + } + + $status = Install-HomebrewPackage @Params + + if ($status) { + $packageCount++ + Write-HomebrewCache | Out-Null + } + } + Write-StatusMessage "- Homebrew packages installation completed! Processed $packageCount packages.`n" -ForegroundColor Green +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Invoke-HomebrewComponentsUninstall.Tests.ps1 b/DevSetup/Private/Providers/Homebrew/Invoke-HomebrewComponentsUninstall.Tests.ps1 new file mode 100644 index 0000000..0c88a1b --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Invoke-HomebrewComponentsUninstall.Tests.ps1 @@ -0,0 +1,94 @@ +BeforeAll { + . (Join-Path $PSScriptRoot "Invoke-HomebrewComponentsUninstall.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Providers\Homebrew\Uninstall-HomebrewPackage.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Providers\Homebrew\Write-HomebrewCache.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1") +} + +Describe "Invoke-HomebrewComponentsUninstall" { + Context "When there are no packages in YAML" { + It "should process 0 packages" { + $YamlData = [PSCustomObject]@{ devsetup = [PSCustomObject]@{ dependencies = [PSCustomObject]@{ homebrew = @() } } } + Mock Write-HomebrewCache { } + Mock Write-StatusMessage { } + Mock Uninstall-HomebrewPackage { $false } + + Invoke-HomebrewComponentsUninstall -YamlData $YamlData + Assert-MockCalled Write-HomebrewCache -Exactly 1 -Scope It # Initial call + Assert-MockCalled Uninstall-HomebrewPackage -Exactly 0 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It # Start and completion + } + } + + Context "When all packages uninstall successfully" { + It "should increment package count and write cache for each" { + $YamlData = [PSCustomObject]@{ devsetup = [PSCustomObject]@{ dependencies = [PSCustomObject]@{ homebrew = @([PSCustomObject]@{ name = "git" }, [PSCustomObject]@{ name = "node" }) } } } + Mock Write-HomebrewCache { } + Mock Uninstall-HomebrewPackage { $true } + Mock Write-StatusMessage { } + + Invoke-HomebrewComponentsUninstall -YamlData $YamlData + Assert-MockCalled Write-HomebrewCache -Exactly 3 -Scope It # Initial + 2 successful + Assert-MockCalled Uninstall-HomebrewPackage -Exactly 2 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It + } + } + + Context "When some packages fail" { + It "should only increment for successful packages" { + $YamlData = [PSCustomObject]@{ devsetup = [PSCustomObject]@{ dependencies = [PSCustomObject]@{ homebrew = @([PSCustomObject]@{ name = "git" }, [PSCustomObject]@{ name = "node" }) } } } + Mock Write-HomebrewCache { } + Mock Uninstall-HomebrewPackage { Param($PackageName) if ($PackageName -eq "git") { $true } else { $false } } + Mock Write-StatusMessage { } + + Invoke-HomebrewComponentsUninstall -YamlData $YamlData + Assert-MockCalled Write-HomebrewCache -Exactly 2 -Scope It # Initial + 1 successful + Assert-MockCalled Uninstall-HomebrewPackage -Exactly 2 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It + } + } + + Context "When DryRun is specified" { + It "should pass WhatIf to Uninstall-HomebrewPackage" { + $YamlData = [PSCustomObject]@{ devsetup = [PSCustomObject]@{ dependencies = [PSCustomObject]@{ homebrew = @([PSCustomObject]@{ name = "git" }) } } } + Mock Write-HomebrewCache { } + Mock Uninstall-HomebrewPackage { $true } + Mock Write-StatusMessage { } + + Invoke-HomebrewComponentsUninstall -YamlData $YamlData -DryRun + Assert-MockCalled Uninstall-HomebrewPackage -Exactly 1 -Scope It -ParameterFilter { $WhatIf -eq $true } + } + } + + Context "Cross-platform compatibility" { + It "should work on Windows" { + $YamlData = [PSCustomObject]@{ devsetup = [PSCustomObject]@{ dependencies = [PSCustomObject]@{ homebrew = @([PSCustomObject]@{ name = "git" }) } } } + Mock Write-HomebrewCache { } + Mock Uninstall-HomebrewPackage { $true } + Mock Write-StatusMessage { } + + Invoke-HomebrewComponentsUninstall -YamlData $YamlData + Assert-MockCalled Uninstall-HomebrewPackage -Exactly 1 -Scope It + } + + It "should work on Linux" { + $YamlData = [PSCustomObject]@{ devsetup = [PSCustomObject]@{ dependencies = [PSCustomObject]@{ homebrew = @([PSCustomObject]@{ name = "git" }) } } } + Mock Write-HomebrewCache { } + Mock Uninstall-HomebrewPackage { $true } + Mock Write-StatusMessage { } + + Invoke-HomebrewComponentsUninstall -YamlData $YamlData + Assert-MockCalled Uninstall-HomebrewPackage -Exactly 1 -Scope It + } + + It "should work on macOS" { + $YamlData = [PSCustomObject]@{ devsetup = [PSCustomObject]@{ dependencies = [PSCustomObject]@{ homebrew = @([PSCustomObject]@{ name = "git" }) } } } + Mock Write-HomebrewCache { } + Mock Uninstall-HomebrewPackage { $true } + Mock Write-StatusMessage { } + + Invoke-HomebrewComponentsUninstall -YamlData $YamlData + Assert-MockCalled Uninstall-HomebrewPackage -Exactly 1 -Scope It + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Invoke-HomebrewComponentsUninstall.ps1 b/DevSetup/Private/Providers/Homebrew/Invoke-HomebrewComponentsUninstall.ps1 new file mode 100644 index 0000000..88065b1 --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Invoke-HomebrewComponentsUninstall.ps1 @@ -0,0 +1,28 @@ +Function Invoke-HomebrewComponentsUninstall { + [CmdletBinding()] + [OutputType([bool])] + Param( + [Parameter(Mandatory=$true)] + [PSCustomObject]$YamlData, + [Parameter(Mandatory=$false)] + [switch]$DryRun = $false + ) + + $packageCount = 0 + Write-HomebrewCache | Out-Null + Write-StatusMessage "- Uninstalling Homebrew packages from configuration:" -ForegroundColor Cyan + foreach($package in $YamlData.devsetup.dependencies.homebrew) { + $Params = @{ + PackageName = $package.name + WhatIf = $DryRun + } + + $status = Uninstall-HomebrewPackage @Params + + if ($status) { + $packageCount++ + Write-HomebrewCache | Out-Null + } + } + Write-StatusMessage "- Homebrew packages uninstallation completed! Processed $packageCount packages.`n" -ForegroundColor Green +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Read-HomebrewCache.Tests.ps1 b/DevSetup/Private/Providers/Homebrew/Read-HomebrewCache.Tests.ps1 new file mode 100644 index 0000000..dd68137 --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Read-HomebrewCache.Tests.ps1 @@ -0,0 +1,78 @@ +BeforeAll { + . (Join-Path $PSScriptRoot "Read-HomebrewCache.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Providers\Homebrew\Get-HomebrewCacheFile.ps1") +} + +Describe "Read-HomebrewCache" { + Context "When cache file exists" { + It "should read and return the cache data as hashtable" { + $mockCachePath = Join-Path $TestDrive "homebrew.cache" + Mock Get-HomebrewCacheFile { $mockCachePath } + Mock Test-Path { $true } + Mock Get-Content { '{"package1": "version1", "package2": "version2"}' } + Mock ConvertFrom-Json { @{ package1 = "version1"; package2 = "version2" } } + + $result = Read-HomebrewCache + $result | Should -BeOfType [hashtable] + $result["package1"] | Should -Be "version1" + $result["package2"] | Should -Be "version2" + Assert-MockCalled Get-HomebrewCacheFile -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 1 -Scope It + Assert-MockCalled Get-Content -Exactly 1 -Scope It + Assert-MockCalled ConvertFrom-Json -Exactly 1 -Scope It + } + } + + Context "When cache file does not exist" { + It "should return an empty hashtable" { + $mockCachePath = Join-Path $TestDrive "homebrew.cache" + Mock Get-HomebrewCacheFile { $mockCachePath } + Mock Test-Path { $false } + Mock Get-Content { } + Mock ConvertFrom-Json { } + + $result = Read-HomebrewCache + $result | Should -BeOfType [hashtable] + $result.Count | Should -Be 0 + Assert-MockCalled Get-HomebrewCacheFile -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 1 -Scope It + Assert-MockCalled Get-Content -Exactly 0 -Scope It + Assert-MockCalled ConvertFrom-Json -Exactly 0 -Scope It + } + } + + Context "Cross-platform compatibility" { + It "should work on Windows" { + $mockCachePath = Join-Path $TestDrive "homebrew.cache" + Mock Get-HomebrewCacheFile { $mockCachePath } + Mock Test-Path { $true } + Mock Get-Content { '{"git": "2.30.1"}' } + Mock ConvertFrom-Json { @{ git = "2.30.1" } } + + $result = Read-HomebrewCache + $result["git"] | Should -Be "2.30.1" + } + + It "should work on Linux" { + $mockCachePath = Join-Path $TestDrive "homebrew.cache" + Mock Get-HomebrewCacheFile { $mockCachePath } + Mock Test-Path { $true } + Mock Get-Content { '{"git": "2.30.1"}' } + Mock ConvertFrom-Json { @{ git = "2.30.1" } } + + $result = Read-HomebrewCache + $result["git"] | Should -Be "2.30.1" + } + + It "should work on macOS" { + $mockCachePath = Join-Path $TestDrive "homebrew.cache" + Mock Get-HomebrewCacheFile { $mockCachePath } + Mock Test-Path { $true } + Mock Get-Content { '{"git": "2.30.1"}' } + Mock ConvertFrom-Json { @{ git = "2.30.1" } } + + $result = Read-HomebrewCache + $result["git"] | Should -Be "2.30.1" + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Read-HomebrewCache.ps1 b/DevSetup/Private/Providers/Homebrew/Read-HomebrewCache.ps1 new file mode 100644 index 0000000..40d0b1a --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Read-HomebrewCache.ps1 @@ -0,0 +1,14 @@ +Function Read-HomebrewCache { + [CmdletBinding()] + [OutputType([hashtable])] + Param() + + $cacheFile = Get-HomebrewCacheFile + + if (Test-Path $cacheFile) { + $cacheData = Get-Content -Path $cacheFile | ConvertFrom-Json -AsHashtable + return $cacheData + } + + return @{} +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Test-HomebrewInstalled.Tests.ps1 b/DevSetup/Private/Providers/Homebrew/Test-HomebrewInstalled.Tests.ps1 new file mode 100644 index 0000000..ccd3e94 --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Test-HomebrewInstalled.Tests.ps1 @@ -0,0 +1,114 @@ +BeforeAll { + . (Join-Path $PSScriptRoot "Test-HomebrewInstalled.ps1") +} + +Describe "Test-HomebrewInstalled" { + Context "When brew is found in PATH" { + It "should return true" { + Mock Get-Command { [PSCustomObject]@{ Path = "/usr/local/bin/brew" } } + Mock Test-Path { $false } # Not needed since Get-Command succeeds + + $result = Test-HomebrewInstalled + $result | Should -Be $true + Assert-MockCalled Get-Command -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 0 -Scope It # Test-Path not called if Get-Command succeeds + } + } + + Context "When brew is not in PATH but found in test paths" { + It "should return true if found in first test path" { + Mock Get-Command { $null } + Mock Test-Path { + Param($Path) + switch ($Path) { + "/usr/local/bin/brew" { $true } + default { $false } + } + } + + $result = Test-HomebrewInstalled + $result | Should -Be $true + Assert-MockCalled Get-Command -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 1 -Scope It -ParameterFilter { $Path -eq "/usr/local/bin/brew" } + } + + It "should return true if found in second test path" { + Mock Get-Command { $null } + Mock Test-Path { + Param($Path) + switch ($Path) { + "/usr/local/bin/brew" { $false } + "/opt/homebrew/bin/brew" { $true } + default { $false } + } + } + + $result = Test-HomebrewInstalled + $result | Should -Be $true + Assert-MockCalled Get-Command -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 2 -Scope It # Checks first two paths + } + + It "should return true if found in third test path" { + Mock Get-Command { $null } + Mock Test-Path { + Param($Path) + switch ($Path) { + "/usr/local/bin/brew" { $false } + "/opt/homebrew/bin/brew" { $false } + "/home/linuxbrew/.linuxbrew/bin/brew" { $true } + default { $false } + } + } + + $result = Test-HomebrewInstalled + $result | Should -Be $true + Assert-MockCalled Get-Command -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 3 -Scope It # Checks all three paths + } + } + + Context "When brew is not found anywhere" { + It "should return false" { + Mock Get-Command { $null } + Mock Test-Path { $false } + + $result = Test-HomebrewInstalled + $result | Should -Be $false + Assert-MockCalled Get-Command -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 3 -Scope It # Checks all three paths + } + } + + Context "Cross-platform compatibility" { + It "should return false on Windows (where Homebrew is unlikely)" { + Mock Get-Command { $null } + Mock Test-Path { $false } + + $result = Test-HomebrewInstalled + $result | Should -Be $false + } + + It "should work on Linux" { + Mock Get-Command { $null } + Mock Test-Path { + Param($Path) + $Path -eq "/home/linuxbrew/.linuxbrew/bin/brew" + } + + $result = Test-HomebrewInstalled + $result | Should -Be $true + } + + It "should work on macOS" { + Mock Get-Command { $null } + Mock Test-Path { + Param($Path) + $Path -eq "/opt/homebrew/bin/brew" + } + + $result = Test-HomebrewInstalled + $result | Should -Be $true + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Test-HomebrewInstalled.ps1 b/DevSetup/Private/Providers/Homebrew/Test-HomebrewInstalled.ps1 new file mode 100644 index 0000000..14617a8 --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Test-HomebrewInstalled.ps1 @@ -0,0 +1,25 @@ +Function Test-HomebrewInstalled { + [CmdletBinding()] + [OutputType([bool])] + Param( + ) + + $TestPaths = @( + '/usr/local/bin/brew', + '/opt/homebrew/bin/brew', + '/home/linuxbrew/.linuxbrew/bin/brew' + ) + + # Check if Homebrew is installed + $Path = (Get-Command "brew" -ErrorAction SilentlyContinue).Path + if ([string]::IsNullOrEmpty($Path)) { + foreach ($p in $TestPaths) { + if (Test-Path $p) { + $Path = $p + break + } + } + } + + return (-not [string]::IsNullOrEmpty($Path)) +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Test-HomebrewPackageInstalled.Tests.ps1 b/DevSetup/Private/Providers/Homebrew/Test-HomebrewPackageInstalled.Tests.ps1 new file mode 100644 index 0000000..b8977c9 --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Test-HomebrewPackageInstalled.Tests.ps1 @@ -0,0 +1,102 @@ +BeforeAll { + . (Join-Path $PSScriptRoot "Test-HomebrewPackageInstalled.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Providers\Homebrew\Test-HomebrewInstalled.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Providers\Homebrew\Read-HomebrewCache.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Enums\InstalledState.ps1") +} + +Describe "Test-HomebrewPackageInstalled" { + Context "When Homebrew is not installed" { + It "should return NotInstalled" { + Mock Test-HomebrewInstalled { $false } + Mock Write-StatusMessage { } + + $result = Test-HomebrewPackageInstalled -PackageName "git" + $result | Should -Be ([InstalledState]::NotInstalled) + Assert-MockCalled Test-HomebrewInstalled -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 1 -Scope It + } + } + + Context "When package is not in cache" { + It "should return NotInstalled" { + Mock Test-HomebrewInstalled { $true } + Mock Read-HomebrewCache { @{ "node" = "14.17.0" } } # Package not in cache + Mock Write-StatusMessage { } + + $result = Test-HomebrewPackageInstalled -PackageName "git" + $result | Should -Be ([InstalledState]::NotInstalled) + Assert-MockCalled Test-HomebrewInstalled -Exactly 1 -Scope It + Assert-MockCalled Read-HomebrewCache -Exactly 1 -Scope It + } + } + + Context "When package is in cache and no minimum version specified" { + It "should return full installed status" { + Mock Test-HomebrewInstalled { $true } + Mock Read-HomebrewCache { @{ "git" = "2.30.1" } } + Mock Write-StatusMessage { } + + $result = Test-HomebrewPackageInstalled -PackageName "git" + $result | Should -Be ([InstalledState]::Installed + [InstalledState]::MinimumVersionMet + [InstalledState]::RequiredVersionMet + [InstalledState]::GlobalVersionMet) + Assert-MockCalled Test-HomebrewInstalled -Exactly 1 -Scope It + Assert-MockCalled Read-HomebrewCache -Exactly 1 -Scope It + } + } + + Context "When package is in cache and minimum version is met" { + It "should return full installed status" { + Mock Test-HomebrewInstalled { $true } + Mock Read-HomebrewCache { @{ "git" = "2.30.1" } } + Mock Write-StatusMessage { } + + $result = Test-HomebrewPackageInstalled -PackageName "git" -MinimumVersion "2.0.0" + $result | Should -Be ([InstalledState]::Installed + [InstalledState]::MinimumVersionMet + [InstalledState]::RequiredVersionMet + [InstalledState]::GlobalVersionMet) + Assert-MockCalled Test-HomebrewInstalled -Exactly 1 -Scope It + Assert-MockCalled Read-HomebrewCache -Exactly 1 -Scope It + } + } + + Context "When package is in cache but minimum version is not met" { + It "should return Installed but not version flags" { + Mock Test-HomebrewInstalled { $true } + Mock Read-HomebrewCache { @{ "git" = "1.0.0" } } + Mock Write-StatusMessage { } + + $result = Test-HomebrewPackageInstalled -PackageName "git" -MinimumVersion "2.0.0" + $result | Should -Be ([InstalledState]::Installed) # Only Installed, no version flags + Assert-MockCalled Test-HomebrewInstalled -Exactly 1 -Scope It + Assert-MockCalled Read-HomebrewCache -Exactly 1 -Scope It + } + } + + Context "Cross-platform compatibility" { + It "should work on Windows" { + Mock Test-HomebrewInstalled { $true } + Mock Read-HomebrewCache { @{ "git" = "2.30.1" } } + Mock Write-StatusMessage { } + + $result = Test-HomebrewPackageInstalled -PackageName "git" + $result | Should -Be ([InstalledState]::Installed + [InstalledState]::MinimumVersionMet + [InstalledState]::RequiredVersionMet + [InstalledState]::GlobalVersionMet) + } + + It "should work on Linux" { + Mock Test-HomebrewInstalled { $true } + Mock Read-HomebrewCache { @{ "git" = "2.30.1" } } + Mock Write-StatusMessage { } + + $result = Test-HomebrewPackageInstalled -PackageName "git" + $result | Should -Be ([InstalledState]::Installed + [InstalledState]::MinimumVersionMet + [InstalledState]::RequiredVersionMet + [InstalledState]::GlobalVersionMet) + } + + It "should work on macOS" { + Mock Test-HomebrewInstalled { $true } + Mock Read-HomebrewCache { @{ "git" = "2.30.1" } } + Mock Write-StatusMessage { } + + $result = Test-HomebrewPackageInstalled -PackageName "git" + $result | Should -Be ([InstalledState]::Installed + [InstalledState]::MinimumVersionMet + [InstalledState]::RequiredVersionMet + [InstalledState]::GlobalVersionMet) + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Test-HomebrewPackageInstalled.ps1 b/DevSetup/Private/Providers/Homebrew/Test-HomebrewPackageInstalled.ps1 new file mode 100644 index 0000000..abfafd5 --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Test-HomebrewPackageInstalled.ps1 @@ -0,0 +1,39 @@ +Function Test-HomebrewPackageInstalled { + [CmdletBinding()] + [OutputType([InstalledState])] + Param( + [Parameter(Mandatory=$true, Position=0)] + [string]$PackageName, + [Parameter(Mandatory=$false, Position=1)] + [Version]$MinimumVersion + ) + + [InstalledState]$PackageStatus = [InstalledState]::NotInstalled + + if(-not (Test-HomebrewInstalled)) { + Write-StatusMessage "Homebrew is not installed" -Verbosity Verbose + return $PackageStatus + } + + $InstalledPackages = Read-HomebrewCache + + if ($InstalledPackages.ContainsKey($PackageName)) { + $PackageStatus += [InstalledState]::Installed + + if($PSBoundParameters.ContainsKey('MinimumVersion')) { + $MinimumVersion = [Version]::Parse($MinimumVersion) + $InstalledVersion = [Version]::Parse($InstalledPackages[$PackageName]) + if ($InstalledVersion -ge $MinimumVersion) { + $PackageStatus += [InstalledState]::MinimumVersionMet + $PackageStatus += [InstalledState]::RequiredVersionMet + $PackageStatus += [InstalledState]::GlobalVersionMet + } + } else { + $PackageStatus += [InstalledState]::MinimumVersionMet + $PackageStatus += [InstalledState]::RequiredVersionMet + $PackageStatus += [InstalledState]::GlobalVersionMet + } + } + + return $PackageStatus +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Uninstall-HomebrewPackage.Tests.ps1 b/DevSetup/Private/Providers/Homebrew/Uninstall-HomebrewPackage.Tests.ps1 new file mode 100644 index 0000000..ce1aa53 --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Uninstall-HomebrewPackage.Tests.ps1 @@ -0,0 +1,141 @@ +BeforeAll { + . (Join-Path $PSScriptRoot "Uninstall-HomebrewPackage.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Test-HasSudoAccess.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Providers\Homebrew\Find-Homebrew.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Providers\Homebrew\Test-HomebrewPackageInstalled.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Invoke-ExternalCommand.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1") +} + +Describe "Uninstall-HomebrewPackage" { + Context "When sudo access is not available" { + It "should return false" { + Mock Test-HasSudoAccess { $false } + Mock Write-StatusMessage { } + + $result = Uninstall-HomebrewPackage -PackageName "git" + $result | Should -Be $false + Assert-MockCalled Test-HasSudoAccess -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 3 -Scope It # Status, sudo message, failed + } + } + + Context "When Homebrew is not installed" { + It "should return false" { + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { $null } + Mock Write-StatusMessage { } + + $result = Uninstall-HomebrewPackage -PackageName "git" + $result | Should -Be $false + Assert-MockCalled Test-HasSudoAccess -Exactly 1 -Scope It + Assert-MockCalled Find-Homebrew -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 3 -Scope It # Status, homebrew message, failed + } + } + + Context "When package is not installed" { + It "should return true" { + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { "/usr/local/bin/brew" } + Mock Test-HomebrewPackageInstalled { [InstalledState]::NotInstalled } + Mock Write-StatusMessage { } + Mock Invoke-ExternalCommand { $false } + + $result = Uninstall-HomebrewPackage -PackageName "git" + $result | Should -Be $true + Assert-MockCalled Test-HasSudoAccess -Exactly 1 -Scope It + Assert-MockCalled Find-Homebrew -Exactly 1 -Scope It + Assert-MockCalled Test-HomebrewPackageInstalled -Exactly 1 -Scope It + Assert-MockCalled Invoke-ExternalCommand -Exactly 0 -Scope It # No uninstall needed + } + } + + Context "When uninstallation succeeds" { + It "should return true" { + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { "/usr/local/bin/brew" } + Mock Test-HomebrewPackageInstalled { [InstalledState]::Installed } + Mock Invoke-ExternalCommand { $true } + Mock Write-StatusMessage { } + $global:LASTEXITCODE = 0 + + $result = Uninstall-HomebrewPackage -PackageName "git" + $result | Should -Be $true + Assert-MockCalled Test-HasSudoAccess -Exactly 1 -Scope It + Assert-MockCalled Find-Homebrew -Exactly 2 -Scope It + Assert-MockCalled Test-HomebrewPackageInstalled -Exactly 1 -Scope It + Assert-MockCalled Invoke-ExternalCommand -Exactly 1 -Scope It + } + } + + Context "When uninstallation fails" { + It "should return false" { + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { "/usr/local/bin/brew" } + Mock Test-HomebrewPackageInstalled { [InstalledState]::Installed } + Mock Invoke-ExternalCommand { $false } + Mock Write-StatusMessage { } + $global:LASTEXITCODE = 1 + + $result = Uninstall-HomebrewPackage -PackageName "git" + $result | Should -Be $false + Assert-MockCalled Test-HasSudoAccess -Exactly 1 -Scope It + Assert-MockCalled Find-Homebrew -Exactly 2 -Scope It + Assert-MockCalled Test-HomebrewPackageInstalled -Exactly 1 -Scope It + Assert-MockCalled Invoke-ExternalCommand -Exactly 1 -Scope It + } + } + + Context "When -WhatIf is used" { + It "should not perform the uninstallation" { + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { "/usr/local/bin/brew" } + Mock Test-HomebrewPackageInstalled { [InstalledState]::Installed } + Mock Write-StatusMessage { } + Mock Invoke-ExternalCommand { $false } + + $result = Uninstall-HomebrewPackage -PackageName "git" -WhatIf + $result | Should -Be $null # ShouldProcess returns null when WhatIf is used + Assert-MockCalled Test-HasSudoAccess -Exactly 0 -Scope It # Should not proceed + Assert-MockCalled Find-Homebrew -Exactly 0 -Scope It + Assert-MockCalled Test-HomebrewPackageInstalled -Exactly 0 -Scope It + Assert-MockCalled Invoke-ExternalCommand -Exactly 0 -Scope It + } + } + + Context "Cross-platform compatibility" { + It "should handle Windows (where Homebrew is unlikely)" { + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { $null } + Mock Write-StatusMessage { } + + $result = Uninstall-HomebrewPackage -PackageName "git" + $result | Should -Be $false + } + + It "should work on Linux" { + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { "/home/linuxbrew/.linuxbrew/bin/brew" } + Mock Test-HomebrewPackageInstalled { [InstalledState]::Installed } + Mock Invoke-ExternalCommand { $true } + Mock Write-StatusMessage { } + $global:LASTEXITCODE = 0 + + $result = Uninstall-HomebrewPackage -PackageName "git" + $result | Should -Be $true + } + + It "should work on macOS" { + Mock Test-HasSudoAccess { $true } + Mock Find-Homebrew { "/opt/homebrew/bin/brew" } + Mock Test-HomebrewPackageInstalled { [InstalledState]::Installed } + Mock Invoke-ExternalCommand { $true } + Mock Write-StatusMessage { } + $global:LASTEXITCODE = 0 + + $result = Uninstall-HomebrewPackage -PackageName "git" + $result | Should -Be $true + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Uninstall-HomebrewPackage.ps1 b/DevSetup/Private/Providers/Homebrew/Uninstall-HomebrewPackage.ps1 new file mode 100644 index 0000000..003f3bb --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Uninstall-HomebrewPackage.ps1 @@ -0,0 +1,42 @@ +Function Uninstall-HomebrewPackage { + [CmdletBinding(SupportsShouldProcess=$true)] + [OutputType([bool])] + Param( + [string]$PackageName + ) + + if ($PSCmdlet.ShouldProcess($PackageName, "brew uninstall")) { + Write-StatusMessage "- Uninstalling Homebrew package '$PackageName'" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline + if(-not (Test-HasSudoAccess)) { + Write-StatusMessage "Sudo Access is required to install Homebrew packages." -ForegroundColor Red -Verbosity Verbose + Write-StatusMessage "[FAILED]" -ForegroundColor Red + return $false + } + + if (-not (Find-Homebrew)) { + Write-StatusMessage "Homebrew is not installed. Please install Homebrew first." -ForegroundColor Red -Verbosity Verbose + Write-StatusMessage "[FAILED]" -ForegroundColor Red + return $false + } + + if(-not (Test-HomebrewPackageInstalled -PackageName $PackageName).HasFlag([InstalledState]::Installed)) { + Write-StatusMessage "Homebrew package '$PackageName' is not installed." -Verbosity Verbose + Write-StatusMessage "[OK]" -ForegroundColor Green + return $true + } + # Uninstall Homebrew package + $BrewArgs = @{ + Command = "bash" + Arguments = @("-c", [string]::Format("{0} uninstall {1}", (Find-Homebrew), $PackageName)) + } + (Invoke-ExternalCommand @BrewArgs) *> $null + if ($LASTEXITCODE -eq 0) { + Write-StatusMessage "[OK]" -ForegroundColor Green + return $true + } else { + Write-StatusMessage "Failed to uninstall Homebrew package '$PackageName'." -Verbosity Verbose + Write-StatusMessage "[FAILED]" -ForegroundColor Red + return $false + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Write-HomebrewCache.Tests.ps1 b/DevSetup/Private/Providers/Homebrew/Write-HomebrewCache.Tests.ps1 new file mode 100644 index 0000000..8e1f04f --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Write-HomebrewCache.Tests.ps1 @@ -0,0 +1,122 @@ +BeforeAll { + . (Join-Path $PSScriptRoot "Write-HomebrewCache.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Providers\Homebrew\Get-HomebrewCacheFile.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Providers\Homebrew\Test-HomebrewInstalled.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Providers\Homebrew\Find-Homebrew.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Invoke-ExternalCommand.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1") +} + +Describe "Write-HomebrewCache" { + Context "When Homebrew is not installed" { + It "should not write anything" { + Mock Get-HomebrewCacheFile { Join-Path $TestDrive "homebrew.cache" } + Mock Test-HomebrewInstalled { $false } + Mock Set-Content { } + Mock Find-Homebrew { $null } + Mock Invoke-ExternalCommand { $null } + + Write-HomebrewCache + Assert-MockCalled Get-HomebrewCacheFile -Exactly 1 -Scope It + Assert-MockCalled Test-HomebrewInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Homebrew -Exactly 0 -Scope It # Not called if not installed + Assert-MockCalled Invoke-ExternalCommand -Exactly 0 -Scope It + Assert-MockCalled Set-Content -Exactly 0 -Scope It # No data to write + } + } + + Context "When Homebrew is installed and write succeeds" { + It "should parse output and write JSON to cache file" { + Mock Get-HomebrewCacheFile { Join-Path $TestDrive "homebrew.cache" } + Mock Test-HomebrewInstalled { $true } + Mock Find-Homebrew { "/usr/local/bin/brew" } + Mock Invoke-ExternalCommand { "git 2.30.1`nnode 14.17.0" } + Mock Set-Content { } + Mock Write-StatusMessage { } + + Write-HomebrewCache + Assert-MockCalled Get-HomebrewCacheFile -Exactly 1 -Scope It + Assert-MockCalled Test-HomebrewInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Homebrew -Exactly 1 -Scope It + Assert-MockCalled Invoke-ExternalCommand -Exactly 1 -Scope It + Assert-MockCalled Set-Content -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 0 -Scope It # No errors + } + } + + Context "When Invoke-ExternalCommand throws an exception" { + It "should handle the exception and write error message" { + Mock Get-HomebrewCacheFile { Join-Path $TestDrive "homebrew.cache" } + Mock Test-HomebrewInstalled { $true } + Mock Find-Homebrew { "/usr/local/bin/brew" } + Mock Invoke-ExternalCommand { throw "Command failed" } + Mock Set-Content { } + Mock Write-StatusMessage { } + + Write-HomebrewCache + Assert-MockCalled Get-HomebrewCacheFile -Exactly 1 -Scope It + Assert-MockCalled Test-HomebrewInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Homebrew -Exactly 1 -Scope It + Assert-MockCalled Invoke-ExternalCommand -Exactly 1 -Scope It + Assert-MockCalled Set-Content -Exactly 0 -Scope It # Exception prevents writing + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It # Error and stack trace + } + } + + Context "When Set-Content throws an exception" { + It "should handle the exception and write error message" { + Mock Get-HomebrewCacheFile { Join-Path $TestDrive "homebrew.cache" } + Mock Test-HomebrewInstalled { $true } + Mock Find-Homebrew { "/usr/local/bin/brew" } + Mock Invoke-ExternalCommand { "git 2.30.1" } + Mock Set-Content { throw "Write failed" } + Mock Write-StatusMessage { } + + Write-HomebrewCache + Assert-MockCalled Get-HomebrewCacheFile -Exactly 1 -Scope It + Assert-MockCalled Test-HomebrewInstalled -Exactly 1 -Scope It + Assert-MockCalled Find-Homebrew -Exactly 1 -Scope It + Assert-MockCalled Invoke-ExternalCommand -Exactly 1 -Scope It + Assert-MockCalled Set-Content -Exactly 1 -Scope It + Assert-MockCalled Write-StatusMessage -Exactly 2 -Scope It # Error and stack trace + } + } + + Context "Cross-platform compatibility" { + It "should work on Windows" { + Mock Get-HomebrewCacheFile { Join-Path $TestDrive "homebrew.cache" } + Mock Test-HomebrewInstalled { $true } + Mock Find-Homebrew { "/usr/local/bin/brew" } + Mock Invoke-ExternalCommand { "git 2.30.1" } + Mock Set-Content { } + Mock Write-StatusMessage { } + + Write-HomebrewCache + Assert-MockCalled Invoke-ExternalCommand -Exactly 1 -Scope It + } + + It "should work on Linux" { + Mock Get-HomebrewCacheFile { Join-Path $TestDrive "homebrew.cache" } + Mock Test-HomebrewInstalled { $true } + Mock Find-Homebrew { "/home/linuxbrew/.linuxbrew/bin/brew" } + Mock Invoke-ExternalCommand { "git 2.30.1" } + Mock Set-Content { } + Mock Write-StatusMessage { } + + Write-HomebrewCache + Assert-MockCalled Invoke-ExternalCommand -Exactly 1 -Scope It + } + + It "should work on macOS" { + Mock Get-HomebrewCacheFile { Join-Path $TestDrive "homebrew.cache" } + Mock Test-HomebrewInstalled { $true } + Mock Find-Homebrew { "/opt/homebrew/bin/brew" } + Mock Invoke-ExternalCommand { "git 2.30.1" } + Mock Set-Content { } + Mock Write-StatusMessage { } + + Write-HomebrewCache + Assert-MockCalled Invoke-ExternalCommand -Exactly 1 -Scope It + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Homebrew/Write-HomebrewCache.ps1 b/DevSetup/Private/Providers/Homebrew/Write-HomebrewCache.ps1 new file mode 100644 index 0000000..9f1852f --- /dev/null +++ b/DevSetup/Private/Providers/Homebrew/Write-HomebrewCache.ps1 @@ -0,0 +1,30 @@ +Function Write-HomebrewCache { + [CmdletBinding()] + [OutputType([void])] + Param() + + $cacheFile = Get-HomebrewCacheFile + $cacheData = @{} + + try { + if (Test-HomebrewInstalled) { + $BrewArgs = @{ + Command = "bash" + Arguments = @("-c", "$(Find-Homebrew) list --versions") + } + + (Invoke-ExternalCommand @BrewArgs) | ForEach-Object { + $parts = $_ -split ' ' + if ($parts.Length -ge 2) { + $packageName = $parts[0] + $packageVersion = $parts[1] + $cacheData[$packageName] = $packageVersion + } + } | Out-Null + $cacheData | ConvertTo-Json | Set-Content -Path $cacheFile + } + } catch { + Write-StatusMessage "Failed to write Homebrew cache: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Powershell/Export-InstalledPowershellModules.ps1 b/DevSetup/Private/Providers/Powershell/Export-InstalledPowershellModules.ps1 index 65b1f91..bd1e7f7 100644 --- a/DevSetup/Private/Providers/Powershell/Export-InstalledPowershellModules.ps1 +++ b/DevSetup/Private/Providers/Powershell/Export-InstalledPowershellModules.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Exports installed PowerShell modules to a YAML configuration file. diff --git a/DevSetup/Private/Providers/Powershell/Install-PowershellModules.Tests.ps1 b/DevSetup/Private/Providers/Powershell/Install-PowershellModules.Tests.ps1 index 4ec0b6d..f497f86 100644 --- a/DevSetup/Private/Providers/Powershell/Install-PowershellModules.Tests.ps1 +++ b/DevSetup/Private/Providers/Powershell/Install-PowershellModules.Tests.ps1 @@ -1,8 +1,8 @@ BeforeAll { - . $PSScriptRoot\Install-PowershellModules.ps1 - . $PSScriptRoot\Install-PowerShellModule.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 + . (Join-Path $PSScriptRoot "Install-PowershellModules.ps1") + . (Join-Path $PSScriptRoot "Install-PowershellModule.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1") Mock Write-StatusMessage { } Mock Test-RunningAsAdmin { return $true } Mock Write-Error {} @@ -49,7 +49,7 @@ Describe "Install-PowershellModules" { Context "When modules are installed successfully (string format)" { It "Should install all modules and return true" { $script:installCalls = @() - Mock Install-PowerShellModule -MockWith { + Mock Install-PowershellModule -MockWith { param($ModuleName, $Force, $AllowClobber, $Scope, $Version) $script:installCalls += $ModuleName return $true @@ -73,7 +73,7 @@ Describe "Install-PowershellModules" { Context "When modules are installed successfully (object format)" { It "Should install all modules and return true" { $script:installCalls = @() - Mock Install-PowerShellModule -MockWith { + Mock Install-PowershellModule -MockWith { param($ModuleName, $Force, $AllowClobber, $Scope, $Version) $script:installCalls += $ModuleName return $true @@ -100,7 +100,7 @@ Describe "Install-PowershellModules" { Context "When some modules fail to install" { It "Should continue and return true" { $script:installCalls = @() - Mock Install-PowerShellModule -MockWith { + Mock Install-PowershellModule -MockWith { param($ModuleName, $Force, $AllowClobber, $Scope, $Version) $script:installCalls += $ModuleName if ($ModuleName -eq "PSReadLine") { return $false } @@ -126,7 +126,7 @@ Describe "Install-PowershellModules" { Context "When module entry is empty or missing name" { It "Should skip invalid entries and return true" { $script:installCalls = @() - Mock Install-PowerShellModule -MockWith { + Mock Install-PowershellModule -MockWith { param($ModuleName, $Force, $AllowClobber, $Scope, $Version) $script:installCalls += $ModuleName return $true @@ -153,7 +153,7 @@ Describe "Install-PowershellModules" { Context "When an exception occurs during installation" { It "Should catch and return false" { - Mock Install-PowerShellModule { throw "Unexpected error" } + Mock Install-PowershellModule { throw "Unexpected error" } $yamlData = @{ devsetup = @{ dependencies = @{ diff --git a/DevSetup/Private/Providers/Powershell/Install-PowershellModules.ps1 b/DevSetup/Private/Providers/Powershell/Install-PowershellModules.ps1 index 2b18c4e..bfb1c47 100644 --- a/DevSetup/Private/Providers/Powershell/Install-PowershellModules.ps1 +++ b/DevSetup/Private/Providers/Powershell/Install-PowershellModules.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Installs PowerShell modules from YAML configuration data. diff --git a/DevSetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.Tests.ps1 b/DevSetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.Tests.ps1 index 9a2dab5..37910d3 100644 --- a/DevSetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.Tests.ps1 +++ b/DevSetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.Tests.ps1 @@ -1,8 +1,72 @@ BeforeAll { . $PSScriptRoot\Test-PowershellModuleInstalled.ps1 . $PSScriptRoot\..\..\..\..\DevSetup\Private\Enums\InstalledState.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-OperatingSystem.ps1 - Mock Test-OperatingSystem { $true } + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-OperatingSystem.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Get-EnvironmentVariable.ps1 + if($PSVersionTable.PSVersion.Major -eq 5) { + $script:LocalModulePath = "$env:USERPROFILE\Documents\WindowsPowerShell\Modules\" + $script:AllUsersModulePath = "$env:ProgramFiles\WindowsPowerShell\Modules\" + Mock Get-EnvironmentVariable { + Param( + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + $Name + ) + switch ($Name) { + "USERPROFILE" { Write-Output $env:USERPROFILE } + "PSModulePath" { Write-Output "$env:USERPROFILE\Documents\WindowsPowerShell\Modules;$env:ProgramFiles\WindowsPowerShell\Modules" } + } + } + Mock Test-OperatingSystem { $true } + } else { + if($IsWindows) { + $script:LocalModulePath = "$env:USERPROFILE\Documents\PowerShell\Modules\" + $script:AllUsersModulePath = "$env:ProgramFiles\PowerShell\Modules\" + Mock Get-EnvironmentVariable { + Param( + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + $Name + ) + switch ($Name) { + "USERPROFILE" { Write-Output $env:USERPROFILE } + "HOME" { Write-Output $env:HOME } + "PSModulePath" { Write-Output "$env:USERPROFILE\Documents\PowerShell\Modules;$env:ProgramFiles\PowerShell\Modules" } + } + } + Mock Test-OperatingSystem { $true } + } + if($IsLinux) { + $script:LocalModulePath = "$env:HOME/.local/share/powershell/Modules/" + $script:AllUsersModulePath = "/usr/local/share/powershell/Modules/" + Mock Get-EnvironmentVariable { + Param( + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + $Name + ) + switch ($Name) { + "USERPROFILE" { Write-Output $env:USERPROFILE } + "HOME" { Write-Output $env:HOME } + "PSModulePath" { Write-Output "$env:HOME/.local/share/powershell/Modules:/usr/local/share/powershell/Modules:/opt/microsoft/powershell/7/Modules" } + } + } + Mock Test-OperatingSystem { $false } + } + if($IsMacOS) { + $script:LocalModulePath = "$env:HOME/.local/share/powershell/Modules/" + $script:AllUsersModulePath = "/usr/local/share/powershell/Modules/" + Mock Get-EnvironmentVariable { + Param( + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + $Name + ) + switch ($Name) { + "USERPROFILE" { Write-Output $env:USERPROFILE } + "HOME" { Write-Output $env:HOME } + "PSModulePath" { Write-Output "$env:HOME/.local/share/powershell/Modules:/usr/local/share/powershell/Modules:/opt/microsoft/powershell/7/Modules" } + } + } + Mock Test-OperatingSystem { $false } + } + } } Describe "Test-PowershellModuleInstalled" { @@ -21,7 +85,7 @@ Describe "Test-PowershellModuleInstalled" { [PSCustomObject]@{ Name = "posh-git" Version = "1.0.0" - Path = "$env:USERPROFILE\Documents\WindowsPowerShell\Modules\posh-git" + Path = "$($script:LocalModulePath)posh-git" } } $result = Test-PowershellModuleInstalled -ModuleName "posh-git" @@ -36,7 +100,7 @@ Describe "Test-PowershellModuleInstalled" { [PSCustomObject]@{ Name = "PSReadLine" Version = "2.2.6" - Path = "$env:USERPROFILE\Documents\PowerShell\Modules\PSReadLine" + Path = "$($script:LocalModulePath)PSReadLine" } } $result = Test-PowershellModuleInstalled -ModuleName "PSReadLine" -Version "2.2.6" @@ -51,7 +115,7 @@ Describe "Test-PowershellModuleInstalled" { [PSCustomObject]@{ Name = "PSReadLine" Version = "2.2.5" - Path = "$env:USERPROFILE\Documents\PowerShell\Modules\PSReadLine" + Path = "$($script:LocalModulePath)PSReadLine" } } $result = Test-PowershellModuleInstalled -ModuleName "PSReadLine" -Version "2.2.6" @@ -66,7 +130,7 @@ Describe "Test-PowershellModuleInstalled" { [PSCustomObject]@{ Name = "PowerShellGet" Version = "2.2.5" - Path = "$env:ProgramFiles\PowerShell\Modules\PowerShellGet" + Path = "$($script:AllUsersModulePath)PowerShellGet" } } $result = Test-PowershellModuleInstalled -ModuleName "PowerShellGet" -Scope "AllUsers" @@ -81,7 +145,7 @@ Describe "Test-PowershellModuleInstalled" { [PSCustomObject]@{ Name = "Az" Version = "9.0.1" - Path = "$env:USERPROFILE\Documents\PowerShell\Modules\Az" + Path = "$($script:LocalModulePath)Az" } } $result = Test-PowershellModuleInstalled -ModuleName "Az" -Scope "CurrentUser" diff --git a/DevSetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.ps1 b/DevSetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.ps1 index f5e1614..ab8f0d7 100644 --- a/DevSetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.ps1 +++ b/DevSetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.ps1 @@ -104,12 +104,13 @@ Function Test-PowershellModuleInstalled { # AllUsers ps7 (linux/macos) # $env:HOME/.local/share/powershell/Modules if((Test-OperatingSystem -Windows)) { - $SearchPath = $env:USERPROFILE + $SearchPath = (Get-EnvironmentVariable USERPROFILE) } else { - $SearchPath = $env:HOME + $SearchPath = (Get-EnvironmentVariable HOME) } + $InstallPaths = @( - $env:PSModulePath -split ([System.IO.Path]::PathSeparator) | ForEach-Object { + (Get-EnvironmentVariable PSModulePath) -split ([System.IO.Path]::PathSeparator) | ForEach-Object { if($_ -match [regex]::Escape("$SearchPath")) { @{ Path = $_; Scope = "CurrentUser" } } else { diff --git a/DevSetup/Private/Providers/Powershell/Uninstall-PowershellModules.Tests.ps1 b/DevSetup/Private/Providers/Powershell/Uninstall-PowershellModules.Tests.ps1 index 17471fb..03f53c7 100644 --- a/DevSetup/Private/Providers/Powershell/Uninstall-PowershellModules.Tests.ps1 +++ b/DevSetup/Private/Providers/Powershell/Uninstall-PowershellModules.Tests.ps1 @@ -1,8 +1,8 @@ BeforeAll { - . $PSScriptRoot\Uninstall-PowershellModules.ps1 - . $PSScriptRoot\Uninstall-PowerShellModule.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 + . (Join-Path $PSScriptRoot "Uninstall-PowershellModules.ps1") + . (Join-Path $PSScriptRoot "Uninstall-PowershellModule.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1") + . (Join-Path $PSScriptRoot "..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1") Mock Write-StatusMessage { } Mock Write-Warning { } Mock Write-Error { } @@ -49,7 +49,7 @@ Describe "Uninstall-PowershellModules" { Context "When modules are uninstalled successfully (string format)" { It "Should uninstall all modules and return true" { $script:uninstallCalls = @() - Mock Uninstall-PowerShellModule -MockWith { + Mock Uninstall-PowershellModule -MockWith { param($ModuleName) $script:uninstallCalls += $ModuleName return $true @@ -73,7 +73,7 @@ Describe "Uninstall-PowershellModules" { Context "When modules are uninstalled successfully (object format)" { It "Should uninstall all modules and return true" { $script:uninstallCalls = @() - Mock Uninstall-PowerShellModule -MockWith { + Mock Uninstall-PowershellModule -MockWith { param($ModuleName) $script:uninstallCalls += $ModuleName return $true @@ -100,7 +100,7 @@ Describe "Uninstall-PowershellModules" { Context "When some modules fail to uninstall" { It "Should continue and return true" { $script:uninstallCalls = @() - Mock Uninstall-PowerShellModule -MockWith { + Mock Uninstall-PowershellModule -MockWith { param($ModuleName) $script:uninstallCalls += $ModuleName if ($ModuleName -eq "PSReadLine") { return $false } @@ -126,7 +126,7 @@ Describe "Uninstall-PowershellModules" { Context "When module entry is empty or missing name" { It "Should skip invalid entries and return true" { $script:uninstallCalls = @() - Mock Uninstall-PowerShellModule -MockWith { + Mock Uninstall-PowershellModule -MockWith { param($ModuleName) $script:uninstallCalls += $ModuleName return $true @@ -153,7 +153,7 @@ Describe "Uninstall-PowershellModules" { Context "When an exception occurs during uninstallation" { It "Should catch and return false" { - Mock Uninstall-PowerShellModule { throw "Unexpected error" } + Mock Uninstall-PowershellModule { throw "Unexpected error" } $yamlData = @{ devsetup = @{ dependencies = @{ diff --git a/DevSetup/Private/Providers/Powershell/Uninstall-PowershellModules.ps1 b/DevSetup/Private/Providers/Powershell/Uninstall-PowershellModules.ps1 index 50b4563..563910a 100644 --- a/DevSetup/Private/Providers/Powershell/Uninstall-PowershellModules.ps1 +++ b/DevSetup/Private/Providers/Powershell/Uninstall-PowershellModules.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Uninstalls multiple PowerShell modules from the system based on YAML configuration. diff --git a/DevSetup/Private/Providers/Scoop/Export-InstalledScoopPackages.ps1 b/DevSetup/Private/Providers/Scoop/Export-InstalledScoopPackages.ps1 index db2107d..9e97e9a 100644 --- a/DevSetup/Private/Providers/Scoop/Export-InstalledScoopPackages.ps1 +++ b/DevSetup/Private/Providers/Scoop/Export-InstalledScoopPackages.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Exports installed Scoop packages and buckets to a YAML configuration file. diff --git a/DevSetup/Private/Providers/Scoop/Find-Scoop.Tests.ps1 b/DevSetup/Private/Providers/Scoop/Find-Scoop.Tests.ps1 index 8b5c4b7..ccd0fe1 100644 --- a/DevSetup/Private/Providers/Scoop/Find-Scoop.Tests.ps1 +++ b/DevSetup/Private/Providers/Scoop/Find-Scoop.Tests.ps1 @@ -1,12 +1,25 @@ BeforeAll { . $PSScriptRoot\Find-Scoop.ps1 . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Get-EnvironmentVariable.ps1 + if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { + Mock Get-EnvironmentVariable { return "$TestDrive\Users\Test User" } + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { + Mock Get-EnvironmentVariable { return "$TestDrive/home/testuser" } + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { + Mock Get-EnvironmentVariable { return "$TestDrive/Users/TestUser" } + } } Describe "Find-Scoop" { Context "When scoop is found by Get-Command" { BeforeEach { - Mock Get-Command { return 'TestDrive:\Users\Test User\scoop\shims\scoop.ps1' } + if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { + Mock Get-Command { return "$TestDrive\Users\Test User\scoop\shims\scoop.ps1" } + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { + Mock Get-Command { return "$TestDrive/home/testuser/scoop/shims/scoop" } + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { + Mock Get-Command { return "$TestDrive/Users/TestUser/scoop/shims/scoop" } + } } It "should return scoop" { $scoop = Find-Scoop @@ -17,7 +30,13 @@ Describe "Find-Scoop" { Context "When scoop is not found by Get-Command or any other option it should return null" { BeforeEach { Mock Get-Command { return $null } - Mock Get-EnvironmentVariable { return 'TestDrive:\Users\Test User' } + if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { + Mock Get-EnvironmentVariable { return "$TestDrive\Users\Test User" } + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { + Mock Get-EnvironmentVariable { return "$TestDrive/home/testuser" } + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { + Mock Get-EnvironmentVariable { return "$TestDrive/Users/TestUser" } + } } It "should return null" { $scoop = Find-Scoop @@ -28,39 +47,87 @@ Describe "Find-Scoop" { Context "When scoop is not found by Get-Command but scoop.ps1 is found" { BeforeEach { Mock Get-Command { return $null } - Mock Get-EnvironmentVariable { return 'TestDrive:\Users\Test User' } - New-Item -Path 'TestDrive:\Users\Test User\scoop\shims' -ItemType Directory -Force | Out-Null - Set-Content 'TestDrive:\Users\Test User\scoop\shims\scoop.ps1' -Value 'Scoop PowerShell Script' + if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { + Mock Get-EnvironmentVariable { return "$TestDrive\Users\Test User" } + New-Item -Path "$TestDrive\Users\Test User\scoop\shims" -ItemType Directory -Force | Out-Null + Set-Content "$TestDrive\Users\Test User\scoop\shims\scoop.ps1" -Value 'Scoop PowerShell Script' + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { + Mock Get-EnvironmentVariable { return "$TestDrive/home/testuser" } + New-Item -Path "$TestDrive/home/testuser/scoop/shims" -ItemType Directory -Force | Out-Null + Set-Content "$TestDrive/home/testuser/scoop/shims/scoop.ps1" -Value 'Scoop Command Script' + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { + Mock Get-EnvironmentVariable { return "$TestDrive/Users/TestUser" } + New-Item -Path "$TestDrive/Users/TestUser/scoop/shims" -ItemType Directory -Force | Out-Null + Set-Content "$TestDrive/Users/TestUser/scoop/shims/scoop.ps1" -Value 'Scoop Command Script' + } } - It "should return TestDrive:\Users\Test User\scoop\shims\scoop.ps1" { + It "should return scoop.ps1" { $scoop = Find-Scoop - $scoop | Should -Be "TestDrive:\Users\Test User\scoop\shims\scoop.ps1" + if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { + $scoop | Should -Be "$TestDrive\Users\Test User\scoop\shims\scoop.ps1" + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { + $scoop | Should -Be "$TestDrive/home/testuser/scoop/shims/scoop.ps1" + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { + $scoop | Should -Be "$TestDrive/Users/TestUser/scoop/shims/scoop.ps1" + } } } Context "When scoop is not found by Get-Command but scoop.cmd is found" { BeforeEach { Mock Get-Command { return $null } - Mock Get-EnvironmentVariable { return 'TestDrive:\Users\Test User' } - New-Item -Path 'TestDrive:\Users\Test User\scoop\shims' -ItemType Directory -Force | Out-Null - Set-Content 'TestDrive:\Users\Test User\scoop\shims\scoop.cmd' -Value 'Scoop Command Script' + if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { + Mock Get-EnvironmentVariable { return "$TestDrive\Users\Test User" } + New-Item -Path "$TestDrive\Users\Test User\scoop\shims" -ItemType Directory -Force | Out-Null + Set-Content "$TestDrive\Users\Test User\scoop\shims\scoop.cmd" -Value 'Scoop Command Script' + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { + Mock Get-EnvironmentVariable { return "$TestDrive/home/testuser" } + New-Item -Path "$TestDrive/home/testuser/scoop/shims" -ItemType Directory -Force | Out-Null + Set-Content "$TestDrive/home/testuser/scoop/shims/scoop.cmd" -Value 'Scoop Command Script' + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { + Mock Get-EnvironmentVariable { return "$TestDrive/Users/TestUser" } + New-Item -Path "$TestDrive/Users/TestUser/scoop/shims" -ItemType Directory -Force | Out-Null + Set-Content "$TestDrive/Users/TestUser/scoop/shims/scoop.cmd" -Value 'Scoop Command Script' + } } - It "should return TestDrive:\Users\Test User\scoop\shims\scoop.cmd" { + It "should return scoop.cmd" { $scoop = Find-Scoop - $scoop | Should -Be "TestDrive:\Users\Test User\scoop\shims\scoop.cmd" + if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { + $scoop | Should -Be "$TestDrive\Users\Test User\scoop\shims\scoop.cmd" + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { + $scoop | Should -Be "$TestDrive/home/testuser/scoop/shims/scoop.cmd" + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { + $scoop | Should -Be "$TestDrive/Users/TestUser/scoop/shims/scoop.cmd" + } } } Context "When scoop is not found by Get-Command but scoop is found" { BeforeEach { Mock Get-Command { return $null } - Mock Get-EnvironmentVariable { return 'TestDrive:\Users\Test User' } - New-Item -Path 'TestDrive:\Users\Test User\scoop\shims' -ItemType Directory -Force | Out-Null - Set-Content 'TestDrive:\Users\Test User\scoop\shims\scoop' -Value 'Scoop Command Script' + if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { + Mock Get-EnvironmentVariable { return "$TestDrive\Users\Test User" } + New-Item -Path "$TestDrive\Users\Test User\scoop\shims" -ItemType Directory -Force | Out-Null + Set-Content "$TestDrive\Users\Test User\scoop\shims\scoop" -Value 'Scoop Command Script' + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { + Mock Get-EnvironmentVariable { return "$TestDrive/home/testuser" } + New-Item -Path "$TestDrive/home/testuser/scoop/shims" -ItemType Directory -Force | Out-Null + Set-Content "$TestDrive/home/testuser/scoop/shims/scoop" -Value 'Scoop Command Script' + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { + Mock Get-EnvironmentVariable { return "$TestDrive/Users/TestUser" } + New-Item -Path "$TestDrive/Users/TestUser/scoop/shims" -ItemType Directory -Force | Out-Null + Set-Content "$TestDrive/Users/TestUser/scoop/shims/scoop" -Value 'Scoop Command Script' + } } - It "should return TestDrive:\Users\Test User\scoop\shims\scoop" { + It "should return scoop" { $scoop = Find-Scoop - $scoop | Should -Be "TestDrive:\Users\Test User\scoop\shims\scoop" + if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { + $scoop | Should -Be "$TestDrive\Users\Test User\scoop\shims\scoop" + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { + $scoop | Should -Be "$TestDrive/home/testuser/scoop/shims/scoop" + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { + $scoop | Should -Be "$TestDrive/Users/TestUser/scoop/shims/scoop" + } } } } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Get-ScoopCacheFile.Tests.ps1 b/DevSetup/Private/Providers/Scoop/Get-ScoopCacheFile.Tests.ps1 index 617a9d7..ac92999 100644 --- a/DevSetup/Private/Providers/Scoop/Get-ScoopCacheFile.Tests.ps1 +++ b/DevSetup/Private/Providers/Scoop/Get-ScoopCacheFile.Tests.ps1 @@ -6,11 +6,23 @@ BeforeAll { Describe "Get-ScoopCacheFile" { Context "When scoop is found by Get-Command" { BeforeEach { - Mock Get-DevSetupCachePath { return 'TestDrive:\Users\Test User\.devsetup\.cache' } + if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { + Mock Get-DevSetupCachePath { return "$TestDrive\Users\Test User\devsetup\.cache" } + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { + Mock Get-DevSetupCachePath { return "$TestDrive/home/testuser/devsetup/.cache" } + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { + Mock Get-DevSetupCachePath { return "$TestDrive/Users/TestUser/devsetup/.cache" } + } } It "should return the correct scoop cache file path" { $scoopCacheFile = Get-ScoopCacheFile - $scoopCacheFile | Should -Be "TestDrive:\Users\Test User\.devsetup\.cache\scoop.cache" + if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { + $scoopCacheFile | Should -Be "$TestDrive\Users\Test User\devsetup\.cache\scoop.cache" + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { + $scoopCacheFile | Should -Be "$TestDrive/home/testuser/devsetup/.cache/scoop.cache" + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { + $scoopCacheFile | Should -Be "$TestDrive/Users/TestUser/devsetup/.cache/scoop.cache" + } } } } \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Install-ScoopComponents.ps1 b/DevSetup/Private/Providers/Scoop/Install-ScoopComponents.ps1 index cf5aa56..fb4b812 100644 --- a/DevSetup/Private/Providers/Scoop/Install-ScoopComponents.ps1 +++ b/DevSetup/Private/Providers/Scoop/Install-ScoopComponents.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Installs Scoop buckets and packages from YAML configuration data. diff --git a/DevSetup/Private/Providers/Scoop/Test-ScoopInstalled.Tests.ps1 b/DevSetup/Private/Providers/Scoop/Test-ScoopInstalled.Tests.ps1 index 2cd4774..c0810d3 100644 --- a/DevSetup/Private/Providers/Scoop/Test-ScoopInstalled.Tests.ps1 +++ b/DevSetup/Private/Providers/Scoop/Test-ScoopInstalled.Tests.ps1 @@ -1,5 +1,13 @@ BeforeAll { . $PSScriptRoot\Test-ScoopInstalled.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Get-EnvironmentVariable.ps1 + if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows)) { + Mock Get-EnvironmentVariable { return "$TestDrive\Users\TestUser" } + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux) { + Mock Get-EnvironmentVariable { return "$TestDrive/home/testuser" } + } elseif ($PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS) { + Mock Get-EnvironmentVariable { return "$TestDrive/Users/TestUser" } + } } Describe "Test-ScoopInstalled" { diff --git a/DevSetup/Private/Providers/Scoop/Test-ScoopInstalled.ps1 b/DevSetup/Private/Providers/Scoop/Test-ScoopInstalled.ps1 index 047594e..304f39b 100644 --- a/DevSetup/Private/Providers/Scoop/Test-ScoopInstalled.ps1 +++ b/DevSetup/Private/Providers/Scoop/Test-ScoopInstalled.ps1 @@ -63,17 +63,17 @@ Function Test-ScoopInstalled { return $true } else { # Check for Scoop in user profile directory - $scoopPath = Join-Path $env:USERPROFILE "scoop\shims\scoop.ps1" + $scoopPath = Join-Path (Get-EnvironmentVariable USERPROFILE) "scoop\shims\scoop.ps1" if (Test-Path $scoopPath) { return $true } - $scoopPath = Join-Path $env:USERPROFILE "scoop\shims\scoop.cmd" + $scoopPath = Join-Path (Get-EnvironmentVariable USERPROFILE) "scoop\shims\scoop.cmd" if (Test-Path $scoopPath) { return $true } - $scoopPath = Join-Path $env:USERPROFILE "scoop\shims\scoop" + $scoopPath = Join-Path (Get-EnvironmentVariable USERPROFILE) "scoop\shims\scoop" if (Test-Path $scoopPath) { return $true } diff --git a/DevSetup/Private/Providers/Scoop/Uninstall-ScoopComponents.ps1 b/DevSetup/Private/Providers/Scoop/Uninstall-ScoopComponents.ps1 index d2b2f6e..4012b20 100644 --- a/DevSetup/Private/Providers/Scoop/Uninstall-ScoopComponents.ps1 +++ b/DevSetup/Private/Providers/Scoop/Uninstall-ScoopComponents.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Uninstalls multiple Scoop components (buckets and packages) from the system based on YAML configuration. diff --git a/DevSetup/Private/Utils/ConvertFrom-Base64.Tests.ps1 b/DevSetup/Private/Utils/ConvertFrom-Base64.Tests.ps1 index 5554228..b9bd915 100644 --- a/DevSetup/Private/Utils/ConvertFrom-Base64.Tests.ps1 +++ b/DevSetup/Private/Utils/ConvertFrom-Base64.Tests.ps1 @@ -25,7 +25,7 @@ Describe "ConvertFrom-Base64" { It "Should decode and write to file, returning true" { $plainText = "Test file output" $base64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($plainText)) - $testFile = "$PSScriptRoot\test_output.txt" + $testFile = New-TemporaryFile if (Test-Path $testFile) { Remove-Item $testFile } $result = ConvertFrom-Base64 -EncodedString $base64 -OutputFile $testFile $result | Should -Be $true diff --git a/DevSetup/Private/Utils/ConvertTo-Base64.Tests.ps1 b/DevSetup/Private/Utils/ConvertTo-Base64.Tests.ps1 index 0f457b2..9499789 100644 --- a/DevSetup/Private/Utils/ConvertTo-Base64.Tests.ps1 +++ b/DevSetup/Private/Utils/ConvertTo-Base64.Tests.ps1 @@ -18,7 +18,7 @@ Describe "ConvertTo-Base64" { Context "When converting a file to Base64" { It "Should return the correct Base64 string" { $inputString = "File content" - $testFile = "${TestDrive}\test_input.txt" + $testFile = New-TemporaryFile Set-Content -Path $testFile -Value $inputString $stringBytes = [System.IO.File]::ReadAllBytes($testFile) $expected = [System.Convert]::ToBase64String($stringBytes) diff --git a/DevSetup/Private/Utils/Find-GitRepositories.ps1 b/DevSetup/Private/Utils/Find-GitRepositories.ps1 index dd7aa66..f7af8b1 100644 --- a/DevSetup/Private/Utils/Find-GitRepositories.ps1 +++ b/DevSetup/Private/Utils/Find-GitRepositories.ps1 @@ -1,4 +1,4 @@ -Function Find-GitRepositories { +Function Find-GitRepository { [CmdletBinding()] Param( [Parameter( @@ -25,7 +25,7 @@ Function Find-GitRepositories { Write-Verbose "[PROCESS] Excluding system folders: $($ExcludeFolders -join ', ')" # Use a more efficient search strategy - function Search-GitRepos { + function Search-GitRepo { param([string]$SearchPath, [string[]]$ExcludeFolders) try { diff --git a/DevSetup/Private/Utils/Get-DevSetupCachePath.Tests.ps1 b/DevSetup/Private/Utils/Get-DevSetupCachePath.Tests.ps1 index 16f617e..47a7d02 100644 --- a/DevSetup/Private/Utils/Get-DevSetupCachePath.Tests.ps1 +++ b/DevSetup/Private/Utils/Get-DevSetupCachePath.Tests.ps1 @@ -4,11 +4,47 @@ BeforeAll { } Describe "Get-DevSetupCachePath" { - BeforeEach { - Mock Get-DevSetupPath { return 'TestDrive:\Users\Test User\.devsetup' } - } - It "should return the correct cache path for a valid user" { - $cachePath = Get-DevSetupCachePath - $cachePath | Should -Be "TestDrive:\Users\Test User\.devsetup\.cache" + if ($PSVersionTable.PSVersion.Major -eq 5) { + Context "When running on Pwsh 5.1" { + BeforeEach { + Mock Get-DevSetupPath { return "$TestDrive\Users\Test User\devsetup" } + } + It "should return the correct cache path for a valid user" { + $cachePath = Get-DevSetupCachePath + $cachePath | Should -Be "$TestDrive\Users\Test User\devsetup\.cache" + } + } + } elseif ($PSVersionTable.PSVersion.Major -ge 6) { + if ($IsWindows) { + Context "When running on Pwsh 6+ on Windows" { + BeforeEach { + Mock Get-DevSetupPath { return "$TestDrive\Users\Test User\devsetup" } + } + It "should return the correct cache path for a valid user" { + $cachePath = Get-DevSetupCachePath + $cachePath | Should -Be "$TestDrive\Users\Test User\devsetup\.cache" + } + } + } elseif ($IsLinux) { + Context "When running on Pwsh 6+ on Linux" { + BeforeEach { + Mock Get-DevSetupPath { return "$TestDrive/home/testuser/devsetup" } + } + It "should return the correct cache path for a valid user" { + $cachePath = Get-DevSetupCachePath + $cachePath | Should -Be "$TestDrive/home/testuser/devsetup/.cache" + } + } + } elseif ($IsMacOS) { + Context "When running on Pwsh 6+ on MacOS" { + BeforeEach { + Mock Get-DevSetupPath { return "$TestDrive/Users/TestUser/devsetup" } + } + It "should return the correct cache path for a valid user" { + $cachePath = Get-DevSetupCachePath + $cachePath | Should -Be "$TestDrive/Users/TestUser/devsetup/.cache" + } + } + } } } \ No newline at end of file diff --git a/DevSetup/Private/Utils/Get-DevSetupEnvPath.Tests.ps1 b/DevSetup/Private/Utils/Get-DevSetupEnvPath.Tests.ps1 index f6a238d..39b924e 100644 --- a/DevSetup/Private/Utils/Get-DevSetupEnvPath.Tests.ps1 +++ b/DevSetup/Private/Utils/Get-DevSetupEnvPath.Tests.ps1 @@ -1,14 +1,51 @@ BeforeAll { . $PSScriptRoot\Get-DevSetupEnvPath.ps1 . $PSScriptRoot\Get-DevSetupPath.ps1 + . $PSScriptRoot\Test-OperatingSystem.ps1 } Describe "Get-DevSetupEnvPath" { - BeforeEach { - Mock Get-DevSetupPath { return 'TestDrive:\Users\Test User\.devsetup' } - } - It "should return the correct environment path for a valid user" { - $envPath = Get-DevSetupEnvPath - $envPath | Should -Be "TestDrive:\Users\Test User\.devsetup\environments" + if ($PSVersionTable.PSVersion.Major -eq 5) { + Context "When running on Pwsh 5.1" { + BeforeEach { + Mock Get-DevSetupPath { return "$TestDrive\Users\Test User\devsetup" } + } + It "should return the correct environment path for a valid user" { + $envPath = Get-DevSetupEnvPath + $envPath | Should -Be "$TestDrive\Users\Test User\devsetup\environments" + } + } + } elseif ($PSVersionTable.PSVersion.Major -ge 6) { + if($IsWindows) { + Context "When running on Pwsh 6+" { + BeforeEach { + Mock Get-DevSetupPath { return "$TestDrive\Users\Test User\devsetup" } + } + It "should return the correct environment path for a valid user" { + $envPath = Get-DevSetupEnvPath + $envPath | Should -Be "$TestDrive\Users\Test User\devsetup\environments" + } + } + } elseif ($IsLinux) { + Context "When running on Linux" { + BeforeEach { + Mock Get-DevSetupPath { return "$TestDrive/home/testuser/devsetup" } + } + It "should return the correct environment path for a valid user" { + $envPath = Get-DevSetupEnvPath + $envPath | Should -Be "$TestDrive/home/testuser/devsetup/environments" + } + } + } elseif ($IsMacOS) { + Context "When running on MacOS" { + BeforeEach { + Mock Get-DevSetupPath { return "$TestDrive/Users/TestUser/devsetup" } + } + It "should return the correct environment path for a valid user" { + $envPath = Get-DevSetupEnvPath + $envPath | Should -Be "$TestDrive/Users/TestUser/devsetup/environments" + } + } + } } } \ No newline at end of file diff --git a/DevSetup/Private/Utils/Get-DevSetupLogPath.ps1 b/DevSetup/Private/Utils/Get-DevSetupLogPath.ps1 new file mode 100644 index 0000000..7e03414 --- /dev/null +++ b/DevSetup/Private/Utils/Get-DevSetupLogPath.ps1 @@ -0,0 +1,13 @@ +Function Get-DevSetupLogPath { + [CmdletBinding()] + param ( + ) + + $LogPath = Join-Path -Path (Get-DevSetupPath) -ChildPath "logs" + + if (-not (Test-Path -Path $LogPath)) { + New-Item -ItemType Directory -Path $LogPath | Out-Null + } + + return $LogPath +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Get-DevSetupPath.Tests.ps1 b/DevSetup/Private/Utils/Get-DevSetupPath.Tests.ps1 index f8a8fc0..0f2bf30 100644 --- a/DevSetup/Private/Utils/Get-DevSetupPath.Tests.ps1 +++ b/DevSetup/Private/Utils/Get-DevSetupPath.Tests.ps1 @@ -6,11 +6,51 @@ BeforeAll { } Describe "Get-DevSetupPath" { - BeforeEach { - Mock Get-EnvironmentVariable { return 'TestDrive:\Users\Test User' } - } - It "should return the correct devsetup for the current user" { - $envPath = Get-DevSetupPath - $envPath | Should -Be "TestDrive:\Users\Test User\devsetup" + if ($PSVersionTable.PSVersion.Major -eq 5) { + Context "When running on Pwsh 5.1" { + BeforeEach { + Mock Get-EnvironmentVariable { return "$TestDrive\Users\Test User" } + } + It "should return the correct devsetup for the current user" { + $envPath = Get-DevSetupPath + $envPath | Should -Be "$TestDrive\Users\Test User\devsetup" + } + } + } elseif ($PSVersionTable.PSVersion.Major -ge 6) { + Context "When running on Pwsh 6+" { + BeforeEach { + if ($IsWindows) { + Mock Get-EnvironmentVariable { return (Join-Path $TestDrive "Users" "Test User") } + } elseif( $IsLinux) { + Mock Get-EnvironmentVariable { return (Join-Path $TestDrive "home" "testuser") } + } elseif ($IsMacOS) { + Mock Get-EnvironmentVariable { return (Join-Path $TestDrive "Users" "TestUser") } + } + Mock Test-OperatingSystem { $true } + } + + if($IsLinux) { + It "should return the correct devsetup for the current user on Linux" { + $envPath = Get-DevSetupPath + $envPath | Should -Be (Join-Path $TestDrive "home" "testuser" "devsetup") + } + } + + if($IsMacOS) { + It "should return the correct devsetup for the current user on MacOS" { + Mock Test-OperatingSystem { Param($Windows, $Linux, $MacOS) { return $MacOS } } + $envPath = Get-DevSetupPath + $envPath | Should -Be (Join-Path $TestDrive "Users" "TestUser" "devsetup") + } + } + + if($IsWindows) { + It "should return the correct devsetup for the current user on Windows" { + Mock Test-OperatingSystem { Param($Windows, $Linux, $MacOS) { return $Windows } } + $envPath = Get-DevSetupPath + $envPath | Should -Be (Join-Path $TestDrive "Users" "Test User" "devsetup") + } + } + } } } \ No newline at end of file diff --git a/DevSetup/Private/Utils/Initialize-DevSetupEnvs.Tests.ps1 b/DevSetup/Private/Utils/Initialize-DevSetupEnvs.Tests.ps1 index 0b5b19b..bac7a36 100644 --- a/DevSetup/Private/Utils/Initialize-DevSetupEnvs.Tests.ps1 +++ b/DevSetup/Private/Utils/Initialize-DevSetupEnvs.Tests.ps1 @@ -1,14 +1,16 @@ BeforeAll { Function Get-GitHubRepository { } Function Install-GitRepository { } - - . $PSScriptRoot\Initialize-DevSetupEnvs.ps1 - . $PSScriptRoot\Write-StatusMessage.ps1 - . $PSScriptRoot\Optimize-DevSetupEnvs.ps1 - . $PSScriptRoot\Get-DevSetupEnvPath.ps1 - . $PSScriptRoot\Get-DevSetupLocalEnvPath.ps1 - . $PSScriptRoot\Get-DevSetupCommunityEnvPath.ps1 - . $PSScriptRoot\Get-DevSetupManifest.ps1 + + Function Set-GitHubConfiguration { } + + . (Join-Path $PSScriptRoot "Initialize-DevSetupEnvs.ps1") + . (Join-Path $PSScriptRoot "Write-StatusMessage.ps1") + . (Join-Path $PSScriptRoot "Optimize-DevSetupEnvs.ps1") + . (Join-Path $PSScriptRoot "Get-DevSetupEnvPath.ps1") + . (Join-Path $PSScriptRoot "Get-DevSetupLocalEnvPath.ps1") + . (Join-Path $PSScriptRoot "Get-DevSetupCommunityEnvPath.ps1") + . (Join-Path $PSScriptRoot "Get-DevSetupManifest.ps1") Mock Get-DevSetupEnvPath { "TestDrive:\DevSetupEnvs" } Mock Get-DevSetupManifest { @{ @@ -20,6 +22,7 @@ BeforeAll { } } Mock Get-GitHubRepository { @{ clone_url = "https://github.com/example/envrepo.git" } } + Mock Set-GitHubConfiguration { $true } Mock Test-Path { $false } Mock Install-GitRepository { $true } Mock Write-StatusMessage { } @@ -52,7 +55,7 @@ Describe "Initialize-DevSetupEnvs" { Context "When EnvironmentsProjectUri is not a .git URL and GitHub API fails" { It "Should write error and return null" { - Mock Get-GitHubRepository { $null } + Mock Get-GitHubRepository { return $null } $result = Initialize-DevSetupEnvs $result | Should -Be $null Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to retrieve repository information or clone_url" } @@ -108,7 +111,7 @@ Describe "Initialize-DevSetupEnvs" { Mock Test-Path { $false } Mock Install-GitRepository { $null } $global:LASTEXITCODE = 1 - $result = Initialize-DevSetupEnvs + Initialize-DevSetupEnvs Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -eq "[Failed]" } } } @@ -118,14 +121,14 @@ Describe "Initialize-DevSetupEnvs" { Mock Test-Path { $false } Mock Install-GitRepository { $null } $global:LASTEXITCODE = 0 - $result = Initialize-DevSetupEnvs + Initialize-DevSetupEnvs Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -eq "[OK]" } } } Context "When Optimize-DevSetupEnvs is called" { It "Should call Optimize-DevSetupEnvs after cloning" { - $result = Initialize-DevSetupEnvs + Initialize-DevSetupEnvs Assert-MockCalled Optimize-DevSetupEnvs -Scope It } } diff --git a/DevSetup/Private/Utils/Initialize-DevSetupEnvs.ps1 b/DevSetup/Private/Utils/Initialize-DevSetupEnvs.ps1 index d8e29ee..8d97788 100644 --- a/DevSetup/Private/Utils/Initialize-DevSetupEnvs.ps1 +++ b/DevSetup/Private/Utils/Initialize-DevSetupEnvs.ps1 @@ -1,4 +1,4 @@ -Function Initialize-DevSetupEnvs { +Function Initialize-DevSetupEnvs { try { # Define environments repository path $environmentsPath = Get-DevSetupEnvPath @@ -21,7 +21,7 @@ Function Initialize-DevSetupEnvs { # Check if the URI ends with .git, if not use Get-GitHubRepository to get clone_url if ($environmentsProjectUri -notlike "*.git") { try { - Set-GitHubConfiguration -DisableTelemetry + Set-GitHubConfiguration -DisableTelemetry | Out-Null #Write-Host "GitHub API access is required to retrieve repository information." -ForegroundColor Yellow #Write-Host "Please create a GitHub Personal Access Token with 'repo' scope at:" -ForegroundColor Yellow #Write-Host "https://github.com/settings/tokens" -ForegroundColor Cyan diff --git a/DevSetup/Private/Utils/Invoke-ExternalCommand.ps1 b/DevSetup/Private/Utils/Invoke-ExternalCommand.ps1 new file mode 100644 index 0000000..12f5155 --- /dev/null +++ b/DevSetup/Private/Utils/Invoke-ExternalCommand.ps1 @@ -0,0 +1,19 @@ +Function Invoke-ExternalCommand { + [CmdletBinding()] + Param( + [Parameter(Mandatory)] + [string]$Command, + [Parameter()] + [string[]]$Arguments + ) + + # Build the full command line + $cmdLine = $Command + if ($Arguments) { + $cmdLine += " " + ($Arguments -join " ") + } + + # Invoke the command and capture output + $output = & $Command @Arguments 2>&1 + return $output +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Optimize-DevSetupEnvs.Tests.ps1 b/DevSetup/Private/Utils/Optimize-DevSetupEnvs.Tests.ps1 index 9818354..e531397 100644 --- a/DevSetup/Private/Utils/Optimize-DevSetupEnvs.Tests.ps1 +++ b/DevSetup/Private/Utils/Optimize-DevSetupEnvs.Tests.ps1 @@ -1,4 +1,5 @@ BeforeAll { + Function Write-EZLog { } Function ConvertFrom-Yaml { } . $PSScriptRoot\Optimize-DevSetupEnvs.ps1 . $PSScriptRoot\Get-DevSetupEnvPath.ps1 @@ -8,7 +9,7 @@ BeforeAll { Mock Get-DevSetupEnvPath { "$TestDrive\DevSetupEnvs" } Mock Get-DevSetupPath { "$TestDrive\DevSetup" } Mock Join-Path { Param($Path, $ChildPath) "$Path\$ChildPath" } - Mock Write-StatusMessage { } + Mock Write-StatusMessage { Write-Error $Message } Mock Write-Warning { } Mock Write-Error { } Mock Write-Debug { } @@ -36,7 +37,7 @@ Describe "Optimize-DevSetupEnvs" { Mock Get-DevSetupEnvPath { $null } $result = Optimize-DevSetupEnvs $result | Should -Be $false - Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "DevSetup environments path not found" } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "DevSetup environments path not found" -and $Verbosity -eq "Warning" } } It "Should warn and return null if path does not exist" { @@ -44,7 +45,7 @@ Describe "Optimize-DevSetupEnvs" { Mock Test-Path { $false } $result = Optimize-DevSetupEnvs $result | Should -Be $false - Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "DevSetup environments path not found" } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "DevSetup environments path not found" -and $Verbosity -eq "Warning" } } } @@ -56,7 +57,7 @@ Describe "Optimize-DevSetupEnvs" { $result | Should -BeOfType 'bool' $result | Should -Be $true Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "Indexing 0 environment files" } - Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -eq "[OK]" } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "[OK]" } } } @@ -70,11 +71,11 @@ Describe "Optimize-DevSetupEnvs" { ) } $result = Optimize-DevSetupEnvs - Assert-MockCalled Write-Error -Scope It -Exactly 0 - #Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Object -match "Indexing 2 environment files" } - #Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -eq "[OK]" } + Assert-MockCalled Write-StatusMessage -Scope It -Exactly 0 -ParameterFilter { $Verbosity -eq "Error" } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "Indexing 2 environment files" } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "[OK]" } $result | Should -BeOfType 'bool' - $result | Should -Be $false + $result | Should -Be $true } } @@ -93,10 +94,10 @@ Describe "Optimize-DevSetupEnvs" { @{ devsetup = @{ configuration = @{ os = @{ name = "Windows" }; version = "1.0.0" } } } } $result = Optimize-DevSetupEnvs - Assert-MockCalled Write-Error -Scope It -Exactly 0 - Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "Failed to process bad.yaml" } + Assert-MockCalled Write-StatusMessage -Scope It -Exactly 0 -ParameterFilter { $Verbosity -eq "Error" } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "Failed to process bad.yaml" -and $Verbosity -eq "Warning" } $result | Should -BeOfType 'bool' - $result | Should -Be $false + $result | Should -Be $true } } @@ -112,7 +113,7 @@ Describe "Optimize-DevSetupEnvs" { Mock Out-File { throw "File error" } $result = Optimize-DevSetupEnvs $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -eq "[Failed]" } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "Failed to optimize DevSetup environments" } } } @@ -121,7 +122,7 @@ Describe "Optimize-DevSetupEnvs" { Mock Get-DevSetupEnvPath { throw "Unexpected error" } $result = Optimize-DevSetupEnvs $result | Should -Be $false - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to optimize DevSetup environments" } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "Failed to optimize DevSetup environments" -and $Verbosity -eq "Error" } } } } \ No newline at end of file diff --git a/DevSetup/Private/Utils/Optimize-DevSetupEnvs.ps1 b/DevSetup/Private/Utils/Optimize-DevSetupEnvs.ps1 index d262fea..8273bbe 100644 --- a/DevSetup/Private/Utils/Optimize-DevSetupEnvs.ps1 +++ b/DevSetup/Private/Utils/Optimize-DevSetupEnvs.ps1 @@ -1,9 +1,12 @@ -Function Optimize-DevSetupEnvs { +Function Optimize-DevSetupEnvs { + [CmdletBinding()] + [OutputType([bool])] + Param() try { # Get the DevSetup environments path $envPath = Get-DevSetupEnvPath if (-not $envPath -or -not (Test-Path $envPath)) { - Write-Warning "DevSetup environments path not found or doesn't exist: $envPath" + Write-StatusMessage "DevSetup environments path not found or doesn't exist: $envPath" -Verbosity Warning return $false } @@ -60,10 +63,10 @@ Function Optimize-DevSetupEnvs { $environments += $envEntry $platformDisplay = if ($platform) { $platform } else { 'Not specified' } - Write-Debug " - Name: $envName, Version: $version, Platform: $platformDisplay" + Write-StatusMessage " - Name: $envName, Version: $version, Platform: $platformDisplay" -Verbosity Debug } catch { - Write-Warning "Failed to process $($devsetupEnvFile.Name): $_" + Write-StatusMessage "Failed to process $($devsetupEnvFile.Name): $_" -Verbosity Warning continue } } @@ -74,20 +77,21 @@ Function Optimize-DevSetupEnvs { try { $jsonOutput = $environments | ConvertTo-Json -Depth 10 - $jsonOutput | Out-File -FilePath $environmentsJsonPath -Encoding UTF8 - Write-Debug "Environment index written to: $environmentsJsonPath" - #Write-Host "Processed $($environments.Count) environment(s) successfully" -ForegroundColor Green + $jsonOutput | Out-File -FilePath $environmentsJsonPath + Write-StatusMessage "Environment index written to: $environmentsJsonPath" -Verbosity Debug Write-StatusMessage "[OK]" -ForegroundColor Green } catch { - Write-StatusMessage "[Failed]" -ForegroundColor Red + Write-StatusMessage "Failed to optimize DevSetup environments: $_" -ForegroundColor Red -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error return $false } return $true } catch { - Write-Error "Failed to optimize DevSetup environments: $_" + Write-StatusMessage "Failed to optimize DevSetup environments: $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error return $false } } \ No newline at end of file diff --git a/DevSetup/Private/Utils/Start-DevSetupSelfUpdate.ps1 b/DevSetup/Private/Utils/Start-DevSetupSelfUpdate.ps1 new file mode 100644 index 0000000..dfff6e6 --- /dev/null +++ b/DevSetup/Private/Utils/Start-DevSetupSelfUpdate.ps1 @@ -0,0 +1,22 @@ +Function Start-DevSetupSelfUpdate { + [CmdletBinding()] + Param() + + $manifest = Get-DevSetupManifest + if($null -eq $manifest) { + throw "Failed to load manifest file" + } + + $communityEnvironmentsProjectUri = $manifest.PrivateData.PSData.EnvironmentsProjectUri + $devsetupProjectUri = $manifest.PrivateData.PSData.ProjectUri + + $currentVersion = Get-DevSetupVersion + + $devsetupCurrentReleaseInfo = Get-GitHubRelease -Uri $devsetupProjectUri | Select-Object -First 1 + $communityEnvironmentsCurrentReleaseInfo = Get-GitHubRelease -Uri $communityEnvironmentsProjectUri | Select-Object -First 1 + + $devsetupCurrentReleaseVersion = [Version]::new(($devsetupCurrentReleaseInfo.name -Replace "v")) + if($currentVersion -lt $devsetupCurrentReleaseVersion) { + + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Test-HasSudoAccess.ps1 b/DevSetup/Private/Utils/Test-HasSudoAccess.ps1 new file mode 100644 index 0000000..97d832e --- /dev/null +++ b/DevSetup/Private/Utils/Test-HasSudoAccess.ps1 @@ -0,0 +1,13 @@ +Function Test-HasSudoAccess { + [CmdletBinding()] + Param( + ) + + # Try running a harmless command with sudo + (bash -c "sudo -n true") *>$null + if ($LASTEXITCODE -eq 0) { + return $true + } else { + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Test-OperatingSystem.Tests.ps1 b/DevSetup/Private/Utils/Test-OperatingSystem.Tests.ps1 index 9c3d61b..3c6c187 100644 --- a/DevSetup/Private/Utils/Test-OperatingSystem.Tests.ps1 +++ b/DevSetup/Private/Utils/Test-OperatingSystem.Tests.ps1 @@ -30,33 +30,76 @@ Describe "Test-OperatingSystem" { } Context "When called with no parameters on PowerShell 5.1" { - It "Should return $null" { + It "Should return $false" { $result = Test-OperatingSystem - $result | Should -Be $null + $result | Should -Be $false } } } if ($PSVersionTable.PSVersion.Major -ge 6) { BeforeAll { Mock Get-PwshVersion { [PSCustomObject]@{ Major = $PSVersionTable.PSVersion.Major } } } - - Context "When called with -Windows on PowerShell 7+" { - It "Should return value of `$IsWindows (default: $true)" { - $result = Test-OperatingSystem -Windows - $result | Should -Be $true - } - It "Should return value of `$IsLinux (default: $false)" { - $result = Test-OperatingSystem -Linux - $result | Should -Be $false + if($IsWindows) { + Context "When called in PowerShell 7+ (Windows)" { + It "Should return value of `$IsWindows (default: $true)" { + $result = Test-OperatingSystem -Windows + $result | Should -Be $true + } + It "Should return value of `$IsLinux (default: $false)" { + $result = Test-OperatingSystem -Linux + $result | Should -Be $false + } + It "Should return value of `$IsMacOS (default: $false)" { + $result = Test-OperatingSystem -MacOS + $result | Should -Be $false + } + It "Should return $false if no parameter is specified" { + $result = Test-OperatingSystem + $result | Should -Be $false + } } - It "Should return value of `$IsMacOS (default: $false)" { - $result = Test-OperatingSystem -MacOS - $result | Should -Be $false + } + + if($IsLinux) { + Context "When called in PowerShell 7+ (Linux)" { + It "Should return value of `$IsWindows (default: $false)" { + $result = Test-OperatingSystem -Windows + $result | Should -Be $false + } + It "Should return value of `$IsLinux (default: $true)" { + $result = Test-OperatingSystem -Linux + $result | Should -Be $true + } + It "Should return value of `$IsMacOS (default: $false)" { + $result = Test-OperatingSystem -MacOS + $result | Should -Be $false + } + It "Should return $false if no parameter is specified" { + $result = Test-OperatingSystem + $result | Should -Be $false + } } - It "Should return $null if no parameter is specified" { - $result = Test-OperatingSystem - $result | Should -Be $null + } + + if($IsMacOS) { + Context "When called in PowerShell 7+ (MacOS)" { + It "Should return value of `$IsWindows (default: $false)" { + $result = Test-OperatingSystem -Windows + $result | Should -Be $false + } + It "Should return value of `$IsLinux (default: $false)" { + $result = Test-OperatingSystem -Linux + $result | Should -Be $false + } + It "Should return value of `$IsMacOS (default: $true)" { + $result = Test-OperatingSystem -MacOS + $result | Should -Be $true + } + It "Should return $false if no parameter is specified" { + $result = Test-OperatingSystem + $result | Should -Be $false + } } - } + } } } \ No newline at end of file diff --git a/DevSetup/Private/Utils/Test-OperatingSystem.ps1 b/DevSetup/Private/Utils/Test-OperatingSystem.ps1 index ebb1f27..b12392c 100644 --- a/DevSetup/Private/Utils/Test-OperatingSystem.ps1 +++ b/DevSetup/Private/Utils/Test-OperatingSystem.ps1 @@ -1,5 +1,6 @@ Function Test-OperatingSystem { [CmdletBinding()] + [OutputType([bool])] Param( [Parameter(Mandatory=$false)] [switch]$Windows, @@ -12,19 +13,19 @@ Function Test-OperatingSystem { ) if((Get-PwshVersion).Major -lt 6) { - $IsWindows = $true - $IsLinux = $false - $IsMacOS = $false + $IsPS5Windows = $true + $IsPS5Linux = $false + $IsPS5MacOS = $false } if($Windows) { - return $IsWindows + return ($IsPS5Windows -or $IsWindows) } if($Linux) { - return $IsLinux + return ($IsPS5Linux -or $IsLinux) } if($MacOS) { - return $IsMacOS + return ($IsPS5MacOS -or $IsMacOS) } - return $null + return $false } \ No newline at end of file diff --git a/DevSetup/Private/Utils/Test-RunningAsAdmin.Tests.ps1 b/DevSetup/Private/Utils/Test-RunningAsAdmin.Tests.ps1 index 6865457..d1cd82f 100644 --- a/DevSetup/Private/Utils/Test-RunningAsAdmin.Tests.ps1 +++ b/DevSetup/Private/Utils/Test-RunningAsAdmin.Tests.ps1 @@ -14,33 +14,35 @@ Describe "Test-RunningAsAdmin" { } } - Context "When running on Windows as administrator" { - It "Should return true" { - Mock Test-OperatingSystem { param($Windows) $true } - class MockPrincipal { - [bool] IsInRole([object]$role) { return $true } + if ($PSVersionTable.PSVersion.Major -eq 5 -or ($PSVersionTable.PSVersion.Major -eq 6 -and $IsWindows)) { + Context "When running on Windows as administrator" { + It "Should return true" { + Mock Test-OperatingSystem { param($Windows) $true } + class MockPrincipal { + [bool] IsInRole([object]$role) { return $true } + } + Mock 'New-Object' -MockWith { + param($type) + return [MockPrincipal]::new() + } + $result = Test-RunningAsAdmin + $result | Should -Be $true } - Mock 'New-Object' -MockWith { - param($type) - return [MockPrincipal]::new() - } - $result = Test-RunningAsAdmin - $result | Should -Be $true } - } - Context "When running on Windows but not as administrator" { - It "Should return false" { - Mock Test-OperatingSystem { param($Windows) $true } - class MockPrincipal { - [bool] IsInRole([object]$role) { return $false } - } - Mock 'New-Object' -MockWith { - param($type) - return [MockPrincipal]::new() + Context "When running on Windows but not as administrator" { + It "Should return false" { + Mock Test-OperatingSystem { param($Windows) $true } + class MockPrincipal { + [bool] IsInRole([object]$role) { return $false } + } + Mock 'New-Object' -MockWith { + param($type) + return [MockPrincipal]::new() + } + $result = Test-RunningAsAdmin + $result | Should -Be $false } - $result = Test-RunningAsAdmin - $result | Should -Be $false } } } \ No newline at end of file diff --git a/DevSetup/Private/Utils/Write-NewConfig.ps1 b/DevSetup/Private/Utils/Write-NewConfig.ps1 index df37b3e..bdb3f93 100644 --- a/DevSetup/Private/Utils/Write-NewConfig.ps1 +++ b/DevSetup/Private/Utils/Write-NewConfig.ps1 @@ -1,7 +1,8 @@ Function Write-NewConfig { Param( [Parameter(Mandatory = $true)] - [string]$OutFile + [string]$OutFile, + [switch]$DryRun = $false ) try { @@ -89,6 +90,7 @@ Function Write-NewConfig { } } + $username = if ($env:USERNAME) { $env:USERNAME } elseif ($env:USER) { $env:USER } else { "Unknown" } # Handle versioning and preserve existing config $currentVersion = "1.0.0" # Default version for new files $baseConfig = @{ @@ -111,7 +113,7 @@ Function Write-NewConfig { description = "Auto-generated development environment configuration" version = $currentVersion createdDate = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") - createdBy = $env:USERNAME + createdBy = $username os = @{ name = $friendlyPlatform version = $friendlyOsVersion @@ -189,16 +191,24 @@ Function Write-NewConfig { return $false } - # Convert from installed Chocolatey packages - Write-Host "`nScanning installed Chocolatey packages..." -ForegroundColor Cyan - if (-not (Export-InstalledChocolateyPackages -Config $OutFile)) { - Write-Warning "Failed to convert Chocolatey packages, but continuing..." - } + if((Test-OperatingSystem -Windows)) { + # Convert from installed Chocolatey packages + Write-Host "`nScanning installed Chocolatey packages..." -ForegroundColor Cyan + if (-not (Export-InstalledChocolateyPackages -Config $OutFile)) { + Write-Warning "Failed to convert Chocolatey packages, but continuing..." + } - # Convert from installed Scoop packages - Write-Host "`nScanning installed Scoop packages..." -ForegroundColor Cyan - if (-not (Export-InstalledScoopPackages -Config $OutFile)) { - Write-Warning "Failed to convert Scoop packages, but continuing..." + # Convert from installed Scoop packages + Write-Host "`nScanning installed Scoop packages..." -ForegroundColor Cyan + if (-not (Export-InstalledScoopPackages -Config $OutFile)) { + Write-Warning "Failed to convert Scoop packages, but continuing..." + } + } else { + # Convert from installed Homebrew packages + Write-Host "`nScanning installed Homebrew packages..." -ForegroundColor Cyan + if (-not (Invoke-HomebrewComponentExport -Config $OutFile -DryRun:$DryRun)) { + Write-Warning "Failed to convert Homebrew packages, but continuing..." + } } # Convert from installed PowerShell modules diff --git a/DevSetup/Private/Utils/Write-StatusMessage.Tests.ps1 b/DevSetup/Private/Utils/Write-StatusMessage.Tests.ps1 index c9cc458..60f4b91 100644 --- a/DevSetup/Private/Utils/Write-StatusMessage.Tests.ps1 +++ b/DevSetup/Private/Utils/Write-StatusMessage.Tests.ps1 @@ -1,10 +1,12 @@ BeforeAll { + Function Write-EZLog {} . $PSScriptRoot\Write-StatusMessage.ps1 Mock Write-Host { param($Message) } Mock Write-Verbose { param($Object) } Mock Write-Debug { param($Object) } Mock Write-Warning { param($Object) } Mock Write-Error { param($Object) } + Mock Write-EZLog { } } Describe "Write-StatusMessage" { diff --git a/DevSetup/Private/Utils/Write-StatusMessage.ps1 b/DevSetup/Private/Utils/Write-StatusMessage.ps1 index 35d6223..82bcec4 100644 --- a/DevSetup/Private/Utils/Write-StatusMessage.ps1 +++ b/DevSetup/Private/Utils/Write-StatusMessage.ps1 @@ -16,6 +16,11 @@ Function Write-StatusMessage { [switch]$NoNewLine ) + if([string]::IsNullOrWhiteSpace($Message)) { + #Write-StatusMessage "Message cannot be empty or whitespace." -Verbosity Error + return + } + if ($Indent -gt 0) { $Message = "$(' ' * $Indent)$Message" } @@ -41,18 +46,23 @@ Function Write-StatusMessage { switch($Verbosity) { "Verbose" { + Write-EZLog -Category INF -Message $Message Write-Verbose @messageParams } "Debug" { + Write-EZLog -Category INF -Message $Message Write-Debug @messageParams } "Warning" { + Write-EZLog -Category WAR -Message $Message Write-Warning @messageParams } "Error" { + Write-EZLog -Category ERR -Message $Message Write-Error @messageParams } "Default" { + #Write-EZLog -Category INF -Message $Message Write-Host @messageParams } } diff --git a/DevSetup/Public/Use-DevSetup.ps1 b/DevSetup/Public/Use-DevSetup.ps1 index df02c7e..99b0d28 100644 --- a/DevSetup/Public/Use-DevSetup.ps1 +++ b/DevSetup/Public/Use-DevSetup.ps1 @@ -113,7 +113,7 @@ Function Use-DevSetup { .FUNCTIONALITY Environment Management, Configuration Installation, System Setup #> - + [CmdletBinding()] Param( [Parameter(Mandatory = $true, ParameterSetName = "Install")] [Parameter(Mandatory = $true, ParameterSetName = "InstallUrl")] @@ -165,7 +165,10 @@ Function Use-DevSetup { [Parameter(Mandatory = $true, ParameterSetName = "InstallPath")] [Parameter(Mandatory = $true, ParameterSetName = "ExportPath")] - [string]$Path + [string]$Path, + + [Parameter(Mandatory = $false)] + [switch]$DryRun = $false ) try { @@ -173,7 +176,7 @@ Function Use-DevSetup { $selectedAction = $PSCmdlet.ParameterSetName.ToLower() - function Repeat-Char($char, $count) { -join (1..$count | ForEach-Object { $char }) } + function Format-RepeatChar($char, $count) { -join (1..$count | ForEach-Object { $char }) } # Display fancy action header # Define box drawing characters using [char] codes @@ -187,15 +190,15 @@ Function Use-DevSetup { $ml = [char]0x2560 # $mr = [char]0x2563 - $tb = "$tl" + (Repeat-Char $h 118) + "$tr" - $bm = "$ml" + (Repeat-Char $h 118) + "$mr" - $bb = "$bl" + (Repeat-Char $h 118) + "$br" - $sp = "$v" + (Repeat-Char " " 118) + "$v" + $tb = "$tl" + (Format-RepeatChar $h 118) + "$tr" + $bm = "$ml" + (Format-RepeatChar $h 118) + "$mr" + $bb = "$bl" + (Format-RepeatChar $h 118) + "$br" + $sp = "$v" + (Format-RepeatChar " " 118) + "$v" Write-Host "" Write-Host "$tb" -ForegroundColor Cyan Write-Host "$sp" -ForegroundColor Cyan - Write-Host "$v" (Repeat-Char " " 25) -ForegroundColor Cyan -NoNewLine + Write-Host "$v" (Format-RepeatChar " " 25) -ForegroundColor Cyan -NoNewLine Write-Host "$b$b$b$b$b$b" -ForegroundColor White -NoNewLine Write-Host "$tr " -ForegroundColor Cyan -NoNewLine Write-Host "$b$b$b$b$b$b$b" -ForegroundColor White -NoNewLine @@ -215,9 +218,9 @@ Function Use-DevSetup { Write-Host "$b$b" -ForegroundColor White -NoNewLine Write-Host "$tr" -ForegroundColor Cyan -NoNewLine Write-Host "$b$b$b$b$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tr" (Repeat-Char " " 24) "$v" -ForegroundColor Cyan + Write-Host "$tr" (Format-RepeatChar " " 24) "$v" -ForegroundColor Cyan - Write-Host "$v" (Repeat-Char " " 25) -ForegroundColor Cyan -NoNewLine + Write-Host "$v" (Format-RepeatChar " " 25) -ForegroundColor Cyan -NoNewLine Write-Host "$b$b" -ForegroundColor White -NoNewLine Write-Host "$tl$h$h" -ForegroundColor Cyan -NoNewLine Write-Host "$b$b" -ForegroundColor White -NoNewLine @@ -241,9 +244,9 @@ Function Use-DevSetup { Write-Host "$b$b" -ForegroundColor White -NoNewLine Write-Host "$tl$h$h" -ForegroundColor Cyan -NoNewLine Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tr" (Repeat-Char " " 23) "$v" -ForegroundColor Cyan + Write-Host "$tr" (Format-RepeatChar " " 23) "$v" -ForegroundColor Cyan - Write-Host "$v" (Repeat-Char " " 25) -ForegroundColor Cyan -NoNewLine + Write-Host "$v" (Format-RepeatChar " " 25) -ForegroundColor Cyan -NoNewLine Write-Host "$b$b" -ForegroundColor White -NoNewLine Write-Host "$v " -ForegroundColor Cyan -NoNewLine Write-Host "$b$b" -ForegroundColor White -NoNewLine @@ -265,9 +268,9 @@ Function Use-DevSetup { Write-Host "$b$b" -ForegroundColor White -NoNewLine Write-Host "$v" -ForegroundColor Cyan -NoNewLine Write-Host "$b$b$b$b$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tl$br" (Repeat-Char " " 23) "$v" -ForegroundColor Cyan + Write-Host "$tl$br" (Format-RepeatChar " " 23) "$v" -ForegroundColor Cyan - Write-Host "$v" (Repeat-Char " " 25) -ForegroundColor Cyan -NoNewLine + Write-Host "$v" (Format-RepeatChar " " 25) -ForegroundColor Cyan -NoNewLine Write-Host "$b$b" -ForegroundColor White -NoNewLine Write-Host "$v " -ForegroundColor Cyan -NoNewLine Write-Host "$b$b" -ForegroundColor White -NoNewLine @@ -289,9 +292,9 @@ Function Use-DevSetup { Write-Host "$b$b" -ForegroundColor White -NoNewLine Write-Host "$v" -ForegroundColor Cyan -NoNewLine Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tl$h$h$h$br" (Repeat-Char " " 24) "$v" -ForegroundColor Cyan + Write-Host "$tl$h$h$h$br" (Format-RepeatChar " " 24) "$v" -ForegroundColor Cyan - Write-Host "$v" (Repeat-Char " " 25) -ForegroundColor Cyan -NoNewLine + Write-Host "$v" (Format-RepeatChar " " 25) -ForegroundColor Cyan -NoNewLine Write-Host "$b$b$b$b$b$b" -ForegroundColor White -NoNewLine Write-Host "$tl$br" -ForegroundColor Cyan -NoNewLine Write-Host "$b$b$b$b$b$b$b" -ForegroundColor White -NoNewLine @@ -307,9 +310,9 @@ Function Use-DevSetup { Write-Host "$b$b$b$b$b$b" -ForegroundColor White -NoNewLine Write-Host "$tl$br" -ForegroundColor Cyan -NoNewLine Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$v" (Repeat-Char " " 28) "$v" -ForegroundColor Cyan + Write-Host "$v" (Format-RepeatChar " " 28) "$v" -ForegroundColor Cyan - Write-Host "$v" (Repeat-Char " " 24) "$bl$h$h$h$h$h$br $bl$h$h$h$h$h$h$br $bl$h$h$h$br $bl$h$h$h$h$h$h$br$bl$h$h$h$h$h$h$br $bl$h$br $bl$h$h$h$h$h$br $bl$h$br" (Repeat-Char " " 28) "$v" -ForegroundColor Cyan + Write-Host "$v" (Format-RepeatChar " " 24) "$bl$h$h$h$h$h$br $bl$h$h$h$h$h$h$br $bl$h$h$h$br $bl$h$h$h$h$h$h$br$bl$h$h$h$h$h$h$br $bl$h$br $bl$h$h$h$h$h$br $bl$h$br" (Format-RepeatChar " " 28) "$v" -ForegroundColor Cyan Write-Host "$v" -ForegroundColor Cyan -NoNewline $version = Get-DevSetupVersion -Local @@ -345,42 +348,49 @@ Function Use-DevSetup { Write-Host "$v" -ForegroundColor Cyan Write-Host "$bb" -ForegroundColor Cyan Write-Host "" - + + $RunDate = Get-Date -Format "yyyy-MM-dd_HH-mm-ss" + $LogFile = Join-Path -Path (Get-DevSetupLogPath) -ChildPath ([string]::Format("devsetup_{0}_{1}.log", $selectedAction, $RunDate)) + $PSDefaultParameterValues = @{ + 'Write-EZLog:LogFile' = $LogFile ; + } + + Write-EZLog -Header switch ($selectedAction) { {$_ -eq 'install' -or $_ -eq 'installpath' -or $_ -eq 'installurl'} { - Write-Host "Installing development environment..." -ForegroundColor Yellow + Write-StatusMessage "Installing development environment..." -ForegroundColor Yellow $ParameterCopy = [hashtable]$PSBoundParameters $ParameterCopy.Remove('Install') Install-DevSetupEnv @ParameterCopy } {$_ -eq 'update' -or $_ -eq 'updatemain' -or $_ -eq 'updatedevelop' -or $_ -eq 'updateversion'} { - Write-Host "Updating devsetup system..." -ForegroundColor Yellow + Write-StatusMessage "Updating devsetup system..." -ForegroundColor Yellow $ParameterCopy = [hashtable]$PSBoundParameters $ParameterCopy.Remove('Update') if($_ -eq 'update') { $ParameterCopy['Latest'] = $true - } + } Update-DevSetup @ParameterCopy | Out-Null } 'init' { - Write-Host "Initializing DevSetup system..." -ForegroundColor Yellow + Write-StatusMessage "Initializing DevSetup system..." -ForegroundColor Yellow Initialize-DevSetup | Out-Null } { $_ -eq 'export' -or $_ -eq 'exportpath' } { - Write-Host "Exporting current development environment..." -ForegroundColor Yellow + Write-StatusMessage "Exporting current development environment..." -ForegroundColor Yellow $ParameterCopy = [hashtable]$PSBoundParameters - $ParameterCopy.Remove('Export') + $ParameterCopy.Remove('Export') Export-DevSetupEnv @ParameterCopy } { $_ -eq 'list' -or $_ -eq 'listplatform' -or $_ -eq 'listprovider' -or $_ -eq 'listproviderplatform' } { $ParameterCopy = [hashtable]$PSBoundParameters - $ParameterCopy.Remove('List') + $ParameterCopy.Remove('List') Show-DevSetupEnvList @ParameterCopy } 'uninstall' { - Write-Host "Uninstalling development environment..." -ForegroundColor Yellow + Write-StatusMessage "Uninstalling development environment..." -ForegroundColor Yellow $ParameterCopy = [hashtable]$PSBoundParameters - $ParameterCopy.Remove('Uninstall') + $ParameterCopy.Remove('Uninstall') Uninstall-DevSetupEnv @ParameterCopy } } @@ -388,6 +398,10 @@ Function Use-DevSetup { #Write-Host "DevSetup action '$selectedAction' completed successfully!" -ForegroundColor Green } catch { - Write-Error "Error executing DevSetup action '$selectedAction': $_" + Write-StatusMessage "Error executing DevSetup action '$selectedAction': $_" -Verbosity Error + Write-StatusMessage $_.ScriptStackTrace -Verbosity Error } + Write-StatusMessage "Log file: $LogFile" -ForegroundColor DarkGray + Write-EZLog -Footer + Write-Host "" } \ No newline at end of file diff --git a/runSecurity.ps1 b/runSecurity.ps1 new file mode 100644 index 0000000..f40f775 --- /dev/null +++ b/runSecurity.ps1 @@ -0,0 +1,11 @@ +#-IncludeRule @("PSAvoidUsingInvokeExpression", "PSAvoidUsingConvertToSecureStringWithPlainText") ` +Set-PSRepository PSGallery -InstallationPolicy Trusted +Install-Module PSScriptAnalyzer -ErrorAction Stop +Install-Module ConvertToSARIF -ErrorAction Stop +Import-Module ConvertToSarif +Invoke-ScriptAnalyzer ` + -Path . ` + -ExcludeRule @("PSAvoidLongLines", "PSAlignAssignmentStatement") ` + -IncludeDefaultRules ` + -Severity @("Error", "Warning") ` + -Recurse | ConvertTo-SARIF -FilePath results.sarif -IgnorePattern 'Tests' \ No newline at end of file diff --git a/runTests.ps1 b/runTests.ps1 index 545a806..19cf388 100644 --- a/runTests.ps1 +++ b/runTests.ps1 @@ -2,5 +2,5 @@ $config = New-PesterConfiguration #$config.Run.PassThru = $true $config.CodeCoverage.Enabled = $true $config.TestResult.Enabled = $true -#$config.Output.Verbosity = "Detailed" +#$config.Output.Verbosity = "GithubActions" Invoke-Pester -Configuration $config \ No newline at end of file