Tests/00-PSModule.tests.ps1

<#
.SYNOPSIS
This is a set of standard tests to ensure a powershell module is valid
.NOTES
This is designed to autodetect the powershell module, and follows the following discovery order:
1. If pester was invoked
#>

[CmdletBinding(DefaultParameterSetName='Search')]
param (
    #Specify an alternate location for the Powershell Module. This is useful when testing a build in another directory
    [Parameter(ParameterSetName='Explicit')][string]$ModuleManifestPath,
    #Specify a directory to search for Powershell Module manifests, used for module autodetection. NOTE: This will use the manifest closest to the root of the search path
    [Parameter(ParameterSetName='Search')][string]$ModuleSearchPath = (Get-Location),
    #How far up the directory tree to recursively search for module manifests.
    [Parameter(ParameterSetName='Search')][int]$Depth=0
)

Describe 'Powershell Module' {
    BeforeAll {
        if ($ENV:PowerCDModuleManifest) {$ModuleManifestPath = $ENV:PowerCDModuleManifest}
        if (-not $ModuleManifestPath) {
            #If we are in a folder named 'Tests', search the parent folder
            if ((get-location | split-path -leaf) -match '^Tests?$') {
                $Depth=1
            }

            $CurrentModuleSearchPath = $ModuleSearchPath
            $i=0
            while ($i -le $Depth) {
                write-verbose "Searching for Powershell Module Manifests in $CurrentModuleSearchPath"
                $ModuleManifestPath = BuildHelpers\Get-PSModuleManifest -WarningAction SilentlyContinue $CurrentModuleSearchPath
                if ($ModuleManifestPath) {break}
                $CurrentModuleSearchPath = Split-Path $CurrentModuleSearchPath
                $i++
            }
        }

        if (-not $ModuleManifestPath) {throw "No powershell module manifest was detected. Please set your working directory to the Powershell Module Folder before starting this test. You may specify one explicitly to this Pester test with the -ModuleManifestPath option"}

        #If an alternate module root was specified, set that to our running directory.
        $ModuleDirectory = Split-Path $ModuleManifestPath

        #Set our path to the detected module directory, if required
        if ($ModuleDirectory -and $ModuleDirectory -ne $pwd.path) {Push-Location $ModuleDirectory}

        #Getting a FileInfo object because it has more metadata then a simple string path.
        $SCRIPT:ModuleManifestFile = Get-Item $ModuleManifestPath -ErrorAction Stop
    }

    $ModuleName = $ModuleManifestFile.basename
    Context ($ModuleName) {
        It 'Has a valid Module Manifest' {
            if ($PSEdition -eq 'Core') {
                $Script:Manifest = Test-ModuleManifest $ModuleManifestFile -Verbose:$false
            } else {
                #Copy the Module Manifest to a temp file for testing. This fixes a bug where
                #Test-ModuleManifest caches the first result, thus not catching changes if subsequent tests are run
                $TempModuleManifestPath = [IO.Path]::GetTempFileName() + '.psd1'
                copy-item $ModuleManifestFile $TempModuleManifestPath
                $Script:Manifest = Test-ModuleManifest $TempModuleManifestPath -Verbose:$false
                remove-item $TempModuleManifestPath -verbose:$false
            }
            $Manifest | Should -Not -BeNullOrEmpty
        }

        It 'Has a valid root module' {
            Test-Path $Manifest.RootModule -Type Leaf | Should Be $true
        }

        It 'Has a valid folder structure (ModuleName\Manifest or ModuleName\Version\Manifest)' {
            $moduleDirectoryErrorMessage = "Module directory structure doesn't match either $ModuleName or $moduleName\$($Manifest.Version)"
            $ModuleManifestDirectory = $ModuleManifestFile.directory
            switch ($ModuleManifestDirectory.basename) {
                $ModuleName {$true}
                $Manifest.Version.toString() {
                    if ($ModuleManifestDirectory.parent -match $ModuleName) {$true} else {throw $moduleDirectoryErrorMessage}
                }
                default {throw $moduleDirectoryErrorMessage}
            }
        }

        It 'Has a valid Description' {
            $Manifest.Description | Should Not BeNullOrEmpty
        }

        It 'Has a valid GUID' {
            [Guid]$Manifest.Guid | Should BeOfType 'System.GUID'
        }

        It 'Has a valid Copyright' {
            $Manifest.Copyright | Should Not BeNullOrEmpty
        }

        #TODO: Problematic with compiled modules, need a new logic
        #
        # It 'Exports all public functions' {
        # #TODO: Try PowerCD AST-based method
        # $FunctionFiles = Get-ChildItem Public -Filter *.ps1
        # $FunctionNames = $FunctionFiles.basename | ForEach-Object {$_ -replace '-', "-$($Manifest.Prefix)"}
        # $ExFunctions = $Manifest.ExportedFunctions.Values.Name
        # if ($ExFunctions -eq '*') {write-warning "Manifest has * for functions. You should individually specify your public functions prior to deployment for better discoverability"}
        # if ($functionNames) {
        # foreach ($FunctionName in $FunctionNames) {
        # $ExFunctions -contains $FunctionName | Should Be $true
        # }
        # }
        # }

        # It 'Has at least 1 exported command' {
        # $Script:Manifest.exportedcommands.count | Should BeGreaterThan 0
        # }

        It 'Can be imported as a module successfully' {
            #Make sure an existing module isn't present
            Remove-Module $ModuleManifestFile.basename -ErrorAction SilentlyContinue
            $SCRIPT:BuildOutputModule = Import-Module $ModuleManifestFile -PassThru -verbose:$false -erroraction stop
            $BuildOutputModule.Name | Should Be $ModuleName
            $BuildOutputModule | Should BeOfType System.Management.Automation.PSModuleInfo
        }
        It 'Can be removed as a module' {
            $BuildOutputModule | Remove-Module -erroraction stop -verbose:$false | Should BeNullOrEmpty
        }

    }
}
Describe 'Powershell Gallery Readiness (PSScriptAnalyzer)' {
    $results = Invoke-ScriptAnalyzer -Path $ModuleManifestFile.directory -Recurse -Setting PSGallery -Severity Error -Verbose:$false
    It 'PSScriptAnalyzer returns zero errors (warnings OK) using the Powershell Gallery ruleset' {
        if ($results) {write-warning ($results | Format-Table -autosize | out-string)}
        $results.Count | Should Be 0
    }
}


#Return to where we started
Pop-Location