AzureArtifactsPowerShellModuleHelper.IntegrationTests.ps1

# These are Integration tests (not unit tests).
# This means that these tests will actually reach out to the specified $FeedUrl and connect/authenticate against it.
# In order for these tests to run successfully:
# - You need to use a real Azure Artifacts $FeedUrl and a real module to import from it.
# Ideally we would mock out any external/infrastructure dependencies; I just haven't had time to yet so for now hit the real dependencies.

param
(
    [Parameter(Mandatory = $false, HelpMessage = 'The Personal Access Token to use to connect to the Azure Artifacts feed.')]
    [string] $AzureArtifactsPersonalAccessToken = 'YourPatGoesHereButDoNotCommitItToSourceControl'
)

Set-StrictMode -Version Latest
[string] $THIS_SCRIPTS_PATH = $PSCommandPath
[string] $moduleFilePathToTest = $THIS_SCRIPTS_PATH.Replace('.IntegrationTests.ps1', '.psm1') | Resolve-Path
Write-Verbose "Importing the module file '$moduleFilePathToTest' to run tests against it." -Verbose
Import-Module -Name $moduleFilePathToTest -Force
[string] $ModuleNameBeingTested = ((Split-Path -Path $moduleFilePathToTest -Leaf) -split '\.')[0] # Filename without the extension.

###########################################################
# You will need to update the following variables with info to pull a real package down from a real feed.
###########################################################
# [string] $FeedUrl = 'https://pkgs.dev.azure.com/Organization/_packaging/Feed/nuget/v2'
[string] $FeedUrl = 'https://pkgs.dev.azure.com/iqmetrix/_packaging/iqmetrix/nuget/v2'
[string] $PowerShellModuleName = 'IQ.DataCenter.ServerConfiguration'
[string] $ValidOlderModuleVersionThatExists = '1.0.40'
[string] $InvalidModuleVersionThatDoesNotExist = '1.0.99999'
[string] $ValidModulePrereleaseVersionThatExists = '1.0.66-ci20191121T214736'
[System.Security.SecureString] $SecurePersonalAccessToken = ($AzureArtifactsPersonalAccessToken | ConvertTo-SecureString -AsPlainText -Force)
[PSCredential] $Credential = New-Object System.Management.Automation.PSCredential 'Username@DoesNotMatter.com', $SecurePersonalAccessToken
[System.Version] $MinimumRequiredPowerShellGetModuleVersion = [System.Version]::Parse('2.2.1')

function Remove-PsRepository([string] $feedUrl)
{
    Get-PSRepository | Where-Object { $_.SourceLocation -ieq $feedUrl } | Unregister-PSRepository
    Get-PSRepository | Where-Object { $_.SourceLocation -ieq $feedUrl } | Should -BeNullOrEmpty
}

function Remove-PowerShellModule([string] $powerShellModuleName)
{
    Remove-Module -Name $powerShellModuleName -Force -ErrorAction SilentlyContinue
    Get-Module -Name $powerShellModuleName | Should -BeNullOrEmpty
}

function Uninstall-PowerShellModule([string] $powerShellModuleName)
{
    Remove-PowerShellModule -powerShellModuleName $powerShellModuleName
    Uninstall-Module -Name $powerShellModuleName -AllVersions -Force
    Get-Module -Name $powerShellModuleName -ListAvailable | Should -BeNullOrEmpty
}

