Tests/QA/ExampleFiles.common.v5.Tests.ps1

<#
    .NOTES
        To run manually:

        $pathToHQRMTests = Join-Path -Path (Get-Module DscResource.Test).ModuleBase -ChildPath 'Tests\QA'

        $container = New-PesterContainer -Path "$pathToHQRMTests/ExampleFiles.common.*.Tests.ps1" -Data @{
            SourcePath = './source'
            # ExcludeSourceFile = @('MyExample.ps1')
        }

        Invoke-Pester -Container $container -Output Detailed
#>

param
(
    [Parameter()]
    [System.String]
    $SourcePath,

    [Parameter()]
    [System.String[]]
    $ExcludeSourceFile,

    [Parameter(ValueFromRemainingArguments = $true)]
    $Args
)

# This test _must_ be outside the BeforeDiscovery-block since Pester 4 does not recognizes it.
$isPesterMinimum5 = (Get-Module -Name Pester).Version -ge '5.1.0'

# Only run if Pester 5.1 or higher.
if (-not $isPesterMinimum5)
{
    Write-Verbose -Message 'Repository is using old Pester version, new HQRM tests for Pester v5 and v6 are skipped.' -Verbose
    return
}

<#
    This _must_ be outside any Pester blocks for correct script parsing.
    Sets Context block's default parameter value to handle Pester v6's ForEach
    change, to keep same behavior as with Pester v5. The default parameter is
    removed at the end of the script to avoid affecting other tests.
#>

$PSDefaultParameterValues['Context:AllowNullOrEmptyForEach'] = $true

BeforeDiscovery {
    # Re-imports the private (and public) functions.
    Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '../../DscResource.Test.psm1') -Force

    if (-not $SourcePath)
    {
        return
    }

    $examplesPath = Join-Path -Path $SourcePath -ChildPath 'Examples'

    # If there are no Examples folder, exit.
    if (-not (Test-Path -Path $examplesPath))
    {
        return
    }

    $exampleFiles = @(Get-ChildItem -Path $examplesPath -Filter '*.ps1' -Recurse | WhereSourceFileNotExcluded -ExcludeSourceFile $ExcludeSourceFile)

    $exampleToTest = @()

    foreach ($exampleFile in $exampleFiles)
    {
        $exampleToTest += @{
            ExampleFile            = $exampleFile
            ExampleDescriptiveName = Join-Path -Path (Split-Path $exampleFile.Directory -Leaf) -ChildPath (Split-Path $exampleFile -Leaf)
        }
    }
}

BeforeAll {
    # Re-imports the private (and public) functions.
    Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '../../DscResource.Test.psm1') -Force
}

AfterAll {
    # Re-import just the public functions.
    Import-Module -Name 'DscResource.Test' -Force
}

Describe 'Common Tests - Validate Example Files' -Tag 'Common Tests - Validate Example Files' {
    Context 'When the example ''<ExampleDescriptiveName>'' exist' -ForEach $exampleToTest {
        It 'Should compile the MOF schema for the example correctly' {
            {
                $mockPassword = ConvertTo-SecureString '&iPm%M5q3K$Hhq=wcEK' -AsPlainText -Force
                $mockCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @('username', $mockPassword)

                $mockConfigurationData = @{
                    AllNodes = @(
                        @{
                            NodeName                    = 'localhost'
                            PsDscAllowPlainTextPassword = $true
                        }
                    )
                }

                <#
                    Set this first because it is used in the final block,
                    and must be set otherwise it fails on not being assigned.
                #>

                $existingCommandName = $null

                try
                {
                    . $ExampleFile.FullName

                    <#
                        Test for either a configuration named 'Example',
                        or parse the name from the filename and try that
                        as the configuration name (requirement for Azure
                        Automation).
                    #>

                    $commandName = @(
                        'Example',
                        (Get-PublishFileName -Path $ExampleFile.FullName)
                    )

                    # Get the first one that matches.
                    $existingCommand = Get-ChildItem -Path 'function:' |
                        Where-Object { $_.Name -in $commandName } |
                        Select-Object -First 1

                    if ($existingCommand)
                    {
                        $existingCommandName = $existingCommand.Name

                        $exampleCommand = Get-Command -Name $existingCommandName -ErrorAction 'SilentlyContinue'

                        if ($exampleCommand)
                        {
                            $exampleParameters = @{}

                            # Remove any common parameters that are available.
                            $commandParameters = $exampleCommand.Parameters.Keys |
                                Where-Object -FilterScript {
                                    ($_ -notin [System.Management.Automation.PSCmdlet]::CommonParameters) -and `
                                    ($_ -notin [System.Management.Automation.PSCmdlet]::OptionalCommonParameters)
                                }

                            foreach ($parameterName in $commandParameters)
                            {
                                $parameterType = $exampleCommand.Parameters[$parameterName].ParameterType.FullName

                                <#
                                    Each credential parameter in the Example function is assigned the
                                    mocked credential. 'PsDscRunAsCredential' is not assigned because
                                    that breaks the example.
                                #>

                                if ($parameterName -ne 'PsDscRunAsCredential' `
                                        -and $parameterType -eq 'System.Management.Automation.PSCredential')
                                {
                                    $exampleParameters.Add($parameterName, $mockCredential)
                                }
                                else
                                {
                                    <#
                                        Check for mandatory parameters.
                                        Assume the parameters are all in the 'all' parameter set.
                                    #>

                                    $isParameterMandatory = $exampleCommand.Parameters[$parameterName].ParameterSets['__AllParameterSets'].IsMandatory
                                    if ($isParameterMandatory)
                                    {
                                        <#
                                            Convert '1' to the type that the parameter expects.
                                            Using '1' since it can be converted to String, Numeric
                                            and Boolean.
                                        #>

                                        $exampleParameters.Add($parameterName, ('1' -as $parameterType))
                                    }
                                }
                            }

                            <#
                                If there is a $ConfigurationData variable that was dot-sourced
                                then use that as the configuration data instead of the mocked
                                configuration data.
                            #>

                            if (Get-Item -Path variable:ConfigurationData -ErrorAction 'SilentlyContinue')
                            {
                                $mockConfigurationData = $ConfigurationData
                            }

                            & $exampleCommand.Name @exampleParameters -ConfigurationData $mockConfigurationData -OutputPath 'TestDrive:\' -ErrorAction 'Continue' -WarningAction 'SilentlyContinue' | Out-Null
                        }
                    }
                    else
                    {
                        throw ('The example ''{0}'' does not contain a configuration named ''{1}''.' -f $exampleDescriptiveName, ($commandName -join "', or '"))
                    }

                }
                finally
                {
                    <#
                        Remove the function we dot-sourced so next example file
                        doesn't use the previous Example-function. Using recurse
                        since it saw child functions when copied in helper functions
                        during debugging, it resulted in an interactive prompt.
                    #>

                    Remove-Item -Path "function:$existingCommandName" -ErrorAction 'SilentlyContinue' -Recurse -Force

                    <#
                        Remove the variable $ConfigurationData if it existed in
                        the file we dot-sourced so next example file doesn't use
                        the previous examples configuration.
                    #>

                    Remove-Item -Path 'variable:ConfigurationData' -ErrorAction 'SilentlyContinue'
                }
            } | Should -Not -Throw
        }
    }
}

$PSDefaultParameterValues.Remove('Context:AllowNullOrEmptyForEach')