functions/_getExtensionFromPSRepository.Tests.ps1

# <copyright file="_getExtensionFromPSRepository.Tests.ps1" company="Endjin Limited">
# Copyright (c) Endjin Limited. All rights reserved.
# </copyright>

BeforeAll {
    # sut
    . $PSCommandPath.Replace('.Tests.ps1','.ps1')

    # in-module dependencies
    . (Join-Path (Split-Path -Parent $PSCommandPath) 'Get-InstalledExtensionDetails.ps1')
    . (Join-Path (Split-Path -Parent $PSCommandPath) '_getCompleteExtensionVersion.ps1')
    . (Join-Path (Split-Path -Parent $PSCommandPath) '_getExtensionVersionRange.ps1')
    . (Join-Path (Split-Path -Parent $PSCommandPath) '_testExtensionVersionInRange.ps1')

    # Needed to create mock PSResourceInfo objects
    Import-Module Microsoft.PowerShell.PSResourceGet

    Set-StrictMode -Version Latest

    # Creates the minimum folder structure of an installed extension, as Save-PSResource would
    function newInstalledExtension([string] $TargetPath, [string] $Name, [string] $Version) {
        $baseVersion,$preReleaseTag = $Version -split '-', 2
        $path = Join-Path $TargetPath $Name $baseVersion
        New-Item -ItemType Directory -Path $path -Force | Out-Null
        Set-Content -Path (Join-Path $path "$Name.psd1") -Value @"
@{
    ModuleVersion = '$baseVersion'
    PrivateData = @{ PSData = @{ Prerelease = '$preReleaseTag' } }
}
"@

    }
}