Describe 'Registering an Azure Artifacts PS Repository' {
    Context 'When relying on retrieving the Azure Artifacts PAT from the environment variable that exists' {
        Mock Get-SecurePersonalAccessTokenFromEnvironmentVariable { return $SecurePersonalAccessToken } -ModuleName $ModuleNameBeingTested

        It 'Should register a new PS repository properly when relying in PAT from environmental variable' {
            # Arrange.
            [string] $expectedRepository = 'AzureArtifactsPowerShellFeed'
            Remove-PsRepository -feedUrl $FeedUrl

            # Act.
            [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl -Repository $expectedRepository

            # Assert.
            $repository | Should -Be $expectedRepository
            Get-PSRepository -Name $repository | Should -Not -BeNullOrEmpty
        }

        It 'Should return an existing PS repository properly when no Repository is specified' {
            # Arrange.
            [string] $expectedRepository = 'AzureArtifactsPowerShellFeed'
            Remove-PsRepository -feedUrl $FeedUrl
            Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl -Repository $expectedRepository

            # Act.
            [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl

            # Assert.
            $repository | Should -Be $expectedRepository
            Get-PSRepository -Name $repository | Should -Not -BeNullOrEmpty
        }

        It 'Should return an existing PS repository properly when a different Repository is specified' {
            # Arrange.
            [string] $expectedRepository = 'AzureArtifactsPowerShellFeed'
            Remove-PsRepository -feedUrl $FeedUrl
            Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl -Repository $expectedRepository

            # Act.
            [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl -Repository 'NameThatShouldNotEndUpInThePSRepositories'

            # Assert.
            $repository | Should -Be $expectedRepository
            Get-PSRepository -Name $repository | Should -Not -BeNullOrEmpty
        }

        It 'Should register a new PS repository properly when piping in the Feed URL' {
            # Arrange.
            [string] $expectedRepository = 'AzureArtifactsPowerShellFeed'
            Remove-PsRepository -feedUrl $FeedUrl

            # Act.
            [string] $repository = ($FeedUrl | Register-AzureArtifactsPSRepository -Repository $expectedRepository)

            # Assert.
            $repository | Should -Be $expectedRepository
            Get-PSRepository -Name $repository | Should -Not -BeNullOrEmpty
        }

        It 'Should register a new PS repository properly when piping in the all of the parameters by property name' {
            # Arrange.
            [string] $expectedRepository = 'AzureArtifactsPowerShellFeed'
            [PSCustomObject] $params = [PSCustomObject]@{
                FeedUrl = $FeedUrl
                Repository = $expectedRepository
                Credential = $Credential
                Scope = 'CurrentUser'
            }
            Remove-PsRepository -feedUrl $FeedUrl

            # Act.
            [string] $repository = ($params | Register-AzureArtifactsPSRepository)

            # Assert.
            $repository | Should -Be $expectedRepository
            Get-PSRepository -Name $repository | Should -Not -BeNullOrEmpty
        }

        It 'Should import the PowerShellGet module properly when it is not imported yet' {
            # Arrange.
            Remove-PowerShellModule -powerShellModuleName PowerShellGet

            # Act.
            Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl

            # Assert.
            $powerShellGetModuleImported = Get-Module -Name PowerShellGet
            $powerShellGetModuleImported | Should -Not -BeNullOrEmpty
            $powerShellGetModuleImported.Version | Should -BeGreaterOrEqual $MinimumRequiredPowerShellGetModuleVersion
        }

        It 'Should remove the existing too-low PowerShellGet module and import a newer version properly' {
            # Arrange.
            [System.Version] $notHighEnoughPowerShellGetModuleVersion = [System.Version]::Parse('2.0.4')
            Remove-PowerShellModule -powerShellModuleName PowerShellGet
            Install-Module -Name PowerShellGet -RequiredVersion $notHighEnoughPowerShellGetModuleVersion -Force
            Import-Module -Name PowerShellGet -RequiredVersion $notHighEnoughPowerShellGetModuleVersion -Force

            # Act.
            Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl

            # Assert.
            $powerShellGetModuleImported = Get-Module -Name PowerShellGet
            $powerShellGetModuleImported | Should -Not -BeNullOrEmpty
            $powerShellGetModuleImported.Version | Should -BeGreaterOrEqual $MinimumRequiredPowerShellGetModuleVersion
        }

        It 'Should import the PowerShellGet module properly when a high enough version is already imported' {
            # Arrange.
            Remove-PowerShellModule -powerShellModuleName PowerShellGet
            Install-Module -Name PowerShellGet -MinimumVersion $MinimumRequiredPowerShellGetModuleVersion -Force
            Import-Module -Name PowerShellGet -MinimumVersion $MinimumRequiredPowerShellGetModuleVersion

            # Act.
            Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl

            # Assert.
            $powerShellGetModuleImported = Get-Module -Name PowerShellGet
            $powerShellGetModuleImported | Should -Not -BeNullOrEmpty
            $powerShellGetModuleImported.Version | Should -BeGreaterOrEqual $MinimumRequiredPowerShellGetModuleVersion
        }
    }

    It 'Should register a new PS repository properly when passing in a valid Credential' {
        # Arrange.
        [string] $expectedRepository = 'AzureArtifactsPowerShellFeed'
        Remove-PsRepository -feedUrl $FeedUrl

        # Act.
        [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl -Repository $expectedRepository -Credential $Credential

        # Assert.
        $repository | Should -Be $expectedRepository
        Get-PSRepository -Name $repository | Should -Not -BeNullOrEmpty
    }

    Context 'When connecting to a feed without using a Credential' {
        Mock Get-AzureArtifactsCredential { return $null } -ModuleName $ModuleNameBeingTested

        It 'Should not throw an error when credentials are not found. (Assumes the FeedUrl allows you to register it without a Credential)' {
            # Arrange.
            [string] $expectedRepository = 'AzureArtifactsPowerShellFeed'
            Remove-PsRepository -feedUrl $FeedUrl

            # Act.
            [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl -Repository $expectedRepository

            # Assert.
            $repository | Should -Be $expectedRepository
            Get-PSRepository -Name $repository | Should -Not -BeNullOrEmpty
        }
    }
}

# Describe 'Importing a PowerShell module from Azure Artifacts' {
# It 'Should import the module properly' {
# # Arrange.
# [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl
# [ScriptBlock] $action = { Import-AzureArtifactsModule -Name $PowerShellModuleName -Repository $repository }
# Remove-PowerShellModule -powerShellModuleName $PowerShellModuleName

# # Act and Assert.
# $action | Should -Not -Throw
# Get-Module -Name $PowerShellModuleName | Should -Not -BeNullOrEmpty
# }

# It 'Should import the module properly when forced' {
# # Arrange.
# [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl
# [ScriptBlock] $action = { Import-AzureArtifactsModule -Name $PowerShellModuleName -Repository $repository -Force }
# Remove-PowerShellModule -powerShellModuleName $PowerShellModuleName

# # Act and Assert.
# $action | Should -Not -Throw
# Get-Module -Name $PowerShellModuleName | Should -Not -BeNullOrEmpty
# }

# It 'Should import the module properly when a specific version is requested' {
# # Arrange.
# [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl
# [ScriptBlock] $action = { Import-AzureArtifactsModule -Name $PowerShellModuleName -Repository $repository -Version $ValidOlderModuleVersionThatExists }
# Remove-PowerShellModule -powerShellModuleName $PowerShellModuleName

# # Act and Assert.
# $action | Should -Not -Throw
# $module = Get-Module -Name $PowerShellModuleName
# $module | Should -Not -BeNullOrEmpty
# $module.Version | Should -Be $ValidOlderModuleVersionThatExists
# }

# # Could not get this one to work, as it complains that the module is in use so it's not able to uninstall it to do a proper test.
# # It 'Should throw an error when trying to import a version that does not exist and no different version exists' {
# # # Arrange.
# # [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl
# # [ScriptBlock] $action = { Import-AzureArtifactsModule -Name $PowerShellModuleName -Repository $repository -Version $InvalidModuleVersionThatDoesNotExist }
# # Remove-PowerShellModule -powerShellModuleName $PowerShellModuleName
# # Uninstall-Module -Name $PowerShellModuleName -Force -AllVersions
# # Write-Host "Versions: " + (Get-Module -Name $PowerShellModuleName -ListAvailable | Format-Table | Out-String)
# # Get-Module -Name $PowerShellModuleName -ListAvailable | Should -BeNullOrEmpty

# # # Act and Assert.
# # $action | Should -Not -Throw
# # }

# It 'Should write an error and continue when trying to import a version that does not exist, but a different version exists' {
# # Arrange.
# [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl
# Import-AzureArtifactsModule -Name $PowerShellModuleName -Repository $repository
# Get-Module -Name $PowerShellModuleName -ListAvailable | Should -Not -BeNullOrEmpty

# # Act
# Import-AzureArtifactsModule -Name $PowerShellModuleName -Repository $repository -Version $InvalidModuleVersionThatDoesNotExist -ErrorAction SilentlyContinue -ErrorVariable err

# # Assert.
# $err.Count | Should -BeGreaterThan 0
# [string] $errors = $err | ForEach-Object { $_.ToString() }
# $errors | Should -Match 'is already installed and will be imported instead.'
# }

# It 'Should throw an error when trying to import a module that does not exist' {
# # Arrange.
# [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl
# [ScriptBlock] $action = { Import-AzureArtifactsModule -Name 'InvalidModuleName' -Repository $repository }

# # Act and Assert.
# $action | Should -Throw "The PowerShell module 'InvalidModuleName' could not be found in the PSRepository"
# }

# It 'Should write an error and continue when an invalid Repository is specified, but the module is already installed' {
# # Arrange.
# [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl
# Import-AzureArtifactsModule -Name $PowerShellModuleName -Repository $repository
# Get-Module -Name $PowerShellModuleName -ListAvailable | Should -Not -BeNullOrEmpty

# # Act.
# Import-AzureArtifactsModule -Name $PowerShellModuleName -Repository 'InvalidRepositoryName' -ErrorAction SilentlyContinue -ErrorVariable err

# # Act and Assert.
# $err.Count | Should -BeGreaterThan 0
# [string] $errors = $err | ForEach-Object { $_.ToString() }
# $errors | Should -Match "Version '.+?' is installed on computer '.+?' though so it will be used.*"
# }

# It 'Should throw an error if the Credential is invalid' {
# # Arrange.
# [System.Security.SecureString] $invalidPat = 'InvalidPat' | ConvertTo-SecureString -AsPlainText -Force
# [PSCredential] $invalidCredential = New-Object System.Management.Automation.PSCredential 'Username@DoesNotMatter.com', $invalidPat
# [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl

# # Act.
# Import-AzureArtifactsModule -Name $PowerShellModuleName -Repository $repository -Credential $invalidCredential -ErrorAction SilentlyContinue -ErrorVariable err

# # Assert.
# $err.Count | Should -BeGreaterThan 0
# [string] $errors = $err | ForEach-Object { $_.ToString() }
# $errors | Should -Match "Perhaps the credentials used are not valid."
# }

# It 'Should not import module Prerelease versions when the Prerelease switch is not provided' {
# # Arrange.
# [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl
# [ScriptBlock] $action = { Import-AzureArtifactsModule -Name $PowerShellModuleName -Repository $repository -Version $ValidModulePrereleaseVersionThatExists }
# Remove-PowerShellModule -powerShellModuleName $PowerShellModuleName

# # Act and Assert.
# $action | Should -Throw "The '-AllowPrerelease' parameter must be specified when using the Prerelease string"
# Get-Module -Name $PowerShellModuleName | Should -BeNullOrEmpty
# }

# It 'Should import module Prerelease versions properly' {
# # Arrange.
# [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl
# [ScriptBlock] $action = { Import-AzureArtifactsModule -Name $PowerShellModuleName -Repository $repository -Version $ValidModulePrereleaseVersionThatExists -AllowPrerelease }
# Remove-PowerShellModule -powerShellModuleName $PowerShellModuleName

# # PowerShell is weird about the way it supports prerelease versions.
# # The directory it installs to and the version it gives it is just the version with the prerelease portion removed.
# # So we need to strip off the prerelease portion of the version number. i.e. what comes after the hyphen.
# [string] $prereleaseVersionsStablePortion = ($ValidModulePrereleaseVersionThatExists -split '-')[0]

# # Act and Assert.
# $action | Should -Not -Throw
# $module = Get-Module -Name $PowerShellModuleName
# $module | Should -Not -BeNullOrEmpty
# $module.Version | Should -Be $prereleaseVersionsStablePortion
# }

# It 'Should import the module properly when piping in the Repository Name' {
# # Arrange.
# [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl
# [ScriptBlock] $action = {
# $repository | Import-AzureArtifactsModule -Name $PowerShellModuleName
# }
# Remove-PowerShellModule -powerShellModuleName $PowerShellModuleName

# # Act and Assert.
# $action | Should -Not -Throw
# Get-Module -Name $PowerShellModuleName | Should -Not -BeNullOrEmpty
# }

# It 'Should import the module properly when piping in all of the parameters by property name' {
# # Arrange.
# [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl
# [PSCustomObject] $params = [PSCustomObject]@{
# Name = $PowerShellModuleName
# Version = $null
# AllowPrerelease = $false
# Repository = $repository
# Credential = $Credential
# Force = $false
# Scope = 'CurrentUser'
# }
# [ScriptBlock] $action = {
# $params | Import-AzureArtifactsModule
# }
# Remove-PowerShellModule -powerShellModuleName $PowerShellModuleName

# # Act and Assert.
# $action | Should -Not -Throw
# Get-Module -Name $PowerShellModuleName | Should -Not -BeNullOrEmpty
# }
# }

Describe 'Finding a PowerShell module from Azure Artifacts' {
    Context 'When relying on retrieving the Azure Artifacts PAT from the environment variable that exists' {
        Mock Get-SecurePersonalAccessTokenFromEnvironmentVariable { return $SecurePersonalAccessToken } -ModuleName $ModuleNameBeingTested

        It 'Should find the module properly' {
            # Arrange.
            [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl
            [ScriptBlock] $action = { Find-AzureArtifactsModule -Name $PowerShellModuleName -Repository $repository }

            # Act and Assert.
            $action | Should -Not -BeNullOrEmpty
        }
    }

    Context 'When connecting to a feed without using a Credential' {
        Mock Get-AzureArtifactsCredential { return $null } -ModuleName $ModuleNameBeingTested

        It 'Should throw an exception' {
            # Arrange.
            [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl
            [scriptblock] $action = { Find-AzureArtifactsModule -Name $PowerShellModuleName -Repository $repository -ErrorAction Stop }

            # Act and Assert.
            $action | Should -Throw "Unable to find repository"
        }
    }

    It 'Should throw an exception when the Credential is invalid' {
        # Arrange.
        [System.Security.SecureString] $invalidPat = 'InvalidPat' | ConvertTo-SecureString -AsPlainText -Force
        [PSCredential] $invalidCredential = New-Object System.Management.Automation.PSCredential 'Username@DoesNotMatter.com', $invalidPat
        [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl
        [scriptblock] $action = { Find-AzureArtifactsModule -Name $PowerShellModuleName -Repository $repository -Credential $invalidCredential -ErrorAction Stop }

        # Act and Assert.
        $action | Should -Throw "No match was found for the specified search criteria and module name"
    }
}

Describe 'Installing a PowerShell module from Azure Artifacts' {
    Context 'When relying on retrieving the Azure Artifacts PAT from the environment variable that exists' {
        Mock Get-SecurePersonalAccessTokenFromEnvironmentVariable { return $SecurePersonalAccessToken } -ModuleName $ModuleNameBeingTested

        It 'Should install the module properly' {
            # Arrange.
            [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl
            [ScriptBlock] $action = { Install-AzureArtifactsModule -Name $PowerShellModuleName -Repository $repository -ErrorAction Stop }
            Uninstall-PowerShellModule -powerShellModuleName $PowerShellModuleName -ErrorAction 'SilentlyContinue'

            # Act and Assert.
            $action | Should -Not -Throw
            Get-Module -Name $PowerShellModuleName -ListAvailable | Should -Not -BeNullOrEmpty
        }
    }
}

Describe 'Updating a PowerShell module from Azure Artifacts' {
    Context 'When relying on retrieving the Azure Artifacts PAT from the environment variable that exists' {
        Mock Get-SecurePersonalAccessTokenFromEnvironmentVariable { return $SecurePersonalAccessToken } -ModuleName $ModuleNameBeingTested

        It 'Should update the module properly when already installed, resulting in 2 versions being installed' {
            # Arrange.
            [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl
            [ScriptBlock] $action = { Update-AzureArtifactsModule -Name $PowerShellModuleName -ErrorAction Stop }
            Uninstall-PowerShellModule -powerShellModuleName $PowerShellModuleName -ErrorAction 'SilentlyContinue'
            Install-AzureArtifactsModule -Name $PowerShellModuleName -Repository $repository -RequiredVersion $ValidOlderModuleVersionThatExists

            # Act and Assert.
            $action | Should -Not -Throw
            $modulesInstalled = Get-Module -Name $PowerShellModuleName -ListAvailable
            $modulesInstalled | Should -Not -BeNullOrEmpty
            $modulesInstalled.Count | Should -BeGreaterThan 1
        }
    }
}

Describe 'Installing-and-Updating a PowerShell module from Azure Artifacts' {
    Context 'When relying on retrieving the Azure Artifacts PAT from the environment variable that exists' {
        Mock Get-SecurePersonalAccessTokenFromEnvironmentVariable { return $SecurePersonalAccessToken } -ModuleName $ModuleNameBeingTested

        It 'Should install the module properly' {
            # Arrange.
            [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl
            [ScriptBlock] $action = { Install-AndUpdateAzureArtifactsModule -Name $PowerShellModuleName -Repository $repository -ErrorAction Stop }
            Uninstall-PowerShellModule -powerShellModuleName $PowerShellModuleName -ErrorAction 'SilentlyContinue'

            # Act and Assert.
            $action | Should -Not -Throw
            Get-Module -Name $PowerShellModuleName -ListAvailable | Should -Not -BeNullOrEmpty
        }

        It 'Should update the module properly when already installed, resulting in 2 versions being installed' {
            # Arrange.
            [string] $repository = Register-AzureArtifactsPSRepository -FeedUrl $FeedUrl
            [ScriptBlock] $action = { Install-AndUpdateAzureArtifactsModule -Name $PowerShellModuleName -Repository $repository -ErrorAction Stop }
            Uninstall-PowerShellModule -powerShellModuleName $PowerShellModuleName -ErrorAction 'SilentlyContinue'
            Install-AzureArtifactsModule -Name $PowerShellModuleName -Repository $repository -RequiredVersion $ValidOlderModuleVersionThatExists

            # Act and Assert.
            $action | Should -Not -Throw
            $modulesInstalled = Get-Module -Name $PowerShellModuleName -ListAvailable
            $modulesInstalled | Should -Not -BeNullOrEmpty
            $modulesInstalled.Count | Should -BeGreaterThan 1
        }
    }
}