diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61c03d5..cccd0f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,7 @@ name: CI on: push: - branches: [ master ] + branches: [master] paths: - "PSDepend/**" - "Tests/**" @@ -18,7 +18,7 @@ on: workflow_dispatch: jobs: - test: + test_pwsh: name: Test (${{ matrix.os }}) runs-on: ${{ matrix.os }} permissions: @@ -26,7 +26,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ ubuntu-latest, windows-latest, macOS-latest ] + os: [ubuntu-latest, windows-latest, macOS-latest] steps: - uses: actions/checkout@v4 - name: Bootstrap @@ -42,9 +42,30 @@ jobs: name: testResults-${{ matrix.os }} path: ./Tests/out/testResults.xml + test_ps51: + name: Test (Windows PowerShell 5.1) + runs-on: windows-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - name: Bootstrap + shell: powershell + run: ./build.ps1 -Bootstrap -Task Init + - name: Test + shell: powershell + run: ./build.ps1 -Task Test + - name: Upload Test Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: testResults-ps51 + path: ./Tests/out/testResults.xml publish-test-results: name: Publish Test Results - needs: test + needs: + - test_pwsh + - test_ps51 runs-on: ubuntu-latest permissions: checks: write diff --git a/PSDepend/PSDependScripts/Command.ps1 b/PSDepend/PSDependScripts/Command.ps1 index ee735ee..720bfe6 100644 --- a/PSDepend/PSDependScripts/Command.ps1 +++ b/PSDepend/PSDependScripts/Command.ps1 @@ -60,12 +60,12 @@ foreach($Depend in $Dependency) { if($FailOnError) { - Write-Error $_ - continue + throw $_ } else { - throw $_ + Write-Error $_ + continue } } } diff --git a/PSDepend/PSDependScripts/Noop.ps1 b/PSDepend/PSDependScripts/Noop.ps1 index 570c7a5..044e100 100644 --- a/PSDepend/PSDependScripts/Noop.ps1 +++ b/PSDepend/PSDependScripts/Noop.ps1 @@ -25,7 +25,7 @@ param ( Write-Verbose "Starting noop run with $($Dependency.count) sources" -[pscustomobject]@{ +[PSCustomObject]@{ PSBoundParameters = $PSBoundParameters Dependency= $Dependency DependencyParameters = $Dependency.Parameters diff --git a/PSDepend/PSDependScripts/PSGalleryModule.ps1 b/PSDepend/PSDependScripts/PSGalleryModule.ps1 index 427f164..36dc584 100644 --- a/PSDepend/PSDependScripts/PSGalleryModule.ps1 +++ b/PSDepend/PSDependScripts/PSGalleryModule.ps1 @@ -114,44 +114,48 @@ param( ) # Extract data from Dependency - $DependencyName = $Dependency.DependencyName - $Name = $Dependency.Name - if(-not $Name) - { - $Name = $DependencyName - } +$DependencyName = $Dependency.DependencyName +$Name = $Dependency.Name +if(-not $Name) { + $Name = $DependencyName +} - $Version = $Dependency.Version - if(-not $Version) - { - $Version = 'latest' - } +$Version = $Dependency.Version +if(-not $Version) { + $Version = 'latest' +} - # We use target as a proxy for Scope - if(-not $Dependency.Target) - { - $Scope = 'AllUsers' - } - else - { - $Scope = $Dependency.Target - } +# We use target as a proxy for Scope +if(-not $Dependency.Target) { + $Scope = 'AllUsers' +} else { + $Scope = $Dependency.Target +} - $Credential = $Dependency.Credential +$Credential = $Dependency.Credential - if('AllUsers', 'CurrentUser' -notcontains $Scope) - { - $command = 'save' - } - else - { - $command = 'install' +if('AllUsers', 'CurrentUser' -notcontains $Scope) { + $command = 'save' +} else { + $command = 'install' +} + +$nugetProvider = @(Get-PackageProvider -ErrorAction SilentlyContinue) | + Where-Object { $_.Name -eq 'NuGet' } | + Select-Object -First 1 + +if(-not $nugetProvider) { + Write-Debug 'NuGet provider not found. Attempting to install NuGet provider.' + # Bootstrap NuGet provider for Windows PowerShell 5.1 and PowerShell 7+. + $installPackageProviderSplat = @{ + Name = 'NuGet' + ForceBootstrap = $true + Force = $true + Scope = 'CurrentUser' + ErrorAction = 'SilentlyContinue' } -if(-not (Get-PackageProvider -Name Nuget)) -{ - # Grab nuget bits. - $null = Get-PackageProvider -Name NuGet -ForceBootstrap | Out-Null + $null = Install-PackageProvider @installPackageProviderSplat } Write-Verbose -Message "Getting dependency [$name] from PowerShell repository [$Repository]" @@ -160,10 +164,10 @@ Write-Verbose -Message "Getting dependency [$name] from PowerShell repository [$ # but allow to rely on all PS repos registered. if($Repository) { $validRepo = Get-PSRepository -Name $Repository -Verbose:$false -ErrorAction SilentlyContinue - if (-not $validRepo) { - Write-Error "[$Repository] has not been setup as a valid PowerShell repository." - return - } + if (-not $validRepo) { + Write-Error "[$Repository] has not been setup as a valid PowerShell repository." + return + } } $params = @{ @@ -174,11 +178,11 @@ $params = @{ Force = $True } -if($PSBoundParameters.ContainsKey('AllowPrerelease')){ +if($PSBoundParameters.ContainsKey('AllowPrerelease')) { $params.Add('AllowPrerelease', $AllowPrerelease) } -if($PSBoundParameters.ContainsKey('AcceptLicense')){ +if($PSBoundParameters.ContainsKey('AcceptLicense')) { $params.Add('AcceptLicense', $AcceptLicense) } @@ -186,35 +190,28 @@ if($Repository) { $params.Add('Repository',$Repository) } -if($Version -and $Version -ne 'latest') -{ +if($Version -and $Version -ne 'latest') { $Params.add('RequiredVersion', $Version) } -if($Credential) -{ - $Params.add('Credential', $Credential) +if($Credential) { + $Params.add('Credential', $Credential) } # This code works for both install and save scenarios. -if($command -eq 'Save') -{ +if($command -eq 'Save') { $ModuleName = Join-Path $Scope $Name $Params.Remove('AllowClobber') $Params.Remove('SkipPublisherCheck') -} -elseif ($Command -eq 'Install') -{ +} elseif ($Command -eq 'Install') { $ModuleName = $Name } # Only use "SkipPublisherCheck" (and other) parameter if "Install-Module" supports it $availableParameters = (Get-Command "Install-Module").Parameters $tempParams = $Params.Clone() -foreach($thisParameter in $Params.Keys) -{ - if(-Not ($availableParameters.ContainsKey($thisParameter))) - { +foreach($thisParameter in $Params.Keys) { + if(-not ($availableParameters.ContainsKey($thisParameter))) { Write-Verbose -Message "Removing parameter [$thisParameter] from [Install-Module] as it is not available" $tempParams.Remove($thisParameter) } @@ -226,33 +223,28 @@ Add-ToPsModulePathIfRequired -Dependency $Dependency -Action $PSDependAction $Existing = $null $Existing = Get-Module -ListAvailable -Name $ModuleName -ErrorAction SilentlyContinue -if($Existing) -{ +if($Existing) { Write-Verbose "Found existing module [$Name]" # Thanks to Brandon Padgett! $ExistingVersion = $Existing | Measure-Object -Property Version -Maximum | Select-Object -ExpandProperty Maximum - $FindModuleParams = @{Name = $Name} + $FindModuleParams = @{Name = $Name } if($Repository) { $FindModuleParams.Add('Repository', $Repository) } - if($Credential) - { + if($Credential) { $FindModuleParams.Add('Credential', $Credential) } - if($AllowPrerelease) - { + if($AllowPrerelease) { $FindModuleParams.Add('AllowPrerelease', $AllowPrerelease) } # Version string, and equal to current - if($Version -and $Version -ne 'latest' -and $Version -eq $ExistingVersion) - { + if($Version -and $Version -ne 'latest' -and $Version -eq $ExistingVersion) { Write-Verbose "You have the requested version [$Version] of [$Name]" # Conditional import Import-PSDependModule -Name $ModuleName -Action $PSDependAction -Version $ExistingVersion - if($PSDependAction -contains 'Test') - { + if($PSDependAction -contains 'Test') { return $true } return $null @@ -267,20 +259,17 @@ if($Existing) [System.Management.Automation.SemanticVersion]::TryParse($GalleryVersion, [ref]$parsedTempSemanticVersion) ) { $GalleryVersion -le $parsedSemanticVersion - } - elseif ([System.Version]::TryParse($ExistingVersion, [ref]$parsedVersion)) { + } elseif ([System.Version]::TryParse($ExistingVersion, [ref]$parsedVersion)) { $GalleryVersion -le $parsedVersion } # latest, and we have latest - if( $Version -and ($Version -eq 'latest' -or $Version -eq '') -and $isGalleryVersionLessEquals) - { + if( $Version -and ($Version -eq 'latest' -or $Version -eq '') -and $isGalleryVersionLessEquals) { Write-Verbose "You have the latest version of [$Name], with installed version [$ExistingVersion] and PSGallery version [$GalleryVersion]" # Conditional import Import-PSDependModule -Name $ModuleName -Action $PSDependAction -Version $ExistingVersion - if($PSDependAction -contains 'Test') - { + if($PSDependAction -contains 'Test') { return $True } return $null @@ -289,24 +278,18 @@ if($Existing) } #No dependency found, return false if we're testing alone... -if( $PSDependAction -contains 'Test' -and $PSDependAction.count -eq 1) -{ +if( $PSDependAction -contains 'Test' -and $PSDependAction.count -eq 1) { return $False } -if($PSDependAction -contains 'Install') -{ - if('AllUsers', 'CurrentUser' -contains $Scope) - { +if($PSDependAction -contains 'Install') { + if('AllUsers', 'CurrentUser' -contains $Scope) { Write-Verbose "Installing [$Name] with scope [$Scope]" Install-Module @params -Scope $Scope - } - else - { + } else { Write-Verbose "Saving [$Name] with path [$Scope]" Write-Verbose "Creating directory path to [$Scope]" - if(-not (Test-Path $Scope -ErrorAction SilentlyContinue)) - { + if(-not (Test-Path $Scope -ErrorAction SilentlyContinue)) { $Null = New-Item -ItemType Directory -Path $Scope -Force -ErrorAction SilentlyContinue } Save-Module @params -Path $Scope diff --git a/PSDepend/Private/Get-ProjectDetail.ps1 b/PSDepend/Private/Get-ProjectDetail.ps1 index 85d1d51..2340bbe 100644 --- a/PSDepend/Private/Get-ProjectDetail.ps1 +++ b/PSDepend/Private/Get-ProjectDetail.ps1 @@ -47,7 +47,7 @@ function Get-ProjectDetail { $RelativePath = '\', $Name ) - [pscustomobject]@{ + [PSCustomObject]@{ Name = $Name Path = Resolve-Path (Join-Path $Path $RelativePath) } diff --git a/PSDepend/Private/SemanticVersion.ps1 b/PSDepend/Private/SemanticVersion.ps1 index 4789656..f1ff909 100644 --- a/PSDepend/Private/SemanticVersion.ps1 +++ b/PSDepend/Private/SemanticVersion.ps1 @@ -687,6 +687,6 @@ namespace System.Management.Automation } '@ -if ($PSVersionTable.PSVersion.Major -lt 6) { - Add-Type -TypeDefinition $code +if ($PSVersionTable.PSVersion.Major -lt 6 -and -not ('System.Management.Automation.SemanticVersion' -as [type])) { + Add-Type -TypeDefinition $code } diff --git a/PSDepend/Public/Get-Dependency.ps1 b/PSDepend/Public/Get-Dependency.ps1 index f9784db..57d5721 100644 --- a/PSDepend/Public/Get-Dependency.ps1 +++ b/PSDepend/Public/Get-Dependency.ps1 @@ -235,7 +235,7 @@ function Get-Dependency { $Dependency -match '::' -and ($Dependency -split '::').count -eq 2 ) { - [pscustomobject]@{ + [PSCustomObject]@{ PSTypeName = 'PSDepend.Dependency' DependencyFile = $DependencyFile DependencyName = ($Dependency -split '::')[1] @@ -260,7 +260,7 @@ function Get-Dependency { $Dependency -notmatch '/' -and -not $DependencyType -or $DependencyType -eq 'PSGalleryModule') { - [pscustomobject]@{ + [PSCustomObject]@{ PSTypeName = 'PSDepend.Dependency' DependencyFile = $DependencyFile DependencyName = $Dependency @@ -286,7 +286,7 @@ function Get-Dependency { $Dependency.split('/').count -eq 2 -and -not $DependencyType -or $DependencyType -eq 'GitHub') { - [pscustomobject]@{ + [PSCustomObject]@{ PSTypeName = 'PSDepend.Dependency' DependencyFile = $DependencyFile DependencyName = $Dependency @@ -310,7 +310,7 @@ function Get-Dependency { $Dependency -match '/' -and -not $DependencyType -or $DependencyType -eq 'Git' ) { - [pscustomobject]@{ + [PSCustomObject]@{ PSTypeName = 'PSDepend.Dependency' DependencyFile = $DependencyFile DependencyName = $Dependency @@ -362,7 +362,7 @@ function Get-Dependency { } $CredentialName = Get-GlobalOption -Name Credential -Prefer $DependencyHash.Credential - [pscustomobject]@{ + [PSCustomObject]@{ PSTypeName = 'PSDepend.Dependency' DependencyFile = $DependencyFile DependencyName = $Dependency diff --git a/PSDepend/Public/Get-PSDependType.ps1 b/PSDepend/Public/Get-PSDependType.ps1 index e356d24..5654ba7 100644 --- a/PSDepend/Public/Get-PSDependType.ps1 +++ b/PSDepend/Public/Get-PSDependType.ps1 @@ -95,7 +95,7 @@ Function Get-PSDependType { else { $Support = @($DependencyDefinitions.$Type.Supports) - [pscustomobject]@{ + [PSCustomObject]@{ DependencyType = $Type Supports = $Support Supported = Test-PlatformSupport -Type $Type -Support $Support diff --git a/Tests/Chocolatey.Type.Tests.ps1 b/Tests/Chocolatey.Type.Tests.ps1 new file mode 100644 index 0000000..927e61d --- /dev/null +++ b/Tests/Chocolatey.Type.Tests.ps1 @@ -0,0 +1,62 @@ +#requires -Module @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } + +BeforeDiscovery { + Import-Module (Join-Path $PSScriptRoot 'Shared/TestHelpers.psm1') -Force + $script:SkipUnsupported = -not (Test-PSDependTypeSupportedHere -DependencyType 'Chocolatey') +} + +BeforeAll { + if (-not $env:BHProjectPath) { + Set-BuildEnvironment -Path "$PSScriptRoot/.." -Force + } + Remove-Module $env:BHProjectName -ErrorAction SilentlyContinue + Import-Module (Join-Path $env:BHProjectPath $env:BHProjectName) -Force + + Import-Module (Join-Path $PSScriptRoot 'Shared/TestHelpers.psm1') -Force + + $script:ScriptPath = Join-Path $env:BHProjectPath 'PSDepend/PSDependScripts/Chocolatey.ps1' +} + +Describe 'Chocolatey script' -Tag 'WindowsOnly' -Skip:$SkipUnsupported { + + BeforeAll { + InModuleScope PSDepend { + # Pretend choco.exe is present so we skip the bootstrap branch + Mock Get-Command { [PSCustomObject]@{ Name = 'choco.exe' } } -ParameterFilter { $Name -eq 'choco.exe' } + # All choco invocations return empty CSV (no packages installed, none found upstream) + Mock Invoke-ExternalCommand { } + Mock Invoke-WebRequest { } + } + } + + It 'Defaults Source to https://chocolatey.org/api/v2/ when not supplied' { + $dep = New-PSDependFixture -DependencyName 'git' -DependencyType 'Chocolatey' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -Force + } + Should -Invoke -CommandName Invoke-ExternalCommand -ModuleName PSDepend -Times 1 -Exactly -ParameterFilter { + ($Arguments -join ' ') -match "--source='https://chocolatey\.org/api/v2/'" + } + } + + It 'Invokes choco upgrade with -Force when -Force switch is set' { + $dep = New-PSDependFixture -DependencyName 'git' -DependencyType 'Chocolatey' -Version '2.0.2' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -Force + } + Should -Invoke -CommandName Invoke-ExternalCommand -ModuleName PSDepend -Times 1 -Exactly -ParameterFilter { + $Arguments -contains 'upgrade' -and $Arguments -contains '--force' + } + } + + It 'Forwards Credential to choco as --username / --password args' { + $cred = New-TestCredential -UserName 'feeduser' -Password 'feedpass' + $dep = New-PSDependFixture -DependencyName 'git' -DependencyType 'Chocolatey' -Version '2.0.2' -Credential $cred + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -Force + } + Should -Invoke -CommandName Invoke-ExternalCommand -ModuleName PSDepend -Times 1 -Exactly -ParameterFilter { + ($Arguments -join ' ') -match "--username='feeduser'" -and ($Arguments -join ' ') -match "--password='feedpass'" + } + } +} diff --git a/Tests/Command.Type.Tests.ps1 b/Tests/Command.Type.Tests.ps1 new file mode 100644 index 0000000..8eb0c36 --- /dev/null +++ b/Tests/Command.Type.Tests.ps1 @@ -0,0 +1,57 @@ +#requires -Module @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } + +BeforeAll { + if (-not $env:BHProjectPath) { + Set-BuildEnvironment -Path "$PSScriptRoot/.." -Force + } + Remove-Module $env:BHProjectName -ErrorAction SilentlyContinue + Import-Module (Join-Path $env:BHProjectPath $env:BHProjectName) -Force + + Import-Module (Join-Path $PSScriptRoot 'Shared/TestHelpers.psm1') -Force + + $script:ScriptPath = Join-Path $env:BHProjectPath 'PSDepend/PSDependScripts/Command.ps1' +} + +Describe 'Command script' { + + It 'Executes the Source string as PowerShell in the current session' { + $flagPath = Join-Path 'TestDrive:' 'flag.txt' + $dep = New-PSDependFixture -DependencyName 'CmdOne' -DependencyType 'Command' -Source "Set-Content -Path '$flagPath' -Value 'ran'" + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + (Get-Content $flagPath) | Should -Be 'ran' + } + + It 'Iterates multiple Source entries' { + $countPath = Join-Path 'TestDrive:' 'count.txt' + $dep = New-PSDependFixture -DependencyName 'CmdMulti' -DependencyType 'Command' -Source @( + "Add-Content '$countPath' 'a'" + "Add-Content '$countPath' 'b'" + ) + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + (Get-Content $countPath) | Should -Be @('a', 'b') + } + + It 'Writes a non-terminating error and continues by default when the Source errors' { + $dep = New-PSDependFixture -DependencyName 'CmdSwallow' -DependencyType 'Command' -Source "throw 'boom'" + $err = $null + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath; ErrRef = [ref]$err } { + & $ScriptPath -Dependency $Dep -ErrorAction SilentlyContinue -ErrorVariable scriptErr + $ErrRef.Value = $scriptErr + } + $err | Should -Not -BeNullOrEmpty + ($err | Out-String) | Should -Match 'boom' + } + + It 'Throws a terminating error when -FailOnError is specified' { + $dep = New-PSDependFixture -DependencyName 'CmdFail' -DependencyType 'Command' -Source "throw 'boom'" + { + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -FailOnError + } + } | Should -Throw -ExpectedMessage '*boom*' + } +} diff --git a/Tests/DotnetSdk.Type.Tests.ps1 b/Tests/DotnetSdk.Type.Tests.ps1 new file mode 100644 index 0000000..9194802 --- /dev/null +++ b/Tests/DotnetSdk.Type.Tests.ps1 @@ -0,0 +1,58 @@ +#requires -Module @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } + +BeforeAll { + if (-not $env:BHProjectPath) { + Set-BuildEnvironment -Path "$PSScriptRoot/.." -Force + } + Remove-Module $env:BHProjectName -ErrorAction SilentlyContinue + Import-Module (Join-Path $env:BHProjectPath $env:BHProjectName) -Force + + Import-Module (Join-Path $PSScriptRoot 'Shared/TestHelpers.psm1') -Force + + $script:ScriptPath = Join-Path $env:BHProjectPath 'PSDepend/PSDependScripts/DotnetSdk.ps1' + $script:OrigPath = $env:PATH +} + +AfterAll { + if ($script:OrigPath) { $env:PATH = $script:OrigPath } +} + +Describe 'DotnetSdk script' { + + BeforeAll { + InModuleScope PSDepend { + Mock Install-Dotnet { } + Mock Test-Dotnet { $false } + } + } + + It 'PSDependAction Test delegates to Test-Dotnet' { + InModuleScope PSDepend { Mock Test-Dotnet { $true } } + $dep = New-PSDependFixture -DependencyName 'release' -DependencyType 'DotnetSdk' -Version '2.1.0' + $result = InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -PSDependAction Test + } + $result | Should -Be $true + Should -Invoke -CommandName Test-Dotnet -ModuleName PSDepend -Times 1 + } + + It 'Calls Install-Dotnet when Test-Dotnet reports SDK is missing' { + $installDir = (New-Item 'TestDrive:/dotnet' -ItemType Directory -Force).FullName + $dep = New-PSDependFixture -DependencyName 'release' -DependencyType 'DotnetSdk' -Version '2.1.0' -Target $installDir + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath; D = $installDir } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-Dotnet -ModuleName PSDepend -Times 1 -Exactly -ParameterFilter { + $Channel -eq 'release' -and $Version -eq '2.1.0' -and $InstallDir -eq $installDir + } + } + + It 'Skips Install-Dotnet when Test-Dotnet reports SDK is present' { + InModuleScope PSDepend { Mock Test-Dotnet { $true } } + $dep = New-PSDependFixture -DependencyName 'LTS' -DependencyType 'DotnetSdk' -Version '2.1.0' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-Dotnet -ModuleName PSDepend -Times 0 + } +} diff --git a/Tests/FileDownload.Type.Tests.ps1 b/Tests/FileDownload.Type.Tests.ps1 new file mode 100644 index 0000000..6ad5135 --- /dev/null +++ b/Tests/FileDownload.Type.Tests.ps1 @@ -0,0 +1,74 @@ +#requires -Module @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } + +BeforeDiscovery { + Import-Module (Join-Path $PSScriptRoot 'Shared/TestHelpers.psm1') -Force + $script:SkipUnsupported = -not (Test-PSDependTypeSupportedHere -DependencyType 'FileDownload') +} + +BeforeAll { + if (-not $env:BHProjectPath) { + Set-BuildEnvironment -Path "$PSScriptRoot/.." -Force + } + Remove-Module $env:BHProjectName -ErrorAction SilentlyContinue + Import-Module (Join-Path $env:BHProjectPath $env:BHProjectName) -Force + + Import-Module (Join-Path $PSScriptRoot 'Shared/TestHelpers.psm1') -Force + + $script:ScriptPath = Join-Path $env:BHProjectPath 'PSDepend/PSDependScripts/FileDownload.ps1' +} + +Describe 'FileDownload script' -Skip:$SkipUnsupported { + + BeforeAll { + InModuleScope PSDepend { + Mock Get-WebFile { } + Mock Add-ToItemCollection { } + } + } + + It 'Downloads to Target with filename parsed from the URL when Target is an existing folder' { + $targetDir = (New-Item 'TestDrive:/dl' -ItemType Directory -Force).FullName + $dep = New-PSDependFixture -DependencyName 'https://example.com/sample.dll' -DependencyType 'FileDownload' -Target $targetDir + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath; T = $targetDir } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Get-WebFile -ModuleName PSDepend -Times 1 -Exactly -ParameterFilter { + $URL -eq 'https://example.com/sample.dll' -and ($Path -like "*sample.dll") + } + } + + It 'Uses Source to override the URL when supplied' { + $targetDir = (New-Item 'TestDrive:/dl2' -ItemType Directory -Force).FullName + $dep = New-PSDependFixture -DependencyName 'ignored-key' -DependencyType 'FileDownload' -Target $targetDir -Source 'https://example.com/other.dll' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Get-WebFile -ModuleName PSDepend -Times 1 -Exactly -ParameterFilter { + $URL -eq 'https://example.com/other.dll' + } + } + + It 'Skips download when the target file already exists' { + $targetDir = (New-Item 'TestDrive:/dl3' -ItemType Directory -Force).FullName + $existingFile = Join-Path $targetDir 'sample.dll' + Set-Content -Path $existingFile -Value 'existing' + + $dep = New-PSDependFixture -DependencyName 'https://example.com/sample.dll' -DependencyType 'FileDownload' -Target $existingFile + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Get-WebFile -ModuleName PSDepend -Times 0 + } + + It 'PSDependAction Test returns $true when the file exists' { + $targetDir = (New-Item 'TestDrive:/dl4' -ItemType Directory -Force).FullName + $existingFile = Join-Path $targetDir 'sample.dll' + Set-Content -Path $existingFile -Value 'existing' + + $dep = New-PSDependFixture -DependencyName 'https://example.com/sample.dll' -DependencyType 'FileDownload' -Target $existingFile + $result = InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -PSDependAction Test + } + $result | Should -Be $true + } +} diff --git a/Tests/FileSystem.Type.Tests.ps1 b/Tests/FileSystem.Type.Tests.ps1 new file mode 100644 index 0000000..9df68a8 --- /dev/null +++ b/Tests/FileSystem.Type.Tests.ps1 @@ -0,0 +1,72 @@ +#requires -Module @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } + +BeforeDiscovery { + Import-Module (Join-Path $PSScriptRoot 'Shared/TestHelpers.psm1') -Force + $script:SkipUnsupported = -not (Test-PSDependTypeSupportedHere -DependencyType 'FileSystem') +} + +BeforeAll { + if (-not $env:BHProjectPath) { + Set-BuildEnvironment -Path "$PSScriptRoot/.." -Force + } + Remove-Module $env:BHProjectName -ErrorAction SilentlyContinue + Import-Module (Join-Path $env:BHProjectPath $env:BHProjectName) -Force + + Import-Module (Join-Path $PSScriptRoot 'Shared/TestHelpers.psm1') -Force + + $script:ScriptPath = Join-Path $env:BHProjectPath 'PSDepend/PSDependScripts/FileSystem.ps1' +} + +Describe 'FileSystem script' -Skip:$SkipUnsupported { + + BeforeAll { + InModuleScope PSDepend { + Mock Copy-Item { } + } + } + + # Use the Pester-supplied $TestDrive filesystem path rather than the + # 'TestDrive:' PSDrive. On Linux/macOS, (New-Item 'TestDrive:/x').FullName + # returns a path that resolves as relative-to-PWD inside the dependency + # script, breaking Get-Hash. + + It 'Copies a file from Source to Target when hashes differ' { + $srcDir = Join-Path $TestDrive 'src' + $tgtDir = Join-Path $TestDrive 'tgt' + $null = New-Item -ItemType Directory -Path $srcDir, $tgtDir -Force + $src = Join-Path $srcDir 'src.txt' + Set-Content -Path $src -Value 'hello' + + $dep = New-PSDependFixture -DependencyName 'fs-file' -DependencyType 'FileSystem' -Source $src -Target $tgtDir + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Copy-Item -ModuleName PSDepend -Times 1 + } + + It 'PSDependAction Test returns $false when the target file is missing from the target directory' { + $srcDir = Join-Path $TestDrive 'src2' + $tgtDir = Join-Path $TestDrive 'missing-tgt' + $null = New-Item -ItemType Directory -Path $srcDir, $tgtDir -Force + $src = Join-Path $srcDir 'src2.txt' + Set-Content -Path $src -Value 'content' + + $dep = New-PSDependFixture -DependencyName 'fs-test' -DependencyType 'FileSystem' -Source $src -Target $tgtDir + $result = InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -PSDependAction Test + } + $result | Should -Be $false + } + + It 'Errors and skips when Source does not exist' { + $tgtDir = Join-Path $TestDrive 'tgt3' + $null = New-Item -ItemType Directory -Path $tgtDir -Force + $missingSrc = Join-Path $tgtDir 'does-not-exist.txt' + $dep = New-PSDependFixture -DependencyName 'fs-missing' -DependencyType 'FileSystem' -Source $missingSrc -Target $tgtDir + + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -ErrorAction SilentlyContinue + } + Should -Invoke -CommandName Copy-Item -ModuleName PSDepend -Times 0 + } +} diff --git a/Tests/Git.Type.Tests.ps1 b/Tests/Git.Type.Tests.ps1 new file mode 100644 index 0000000..7f9a1a2 --- /dev/null +++ b/Tests/Git.Type.Tests.ps1 @@ -0,0 +1,77 @@ +#requires -Module @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } + +BeforeAll { + if (-not $env:BHProjectPath) { + Set-BuildEnvironment -Path "$PSScriptRoot/.." -Force + } + Remove-Module $env:BHProjectName -ErrorAction SilentlyContinue + Import-Module (Join-Path $env:BHProjectPath $env:BHProjectName) -Force + + Import-Module (Join-Path $PSScriptRoot 'Shared/TestHelpers.psm1') -Force + + $script:ScriptPath = Join-Path $env:BHProjectPath 'PSDepend/PSDependScripts/Git.ps1' +} + +Describe 'Git script' { + + BeforeAll { + InModuleScope PSDepend { + # Simulate `git clone ` by materialising the repo directory + # under PWD so the script's subsequent `Set-Location $RepoPath` + # succeeds. Without this, CI fails on the non-terminating error + # from line 173 of Git.ps1. + Mock Invoke-ExternalCommand { + if ($Arguments -contains 'clone') { + $url = $Arguments | Where-Object { $_ -ne 'clone' } | Select-Object -First 1 + if ($url) { + $repoName = ($url.TrimEnd('/') -split '/')[-1] -replace '\.git$', '' + if ($repoName -and -not (Test-Path $repoName)) { + $null = New-Item -ItemType Directory -Path $repoName -Force + } + } + } + } + Mock Import-PSDependModule { } + Mock Add-ToItemCollection { } + } + } + + It 'Clones the repo via git when the repo folder does not yet exist under Target' { + $targetDir = (New-Item 'TestDrive:/git-target' -ItemType Directory -Force).FullName + $dep = New-PSDependFixture -DependencyName 'https://example.com/user/repo.git' -DependencyType 'Git' -Target $targetDir + + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + # git clone + git checkout + Should -Invoke -CommandName Invoke-ExternalCommand -ModuleName PSDepend -Times 1 -ParameterFilter { + $Arguments -contains 'clone' + } + Should -Invoke -CommandName Invoke-ExternalCommand -ModuleName PSDepend -Times 1 -ParameterFilter { + $Arguments -contains 'checkout' + } + } + + It 'Converts account/repo shorthand to a GitHub URL' { + $targetDir = (New-Item 'TestDrive:/git-target2' -ItemType Directory -Force).FullName + $dep = New-PSDependFixture -DependencyName 'user/repo' -DependencyType 'Git' -Target $targetDir + + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Invoke-ExternalCommand -ModuleName PSDepend -Times 1 -ParameterFilter { + $Arguments -contains 'clone' -and ($Arguments -contains 'https://github.com/user/repo.git') + } + } + + It 'PSDependAction Test returns $false when the repo path does not exist' { + $targetDir = (New-Item 'TestDrive:/git-test' -ItemType Directory -Force).FullName + $dep = New-PSDependFixture -DependencyName 'https://example.com/user/repo.git' -DependencyType 'Git' -Target $targetDir + + $result = InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -PSDependAction Test + } + $result | Should -Be $false + Should -Invoke -CommandName Invoke-ExternalCommand -ModuleName PSDepend -Times 0 + } +} diff --git a/Tests/GitHub.Type.Tests.ps1 b/Tests/GitHub.Type.Tests.ps1 new file mode 100644 index 0000000..1954702 --- /dev/null +++ b/Tests/GitHub.Type.Tests.ps1 @@ -0,0 +1,53 @@ +#requires -Module @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } + +BeforeAll { + if (-not $env:BHProjectPath) { + Set-BuildEnvironment -Path "$PSScriptRoot/.." -Force + } + Remove-Module $env:BHProjectName -ErrorAction SilentlyContinue + Import-Module (Join-Path $env:BHProjectPath $env:BHProjectName) -Force + + Import-Module (Join-Path $PSScriptRoot 'Shared/TestHelpers.psm1') -Force + + $script:ScriptPath = Join-Path $env:BHProjectPath 'PSDepend/PSDependScripts/GitHub.ps1' +} + +Describe 'GitHub script' { + + BeforeAll { + InModuleScope PSDepend { + Mock Get-Module { } -ParameterFilter { $ListAvailable } + Mock Invoke-RestMethod { @() } # No tags returned → treated as branch + Mock Import-PSDependModule { } + } + } + + Context 'PSDependAction = Test only' { + It 'Returns $false when module is not installed locally' { + $targetDir = (New-Item 'TestDrive:/gh-test' -ItemType Directory -Force).FullName + $dep = New-PSDependFixture -DependencyName 'someuser/somerepo' -DependencyType 'GitHub' -Target $targetDir -Version 'master' + + $result = InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -PSDependAction Test -WarningAction SilentlyContinue + } + $result | Should -Be $false + } + } + + Context 'PSDependAction = Test when already installed and matches' { + It 'Returns $true when local version matches requested numeric version' { + InModuleScope PSDepend { + Mock Get-Module { + [PSCustomObject]@{ Name = 'somerepo'; Version = [version]'1.2.3' } + } -ParameterFilter { $ListAvailable } + } + $targetDir = (New-Item 'TestDrive:/gh-match' -ItemType Directory -Force).FullName + $dep = New-PSDependFixture -DependencyName 'someuser/somerepo' -DependencyType 'GitHub' -Target $targetDir -Version '1.2.3' + + $result = InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -PSDependAction Test -WarningAction SilentlyContinue + } + $result | Should -Be $true + } + } +} diff --git a/Tests/Noop.Type.Tests.ps1 b/Tests/Noop.Type.Tests.ps1 new file mode 100644 index 0000000..3c13a39 --- /dev/null +++ b/Tests/Noop.Type.Tests.ps1 @@ -0,0 +1,31 @@ +#requires -Module @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } + +BeforeAll { + if (-not $env:BHProjectPath) { + Set-BuildEnvironment -Path "$PSScriptRoot/.." -Force + } + Remove-Module $env:BHProjectName -ErrorAction SilentlyContinue + Import-Module (Join-Path $env:BHProjectPath $env:BHProjectName) -Force + + Import-Module (Join-Path $PSScriptRoot 'Shared/TestHelpers.psm1') -Force + + $script:ScriptPath = Join-Path $env:BHProjectPath 'PSDepend/PSDependScripts/Noop.ps1' +} + +Describe 'Noop script' { + It 'Returns an object containing the supplied Dependency' { + $dep = New-PSDependFixture -DependencyName 'NoopOne' + $result = InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + $result.Dependency.DependencyName | Should -Be 'NoopOne' + } + + It 'Passes StringParameter through to PSBoundParameters' { + $dep = New-PSDependFixture -DependencyName 'NoopTwo' + $result = InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -StringParameter 'hello', 'world' + } + $result.PSBoundParameters['StringParameter'] | Should -Be @('hello', 'world') + } +} diff --git a/Tests/Npm.Type.Tests.ps1 b/Tests/Npm.Type.Tests.ps1 new file mode 100644 index 0000000..2050ca0 --- /dev/null +++ b/Tests/Npm.Type.Tests.ps1 @@ -0,0 +1,64 @@ +#requires -Module @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } + +BeforeAll { + if (-not $env:BHProjectPath) { + Set-BuildEnvironment -Path "$PSScriptRoot/.." -Force + } + Remove-Module $env:BHProjectName -ErrorAction SilentlyContinue + Import-Module (Join-Path $env:BHProjectPath $env:BHProjectName) -Force + + Import-Module (Join-Path $PSScriptRoot 'Shared/TestHelpers.psm1') -Force + + $script:ScriptPath = Join-Path $env:BHProjectPath 'PSDepend/PSDependScripts/Npm.ps1' +} + +Describe 'Npm script' { + + BeforeAll { + InModuleScope PSDepend { + Mock Get-NodeModule { @{} } + Mock Install-NodeModule { } + } + } + + It 'Installs globally when Target is "global"' { + $dep = New-PSDependFixture -DependencyName 'left-pad' -DependencyType 'Npm' -Target 'global' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-NodeModule -ModuleName PSDepend -Times 1 -Exactly -ParameterFilter { + $Global -eq $true -and $PackageName -eq 'left-pad' + } + } + + It 'Installs locally (no -Global) when Target is a path' { + $targetDir = (New-Item 'TestDrive:/npm-target' -ItemType Directory -Force).FullName + $dep = New-PSDependFixture -DependencyName 'left-pad' -DependencyType 'Npm' -Target $targetDir + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-NodeModule -ModuleName PSDepend -Times 1 -Exactly -ParameterFilter { + -not $Global -and $PackageName -eq 'left-pad' + } + } + + It 'PSDependAction Test returns $false when module is not installed' { + $dep = New-PSDependFixture -DependencyName 'left-pad' -DependencyType 'Npm' -Target 'global' + $result = InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -PSDependAction Test + } + $result | Should -Be $false + Should -Invoke -CommandName Install-NodeModule -ModuleName PSDepend -Times 0 + } + + It 'PSDependAction Test returns $true when an installed version exists' { + InModuleScope PSDepend { + Mock Get-NodeModule { @{ 'left-pad' = @{ Version = '1.3.0' } } } + } + $dep = New-PSDependFixture -DependencyName 'left-pad' -DependencyType 'Npm' -Target 'global' + $result = InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -PSDependAction Test + } + $result | Should -Be $true + } +} diff --git a/Tests/Nuget.Type.Tests.ps1 b/Tests/Nuget.Type.Tests.ps1 new file mode 100644 index 0000000..2822918 --- /dev/null +++ b/Tests/Nuget.Type.Tests.ps1 @@ -0,0 +1,55 @@ +#requires -Module @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } + +BeforeAll { + if (-not $env:BHProjectPath) { + Set-BuildEnvironment -Path "$PSScriptRoot/.." -Force + } + Remove-Module $env:BHProjectName -ErrorAction SilentlyContinue + Import-Module (Join-Path $env:BHProjectPath $env:BHProjectName) -Force + + Import-Module (Join-Path $PSScriptRoot 'Shared/TestHelpers.psm1') -Force + + $script:ScriptPath = Join-Path $env:BHProjectPath 'PSDepend/PSDependScripts/Nuget.ps1' +} + +Describe 'Nuget script' { + + BeforeAll { + InModuleScope PSDepend { + Mock Invoke-ExternalCommand { } + Mock Find-NugetPackage { [PSCustomObject]@{ Version = '1.0.0' } } + # Pretend nuget.exe is available so we don't trigger the missing-tool Write-Error + Mock Get-Command { [PSCustomObject]@{ Name = 'nuget' } } -ParameterFilter { $Name -eq 'Nuget' } + } + } + + It 'Errors when Target is not provided' { + $dep = New-PSDependFixture -DependencyName 'Newtonsoft.Json' -DependencyType 'Nuget' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -ErrorAction SilentlyContinue + } + Should -Invoke -CommandName Invoke-ExternalCommand -ModuleName PSDepend -Times 0 + } + + It 'Invokes nuget install when no existing package is found at the target' { + $targetDir = (New-Item 'TestDrive:/nuget-target' -ItemType Directory -Force).FullName + $dep = New-PSDependFixture -DependencyName 'Newtonsoft.Json' -DependencyType 'Nuget' -Target $targetDir + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Invoke-ExternalCommand -ModuleName PSDepend -Times 1 -ParameterFilter { + $Arguments -contains 'install' + } + } + + It 'Adds -version arg when an explicit version is requested' { + $targetDir = (New-Item 'TestDrive:/nuget-version' -ItemType Directory -Force).FullName + $dep = New-PSDependFixture -DependencyName 'Newtonsoft.Json' -DependencyType 'Nuget' -Target $targetDir -Version '12.0.2' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Invoke-ExternalCommand -ModuleName PSDepend -Times 1 -ParameterFilter { + $Arguments -contains '-version' -and $Arguments -contains '12.0.2' + } + } +} diff --git a/Tests/PSDepend.Tests.ps1 b/Tests/PSDepend.Tests.ps1 index cbdb7fb..b39aaf1 100644 --- a/Tests/PSDepend.Tests.ps1 +++ b/Tests/PSDepend.Tests.ps1 @@ -187,6 +187,8 @@ Describe "Install-Dependency PS$PSVersion" -Tag 'Unit' { BeforeAll { Set-StrictMode -Version latest Mock Install-Module {} -ModuleName PSDepend + Mock Get-PackageProvider { [PSCustomObject]@{ Name = 'NuGet' } } -ModuleName PSDepend + Mock Install-PackageProvider {} -ModuleName PSDepend } AfterAll { Set-StrictMode -Off } @@ -214,17 +216,4 @@ Describe "Invoke-DependencyScript PS$PSVersion" -Tag 'Unit' { Should -Not -Throw } } - - Context 'Test action' { - BeforeAll { - Set-StrictMode -Version latest - Mock Get-Module { [pscustomobject]@{ Version = '1.2.5' } } -ModuleName PSDepend - } - AfterAll { Set-StrictMode -Off } - - It 'Returns $true when the module is installed at the required version with -Quiet' { - $Dep = Get-Dependency -Path $TestDepends\psgallerymodule.sameversion.depend.psd1 - $Dep | Invoke-DependencyScript -PSDependAction Test -Quiet | Should -Be $True - } - } } \ No newline at end of file diff --git a/Tests/PSGalleryModule.Type.Tests.ps1 b/Tests/PSGalleryModule.Type.Tests.ps1 new file mode 100644 index 0000000..09dac5a --- /dev/null +++ b/Tests/PSGalleryModule.Type.Tests.ps1 @@ -0,0 +1,160 @@ +#requires -Module @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } + +BeforeAll { + if (-not $env:BHProjectPath) { + Set-BuildEnvironment -Path "$PSScriptRoot/.." -Force + } + Remove-Module $env:BHProjectName -ErrorAction SilentlyContinue + Import-Module (Join-Path $env:BHProjectPath $env:BHProjectName) -Force + + Import-Module (Join-Path $PSScriptRoot 'Shared/TestHelpers.psm1') -Force + + $script:ScriptPath = Join-Path $env:BHProjectPath 'PSDepend/PSDependScripts/PSGalleryModule.ps1' + $script:TestCred = New-TestCredential + $script:OrigPSModulePath = $env:PSModulePath +} + +AfterAll { + if ($script:OrigPSModulePath) { + $env:PSModulePath = $script:OrigPSModulePath + } +} + +Describe 'PSGalleryModule script' { + + BeforeAll { + InModuleScope PSDepend { + Mock Get-PackageProvider { [PSCustomObject]@{ Name = 'NuGet' } } + Mock Get-PSRepository { [PSCustomObject]@{ Name = 'PSGallery' } } + Mock Get-Module { } -ParameterFilter { $ListAvailable } + Mock Find-Module { [PSCustomObject]@{ Name = 'TestModule'; Version = [version]'2.0.0' } } + Mock Install-Module { } + Mock Save-Module { } + Mock Import-PSDependModule { } + Mock Add-ToPsModulePathIfRequired { } + } + } + + Context 'Contract: default Version handling' { + It 'Defaults to latest (no RequiredVersion in splat) when Version is not supplied' { + $dep = New-PSDependFixture -DependencyName 'TestModule' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-Module -ModuleName PSDepend -Times 1 -Exactly -ParameterFilter { + -not $PSBoundParameters.ContainsKey('RequiredVersion') + } + } + + It 'Passes RequiredVersion when an explicit version is supplied' { + $dep = New-PSDependFixture -DependencyName 'TestModule' -Version '1.2.3' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-Module -ModuleName PSDepend -Times 1 -Exactly -ParameterFilter { + $RequiredVersion -eq '1.2.3' + } + } + } + + Context 'Contract: Name falls back to DependencyName' { + It 'Uses DependencyName as the module name when Name is not set' { + $dep = New-PSDependFixture -DependencyName 'FallbackModule' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-Module -ModuleName PSDepend -Times 1 -Exactly -ParameterFilter { + $Name -eq 'FallbackModule' + } + } + + It 'Prefers Name over DependencyName when both are set' { + $dep = New-PSDependFixture -DependencyName 'IgnoredKey' -Name 'RealModule' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-Module -ModuleName PSDepend -Times 1 -Exactly -ParameterFilter { + $Name -eq 'RealModule' + } + } + } + + Context 'PSDependAction = Test only' { + It 'Returns $false when module is not installed' { + $dep = New-PSDependFixture -DependencyName 'TestModule' + $result = InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -PSDependAction Test + } + $result | Should -Be $false + Should -Invoke -CommandName Install-Module -ModuleName PSDepend -Times 0 + } + + It 'Returns $true when installed version matches requested version' { + InModuleScope PSDepend { + Mock Get-Module { [PSCustomObject]@{ Name = 'TestModule'; Version = [version]'1.2.3' } } -ParameterFilter { $ListAvailable } + } + $dep = New-PSDependFixture -DependencyName 'TestModule' -Version '1.2.3' + $result = InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -PSDependAction Test + } + $result | Should -Be $true + Should -Invoke -CommandName Install-Module -ModuleName PSDepend -Times 0 + } + } + + Context 'PSDependAction = Test,Install short-circuits when satisfied' { + BeforeAll { + InModuleScope PSDepend { + Mock Get-Module { [PSCustomObject]@{ Name = 'TestModule'; Version = [version]'2.0.0' } } -ParameterFilter { $ListAvailable } + } + } + + It 'Skips Install-Module but still calls Import-PSDependModule' { + $dep = New-PSDependFixture -DependencyName 'TestModule' -Version 'latest' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -PSDependAction Test, Install + } + Should -Invoke -CommandName Install-Module -ModuleName PSDepend -Times 0 + Should -Invoke -CommandName Import-PSDependModule -ModuleName PSDepend -Times 1 + } + } + + Context 'Target as path uses Save-Module instead of Install-Module' { + It 'Calls Save-Module with the target path' { + $savePath = (New-Item 'TestDrive:/save' -ItemType Directory -Force).FullName + $dep = New-PSDependFixture -DependencyName 'TestModule' -Target $savePath + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath; SavePath = $savePath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Save-Module -ModuleName PSDepend -Times 1 -Exactly -ParameterFilter { + $Path -eq $savePath + } + Should -Invoke -CommandName Install-Module -ModuleName PSDepend -Times 0 + } + } + + Context 'Credential pass-through' { + It 'Forwards Credential to Install-Module' { + $dep = New-PSDependFixture -DependencyName 'PrivateModule' -Credential $script:TestCred + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-Module -ModuleName PSDepend -Times 1 -Exactly -ParameterFilter { + $Credential -and $Credential.UserName -eq 'testUser' + } + } + } + + Context 'Repository validation' { + It 'Errors and skips install when repository is unknown' { + InModuleScope PSDepend { + Mock Get-PSRepository { } -ParameterFilter { $Name -eq 'BogusRepo' } + } + $dep = New-PSDependFixture -DependencyName 'TestModule' -Parameters @{ Repository = 'BogusRepo' } + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -Repository 'BogusRepo' -ErrorAction SilentlyContinue + } + Should -Invoke -CommandName Install-Module -ModuleName PSDepend -Times 0 + } + } +} diff --git a/Tests/PSGalleryNuget.Type.Tests.ps1 b/Tests/PSGalleryNuget.Type.Tests.ps1 new file mode 100644 index 0000000..cfe864a --- /dev/null +++ b/Tests/PSGalleryNuget.Type.Tests.ps1 @@ -0,0 +1,54 @@ +#requires -Module @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } + +BeforeAll { + if (-not $env:BHProjectPath) { + Set-BuildEnvironment -Path "$PSScriptRoot/.." -Force + } + Remove-Module $env:BHProjectName -ErrorAction SilentlyContinue + Import-Module (Join-Path $env:BHProjectPath $env:BHProjectName) -Force + + Import-Module (Join-Path $PSScriptRoot 'Shared/TestHelpers.psm1') -Force + + $script:ScriptPath = Join-Path $env:BHProjectPath 'PSDepend/PSDependScripts/PSGalleryNuget.ps1' +} + +Describe 'PSGalleryNuget script' { + + BeforeAll { + InModuleScope PSDepend { + Mock Invoke-ExternalCommand { } + Mock Find-NugetPackage { [PSCustomObject]@{ Version = '1.0.0' } } + Mock Add-ToPsModulePathIfRequired { } + Mock Import-PSDependModule { } + Mock Get-Command { [PSCustomObject]@{ Name = 'nuget' } } -ParameterFilter { $Name -eq 'Nuget' } + } + } + + It 'Errors when Target is not provided' { + $dep = New-PSDependFixture -DependencyName 'PSDeploy' -DependencyType 'PSGalleryNuget' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -ErrorAction SilentlyContinue + } + Should -Invoke -CommandName Invoke-ExternalCommand -ModuleName PSDepend -Times 0 + } + + It 'Invokes nuget install when no module is present at the target' { + $targetDir = (New-Item 'TestDrive:/psgnuget-target' -ItemType Directory -Force).FullName + $dep = New-PSDependFixture -DependencyName 'PSDeploy' -DependencyType 'PSGalleryNuget' -Target $targetDir + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Invoke-ExternalCommand -ModuleName PSDepend -Times 1 -ParameterFilter { + $Arguments -contains 'install' + } + } + + It 'Imports the module via Import-PSDependModule after install' { + $targetDir = (New-Item 'TestDrive:/psgnuget-target2' -ItemType Directory -Force).FullName + $dep = New-PSDependFixture -DependencyName 'PSDeploy' -DependencyType 'PSGalleryNuget' -Target $targetDir + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Import-PSDependModule -ModuleName PSDepend -Times 1 + } +} diff --git a/Tests/PSModuleGallery.Type.Tests.ps1 b/Tests/PSModuleGallery.Type.Tests.ps1 index 8f9cc67..dd2b8c3 100644 --- a/Tests/PSModuleGallery.Type.Tests.ps1 +++ b/Tests/PSModuleGallery.Type.Tests.ps1 @@ -135,7 +135,7 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { BeforeAll { Mock Install-Module {} -ModuleName PSDepend Mock Get-Module { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.5' } } -ModuleName PSDepend @@ -155,7 +155,7 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { BeforeAll { Mock Install-Module {} -ModuleName PSDepend Mock Get-Module { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.5-preview0002' } } -ModuleName PSDepend @@ -175,12 +175,12 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { BeforeAll { Mock Install-Module {} -ModuleName PSDepend Mock Get-Module { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.5' } } -ModuleName PSDepend Mock Find-Module { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.5' } } -ModuleName PSDepend @@ -199,12 +199,12 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { BeforeAll { Mock Install-Module {} -ModuleName PSDepend Mock Get-Module { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.5-preview0002' } } -ModuleName PSDepend Mock Find-Module { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.5-preview0002' } } -ModuleName PSDepend @@ -228,7 +228,7 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { It 'Returns $true when it finds an existing module (Version)' { Mock Get-Module { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.5' } } -ModuleName PSDepend @@ -240,7 +240,7 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { It 'Returns $true when it finds an existing module (SemVersion)' { Mock Get-Module { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.5-preview0002' } } -ModuleName PSDepend @@ -252,12 +252,12 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { It 'Returns $true when it finds an existing latest module (Version)' { Mock Get-Module { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.5' } } -ModuleName PSDepend Mock Find-Module { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.5' } } -ModuleName PSDepend @@ -269,12 +269,12 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { It 'Returns $true when it finds an existing latest module (SemVersion)' { Mock Get-Module { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.5-preview0002' } } -ModuleName PSDepend Mock Find-Module { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.5-preview0002' } } -ModuleName PSDepend @@ -302,7 +302,7 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { It "Returns `$false when it finds an existing module with a lower version (Version)" { Mock Get-Module { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.4' } } -ModuleName PSDepend @@ -314,7 +314,7 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { It 'Returns $false when it finds an existing module with a lower version (SemVersion)' { Mock Get-Module { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.5-preview0001' } } -ModuleName PSDepend @@ -326,7 +326,7 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { It 'Returns $false when it finds an existing module with a lower version (SemVersion-Version)' { Mock Get-Module { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.4' } } -ModuleName PSDepend @@ -338,12 +338,12 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { It 'Returns $false when it finds an existing module with a lower version than latest (Version)' { Mock Get-Module { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.4' } } -ModuleName PSDepend Mock Find-Module { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.5' } } -ModuleName PSDepend @@ -355,12 +355,12 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { It 'Returns $false when it finds an existing module with a lower version than latest (SemVersion)' { Mock Get-Module { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.5-preview0001' } } -ModuleName PSDepend Mock Find-Module { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.5-preview0002' } } -ModuleName PSDepend @@ -416,12 +416,12 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { Mock Install-Module -ModuleName PSDepend Mock Import-Module -ModuleName PSDepend Mock Get-Module { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.5' } } -ModuleName PSDepend Mock Find-Module { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.5' } } -ModuleName PSDepend @@ -485,7 +485,7 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { Context 'Installs Module' { BeforeAll { Mock Invoke-ExternalCommand { - [pscustomobject]@{ + [PSCustomObject]@{ PSB = $PSBoundParameters Arg = $Args } @@ -546,7 +546,7 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { Context 'Installs dependency' { BeforeAll { Mock Get-WebFile { - [pscustomobject]@{ + [PSCustomObject]@{ PSB = $PSBoundParameters Arg = $Args } @@ -623,7 +623,7 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { Mock Test-Path { return $True } -ModuleName PSDepend -ParameterFilter { $Path -match 'jenkins' } Mock Invoke-ExternalCommand {} -ModuleName PSDepend Mock Import-LocalizedData { - [pscustomobject]@{ + [PSCustomObject]@{ ModuleVersion = '1.2.5' } } -ModuleName PSDepend -ParameterFilter { $FileName -eq 'jenkins.psd1' } @@ -644,12 +644,12 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { Mock Test-Path { return $True } -ModuleName PSDepend -ParameterFilter { $Path -match 'jenkins' } Mock Invoke-ExternalCommand {} -ModuleName PSDepend Mock Import-LocalizedData { - [pscustomobject]@{ + [PSCustomObject]@{ ModuleVersion = '1.2.5' } } -ModuleName PSDepend -ParameterFilter { $FileName -eq 'jenkins.psd1' } Mock Find-NugetPackage { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.5' } } -ModuleName PSDepend @@ -674,7 +674,7 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { It 'Returns $true when it finds an existing module' { Mock Import-LocalizedData { - [pscustomobject]@{ + [PSCustomObject]@{ ModuleVersion = '1.2.5' } } -ModuleName PSDepend -ParameterFilter { $FileName -eq 'jenkins.psd1' } @@ -686,12 +686,12 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { It 'Returns $true when it finds an existing latest module' { Mock Import-LocalizedData { - [pscustomobject]@{ + [PSCustomObject]@{ ModuleVersion = '1.2.5' } } -ModuleName PSDepend -ParameterFilter { $FileName -eq 'jenkins.psd1' } Mock Find-NugetPackage { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.5' } } -ModuleName PSDepend @@ -711,7 +711,7 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { It "Returns `$false when it finds an existing module with a lower version" { Mock Import-LocalizedData { - [pscustomobject]@{ + [PSCustomObject]@{ ModuleVersion = '1.2.4' } } -ModuleName PSDepend -ParameterFilter { $FileName -eq 'jenkins.psd1' } @@ -724,12 +724,12 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { It "Returns `$false when it finds an existing module with a lower version than latest" { Mock Import-LocalizedData { - [pscustomobject]@{ + [PSCustomObject]@{ ModuleVersion = '1.2.4' } } -ModuleName PSDepend -ParameterFilter { $FileName -eq 'jenkins.psd1' } Mock Find-NugetPackage { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.5' } } -ModuleName PSDepend @@ -788,12 +788,12 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { Mock Invoke-ExternalCommand {} -ModuleName PSDepend Mock Import-Module -ModuleName PSDepend Mock Import-LocalizedData { - [pscustomobject]@{ + [PSCustomObject]@{ ModuleVersion = '1.2.5' } } -ModuleName PSDepend -ParameterFilter { $FileName -eq 'imaginary.psd1' } Mock Find-NugetPackage { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.2.5' } } -ModuleName PSDepend @@ -898,7 +898,7 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { Context 'Installs Packages' { BeforeAll { - Mock Get-PackageSource { @([pscustomobject]@{Name = 'chocolatey'; ProviderName = 'chocolatey'}) } -ModuleName PSDepend + Mock Get-PackageSource { @([PSCustomObject]@{Name = 'chocolatey'; ProviderName = 'chocolatey'}) } -ModuleName PSDepend Mock Get-Package -ModuleName PSDepend Mock Install-Package { $True } -ModuleName PSDepend $script:Results = Invoke-PSDepend @Verbose -Path "$TestDepends\package.depend.psd1" -Force @@ -926,10 +926,10 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { Context 'Same package version exists' { BeforeAll { - Mock Get-PackageSource { @([pscustomobject]@{Name = 'chocolatey'; ProviderName = 'chocolatey'}) } -ModuleName PSDepend + Mock Get-PackageSource { @([PSCustomObject]@{Name = 'chocolatey'; ProviderName = 'chocolatey'}) } -ModuleName PSDepend Mock Install-Package -ModuleName PSDepend Mock Get-Package { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.1' } } -ModuleName PSDepend @@ -947,15 +947,15 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { Context 'Latest package required, and already installed' { BeforeAll { - Mock Get-PackageSource { @([pscustomobject]@{Name = 'chocolatey'; ProviderName = 'chocolatey'}) } -ModuleName PSDepend + Mock Get-PackageSource { @([PSCustomObject]@{Name = 'chocolatey'; ProviderName = 'chocolatey'}) } -ModuleName PSDepend Mock Install-Package -ModuleName PSDepend Mock Get-Package { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.1' } } -ModuleName PSDepend Mock Find-Package { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.1' } } -ModuleName PSDepend @@ -972,14 +972,14 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { Context 'Test-Dependency' { BeforeEach { - Mock Get-PackageSource { @([pscustomobject]@{Name = 'chocolatey'; ProviderName = 'chocolatey'}) } -ModuleName PSDepend + Mock Get-PackageSource { @([PSCustomObject]@{Name = 'chocolatey'; ProviderName = 'chocolatey'}) } -ModuleName PSDepend Mock Install-Package {} -ModuleName PSDepend Mock Find-Package {} -ModuleName PSDepend } It 'Returns $true when it finds an existing module' { Mock Get-Package { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.1' } } -ModuleName PSDepend @@ -991,12 +991,12 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { It 'Returns $true when it finds an existing latest module' { Mock Get-Package { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.1' } } -ModuleName PSDepend Mock Find-Package { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.1' } } -ModuleName PSDepend @@ -1016,7 +1016,7 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { It "Returns `$false when it finds an existing module with a lower version" { Mock Get-Package { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.0' } } -ModuleName PSDepend @@ -1028,12 +1028,12 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { It "Returns `$false when it finds an existing module with a lower version than latest" { Mock Get-Package { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.0' } } -ModuleName PSDepend Mock Find-Package { - [pscustomobject]@{ + [PSCustomObject]@{ Version = '1.1' } } -ModuleName PSDepend @@ -1113,12 +1113,12 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { } It 'Returns $true if the module is installed' { - Mock Get-NodeModule { return [pscustomobject]@{ + Mock Get-NodeModule { return [PSCustomObject]@{ 'gitbook-cli' = @{ version = '2.3.0' } } } -ParameterFilter { $Global -eq $true } -ModuleName PSDepend - Mock Get-NodeModule { return [pscustomobject]@{ + Mock Get-NodeModule { return [PSCustomObject]@{ 'gitbook-summary' = @{ version = '1.2.3' } @@ -1227,7 +1227,7 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { $script:SavePath = (New-Item 'TestDrive:/PSDependPesterTest' -ItemType Directory -Force).FullName # Simulate choco.exe being present so tests don't hit the install-chocolatey branch by default - Mock Get-Command -ParameterFilter { $Name -eq 'choco.exe' } -MockWith { [pscustomobject]@{Name = 'choco.exe'} } -ModuleName PSDepend + Mock Get-Command -ParameterFilter { $Name -eq 'choco.exe' } -MockWith { [PSCustomObject]@{Name = 'choco.exe'} } -ModuleName PSDepend # Default catch-all for Invoke-ExternalCommand; individual tests register specific ParameterFilter mocks Mock Invoke-ExternalCommand -ModuleName PSDepend } diff --git a/Tests/Package.Type.Tests.ps1 b/Tests/Package.Type.Tests.ps1 new file mode 100644 index 0000000..a808789 --- /dev/null +++ b/Tests/Package.Type.Tests.ps1 @@ -0,0 +1,66 @@ +#requires -Module @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } + +BeforeAll { + if (-not $env:BHProjectPath) { + Set-BuildEnvironment -Path "$PSScriptRoot/.." -Force + } + Remove-Module $env:BHProjectName -ErrorAction SilentlyContinue + Import-Module (Join-Path $env:BHProjectPath $env:BHProjectName) -Force + + Import-Module (Join-Path $PSScriptRoot 'Shared/TestHelpers.psm1') -Force + + $script:ScriptPath = Join-Path $env:BHProjectPath 'PSDepend/PSDependScripts/Package.ps1' + + # PackageManagement cmdlets have complex dynamic parameter sets that Pester + # cannot mock directly. Inject simple stub functions into the module's + # script scope so Pester can wrap them with Mock. + & (Get-Module PSDepend) { + function script:Get-PackageSource { [CmdletBinding()] param() } + function script:Get-PackageProvider { [CmdletBinding()] param() } + function script:Get-Package { [CmdletBinding()] param([string]$Name, [string]$ProviderName, [string]$RequiredVersion, [string]$Destination, [string]$ErrorAction) } + function script:Find-Package { [CmdletBinding()] param([string]$Name, [string]$Source) } + function script:Install-Package { [CmdletBinding()] param([string]$Name, [string]$Source, [string]$RequiredVersion, [string]$Destination, [string]$Scope, [switch]$Force) } + } +} + +Describe 'Package script' { + + BeforeAll { + InModuleScope PSDepend { + Mock Get-PackageSource { [PSCustomObject]@{ Name = 'nuget.org'; ProviderName = 'Nuget' } } + Mock Get-PackageProvider { @( [PSCustomObject]@{ Name = 'Nuget' }, [PSCustomObject]@{ Name = 'PowerShellGet' } ) } + Mock Get-Package { } + Mock Find-Package { [PSCustomObject]@{ Name = 'jquery'; Version = '1.0.0' } } + Mock Install-Package { } + } + } + + It 'Calls Install-Package when no existing package is found' { + $targetDir = (New-Item 'TestDrive:/pkg' -ItemType Directory -Force).FullName + $dep = New-PSDependFixture -DependencyName 'jquery' -DependencyType 'Package' -Target $targetDir -Source 'nuget.org' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-Package -ModuleName PSDepend -Times 1 -Exactly -ParameterFilter { + $Name -eq 'jquery' -and $Source -eq 'nuget.org' + } + } + + It 'Errors and returns when Source is not a known PackageSource' { + $targetDir = (New-Item 'TestDrive:/pkg2' -ItemType Directory -Force).FullName + $dep = New-PSDependFixture -DependencyName 'jquery' -DependencyType 'Package' -Target $targetDir -Source 'BogusFeed' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -ErrorAction SilentlyContinue + } + Should -Invoke -CommandName Install-Package -ModuleName PSDepend -Times 0 + } + + It 'Throws when Nuget provider is selected but no Target is supplied' { + $dep = New-PSDependFixture -DependencyName 'jquery' -DependencyType 'Package' -Source 'nuget.org' + { + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + } | Should -Throw -ExpectedMessage '*Nuget*Target*' + } +} diff --git a/Tests/Shared/TestHelpers.psm1 b/Tests/Shared/TestHelpers.psm1 new file mode 100644 index 0000000..f5a4df0 --- /dev/null +++ b/Tests/Shared/TestHelpers.psm1 @@ -0,0 +1,96 @@ +function New-PSDependFixture { + [CmdletBinding()] + param( + [string]$DependencyName = 'TestModule', + [string]$DependencyType = 'PSGalleryModule', + [AllowNull()][object]$Name = $null, + [AllowNull()][object]$Version = $null, + [AllowNull()][object]$Target = $null, + [AllowNull()][object]$Source = $null, + [hashtable]$Parameters = @{}, + [PSCredential]$Credential, + [switch]$AddToPath + ) + + [PSCustomObject]@{ + PSTypeName = 'PSDepend.Dependency' + DependencyFile = $null + DependencyName = $DependencyName + DependencyType = $DependencyType + Name = $Name + Version = $Version + Parameters = $Parameters + Source = $Source + Target = $Target + AddToPath = [bool]$AddToPath + Tags = @() + DependsOn = $null + PreScripts = $null + PostScripts = $null + Credential = $Credential + Raw = @{} + } +} + +function New-TestCredential { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingPlainTextForPassword', + '', + Justification = 'Dummy credential for testing.' + )] + [CmdletBinding()] + param( + [string]$UserName = 'testUser', + [string]$Password = 'testPassword' + ) + + [PSCredential]::new( + $UserName, + (ConvertTo-SecureString $Password -AsPlainText -Force) + ) +} + +function Test-PSDependTypeSupportedHere { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$DependencyType, + [string]$MapPath = ( + # Use Path::Combine because Join-Path was stupid long + [System.IO.Path]::Combine( + $PSScriptRoot, + '..', + '..', + 'PSDepend', + 'PSDependMap.psd1' + ) + ) + ) + + $map = Import-PowerShellDataFile -Path $MapPath + if (-not $map.ContainsKey($DependencyType)) { + return $false + } + $support = @($map[$DependencyType].Supports) + + if ($PSVersionTable.PSEdition -eq 'Core') { + $windowsCoreOk = $IsWindows -and ($support -contains 'windows') + if (-not $windowsCoreOk -and $support -notcontains 'core') { + return $false + } + } elseif ($support -notcontains 'windows') { + return $false + } + + if ($IsLinux -and $support -notcontains 'linux') { + return $false + } + if ($IsMacOS -and $support -notcontains 'macos') { + return $false + } + if ($IsWindows -and $support -notcontains 'windows') { + return $false + } + $true +} + +Export-ModuleMember -Function New-PSDependFixture, New-TestCredential, Test-PSDependTypeSupportedHere diff --git a/Tests/Task.Type.Tests.ps1 b/Tests/Task.Type.Tests.ps1 new file mode 100644 index 0000000..3dd3caa --- /dev/null +++ b/Tests/Task.Type.Tests.ps1 @@ -0,0 +1,57 @@ +#requires -Module @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } + +BeforeAll { + if (-not $env:BHProjectPath) { + Set-BuildEnvironment -Path "$PSScriptRoot/.." -Force + } + Remove-Module $env:BHProjectName -ErrorAction SilentlyContinue + Import-Module (Join-Path $env:BHProjectPath $env:BHProjectName) -Force + + Import-Module (Join-Path $PSScriptRoot 'Shared/TestHelpers.psm1') -Force + + $script:ScriptPath = Join-Path $env:BHProjectPath 'PSDepend/PSDependScripts/Task.ps1' +} + +Describe 'Task script' { + + It 'Dot-sources a task script that exists on disk' { + $taskFile = Join-Path 'TestDrive:' 'task1.ps1' + $flagFile = Join-Path 'TestDrive:' 'taskflag.txt' + $resolvedFlag = (Resolve-Path 'TestDrive:').ProviderPath + $absoluteFlag = Join-Path $resolvedFlag 'taskflag.txt' + Set-Content -Path $taskFile -Value "Set-Content -Path '$absoluteFlag' -Value 'task-ran'" + + $dep = New-PSDependFixture -DependencyName 'TaskOne' -DependencyType 'Task' -Source (Resolve-Path $taskFile).ProviderPath + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + (Get-Content $absoluteFlag) | Should -Be 'task-ran' + } + + It 'Passes Parameters to the task script via splat' { + $taskFile = Join-Path 'TestDrive:' 'task-params.ps1' + $flagBase = (Resolve-Path 'TestDrive:').ProviderPath + $outFile = Join-Path $flagBase 'taskparam.txt' + Set-Content -Path $taskFile -Value "param(`$Greeting) Set-Content -Path '$outFile' -Value `$Greeting" + + $dep = New-PSDependFixture -DependencyName 'TaskParam' -DependencyType 'Task' ` + -Source (Resolve-Path $taskFile).ProviderPath ` + -Parameters @{ Greeting = 'hi-from-test' } + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + (Get-Content $outFile) | Should -Be 'hi-from-test' + } + + It 'Warns and does not throw when the task file is missing' { + $missingPath = Join-Path $TestDrive 'nope.ps1' + $dep = New-PSDependFixture -DependencyName 'TaskMissing' -DependencyType 'Task' -Source $missingPath + $warnings = $null + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath; WarnRef = [ref]$warnings } { + & $ScriptPath -Dependency $Dep -WarningVariable warn -WarningAction SilentlyContinue + $WarnRef.Value = $warn + } + $warnings | Should -Not -BeNullOrEmpty + ($warnings | Out-String) | Should -Match 'Could not find task file' + } +} diff --git a/cspell.json b/cspell.json new file mode 100644 index 0000000..558d5d7 --- /dev/null +++ b/cspell.json @@ -0,0 +1,14 @@ +{ + "version": "0.2", + "ignorePaths": [], + "dictionaryDefinitions": [], + "dictionaries": [ + "powershell" + ], + "words": [ + "choco", + "psake" + ], + "ignoreWords": [], + "import": [] +} \ No newline at end of file diff --git a/docs/PSDependScripts-ReviewerChecklist.md b/docs/PSDependScripts-ReviewerChecklist.md new file mode 100644 index 0000000..c9187b9 --- /dev/null +++ b/docs/PSDependScripts-ReviewerChecklist.md @@ -0,0 +1,145 @@ +# PSDependScripts — Reviewer Checklist + +A guide for writing and reviewing dependency scripts in `PSDepend/PSDependScripts/`. +Each script implements a single `DependencyType` (e.g. `PSGalleryModule`, `Git`, +`Chocolatey`) and is dot-sourced by `Invoke-DependencyScript`. + +## Established patterns (best practices) + +### 1. Standard parameter contract + +- First parameter is `[PSTypeName('PSDepend.Dependency')][psobject[]]$Dependency`. +- `$PSDependAction` is `[ValidateSet(...)][string[]]` with **only** the actions + the script actually implements (`Test`, `Install`, `Import`) and defaults to + `@('Install')`. +- Type-specific options (`Repository`, `Source`, `Force`, `Global`, + `ProviderName`, etc.) are top-level parameters — populated via + `Parameters = @{ ... }` splat from the dependency hashtable. + +### 2. Comment-based help is mandatory and follows a shape + +- `.SYNOPSIS` / `.DESCRIPTION`. +- A **"Relevant Dependency metadata"** block describing how each + `$Dependency.*` field (`Name`, `Version`, `Target`, `Source`, `Credential`, + `AddToPath`, `Parameters.*`) is interpreted *by this dependency type*. +- A `.PARAMETER PSDependAction` block listing supported actions. +- At least one `.EXAMPLE` showing a real `@{ }` dependency hashtable. Most + scripts include both a simple and an advanced example. + +### 3. Field extraction at the top + +Resolve the inputs once into locals before the main body runs: + +- `$Name = $Dependency.Name`; fall back to `$Dependency.DependencyName`. +- `$Version = $Dependency.Version`; default to `'latest'`. +- `$Target` / `$Source` / `$Credential` with sensible defaults. + +### 4. `PSDependAction` semantics + +- `Test` alone returns `$true` / `$false`. +- `Test` combined with `Install` returns `$true` early when satisfied and falls + through to install otherwise. +- `Install` does the work; `Import` (where supported) calls + `Import-PSDependModule` against the resolved path. +- The canonical "nothing found, test-only" guard appears verbatim across scripts: + + ```powershell + if ($PSDependAction -contains 'Test' -and $PSDependAction.Count -eq 1) { + return $false + } + ``` + +### 5. External tool prerequisites + +- Probe with `Get-Command -ErrorAction SilentlyContinue` and use + `Write-Error` (not `throw`) when missing, so the dependency engine can + continue with the rest of the run. +- Invoke external tools via `Invoke-ExternalCommand` (see `Git.ps1`, + `Chocolatey.ps1`) rather than `& tool`, so output capture is consistent. + +### 6. Path / scope semantics + +- `Target` doubles as Scope: `AllUsers` / `CurrentUser` are install scopes; + any other value is a filesystem path (Save vs Install branch). +- `AddToPath` consistently prepends to `$env:PATH` and/or `$env:PSModulePath` + via `Add-ToItemCollection`. + +### 7. Logging discipline + +- `Write-Verbose` for normal progress on each decision branch. +- `Write-Error` (not `throw`) for recoverable failures. +- `Write-Warning` for skip-and-continue cases (see `Task.ps1`). + +## Reviewer checklist + +Use this as a PR review checklist when adding or modifying a script under +`PSDepend/PSDependScripts/`. + +### Contract + +- [ ] First param is `[PSTypeName('PSDepend.Dependency')][psobject[]]$Dependency`. +- [ ] `PSDependAction` is `[ValidateSet(...)]` and lists only implemented actions. +- [ ] Type-specific params are top-level (not buried in + `$Dependency.Parameters` lookups inside the body). + +### Help + +- [ ] `.SYNOPSIS` and `.DESCRIPTION` are present. +- [ ] "Relevant Dependency metadata" block enumerates **every** `$Dependency.*` + field the script reads. +- [ ] Every parameter has a `.PARAMETER` entry. +- [ ] At least one `.EXAMPLE` with a runnable `@{ }` hashtable. + +### Dependency-field handling + +- [ ] `Name` falls back to `DependencyName`. +- [ ] `Version` defaults to `'latest'` (or documents why not). +- [ ] `Target` default + scope-vs-path interpretation is documented. +- [ ] `Credential` is honored when the underlying provider supports it. +- [ ] `AddToPath` is honored where the install location is filesystem-based. + +### Action semantics + +- [ ] `Test` alone returns a single boolean. +- [ ] `Test` + `Install` short-circuits cleanly when satisfied (no install, + but `Import-PSDependModule` still runs if applicable). +- [ ] Test-only "nothing found" returns `$false` via the canonical guard. +- [ ] `Import` (if supported) goes through `Import-PSDependModule`. + +### Robustness + +- [ ] External tool dependencies probed via + `Get-Command -ErrorAction SilentlyContinue`. +- [ ] External invocations go through `Invoke-ExternalCommand`. +- [ ] Failures use `Write-Error` (not `throw`) unless terminating is intended + (e.g. `FailOnError`). +- [ ] No `Out-Null` / `2>$null` swallowing of error streams. +- [ ] Verbose messages on each decision branch. +- [ ] Cross-platform paths use `Join-Path` (not string concat with `\`). + +### Version comparison (for installers) + +- [ ] Both `[SemanticVersion]::TryParse` and `[Version]::TryParse` are + attempted before comparing (see `PSGalleryModule.ps1` lines 262–273). +- [ ] `'latest'` vs explicit-version paths each produce a defensible + early-return. + +### Security / hygiene + +- [ ] No plaintext credentials emitted in `Write-Verbose`. +- [ ] No `Invoke-Expression` on dependency data. `Command.ps1` uses + `[ScriptBlock]::Create` — that's the documented trust boundary; + any new script doing this needs an explicit opt-in + (e.g. `FailOnError`-style). +- [ ] TLS 1.2 forced before `Invoke-WebRequest` against public registries + (see `Chocolatey.ps1:182`). + +### Known smells to call out + +- Helper functions defined inside dependency scripts (e.g. `Parse-URLForFile` + in `FileDownload.ps1`, `Get-ChocoInstalledPackage` in `Chocolatey.ps1`) + should arguably live in `PSDepend/Private/`. Flag if a new script adds + local helpers — both for reuse and because **inline helpers cannot be + mocked by Pester**. +- Direct `Invoke-Expression`-style dot-sourcing of user-supplied strings + (see `Command.ps1`) requires explicit acknowledgement.