functions/_resolveExtensionVersionRanges.Tests.ps1
|
# <copyright file="_resolveExtensionVersionRanges.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) '_getExtensionVersionRange.ps1') . (Join-Path (Split-Path -Parent $PSCommandPath) '_getCompleteExtensionVersion.ps1') . (Join-Path (Split-Path -Parent $PSCommandPath) '_testExtensionVersionInRange.ps1') . (Join-Path (Split-Path -Parent $PSCommandPath) '_getVersionFromGitTag.ps1') . (Join-Path (Split-Path -Parent $PSCommandPath) '_getNormalizedGitRepositoryUrl.ps1') # Defined so that it can be mocked function Register-ExtensionAndDependencies { param($ExtensionConfig, $TargetPath, $RequiredBy, $LockedExtensions) } Set-StrictMode -Version Latest # Creates the metadata of a registered extension, as produced by Register-ExtensionAndDependencies function newRegisteredExtension { param ( [string] $Name = 'Common', [string] $Type = 'psrepository', [string] $MinimumVersion, [string] $Requested, [string] $Resolved, [string] $ResolvedRef, [string] $RequiredBy ) $lockEntry = [ordered]@{ source = [ordered]@{ type = $Type } requested = $MinimumVersion ? (_getExtensionVersionRange -MinimumVersion $MinimumVersion).Text : ($Requested ? $Requested : $null) resolved = $Resolved ? $Resolved : $null direct = !$RequiredBy requiredBy = $RequiredBy ? @($RequiredBy) : @() } if ($ResolvedRef) { $lockEntry.resolvedRef = $ResolvedRef } $extension = @{ Name = $Name Enabled = $true Path = "$Name-$Resolved$ResolvedRef" LockEntry = $lockEntry } if ($MinimumVersion) { $extension.MinimumVersion = $MinimumVersion } $extension } } Describe '_resolveExtensionVersionRanges' { BeforeAll { $targetPath = 'TestDrive:/extensions' Mock Write-Host {} Mock Write-Warning {} # By default, no other version is available Mock Register-ExtensionAndDependencies {} } It 'Ignores an extension that was not installed because it does not match the lock file' { $mismatch = newRegisteredExtension -RequiredBy 'DotNet' -MinimumVersion '1.6' $mismatch.Enabled = $false $mismatch.LockMismatch = $true $extensions = @( $mismatch newRegisteredExtension -RequiredBy 'PowerShell' -MinimumVersion '1.4' -Resolved '1.5.0' ) $result = @(_resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath -LockedExtensions @{}) $result[0] | Should -Be $mismatch Should -Invoke Register-ExtensionAndDependencies -Times 0 } It 'Uses an occurrence from the configuration in place of an earlier one that does not match the lock file, with a warning' { $mismatch = newRegisteredExtension -RequiredBy 'PowerShell' -MinimumVersion '1.5' $mismatch.Enabled = $false $mismatch.LockMismatch = $true $extensions = @( newRegisteredExtension -Name 'PowerShell' -Requested '1.0.0' -Resolved '1.0.0' $mismatch newRegisteredExtension -Requested '1.3.0' -Resolved '1.3.0' ) $result = @(_resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath -LockedExtensions @{}) @($result | ForEach-Object { $_.Path }) | Should -Be @('PowerShell-1.0.0', 'Common-1.3.0') $result[1].LockEntry.requiredBy | Should -Be @('PowerShell') Should -Invoke Register-ExtensionAndDependencies -Times 0 Should -Invoke Write-Warning -Times 1 -ParameterFilter { $Message -eq "Extension 'Common' 1.3.0 is used because it is in the configuration, but 'PowerShell' requires [1.5.0,2.0.0)" } } It 'Ignores an occurrence from the configuration that does not match the lock file' { $mismatch = newRegisteredExtension -Requested '1.3.0' $mismatch.Enabled = $false $mismatch.LockMismatch = $true $extensions = @( newRegisteredExtension -Name 'PowerShell' -Requested '1.0.0' -Resolved '1.0.0' newRegisteredExtension -RequiredBy 'PowerShell' -MinimumVersion '1.5' -Resolved '1.6.0' $mismatch ) $result = @(_resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath -LockedExtensions @{}) # The occurrence from the configuration is first, so that the lock file check reports it $result.Count | Should -Be 3 $result[1] | Should -Be $mismatch Should -Invoke Write-Warning -Times 0 } It 'Keeps only the first occurrence when it satisfies every requirement, and records every extension that requires it' { $extensions = @( newRegisteredExtension -RequiredBy 'DotNet' -MinimumVersion '1.2' -Resolved '1.6.0' newRegisteredExtension -RequiredBy 'PowerShell' -MinimumVersion '1.5' -Resolved '1.6.0' ) $result = @(_resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath) $result.Count | Should -Be 1 [object]::ReferenceEquals($result[0], $extensions[0]) | Should -Be $true $result[0].LockEntry.requiredBy | Should -Be @('DotNet', 'PowerShell') Should -Invoke Write-Warning -Times 0 } It 'Discards the dependencies that only a discarded occurrence requires' { $usedDependency = newRegisteredExtension -Name 'Shared' -RequiredBy 'Common' -Requested 'main' -Resolved 'commit' $discardedDependency = newRegisteredExtension -Name 'Old' -RequiredBy 'Common' -Requested 'main' -Resolved 'commit' $nestedDependency = newRegisteredExtension -Name 'Nested' -RequiredBy 'Old' -Requested 'main' -Resolved 'commit' $discardedDependency.dependencies = @($nestedDependency) $old = newRegisteredExtension -RequiredBy 'DotNet' -MinimumVersion '1.2' -Resolved '1.3.0' $old.dependencies = @($discardedDependency) $new = newRegisteredExtension -RequiredBy 'PowerShell' -MinimumVersion '1.5' -Resolved '1.6.0' $new.dependencies = @($usedDependency) $extensions = @( newRegisteredExtension -Name 'DotNet' -Requested '1.0.0' -Resolved '1.0.0' $old $discardedDependency $nestedDependency newRegisteredExtension -Name 'PowerShell' -Requested '1.0.0' -Resolved '1.0.0' $new $usedDependency ) $result = @(_resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath) @($result | ForEach-Object { $_.Name }) | Should -Be @('DotNet', 'Common', 'PowerShell', 'Shared') $result[1].Path | Should -Be 'Common-1.6.0' } It 'Keeps a dependency of a discarded occurrence when it is the version chosen for every extension that requires it' { $chosenDependency = newRegisteredExtension -Name 'Shared' -RequiredBy 'Common' -MinimumVersion '1.5' -Resolved '1.6.0' $otherDependency = newRegisteredExtension -Name 'Shared' -RequiredBy 'PowerShell' -MinimumVersion '1.0' -Resolved '1.0.0' $old = newRegisteredExtension -RequiredBy 'DotNet' -MinimumVersion '1.2' -Resolved '1.3.0' $old.dependencies = @($chosenDependency) $extensions = @( newRegisteredExtension -Name 'DotNet' -Requested '1.0.0' -Resolved '1.0.0' $old $chosenDependency newRegisteredExtension -Name 'PowerShell' -Requested '1.0.0' -Resolved '1.0.0' newRegisteredExtension -RequiredBy 'PowerShell' -MinimumVersion '1.5' -Resolved '1.6.0' $otherDependency ) $result = @(_resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath) @($result | ForEach-Object { $_.Path }) | Should -Be @('DotNet-1.0.0', 'Common-1.6.0', 'Shared-1.6.0', 'PowerShell-1.0.0') } It 'Uses the newest occurrence that satisfies every requirement, in place of the first one' { $extensions = @( newRegisteredExtension -Name 'DotNet' -Requested '1.0.0' -Resolved '1.0.0' newRegisteredExtension -RequiredBy 'DotNet' -MinimumVersion '1.2' -Resolved '1.3.0' newRegisteredExtension -Name 'PowerShell' -Requested '1.0.0' -Resolved '1.0.0' newRegisteredExtension -RequiredBy 'PowerShell' -MinimumVersion '1.5' -Resolved '1.6.0' ) $result = @(_resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath) @($result | ForEach-Object { $_.Path }) | Should -Be @('DotNet-1.0.0', 'Common-1.6.0', 'PowerShell-1.0.0') $result[1].LockEntry.requiredBy | Should -Be @('PowerShell', 'DotNet') } It 'Uses the release tags of git extensions' { $extensions = @( newRegisteredExtension -Type 'git' -RequiredBy 'DotNet' -MinimumVersion '1.2' -Resolved 'commit1' -ResolvedRef 'refs/tags/1.3.0' newRegisteredExtension -Type 'git' -RequiredBy 'PowerShell' -MinimumVersion '1.5' -Resolved 'commit2' -ResolvedRef 'refs/tags/v1.6.0' ) $result = @(_resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath) $result[0].LockEntry.resolvedRef | Should -Be 'refs/tags/v1.6.0' } It 'Uses a release in the range in place of a git branch, whose version is unknown' { $extensions = @( newRegisteredExtension -Type 'git' -RequiredBy 'DotNet' -Requested 'main' -Resolved 'commit1' newRegisteredExtension -Type 'git' -RequiredBy 'PowerShell' -MinimumVersion '1.5' -Resolved 'commit2' -ResolvedRef 'refs/tags/1.6.0' ) $result = @(_resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath) $result[0].LockEntry.resolvedRef | Should -Be 'refs/tags/1.6.0' } It 'Keeps an occurrence from the configuration, with a warning' { $extensions = @( newRegisteredExtension -Requested '1.3.0' -Resolved '1.3.0' newRegisteredExtension -RequiredBy 'PowerShell' -MinimumVersion '1.5' -Resolved '1.6.0' ) $result = @(_resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath) $result[0].Path | Should -Be 'Common-1.3.0' Should -Invoke Write-Warning -Times 1 -ParameterFilter { $Message -eq "Extension 'Common' 1.3.0 is used because it is in the configuration, but 'PowerShell' requires [1.5.0,2.0.0)" } } It 'Uses an occurrence from the configuration that is not first, with a warning' { $extensions = @( newRegisteredExtension -Name 'PowerShell' -Requested '1.0.0' -Resolved '1.0.0' newRegisteredExtension -RequiredBy 'PowerShell' -MinimumVersion '1.5' -Resolved '1.6.0' newRegisteredExtension -Requested '1.3.0' -Resolved '1.3.0' ) $result = @(_resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath) @($result | ForEach-Object { $_.Path }) | Should -Be @('PowerShell-1.0.0', 'Common-1.3.0') $result[1].LockEntry.direct | Should -Be $true $result[1].LockEntry.requiredBy | Should -Be @('PowerShell') Should -Invoke Write-Warning -Times 1 -ParameterFilter { $Message -eq "Extension 'Common' 1.3.0 is used because it is in the configuration, but 'PowerShell' requires [1.5.0,2.0.0)" } } It 'Uses an occurrence from the configuration that is not first, even when the first one satisfies every requirement' { $extensions = @( newRegisteredExtension -RequiredBy 'PowerShell' -MinimumVersion '1.5' -Resolved '1.7.0' newRegisteredExtension -MinimumVersion '1.5' -Resolved '1.6.0' ) $result = @(_resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath) $result[0].Path | Should -Be 'Common-1.6.0' Should -Invoke Write-Warning -Times 0 } It 'Uses a version without a pre-release label in place of a pre-release that another extension does not include' { $preReleaseExtension = newRegisteredExtension -RequiredBy 'DotNet' -MinimumVersion '1.4' -Resolved '1.6.0-beta.1' $preReleaseExtension.PreRelease = $true $extensions = @( $preReleaseExtension newRegisteredExtension -RequiredBy 'PowerShell' -MinimumVersion '1.4' -Resolved '1.5.0' ) $result = @(_resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath) $result[0].Path | Should -Be 'Common-1.5.0' } It 'Keeps a pre-release when every extension includes pre-release versions' { $extensions = @( newRegisteredExtension -RequiredBy 'DotNet' -MinimumVersion '1.4' -Resolved '1.6.0-beta.1' newRegisteredExtension -RequiredBy 'PowerShell' -MinimumVersion '1.4' -Resolved '1.5.0' ) $extensions | ForEach-Object { $_.PreRelease = $true } $result = @(_resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath) $result[0].Path | Should -Be 'Common-1.6.0-beta.1' } It 'Throws when only a pre-release satisfies every range, one extension does not include pre-release versions, and no other version is available' { $preReleaseExtension = newRegisteredExtension -RequiredBy 'DotNet' -MinimumVersion '1.6' -Resolved '1.7.0-beta.1' $preReleaseExtension.PreRelease = $true $extensions = @( $preReleaseExtension newRegisteredExtension -RequiredBy 'PowerShell' -MinimumVersion '1.4' -Resolved '1.5.0' ) $expectedMessage = "No version of extension 'Common' satisfies every extension that requires it:`n" + " - 'DotNet' requires [1.6.0,2.0.0), including pre-release versions (resolved to 1.7.0-beta.1)`n" + " - 'PowerShell' requires [1.4.0,2.0.0) (resolved to 1.5.0)`n" { _resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath } | Should -Throw -ExpectedMessage "$([WildcardPattern]::Escape($expectedMessage))*" } Context 'When no registered version satisfies every range, but the ranges overlap' { BeforeEach { # A pre-release satisfies the first range, and an installed copy satisfies the second, but neither # satisfies both $preReleaseExtension = newRegisteredExtension -RequiredBy 'DotNet' -MinimumVersion '1.6' -Resolved '1.7.0-beta.1' $preReleaseExtension.PreRelease = $true $extensions = @( $preReleaseExtension newRegisteredExtension -RequiredBy 'PowerShell' -MinimumVersion '1.4' -Resolved '1.5.0' ) $extensions | ForEach-Object { $_.LockEntry.source.repository = 'PSGallery' } $lockedExtensions = @{ Common = @{ resolved = '1.6.0' } } # The repository has a version in both ranges, which depends on another extension Mock Register-ExtensionAndDependencies { $installed = newRegisteredExtension -RequiredBy $RequiredBy -MinimumVersion $ExtensionConfig.MinimumVersion -Resolved '1.6.0' $installed.LockEntry.source.repository = $ExtensionConfig.PSRepository $installed newRegisteredExtension -Name 'Dependency' -RequiredBy 'Common' -MinimumVersion '1.0' -Resolved '1.0.0' } } It 'Installs the newest version in every range, from the same source, without pre-release versions' { $result = @(_resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath -LockedExtensions $lockedExtensions) $result[0].Path | Should -Be 'Common-1.6.0' Should -Invoke Register-ExtensionAndDependencies -Times 1 -ParameterFilter { $ExtensionConfig.Name -eq 'Common' -and $ExtensionConfig.MinimumVersion -eq '1.6.0' -and $ExtensionConfig.PSRepository -eq 'PSGallery' -and !$ExtensionConfig.ContainsKey('PreRelease') -and $TargetPath -eq 'TestDrive:/extensions' -and $RequiredBy -eq 'DotNet' -and $LockedExtensions.Common.resolved -eq '1.6.0' } } It 'Keeps only the installed version, and adds its dependencies' { $result = @(_resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath) @($result | ForEach-Object { $_.Name }) | Should -Be @('Common', 'Dependency') $result[0].Path | Should -Be 'Common-1.6.0' $result[0].LockEntry.requiredBy | Should -Be @('DotNet', 'PowerShell') } It 'Installs from a git repository' { $extensions | ForEach-Object { $_.LockEntry.source = [ordered]@{ type = 'git'; folderPath = 'module'; repository = 'https://github.com/zerofailed/Common' } $_.LockEntry.resolvedRef = "refs/tags/$($_.LockEntry.resolved)" } Mock Register-ExtensionAndDependencies { newRegisteredExtension -Type 'git' -RequiredBy $RequiredBy -MinimumVersion $ExtensionConfig.MinimumVersion -Resolved 'commit' -ResolvedRef 'refs/tags/1.6.0' } $result = @(_resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath) $result[0].LockEntry.resolvedRef | Should -Be 'refs/tags/1.6.0' Should -Invoke Register-ExtensionAndDependencies -Times 1 -ParameterFilter { $ExtensionConfig.GitRepository -eq 'https://github.com/zerofailed/Common' -and $ExtensionConfig.GitRepositoryFolderPath -eq 'module' -and !$ExtensionConfig.ContainsKey('PSRepository') } } It 'Installs from a git repository that is referenced with and without a trailing .git' { $extensions | ForEach-Object { $_.LockEntry.source = [ordered]@{ type = 'git'; folderPath = 'module'; repository = 'https://github.com/zerofailed/Common' } $_.LockEntry.resolvedRef = "refs/tags/$($_.LockEntry.resolved)" } $extensions[1].LockEntry.source.repository = 'https://github.com/zerofailed/Common.git' Mock Register-ExtensionAndDependencies { newRegisteredExtension -Type 'git' -RequiredBy $RequiredBy -MinimumVersion $ExtensionConfig.MinimumVersion -Resolved 'commit' -ResolvedRef 'refs/tags/1.6.0' } $result = @(_resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath) $result[0].LockEntry.resolvedRef | Should -Be 'refs/tags/1.6.0' Should -Invoke Register-ExtensionAndDependencies -Times 1 } It 'Throws, with the reason, when the version cannot be installed' { Mock Register-ExtensionAndDependencies { throw 'No release matches' } { _resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath } | Should -Throw "No version of extension 'Common'*A version in every range could not be installed: No release matches*" } It 'Throws when no version in every range is available' { Mock Register-ExtensionAndDependencies { @{ Name = 'Common'; Enabled = $false } } { _resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath } | Should -Throw "No version of extension 'Common'*" } It 'Does not install a version when the extensions require it from different sources' { $extensions[1].LockEntry.source.repository = 'MyRepository' { _resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath } | Should -Throw "No version of extension 'Common'*" Should -Invoke Register-ExtensionAndDependencies -Times 0 } } It 'Throws when no version satisfies every requirement' { $extensions = @( newRegisteredExtension -RequiredBy 'DotNet' -MinimumVersion '1.2' -Resolved '1.6.0' newRegisteredExtension -RequiredBy 'PowerShell' -MinimumVersion '2.0' -Resolved '2.1.0' ) $expectedMessage = "No version of extension 'Common' satisfies every extension that requires it:`n" + " - 'DotNet' requires [1.2.0,2.0.0) (resolved to 1.6.0)`n" + " - 'PowerShell' requires [2.0.0,3.0.0) (resolved to 2.1.0)`n" + "They may need different major versions. To use one version for all of them, add 'Common' to the configuration." { _resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath } | Should -Throw -ExpectedMessage ([WildcardPattern]::Escape($expectedMessage)) } It 'Includes exact versions in the requirements' { $extensions = @( newRegisteredExtension -Type 'git' -RequiredBy 'DotNet' -Requested 'refs/tags/1.3.0' -Resolved 'commit1' newRegisteredExtension -Type 'git' -RequiredBy 'PowerShell' -MinimumVersion '1.5' -Resolved 'commit2' -ResolvedRef 'refs/tags/1.6.0' ) { _resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath } | Should -Throw "No version of extension 'Common'*" } It 'Includes exact module versions that have fewer than 3 numeric parts in the requirements' { $extensions = @( newRegisteredExtension -RequiredBy 'DotNet' -Requested '1.4' -Resolved '1.4.0' newRegisteredExtension -RequiredBy 'PowerShell' -MinimumVersion '1.5' -Resolved '1.6.0' ) { _resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath } | Should -Throw "No version of extension 'Common'*'DotNet' requires 1.4 (resolved to 1.4.0)*" Should -Invoke Register-ExtensionAndDependencies -Times 0 } It 'Ignores extensions that are not required with a minimum version' { $extensions = @( newRegisteredExtension -RequiredBy 'DotNet' -Requested '1.0.0' -Resolved '1.0.0' newRegisteredExtension -RequiredBy 'PowerShell' -Requested '2.0.0' -Resolved '2.0.0' ) $result = @(_resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath) $result[0].Path | Should -Be 'Common-1.0.0' Should -Invoke Write-Warning -Times 0 } It 'Ignores extensions that were not registered' { $extensions = @( @{ Name = 'Common'; Enabled = $false } newRegisteredExtension -RequiredBy 'PowerShell' -MinimumVersion '1.5' -Resolved '1.6.0' ) $result = @(_resolveExtensionVersionRanges -Extensions $extensions -TargetPath $targetPath) $result[0].ContainsKey('LockEntry') | Should -Be $false } } |