diff --git a/PSDepend/PSDependMap.psd1 b/PSDepend/PSDependMap.psd1 index d150c72..f6674b0 100644 --- a/PSDepend/PSDependMap.psd1 +++ b/PSDepend/PSDependMap.psd1 @@ -6,87 +6,93 @@ # In some cases, it may be beneficial to include 'aliases'. Just add nodes for these. @{ - Chocolatey = @{ - Script = 'Chocolatey.ps1' + Chocolatey = @{ + Script = 'Chocolatey.ps1' Description = 'Install a Chocolatey package from a Chocolatey feed' - Supports = 'windows' + Supports = 'windows' } - Command = @{ - Script = 'Command.ps1' + Command = @{ + Script = 'Command.ps1' Description = 'Invoke a command in PowerShell' - Supports = 'windows', 'core', 'macos', 'linux' + Supports = 'windows', 'core', 'macos', 'linux' } - DotnetSdk = @{ - Script = 'DotnetSdk.ps1' + DotnetSdk = @{ + Script = 'DotnetSdk.ps1' Description = "Installs the .NET Core SDK" - Supports = 'windows', 'core', 'macos', 'linux' + Supports = 'windows', 'core', 'macos', 'linux' } - FileDownload = @{ - Script = 'FileDownload.ps1' + FileDownload = @{ + Script = 'FileDownload.ps1' Description = 'Download a file' - Supports = 'windows' + Supports = 'windows' } - FileSystem = @{ - Script = 'FileSystem.ps1' + FileSystem = @{ + Script = 'FileSystem.ps1' Description = 'Copy a file or folder' - Supports = 'windows' + Supports = 'windows' } - Git = @{ - Script = 'Git.ps1' + Git = @{ + Script = 'Git.ps1' Description = 'Clone a git repository' - Supports = 'windows', 'core', 'macos', 'linux' + Supports = 'windows', 'core', 'macos', 'linux' } - GitHub = @{ - Script = 'GitHub.ps1' + GitHub = @{ + Script = 'GitHub.ps1' Description = 'Download and extract a GitHub repo' - Supports = 'windows', 'core', 'macos', 'linux' + Supports = 'windows', 'core', 'macos', 'linux' } - Npm = @{ - Script = 'Npm.ps1' + Npm = @{ + Script = 'Npm.ps1' Description = 'Install a node package' - Supports = 'windows', 'core', 'macos', 'linux' + Supports = 'windows', 'core', 'macos', 'linux' } - Noop = @{ - Script = 'Noop.ps1' + Noop = @{ + Script = 'Noop.ps1' Description = 'Display parameters that a depends script would receive. Use for testing and validation' - Supports = 'windows', 'core', 'macos', 'linux' + Supports = 'windows', 'core', 'macos', 'linux' } - Nuget = @{ - Script = 'Nuget.ps1' + Nuget = @{ + Script = 'Nuget.ps1' Description = 'Install a Nuget package from a Nuget feed' - Supports = 'windows', 'core', 'macos', 'linux' + Supports = 'windows', 'core', 'macos', 'linux' } - Package = @{ - Script = 'Package.ps1' + Package = @{ + Script = 'Package.ps1' Description = 'EXPERIMENTAL: Install a package via PackageManagement Install-Package' - Supports = 'windows', 'core', 'macos', 'linux' + Supports = 'windows', 'core', 'macos', 'linux' } PSGalleryModule = @{ - Script= 'PSGalleryModule.ps1' - Description = 'Install a PowerShell module from the PowerShell Gallery' - Supports = 'windows', 'core', 'macos', 'linux' + Script = 'PSGalleryModule.ps1' + Description = 'Install a PowerShell module from the PowerShell Gallery (legacy, PowerShellGet v2 - prefer PSResourceGet for new projects)' + Supports = 'windows', 'core', 'macos', 'linux' } - PSGalleryNuget = @{ - Script = 'PSGalleryNuget.ps1' + PSGalleryNuget = @{ + Script = 'PSGalleryNuget.ps1' Description = 'Install a PowerShell module from the PowerShell Gallery without the PowerShellGet dependency' - Supports = 'windows', 'core', 'macos', 'linux' + Supports = 'windows', 'core', 'macos', 'linux' } - Task = @{ - Script = 'Task.ps1' + PSResourceGet = @{ + Script = 'PSResourceGet.ps1' + Description = 'Install a PowerShell module from a PowerShell repository using PSResourceGet (preferred over PSGalleryModule)' + Supports = 'windows', 'core', 'macos', 'linux' + } + + Task = @{ + Script = 'Task.ps1' Description = 'Support dependencies by handling simple tasks' - Supports = 'windows', 'core', 'macos', 'linux' + Supports = 'windows', 'core', 'macos', 'linux' } } diff --git a/PSDepend/PSDependScripts/PSResourceGet.ps1 b/PSDepend/PSDependScripts/PSResourceGet.ps1 new file mode 100644 index 0000000..97374bf --- /dev/null +++ b/PSDepend/PSDependScripts/PSResourceGet.ps1 @@ -0,0 +1,330 @@ +<# + .SYNOPSIS + Installs a PowerShell resource from a PowerShell repository using PSResourceGet. + + .DESCRIPTION + Installs a PowerShell module from a PowerShell repository (such as the PowerShell Gallery) + using PSResourceGet (Microsoft.PowerShell.PSResourceGet), the successor to the deprecated + PowerShellGet v2 module. PSResourceGet must be installed before using this provider. + + Prefer this provider over PSGalleryModule for new projects. PSGalleryModule targets + PowerShellGet v2 (Install-Module); this provider targets PSResourceGet v3 (Install-PSResource). + + Relevant Dependency metadata: + Name: The name of the module to install + Version: Used to identify existing installs and as -Version for installation. + Supports NuGet range syntax (e.g. '[1.0.0, ]'). Defaults to 'latest'. + Target: Used as -Scope for Install-PSResource (CurrentUser or AllUsers). + If this is a filesystem path, Save-PSResource is used instead. + Defaults to 'CurrentUser'. + AddToPath: If Target is a filesystem path, prepend that path to $env:PSModulePath. + Credential: A [PSCredential] for authenticating against a private repository. + Use Get-Credential or [PSCredential]::new() to construct one. + + This provider calls the following PSResourceGet cmdlets: + - Find-PSResource + - Install-PSResource + - Save-PSResource + + .PARAMETER Dependency + The PSDepend.Dependency object passed by Invoke-PSDepend. Not supplied directly by the caller. + + .PARAMETER Repository + PSResource repository to download from. + Defaults to PSGallery. + + .PARAMETER NoClobber + Prevents installation if the module would overwrite commands already present on the system. + + .PARAMETER AcceptLicense + Suppresses the license acceptance prompt during installation. + + .PARAMETER Prerelease + If specified, allows installation of prerelease versions. + + If specified along with version 'latest', a prerelease will be selected + if it is the most recent available version. + + Sorting assumes prereleases are named appropriately + (e.g. alpha < beta < rc). + + .PARAMETER Import + If specified, imports the module into the global scope after installation. + + Deprecated. Use PSDependAction = 'Import' instead. This parameter may be + removed in a future release. + + .PARAMETER PSDependAction + Test, Install, or Import the module. + Defaults to Install. + + Test: Returns $true or $false depending on whether the dependency is present + Install: Installs the dependency + Import: Imports the dependency + + .EXAMPLE + @{ + BuildHelpers = @{ + DependencyType = 'PSResourceGet' + Version = 'latest' + } + InvokeBuild = @{ + DependencyType = 'PSResourceGet' + Version = '3.2.1' + } + } + + # Install the latest BuildHelpers and version 3.2.1 of InvokeBuild from PSGallery. + # Omitting Version, or setting it to '', also resolves to latest. + + .EXAMPLE + @{ + BuildHelpers = @{ + DependencyType = 'PSResourceGet' + Target = 'C:\Build' + } + } + + # Save the latest BuildHelpers module from PSGallery to C:\Build + # (i.e. C:\Build\BuildHelpers will be the module folder) + + .EXAMPLE + @{ + BuildHelpers = @{ + DependencyType = 'PSResourceGet' + Parameters = @{ + Repository = 'PSPrivateGallery' + } + } + } + + # Install the latest BuildHelpers from a registered private repository. + # Register the repository first with Register-PSResourceRepository. + # + # Examples of private repositories include: + # - Artifactory + # - ProGet + # - GitLab Package Registry + + .EXAMPLE + @{ + 'vmware.powercli' = @{ + DependencyType = 'PSResourceGet' + Parameters = @{ + Prerelease = $true + } + } + } + + # Install the latest version of PowerCLI, allowing prerelease versions. +#> + +[CmdletBinding()] +param( + [PSTypeName('PSDepend.Dependency')] + [psobject[]]$Dependency, + + [AllowNull()] + [string]$Repository = 'PSGallery', + + [switch]$NoClobber, + + [switch]$AcceptLicense, + + [switch]$Prerelease, + + [switch]$Import, + + [ValidateSet('Test', 'Install', 'Import')] + [string[]]$PSDependAction = @('Install') +) + +if (-not (Get-Command -Name Install-PSResource -ErrorAction SilentlyContinue)) { + Write-Error "PSResourceGet (Microsoft.PowerShell.PSResourceGet) is required but not available. Install it before using the PSResourceGet dependency type." + return +} + +# Extract data from Dependency +$DependencyName = $Dependency.DependencyName +$Name = $Dependency.Name +if (-not $Name) { + $Name = $DependencyName +} + +$Version = $Dependency.Version +if (-not $Version) { + $Version = 'latest' +} + +# Target doubles as Scope: AllUsers/CurrentUser = install scope; any other value = filesystem path +if (-not $Dependency.Target) { + $Scope = 'CurrentUser' +} else { + $Scope = $Dependency.Target +} + +$Credential = $Dependency.Credential + +if ('AllUsers', 'CurrentUser' -notcontains $Scope) { + $command = 'save' +} else { + $command = 'install' +} + +Write-Verbose -Message "Getting dependency [$Name] from PowerShell repository [$Repository]" + +if ($Repository) { + $validRepo = Get-PSResourceRepository -Name $Repository -Verbose:$false -ErrorAction SilentlyContinue + if (-not $validRepo) { + Write-Error "[$Repository] has not been set up as a valid PowerShell repository." + return + } +} + +# TrustRepository defaults to $true so unattended / CI installs do not hang on a trust prompt +$params = @{ + Name = $Name + TrustRepository = $true +} + +if ($PSBoundParameters.ContainsKey('NoClobber')) { + $params.Add('NoClobber', $NoClobber) +} + +if ($PSBoundParameters.ContainsKey('Prerelease')) { + $params.Add('Prerelease', $Prerelease) +} + +if ($PSBoundParameters.ContainsKey('AcceptLicense')) { + $params.Add('AcceptLicense', $AcceptLicense) +} + +if ($Repository) { + $params.Add('Repository', $Repository) +} + +if ($Version -and $Version -ne 'latest') { + $params.Add('Version', $Version) +} + +if ($Credential) { + $params.Add('Credential', $Credential) +} + +if ($command -eq 'save') { + $ModuleName = Join-Path $Scope $Name +} elseif ($command -eq 'install') { + $ModuleName = $Name +} + +# Filter params to only those accepted by the target command +$targetCmd = if ($command -eq 'save') { + 'Save-PSResource' +} else { + 'Install-PSResource' +} +$availableParameters = (Get-Command $targetCmd).Parameters +$tempParams = $params.Clone() +foreach ($thisParameter in $params.Keys) { + if (-not $availableParameters.ContainsKey($thisParameter)) { + Write-Verbose -Message "Removing parameter [$thisParameter] from [$targetCmd] as it is not available" + $tempParams.Remove($thisParameter) + } +} +$params = $tempParams.Clone() + +Add-ToPsModulePathIfRequired -Dependency $Dependency -Action $PSDependAction + +$Existing = Get-Module -ListAvailable -Name $ModuleName -ErrorAction SilentlyContinue + +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 } + if ($Repository) { + $FindModuleParams.Add('Repository', $Repository) + } + if ($Credential) { + $FindModuleParams.Add('Credential', $Credential) + } + if ($Prerelease) { + $FindModuleParams.Add('Prerelease', $true) + } + + # Version string, and that version is already installed (may not be the maximum) + $matchedExisting = if ($Version -and $Version -ne 'latest') { + $Existing | Where-Object { + Test-VersionEquality -ReferenceVersion $_.Version -DifferenceVersion $Version + } | Select-Object -First 1 + } + if ($matchedExisting) { + Write-Verbose "You have the requested version [$Version] of [$Name]" + Import-PSDependModule -Name $ModuleName -Action $PSDependAction -Version $matchedExisting.Version + + if ($PSDependAction -contains 'Test') { + return $true + } + return $null + } + + $GalleryVersion = Find-PSResource @FindModuleParams | Measure-Object -Property Version -Maximum | Select-Object -ExpandProperty Maximum + # Compare using SemanticVersion first (PSResourceGet uses SemVer); fall back to System.Version + [System.Version]$parsedVersion = $null + [System.Version]$parsedGalleryVersion = $null + [System.Management.Automation.SemanticVersion]$parsedSemanticVersion = $null + [System.Management.Automation.SemanticVersion]$parsedTempSemanticVersion = $null + $existingIsUpToDate = if ( + [System.Management.Automation.SemanticVersion]::TryParse([string]$ExistingVersion, [ref]$parsedSemanticVersion) -and + [System.Management.Automation.SemanticVersion]::TryParse([string]$GalleryVersion, [ref]$parsedTempSemanticVersion) + ) { + $parsedTempSemanticVersion -le $parsedSemanticVersion + } elseif ( + [System.Version]::TryParse([string]$ExistingVersion, [ref]$parsedVersion) -and + [System.Version]::TryParse([string]$GalleryVersion, [ref]$parsedGalleryVersion) + ) { + $parsedGalleryVersion -le $parsedVersion + } else { + $false + } + + # latest, and we have latest + if ($Version -and ($Version -eq 'latest' -or $Version -eq '') -and $existingIsUpToDate) { + Write-Verbose "You have the latest version of [$Name], with installed version [$ExistingVersion] and repository version [$GalleryVersion]" + Import-PSDependModule -Name $ModuleName -Action $PSDependAction -Version $ExistingVersion + + if ($PSDependAction -contains 'Test') { + return $true + } + return $null + } + Write-Verbose "Continuing to install [$Name]: Requested version [$Version], existing version [$ExistingVersion]" +} + +# No dependency found, return false if we're testing alone... +if ($PSDependAction -contains 'Test' -and $PSDependAction.count -eq 1) { + return $false +} + +if ($PSDependAction -contains 'Install') { + if ('AllUsers', 'CurrentUser' -contains $Scope) { + Write-Verbose "Installing [$Name] with scope [$Scope]" + Install-PSResource @params -Scope $Scope + } else { + Write-Verbose "Saving [$Name] to path [$Scope]" + Write-Verbose "Creating directory path to [$Scope]" + if (-not (Test-Path $Scope -ErrorAction SilentlyContinue)) { + $null = New-Item -ItemType Directory -Path $Scope -Force -ErrorAction SilentlyContinue + } + Save-PSResource @params -Path $Scope + } +} + +# Conditional import — params['Version'] may be a NuGet range; resolve to a concrete installed version +$importVs = $params['Version'] +if ($importVs -and $importVs -match '[\[\](,]') { + $importVs = Get-Module -ListAvailable -Name $ModuleName -ErrorAction SilentlyContinue | + Measure-Object -Property Version -Maximum | + Select-Object -ExpandProperty Maximum +} +Import-PSDependModule -Name $ModuleName -Action $PSDependAction -Version $importVs diff --git a/Tests/PSResourceGet.Type.Tests.ps1 b/Tests/PSResourceGet.Type.Tests.ps1 new file mode 100644 index 0000000..6af5ecd --- /dev/null +++ b/Tests/PSResourceGet.Type.Tests.ps1 @@ -0,0 +1,267 @@ +#requires -Module @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } + +BeforeAll { + if (-not $env:BHProjectPath) { + & "$PSScriptRoot\..\build.ps1" -Task 'Build' + } + 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/PSResourceGet.ps1' + $script:TestCred = New-TestCredential + $script:OrigPSModulePath = $env:PSModulePath +} + +AfterAll { + if ($script:OrigPSModulePath) { + $env:PSModulePath = $script:OrigPSModulePath + } +} + +Describe 'PSResourceGet script' { + + BeforeAll { + InModuleScope PSDepend { + # Stubs for PSResourceGet cmdlets — needed so Pester can mock them on machines where + # Microsoft.PowerShell.PSResourceGet is not installed. Parameter declarations must + # match what PSResourceGet.ps1 passes so the param-stripping loop keeps them intact. + function Get-PSResourceRepository { + [CmdletBinding()] param([string]$Name) + } + function Find-PSResource { + [CmdletBinding()] param( + [string]$Name, [string]$Repository, + [PSCredential]$Credential, [switch]$Prerelease + ) + } + function Install-PSResource { + [CmdletBinding()] param( + [string]$Name, [string]$Version, [string]$Repository, + [switch]$TrustRepository, [switch]$NoClobber, + [switch]$AcceptLicense, [switch]$Prerelease, + [PSCredential]$Credential, [string]$Scope + ) + } + function Save-PSResource { + [CmdletBinding()] param( + [string]$Name, [string]$Version, [string]$Repository, + [switch]$TrustRepository, [switch]$NoClobber, + [switch]$AcceptLicense, [switch]$Prerelease, + [PSCredential]$Credential, [string]$Path + ) + } + + Mock Get-PSResourceRepository { [PSCustomObject]@{ Name = 'PSGallery'; Trusted = $true } } + Mock Get-Module { } -ParameterFilter { $ListAvailable } + Mock Find-PSResource { [PSCustomObject]@{ Name = 'TestModule'; Version = [version]'2.0.0' } } + Mock Install-PSResource { } + Mock Save-PSResource { } + Mock Import-PSDependModule { } + Mock Add-ToPsModulePathIfRequired { } + } + } + + Context 'Contract: default Version handling' { + It 'Omits -Version when Version is not supplied (installs latest)' { + $dep = New-PSDependFixture -DependencyName 'TestModule' -DependencyType 'PSResourceGet' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 1 -Exactly ` + -ParameterFilter { -not $PSBoundParameters.ContainsKey('Version') } + } + + It 'Passes -Version when an explicit version is supplied' { + $dep = New-PSDependFixture -DependencyName 'TestModule' -DependencyType 'PSResourceGet' -Version '1.2.3' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 1 -Exactly ` + -ParameterFilter { $Version -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' -DependencyType 'PSResourceGet' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-PSResource -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' -DependencyType 'PSResourceGet' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-PSResource -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' -DependencyType 'PSResourceGet' + $result = InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -PSDependAction Test + } + $result | Should -Be $false + Should -Invoke -CommandName Install-PSResource -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' -DependencyType 'PSResourceGet' -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-PSResource -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-PSResource but still calls Import-PSDependModule' { + $dep = New-PSDependFixture -DependencyName 'TestModule' -DependencyType 'PSResourceGet' -Version 'latest' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -PSDependAction Test, Install + } + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 0 + Should -Invoke -CommandName Import-PSDependModule -ModuleName PSDepend -Times 1 + } + } + + Context 'NuGet range version syntax' { + It 'Passes the range string to Install-PSResource and resolves a concrete version for Import' { + InModuleScope PSDepend { + Mock Get-Module { } -ParameterFilter { $ListAvailable } + Mock Get-Module { [PSCustomObject]@{ Name = 'TestModule'; Version = [version]'1.5.0' } } ` + -ParameterFilter { -not $ListAvailable } + } + $dep = New-PSDependFixture -DependencyName 'TestModule' -DependencyType 'PSResourceGet' -Version '[1.0.0, )' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -PSDependAction Install, Import + } + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 1 -Exactly ` + -ParameterFilter { $Version -eq '[1.0.0, )' } + Should -Invoke -CommandName Import-PSDependModule -ModuleName PSDepend -Times 1 -Exactly ` + -ParameterFilter { $Version -ne '[1.0.0, )' } + } + } + + Context 'Latest version comparison' { + It 'Installs when installed version is behind the repository version' { + InModuleScope PSDepend { + Mock Get-Module { [PSCustomObject]@{ Name = 'TestModule'; Version = [version]'2.8.0' } } ` + -ParameterFilter { $ListAvailable } + Mock Find-PSResource { [PSCustomObject]@{ Name = 'TestModule'; Version = [version]'2.10.0' } } + } + $dep = New-PSDependFixture -DependencyName 'TestModule' -DependencyType 'PSResourceGet' -Version 'latest' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 1 + } + } + + Context 'Multiple installed versions — requested version present but not the maximum' { + It 'Returns $true and skips Install when the requested version is installed (even if a higher version also exists)' { + InModuleScope PSDepend { + Mock Get-Module { + @( + [PSCustomObject]@{ Name = 'TestModule'; Version = [version]'1.2.3' } + [PSCustomObject]@{ Name = 'TestModule'; Version = [version]'2.0.0' } + ) + } -ParameterFilter { $ListAvailable } + } + $dep = New-PSDependFixture -DependencyName 'TestModule' -DependencyType 'PSResourceGet' -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-PSResource -ModuleName PSDepend -Times 0 + } + } + + Context 'Target as path uses Save-PSResource instead of Install-PSResource' { + It 'Calls Save-PSResource with -Path and skips Install-PSResource' { + $savePath = (New-Item 'TestDrive:/psresourceget-save' -ItemType Directory -Force).FullName + $dep = New-PSDependFixture -DependencyName 'TestModule' -DependencyType 'PSResourceGet' -Target $savePath + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Save-PSResource -ModuleName PSDepend -Times 1 -Exactly ` + -ParameterFilter { $Path -eq $savePath } + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 0 + } + } + + Context 'Credential pass-through' { + It 'Forwards Credential to Install-PSResource' { + $dep = New-PSDependFixture -DependencyName 'PrivateModule' -DependencyType 'PSResourceGet' ` + -Credential $script:TestCred + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 1 -Exactly ` + -ParameterFilter { $Credential -and $Credential.UserName -eq 'testUser' } + } + } + + Context 'Repository validation' { + BeforeAll { + InModuleScope PSDepend { + Mock Get-PSResourceRepository { } -ParameterFilter { $Name -eq 'BogusRepo' } + } + } + + It 'Writes an error and skips install when the repository is not registered' { + $dep = New-PSDependFixture -DependencyName 'TestModule' -DependencyType 'PSResourceGet' ` + -Parameters @{ Repository = 'BogusRepo' } + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -Repository 'BogusRepo' -ErrorAction SilentlyContinue + } + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 0 + } + } + + Context 'PSResourceGet availability guard' { + It 'Returns early without installing when Install-PSResource is not available' { + InModuleScope PSDepend { + # Intercept the guard check: Get-Command is safe to mock inside an It block + # (Pester's own mock setup finished in BeforeAll; this only affects the script's call) + Mock Get-Command { } -ParameterFilter { $Name -eq 'Install-PSResource' } + } + $dep = New-PSDependFixture -DependencyName 'TestModule' -DependencyType 'PSResourceGet' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -ErrorAction SilentlyContinue + } + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 0 + } + } + + Context 'TrustRepository' { + It 'Always passes TrustRepository to Install-PSResource for unattended use' { + $dep = New-PSDependFixture -DependencyName 'TestModule' -DependencyType 'PSResourceGet' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 1 -Exactly ` + -ParameterFilter { $TrustRepository -eq $true } + } + } +}