Checks/Script.Tests.ps1

param(
    [parameter(Mandatory = $true)]
    [string[]]$Source,

    [parameter(Mandatory = $false)]
    [string[]]$ScriptAnalyzerRulesPath
)

BeforeDiscovery {

    $scriptFiles = @()

    $Source | ForEach-Object {

        $fileProperties = (Get-Item -Path $_)

        $scriptFiles += @{
            'FullName' = $_
            'Name' = $fileProperties.Name
            'Directory' = $fileProperties.Directory

        }

    }

    if ( -not ($ScriptAnalyzerRulesPath -is [Array])) {
        $ScriptAnalyzerRulesPath = @($ScriptAnalyzerRulesPath)
    }

    $rulesPath = @()

    $ScriptAnalyzerRulesPath | ForEach-Object {

        $rulesPath += @{
            'Path' = $_

        }

    }

}

Describe "Script Tests" -Tag "Script" {

    Context "Script: <File.Name> at <File.Directory>" -ForEach $scriptFiles {

        BeforeAll {

            $file = $_

        }

        BeforeEach {

            $scriptFile = $file.FullName
            $fileContent = Get-FileContent -Path $file.FullName

            if (-not([string]::IsNullOrEmpty($fileContent))) {
                ($ParsedFile, $ErrorCount) = Get-ParsedContent -Content $fileContent
            }
            else {
                Write-Warning "File is empty"
                $ParsedFile = $null
                $ErrorCount = 1
            }

        }

        It "check script has valid PowerShell syntax" -Tag "ValidSyntax" {

            $ErrorCount | Should -Be 0

        }

        It "check help must contain required elements" -Tag "HelpMustContainRequiredElements" {

            {

                $helpComments = ($ParsedFile | Where-Object { $_.Type -eq "Comment" } | Select-Object -First 1)
                if ([string]::IsNullOrEmpty($helpComments)) {
                    throw "No help block found"
                }
                $helpTokens = Convert-Help -Help $helpComments.Content
                Test-RequiredToken -HelpTokens $helpTokens

            } |
                Should -Not -Throw

        }

        It "check help must not contain unspecified elements" -Tag "HelpMustContainUnspecifiedElements" {

            {

                $helpComments = ($ParsedFile | Where-Object { $_.Type -eq "Comment" } | Select-Object -First 1)
                if ([string]::IsNullOrEmpty($helpComments)) {
                    throw "No help block found"
                }
                $helpTokens = Convert-Help -Help $helpComments.Content
                Test-UnspecifiedToken -HelpTokens $helpTokens

            } |
                Should -Not -Throw

        }

        It "check help elements text is not empty" -Tag "HelpElementsNotEmpty" {

            {

                $helpComments = ($ParsedFile | Where-Object { $_.Type -eq "Comment" } | Select-Object -First 1)
                if ([string]::IsNullOrEmpty($helpComments)) {
                    throw "No help block found"
                }
                $helpTokens = Convert-Help -Help $helpComments.Content
                Test-HelpTokensTextIsValid -HelpTokens $helpTokens

            } | Should -Not -Throw

        }

        It "check help elements Min/Max counts are valid" -Tag "HelpElementsMinMaxCount" {

            {

                $helpComments = ($ParsedFile | Where-Object { $_.Type -eq "Comment" } | Select-Object -First 1)
                if ([string]::IsNullOrEmpty($helpComments)) {
                    throw "No help block found"
                }
                $helpTokens = Convert-Help -Help $helpComments.Content
                Test-HelpTokensCountIsValid -HelpTokens $helpTokens

            } | Should -Not -Throw

        }

        It "check script contains [CmdletBinding] attribute" -Tag "ContainsCmdletBinding" {

            $cmdletBindingCount = (@(Get-TokenMarker -ParsedContent $ParsedFile -Type "Attribute" -Content "CmdletBinding")).Count

            $cmdletBindingCount | Should -Be 1

        }

        It "check script contains [OutputType] attribute" -Tag "ContainsOutputType" {

            $outputTypeCount = (@(Get-TokenMarker -ParsedContent $ParsedFile -Type "Attribute" -Content "OutputType")).Count

            $outputTypeCount | Should -Be 1

        }

        It "check script [OutputType] attribute is not empty" -Tag "OutputTypeNotEmpty" {

            $outputTypeToken = (Get-Token -ParsedContent $ParsedFile -Type "Attribute" -Content "OutputType")

            $outputTypeValue = @($outputTypeToken | Where-Object { $_.Type -eq "Type" })

            $outputTypeValue | Should -Not -BeNullOrEmpty

        }

        # Note: Disabled because I'm questioning the validity of the rule. So many function haven't got a need for params
        # It "check script contains param attribute" -Tag "ContainsParam" {

        # $paramCount = (@(Get-TokenMarker -ParsedContent $ParsedFile -Type "Keyword" -Content "param")).Count

        # $paramCount | Should -Be 1

        # }

        It "check script param block variables have type" -Tag "ParamVariablesHaveType" {

            $parameterVariables = Get-ScriptParameter -Content $fileContent

            if ($parameterVariables.Count -eq 0) {

                Set-ItResult -Inconclusive -Because "No parameters found"

            }

            {

                Test-ParameterVariablesHaveType -ParameterVariables $parameterVariables

            } | Should -Not -Throw

        }

        It "check .PARAMETER help matches variables in param block" -Tag "HelpMatchesParamVariables" {

            {

                $helpComments = ($ParsedFile | Where-Object { $_.Type -eq "Comment" } | Select-Object -First 1)
                if ([string]::IsNullOrEmpty($helpComments)) {
                    throw "No help block found"
                }
                $parameterVariables = Get-ScriptParameter -Content $fileContent
                $helpTokens = Convert-Help -Help $helpComments.Content

                Test-HelpTokensParamsMatch -HelpTokens $helpTokens -ParameterVariables $parameterVariables

            } | Should -Not -Throw

        }

        It "check script contains no PSScriptAnalyzer suppressions" -Tag "NoScriptAnalyzerSuppressions" {

            $suppressCount = (@(Get-TokenMarker -ParsedContent $ParsedFile -Type "Attribute" -Content "Diagnostics.CodeAnalysis.SuppressMessageAttribute")).Count
            $suppressCount | Should -Be 0

            $suppressCount = (@(Get-TokenMarker -ParsedContent $ParsedFile -Type "Attribute" -Content "Diagnostics.CodeAnalysis.SuppressMessage")).Count
            $suppressCount | Should -Be 0

        }

        It "check script contains no PSScriptAnalyzer failures" -Tag "NoScriptAnalyzerFailures" {

            $AnalyserFailures = @(Invoke-ScriptAnalyzer -Path $scriptFile)

            ($AnalyserFailures | ForEach-Object { $_.Message }) | Should -BeNullOrEmpty

        }

        It "check script contains no PSScriptAnalyser rule failures '<_.Path>" -Tag "NoScriptAnalyzerExtraRulesFailures" -TestCases $rulesPath {

            param($Path)

            if ( [string]::IsNullOrEmpty($Path)) {

                Set-ItResult -Inconclusive -Because "Empty ScriptAnalyzerRulesPath '$Path'"

            }

            if ( -not (Test-Path -Path $Path -ErrorAction SilentlyContinue)) {

                Set-ItResult -Inconclusive -Because "ScriptAnalyzerRulesPath path '$Path' not found"

            }

            $AnalyserFailures = @(Invoke-ScriptAnalyzer -Path $scriptFile -CustomRulePath $Path)

            $AnalyserFailures | ForEach-Object { $_.Message } | Should -BeNullOrEmpty

        }

        It "check Import-Module statements have valid format" -Tag "ValidImportModuleStatements" {

            $importModuleTokens = @($ParsedFile | Where-Object { $_.Type -eq "Command" -and $_.Content -eq "Import-Module" })

            if ($importModuleTokens.Count -eq 0) {

                Set-ItResult -Inconclusive -Because "No Import-Module statements found"

            }

            {

                Test-ImportModuleIsValid -ParsedContent $ParsedFile -ImportModuleTokens $importModuleTokens

            } | Should -Not -Throw

        }

    }

}