Describe '_getExtensionFromPSRepository' {

    BeforeAll {
        $name = 'ZeroFailed.Build.Test'
        $latestVersion = '2.0.0'

        Mock Write-Host {}
        Mock Write-Warning {}

        # Simulate a repository that has the requested version, or the latest version when none is requested
        Mock Find-PSResource {
            $baseVersion,$preReleaseTag = ($Version ? $Version : $latestVersion) -split '-', 2
            New-MockObject -Type 'Microsoft.PowerShell.PSResourceGet.UtilClasses.PSResourceInfo' -Properties @{
                Name = $Name
                Version = [version]$baseVersion
                Prerelease = $preReleaseTag
                # Bound to Save-PSResource's -Repository parameter when the object is piped to it
                Repository = 'PSGallery'
            }
        }
        Mock Save-PSResource {
            $resource = $InputObject | Select-Object -First 1
            $fullVersion = $resource.Prerelease ? "$($resource.Version)-$($resource.Prerelease)" : "$($resource.Version)"
            newInstalledExtension -TargetPath $Path -Name $resource.Name -Version $fullVersion
        }
    }

    BeforeEach {
        $targetPath = Join-Path $TestDrive "extensions-$(New-Guid)"
    }

    Context 'When no version is locked' {
        It 'Installs the latest version and returns it as the resolved version' {
            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'PSGallery' -TargetPath $targetPath

            $result.Path | Should -Be (Join-Path $targetPath $name $latestVersion)
            $result.Enabled | Should -Be $true
            $result.Version | Should -Be $latestVersion
            $result.ResolvedVersion | Should -Be $latestVersion
        }

        It 'Only searches the configured repository' {
            _getExtensionFromPSRepository -Name $name -PSRepository 'MyRepository' -TargetPath $targetPath | Out-Null

            Should -Invoke Find-PSResource -Times 1 -ParameterFilter { $Repository -eq 'MyRepository' }
        }

        It 'Uses an installed version without checking the repository' {
            newInstalledExtension -TargetPath $targetPath -Name $name -Version '1.5.0'

            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'PSGallery' -TargetPath $targetPath

            $result.ResolvedVersion | Should -Be '1.5.0'
            Should -Invoke Find-PSResource -Times 0
        }

        It 'Uses an installed pre-release of the requested version when pre-release versions are allowed' {
            newInstalledExtension -TargetPath $targetPath -Name $name -Version '1.0.0-beta1'

            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'PSGallery' -TargetPath $targetPath -Version '1.0.0' -PreRelease

            $result.Path | Should -Be (Join-Path $targetPath $name '1.0.0')
            $result.ResolvedVersion | Should -Be '1.0.0-beta1'
            Should -Invoke Find-PSResource -Times 0
            Should -Invoke Save-PSResource -Times 0
        }
    }

    Context 'When a version is locked' {
        It 'Installs the locked version even when a newer version is installed' {
            newInstalledExtension -TargetPath $targetPath -Name $name -Version '2.0.0'

            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'PSGallery' -TargetPath $targetPath -LockedVersion '1.0.0'

            $result.Path | Should -Be (Join-Path $targetPath $name '1.0.0')
            $result.ResolvedVersion | Should -Be '1.0.0'
            $result.Version | Should -Be '1.0.0'
            Should -Invoke Find-PSResource -Times 1 -ParameterFilter { $Version -eq '1.0.0' -and !$Prerelease }
        }

        It 'Includes pre-release versions when the locked version is a pre-release' {
            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'PSGallery' -TargetPath $targetPath -LockedVersion '2.1.0-beta0001'

            $result.Path | Should -Be (Join-Path $targetPath $name '2.1.0')
            $result.ResolvedVersion | Should -Be '2.1.0-beta0001'
            Should -Invoke Find-PSResource -Times 1 -ParameterFilter { $Version -eq '2.1.0-beta0001' -and $Prerelease }
        }

        It 'Replaces an installed pre-release of the locked version when the locked version is not a pre-release, even when pre-release versions are allowed' {
            newInstalledExtension -TargetPath $targetPath -Name $name -Version '1.0.0-beta1'

            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'PSGallery' -TargetPath $targetPath -PreRelease -LockedVersion '1.0.0'

            $result.Path | Should -Be (Join-Path $targetPath $name '1.0.0')
            $result.ResolvedVersion | Should -Be '1.0.0'
            Should -Invoke Find-PSResource -Times 1 -ParameterFilter { $Version -eq '1.0.0' -and !$Prerelease }
            Should -Invoke Save-PSResource -Times 1
        }

        It 'Uses the installed extension when it is the locked version' {
            newInstalledExtension -TargetPath $targetPath -Name $name -Version '1.0.0'
            newInstalledExtension -TargetPath $targetPath -Name $name -Version '2.0.0'

            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'PSGallery' -TargetPath $targetPath -LockedVersion '1.0.0'

            $result.ResolvedVersion | Should -Be '1.0.0'
            Should -Invoke Find-PSResource -Times 0
            Should -Invoke Save-PSResource -Times 0
        }

        It 'Installs a requested version that has fewer than 3 numeric parts, and uses it once it is installed' {
            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'PSGallery' -TargetPath $targetPath -Version '1.4'

            $result.Path | Should -Be (Join-Path $targetPath $name '1.4.0')
            $result.ResolvedVersion | Should -Be '1.4.0'
            $result.ContainsKey('Version') | Should -Be $false

            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'PSGallery' -TargetPath $targetPath -Version '1.4'

            $result.ResolvedVersion | Should -Be '1.4.0'
            Should -Invoke Find-PSResource -Times 1 -Exactly
        }

        It 'Does not overwrite the requested version in the returned metadata' {
            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'PSGallery' -TargetPath $targetPath -Version '1.0.0' -LockedVersion '1.0.0'

            $result.ContainsKey('Version') | Should -Be $false
            $result.ResolvedVersion | Should -Be '1.0.0'
        }
    }

    Context 'When a minimum version is requested' {
        BeforeAll {
            # Simulate a repository with several versions, in no particular order, including some outside the range
            Mock Find-PSResource -ParameterFilter { $Version -and $Version.StartsWith('[') } {
                foreach ($availableVersion in '1.4.0', '1.6.0', '1.5.2', '1.7.0-beta.1', '2.0.0') {
                    $baseVersion,$preReleaseTag = $availableVersion -split '-', 2
                    New-MockObject -Type 'Microsoft.PowerShell.PSResourceGet.UtilClasses.PSResourceInfo' -Properties @{
                        Name = $Name
                        Version = [version]$baseVersion
                        Prerelease = $preReleaseTag
                        Repository = 'PSGallery'
                    }
                }
            }
        }

        It 'Installs the newest version in the range' {
            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'PSGallery' -TargetPath $targetPath -MinimumVersion '1.4.0'

            $result.Path | Should -Be (Join-Path $targetPath $name '1.6.0')
            $result.Enabled | Should -Be $true
            $result.ResolvedVersion | Should -Be '1.6.0'
            Should -Invoke Find-PSResource -Times 1 -ParameterFilter { $Version -eq '[1.4.0,2.0.0)' -and !$Prerelease }
        }

        It 'Only searches the configured repository for versions in the range' {
            _getExtensionFromPSRepository -Name $name -PSRepository 'MyRepository' -TargetPath $targetPath -MinimumVersion '1.4.0' | Out-Null

            Should -Invoke Find-PSResource -Times 1 -ParameterFilter { $Version -eq '[1.4.0,2.0.0)' -and $Repository -eq 'MyRepository' }
        }

        It 'Includes pre-release versions in the range when they are requested' {
            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'PSGallery' -TargetPath $targetPath -MinimumVersion '1.4.0' -PreRelease

            $result.Path | Should -Be (Join-Path $targetPath $name '1.7.0')
            $result.ResolvedVersion | Should -Be '1.7.0-beta.1'
        }

        It 'Uses an installed version in the range without checking the repository' {
            newInstalledExtension -TargetPath $targetPath -Name $name -Version '1.5.0'

            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'PSGallery' -TargetPath $targetPath -MinimumVersion '1.4.0'

            $result.ResolvedVersion | Should -Be '1.5.0'
            Should -Invoke Find-PSResource -Times 0
        }

        It 'Ignores installed versions outside the range' {
            newInstalledExtension -TargetPath $targetPath -Name $name -Version '1.3.0'
            newInstalledExtension -TargetPath $targetPath -Name $name -Version '2.0.0'

            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'PSGallery' -TargetPath $targetPath -MinimumVersion '1.4.0'

            $result.ResolvedVersion | Should -Be '1.6.0'
            Should -Invoke Find-PSResource -Times 1
        }

        It 'Disables the extension when no version is in the range' {
            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'PSGallery' -TargetPath $targetPath -MinimumVersion '3.0.0'

            $result.Enabled | Should -Be $false
            $result.ResolvedVersion | Should -BeNullOrEmpty
            Should -Invoke Write-Warning -Times 1 -ParameterFilter { $Message -like 'SKIPPED*' }
        }

        It 'Installs the locked version instead of the newest version in the range' {
            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'PSGallery' -TargetPath $targetPath -MinimumVersion '1.4.0' -LockedVersion '1.4.0'

            $result.Path | Should -Be (Join-Path $targetPath $name '1.4.0')
            $result.ResolvedVersion | Should -Be '1.4.0'
            Should -Invoke Find-PSResource -Times 1 -ParameterFilter { $Version -eq '1.4.0' }
        }

        It 'Does not overwrite the minimum version in the returned metadata' {
            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'PSGallery' -TargetPath $targetPath -MinimumVersion '1.4.0'

            $result.ContainsKey('Version') | Should -Be $false
        }
    }

    Context 'The repository that an extension was installed from' {
        BeforeAll {
            # Records the repository of an installed extension, as an install by ZeroFailed does
            function setInstalledRepository([string] $Path, [string] $Repository) {
                @{ repository = $Repository } | ConvertTo-Json | Set-Content -Path (Join-Path $Path '.zf-source.json')
            }
        }

        It 'Records the repository that the extension was installed from' {
            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'MyRepository' -TargetPath $targetPath

            $sourceInfo = Get-Content -Raw (Join-Path $result.Path '.zf-source.json') | ConvertFrom-Json -AsHashtable
            $sourceInfo.repository | Should -Be 'MyRepository'
            $sourceInfo.version | Should -Be $latestVersion
        }

        It 'Uses an installed extension from the same repository, ignoring case' {
            newInstalledExtension -TargetPath $targetPath -Name $name -Version '1.5.0'
            setInstalledRepository -Path (Join-Path $targetPath $name '1.5.0') -Repository 'myrepository'

            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'MyRepository' -TargetPath $targetPath

            $result.ResolvedVersion | Should -Be '1.5.0'
            Should -Invoke Find-PSResource -Times 0
        }

        It 'Uses an installed extension with no record of its repository, as earlier versions of ZeroFailed installed' {
            newInstalledExtension -TargetPath $targetPath -Name $name -Version '1.5.0'

            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'MyRepository' -TargetPath $targetPath

            $result.ResolvedVersion | Should -Be '1.5.0'
            Should -Invoke Find-PSResource -Times 0
        }

        It 'Installs the extension again when it was installed from a different repository' {
            $installedPath = Join-Path $targetPath $name '1.0.0'
            newInstalledExtension -TargetPath $targetPath -Name $name -Version '1.0.0'
            setInstalledRepository -Path $installedPath -Repository 'OtherRepository'

            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'MyRepository' -TargetPath $targetPath -Version '1.0.0'

            $result.Path | Should -Be $installedPath
            Should -Invoke Find-PSResource -Times 1 -ParameterFilter { $Version -eq '1.0.0' -and $Repository -eq 'MyRepository' }
            (Get-Content -Raw (Join-Path $installedPath '.zf-source.json') | ConvertFrom-Json).repository | Should -Be 'MyRepository'
        }

        It 'Throws, without installing, when an extension from a different repository cannot be removed' {
            $installedPath = Join-Path $targetPath $name '1.0.0'
            newInstalledExtension -TargetPath $targetPath -Name $name -Version '1.0.0'
            setInstalledRepository -Path $installedPath -Repository 'OtherRepository'
            Mock Remove-Item { Write-Error 'The file is in use' }

            { _getExtensionFromPSRepository -Name $name -PSRepository 'MyRepository' -TargetPath $targetPath -Version '1.0.0' } |
                Should -Throw "Extension $name (1.0.0) was installed from another repository, but it could not be removed: The file is in use Delete '$installedPath', then run again."
            Should -Invoke Remove-Item -Times 1 -Exactly
            Should -Invoke Find-PSResource -Times 0
        }

        It 'Uses another installed version from the same repository in place of one from a different repository' {
            newInstalledExtension -TargetPath $targetPath -Name $name -Version '1.6.0'
            setInstalledRepository -Path (Join-Path $targetPath $name '1.6.0') -Repository 'OtherRepository'
            newInstalledExtension -TargetPath $targetPath -Name $name -Version '1.5.0'
            setInstalledRepository -Path (Join-Path $targetPath $name '1.5.0') -Repository 'MyRepository'

            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'MyRepository' -TargetPath $targetPath -MinimumVersion '1.4.0'

            $result.ResolvedVersion | Should -Be '1.5.0'
            Join-Path $targetPath $name '1.6.0' | Should -Not -Exist
            Should -Invoke Find-PSResource -Times 0
        }
    }

    Context 'When the extension is not available in the repository' {
        BeforeAll {
            Mock Find-PSResource {}
        }

        It 'Disables the extension and returns no resolved version' {
            $result = _getExtensionFromPSRepository -Name $name -PSRepository 'PSGallery' -TargetPath $targetPath

            $result.Enabled | Should -Be $false
            $result.ResolvedVersion | Should -BeNullOrEmpty
            Should -Invoke Write-Warning -Times 1 -ParameterFilter { $Message -like 'SKIPPED*' }
        }
    }
}