Tests/Integration/MSFT_xWindowsProcess.Integration.Tests.ps1

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
param ()

<#
    Please note that some of these tests depend on each other.
    They must be run in the order given - if one test fails, subsequent tests may
    also fail.
#>

$errorActionPreference = 'Stop'
Set-StrictMode -Version 'Latest'

Import-Module -Name (Join-Path -Path (Split-Path $PSScriptRoot -Parent) `
                               -ChildPath 'CommonTestHelper.psm1') `
                               -Force

if (Test-SkipContinuousIntegrationTask -Type 'Integration')
{
    return
}

$script:testEnvironment = Enter-DscResourceTestEnvironment `
    -DscResourceModuleName 'xPSDesiredStateConfiguration' `
    -DscResourceName 'MSFT_xWindowsProcess' `
    -TestType 'Integration'

<#
    .SYNOPSIS
        Starts the specified DSC Configuration and verifies that it executes
        without throwing.
 
    .PARAMETER ConfigFile
        Path to the DSC Config script.
 
    .PARAMETER ConfigurationName
        The Name of the DSC Configuration.
 
    .PARAMETER ConfigurationPath
        Path to the Compiled DSC Configuration.
 
    .PARAMETER DscParams
        Parameters to pass to Start-DscConfiguration.
#>

function Start-DscConfigurationAndVerify
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateScript({Test-Path -Path $_})]
        [System.String]
        $ConfigFile,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ConfigurationName,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ConfigurationPath,

        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $DscParams
    )

    It 'Should compile without throwing' {
        {
            .$configFile -ConfigurationName $configurationName
            & $configurationName @dscParams
            Start-DscConfiguration -Path $configurationPath -ErrorAction 'Stop' -Wait -Force
        } | Should -Not -Throw
    }
}

<#
    .SYNOPSIS
        Performs common post Set-DscConfiguration tests.
 
    .PARAMETER Path
        Corresponds to the Path parameter of xWindowsProcess.
 
    .PARAMETER Arguments
        Corresponds to the Arguments parameter of xWindowsProcess.
 
    .PARAMETER Ensure
        Corresponds to the Ensure parameter of xWindowsProcess.
 
    .PARAMETER ProcessCount
        The expected count of running processes for the specified Process.
        Allows Null.
#>

function Start-GetDscConfigurationAndVerify
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateScript({Test-Path -Path $_})]
        [System.String]
        $Path,

        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [System.String]
        $Arguments,

        [Parameter(Mandatory = $true)]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure,

        [Parameter()]
        [Nullable[System.Int32]]
        $ProcessCount
    )

    It 'Should call Get-DscConfiguration without throwing and get expected results' {
        { $script:currentConfig = Get-DscConfiguration -ErrorAction 'Stop' } | Should -Not -Throw

        $script:currentConfig.Path | Should -Be $Path
        $script:currentConfig.Arguments | Should -Be $Arguments
        $script:currentConfig.Ensure | Should -Be $Ensure
        $script:currentConfig.ProcessCount | Should -Be $ProcessCount
    }
}

<#
    .SYNOPSIS
        Starts the test process using DSC and verifies that it is started.
 
    .PARAMETER Path
        Corresponds to the Path parameter of xWindowsProcess.
 
    .PARAMETER Arguments
        Corresponds to the Arguments parameter of xWindowsProcess.
 
    .PARAMETER Ensure
        Corresponds to the Ensure parameter of xWindowsProcess.
 
    .PARAMETER ProcessCount
        The expected count of running processes for the specified Process.
        Allows Null.
 
    .PARAMETER ContextLabel
        The Context label to pass to Pester.
 
    .PARAMETER DscParams
        Parameters to pass to Start and Get-DscConfiguration.
#>

function Start-TestProcessUsingDscAndVerify
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateScript({Test-Path -Path $_})]
        [System.String]
        $Path,

        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [System.String]
        $Arguments,

        [Parameter(Mandatory = $true)]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure,

        [Parameter()]
        [Nullable[System.Int32]]
        $ProcessCount,

        [Parameter()]
        [System.String]
        $ContextLabel = 'Should start a new instance of the test process',

        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $DscParams
    )

    Context $ContextLabel {
        $configurationName = 'MSFT_xWindowsProcess_StartProcess'
        $configurationPath = Join-Path -Path $TestDrive -ChildPath $configurationName
        $dscParams.OutputPath = $configurationPath
        $dscParams.Ensure = 'Present'

        Start-DscConfigurationAndVerify `
            -ConfigFile $configFile `
            -ConfigurationName $configurationName `
            -ConfigurationPath $configurationPath `
            -DscParams $dscParams

        Start-GetDscConfigurationAndVerify `
            -Path $Path `
            -Arguments $Arguments `
            -Ensure 'Present' `
            -ProcessCount 1
    }
}

<#
    .SYNOPSIS
        Stops the test process using DSC and verifies that it is stopped.
 
    .PARAMETER Path
        Corresponds to the Path parameter of xWindowsProcess.
 
    .PARAMETER Arguments
        Corresponds to the Arguments parameter of xWindowsProcess.
 
    .PARAMETER Ensure
        Corresponds to the Ensure parameter of xWindowsProcess.
 
    .PARAMETER ProcessCount
        The expected count of running processes for the specified Process.
        Allows Null.
 
    .PARAMETER ContextLabel
        The Context label to pass to Pester.
 
    .PARAMETER DscParams
        Parameters to pass to Start and Get-DscConfiguration.
#>

function Stop-TestProcessUsingDscAndVerify
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateScript({Test-Path -Path $_})]
        [System.String]
        $Path,

        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [System.String]
        $Arguments,

        [Parameter(Mandatory = $true)]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure,

        [Parameter()]
        [Nullable[System.Int32]]
        $ProcessCount,

        [Parameter()]
        [System.String]
        $ContextLabel = 'Should stop all instances of the test process',

        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $DscParams
    )

    Context $ContextLabel {
        $configurationName = 'MSFT_xWindowsProcess_StopAllProcesses'
        $configurationPath = Join-Path -Path $TestDrive -ChildPath $configurationName
        $dscParams.OutputPath = $configurationPath
        $dscParams.Ensure = 'Absent'

        Start-DscConfigurationAndVerify `
            -ConfigFile $configFile `
            -ConfigurationName $configurationName `
            -ConfigurationPath $configurationPath `
            -DscParams $dscParams

        Start-GetDscConfigurationAndVerify `
            -Path $Path `
            -Arguments $Arguments `
            -Ensure 'Absent' `
            -ProcessCount $null
    }

    # Force a stop on any running test processes just in case they failed to stop via DSC
    Get-Process | Where-Object -FilterScript {$_.Path -like $Path} | `
        Stop-Process -Confirm:$false -Force
}

<#
    .SYNOPSIS
        Starts two instances of the test process, one using DSC, and one manually,
        and tests whether Get-DscConfiguration returns the right number of
        processes.
 
    .PARAMETER Path
        Corresponds to the Path parameter of xWindowsProcess.
 
    .PARAMETER Arguments
        Corresponds to the Arguments parameter of xWindowsProcess.
 
    .PARAMETER Ensure
        Corresponds to the Ensure parameter of xWindowsProcess.
 
    .PARAMETER ProcessCount
        The expected count of running processes for the specified Process.
        Allows Null.
 
    .PARAMETER ContextLabel
        The Context label to pass to Pester.
 
    .PARAMETER DscParams
        Parameters to pass to Start and Get-DscConfiguration.
#>

function Start-AdditionalTestProcessAndVerify
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateScript({Test-Path -Path $_})]
        [System.String]
        $Path,

        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [System.String]
        $Arguments,

        [Parameter(Mandatory = $true)]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure,

        [Parameter()]
        [Nullable[System.Int32]]
        $ProcessCount,

        [Parameter()]
        [System.String]
        $ContextLabel = 'Should return the correct amount of processes when more than 1 are running',

        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $DscParams
    )

    Context $ContextLabel {
        $configurationName = 'MSFT_xWindowsProcess_CheckForMultipleProcesses'
        $configurationPath = Join-Path -Path $TestDrive -ChildPath $configurationName
        $dscParams.OutputPath = $configurationPath
        $dscParams.Ensure = 'Present'

        Start-DscConfigurationAndVerify `
            -ConfigFile $configFile `
            -ConfigurationName $configurationName `
            -ConfigurationPath $configurationPath `
            -DscParams $dscParams

        # Start another instance of the same process using the same credentials.
        $startProcessParams = @{
            FilePath = $Path
        }

        if (!([String]::IsNullOrEmpty($Arguments)))
        {
            $startProcessParams.ArgumentList = @("`"$Arguments`"")
        }

        if ($null -ne $Credential)
        {
            $startProcessParams.Add('Credential', $Credential)
        }

        Start-Process @startProcessParams

        # Run Get-DscConfiguration and verify that 2 processes are detected
        Start-GetDscConfigurationAndVerify `
            -Path $Path `
            -Arguments $Arguments `
            -Ensure 'Present' `
            -ProcessCount 2
    }
}

<#
    .SYNOPSIS
        Performs commmon sets of tests using
        Start, Get, Set, and Test - DscConfiguration.
 
    .PARAMETER Path
        Corresponds to the Path parameter of xWindowsProcess.
 
    .PARAMETER Arguments
        Corresponds to the Arguments parameter of xWindowsProcess.
 
    .PARAMETER ConfigFile
        Path to the DSC Configuration script.
 
    .PARAMETER Credential
        Corresponds to the Credential parameter of xWindowsProcess.
#>

function Invoke-CommonResourceTesting
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $DescribeLabel,

        [Parameter(Mandatory = $true)]
        [ValidateScript({Test-Path -Path $_})]
        [System.String]
        $Path,

        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [System.String]
        $Arguments,

        [Parameter(Mandatory = $true)]
        [System.String]
        $ConfigFile,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential
    )

    Describe $DescribeLabel {
        $ConfigData = @{
            AllNodes = @(
                @{
                    NodeName = '*'
                    PSDscAllowPlainTextPassword = $true
                }
                @{
                    NodeName = 'localhost'
                }
            )
        }

        $dscParams = @{
            Path = $Path
            Arguments = $Arguments
            Ensure = 'Present'
            ErrorAction = 'Stop'
            OutputPath = ''
            ConfigurationData = $ConfigData
        }

        if ($null -ne $Credential)
        {
            $dscParams.Add('Credential', $Credential)
        }

        # Stop all test process instances and DSC configurations.
        Stop-TestProcessUsingDscAndVerify `
            -Path $Path `
            -Arguments $Arguments `
            -Ensure 'Absent' `
            -ProcessCount $null `
            -DscParams $dscParams

        # Start test process using DSC.
        Start-TestProcessUsingDscAndVerify `
            -Path $Path `
            -Arguments $Arguments `
            -Ensure 'Present' `
            -ProcessCount 1 `
            -DscParams $dscParams

        # Run same config again. Should not start a second new test Process instance when one is already running.
        Start-TestProcessUsingDscAndVerify `
            -Path $Path `
            -Arguments $Arguments `
            -Ensure 'Present' `
            -ProcessCount 1 `
            -ContextLabel 'Should detect when multiple process instances are running' `
            -DscParams $dscParams

        # Stop all test process instances and DSC configurations.
        Stop-TestProcessUsingDscAndVerify `
            -Path $Path `
            -Arguments $Arguments `
            -Ensure 'Absent' `
            -ProcessCount $null `
            -DscParams $dscParams

        # Start test process using DSC, then start a test process outside of DSC
        Start-AdditionalTestProcessAndVerify `
            -Path $Path `
            -Arguments $Arguments `
            -Ensure 'Absent' `
            -ProcessCount 2 `
            -DscParams $dscParams

        # Stop all test process instances and DSC configurations.
        Stop-TestProcessUsingDscAndVerify `
            -Path $Path `
            -Arguments $Arguments `
            -Ensure 'Absent' `
            -ProcessCount $null `
            -DscParams $dscParams
    }
}

try
{
    # Setup test process paths.
    $notepadExePath = Resolve-Path -Path ([System.IO.Path]::Combine($env:SystemRoot, 'System32', 'notepad.exe'))
    $powershellExePath = Resolve-Path -Path ([System.IO.Path]::Combine($env:SystemRoot, 'System32', 'WindowsPowershell', 'v1.0', 'powershell.exe'))
    $iexplorerExePath = Resolve-Path -Path ([System.IO.Path]::Combine( $env:ProgramFiles, 'internet explorer', 'iexplore.exe'))

    # Setup test combination variables
    $testPathAndArgsCombos = @(
        @{
            Description = 'Process Path Without Spaces, No Arguments'
            Path = $notepadExePath
            Arguments = ''
        }

        @{
            Description = 'Process Path With Spaces, No Arguments'
            Path = $iexplorerExePath
            Arguments = ''
        }

        @{
            Description = 'Process Path Without Spaces, Arguments Without Spaces'
            Path = $powershellExePath
            Arguments = "30|Start-Sleep"
        }

        @{
            Description = 'Process Path With Spaces, Arguments Without Spaces'
            Path = $iexplorerExePath
            Arguments = 'https://github.com/PowerShell/xPSDesiredStateConfiguration'
        }

        @{
            Description = 'Process Path Without Spaces, Arguments With Spaces'
            Path = $powershellExePath
            Arguments = "Start-Sleep -Seconds 30"
        }

        @{
            Description = 'Process Path With Spaces, Arguments With Spaces'
            Path = $iexplorerExePath
            Arguments = "https://github.com/PowerShell/xPSDesiredStateConfiguration with spaces"
        }
    )

    $credentialCombos = @(
        @{
            Description = 'No Credentials'
            Credential = $null
            ConfigFile = Join-Path -Path $PSScriptRoot -ChildPath 'MSFT_xWindowsProcess.config.ps1'
        }

        @{
            Description = 'With Credentials'
            Credential = Get-TestAdministratorAccountCredential
            ConfigFile = Join-Path -Path $PSScriptRoot -ChildPath 'MSFT_xWindowsProcessWithCredential.config.ps1'
        }
    )

    # Perform tests on each variable combination
    foreach ($pathAndArgsCombo in $testPathAndArgsCombos)
    {
        foreach ($credentialCombo in $credentialCombos)
        {
            $params = @{
                Path = $pathAndArgsCombo.Path
                Arguments = $pathAndArgsCombo.Arguments
                Credential = $credentialCombo.Credential
                ConfigFile = $credentialCombo.ConfigFile
            }

            $params.Add('DescribeLabel', "$($pathAndArgsCombo.Description), $($credentialCombo.Description)")
            $params.Remove('FolderDescription')
            $params.Remove('CredentialDescription')

            Invoke-CommonResourceTesting @params
        }
    }
}
finally
{
    Exit-DscResourceTestEnvironment -TestEnvironment $script:testEnvironment
}