Tests/TestHelpers/xExchangeTestHelper.psm1

<#
    .SYNOPSIS
        Function to be used within pester for end to end testing of
        Get/Set/Test-TargetResource. Function first calls Set-TargetResource
        with provided parameters, then runs Get and Test-TargetResource, and
        ensures they match $ExpectedGetResults and $ExpectedTestResult.
 
    .PARAMETER Params
        The Parameters to pass when calling Get/Set/Test-TargetResource.
 
    .PARAMETER ContextLabel
        The label to use within the Context block of tests.
 
    .PARAMETER ExpectedGetResults
        A hashtable containing the expected return values from
        Get-TargetResource.
 
    .PARAMETER ExpectedTestResult
        The expected return value from Test-TargetResource.
#>

function Test-TargetResourceFunctionality
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [System.Collections.Hashtable]
        $Params,

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

        [Parameter()]
        [System.Collections.Hashtable]
        $ExpectedGetResults,

        [Parameter()]
        [System.Boolean]
        $ExpectedTestResult = $true
    )

    Context $ContextLabel {
        $addedVerbose = $false

        if ($null -eq ($Params.Keys | Where-Object -FilterScript {$_ -like 'Verbose'}))
        {
            $Params.Add('Verbose', $true)
            $addedVerbose = $true
        }

        [System.Boolean] $testResult = Test-TargetResource @Params

        Write-Verbose -Message "Test-TargetResource results before running Set-TargetResource: $testResult"

        Set-TargetResource @Params

        [System.Collections.Hashtable] $getResult = Get-TargetResource @Params
        [System.Boolean] $testResult = Test-TargetResource @Params

        # The ExpectedGetResults are $null, so let's check that what we got back is $null
        if ($null -eq $ExpectedGetResults)
        {
            It 'Get-TargetResource: Should Be Null' {
                $getResult | Should -BeNullOrEmpty
            }
        }
        else
        {
            Test-CommonGetTargetResourceFunctionality -GetResult $getResult

            # Test each individual key in $ExpectedGetResult to see if they exist, and if the expected value matches
            foreach ($key in $ExpectedGetResults.Keys)
            {
                $getContainsKey = $getResult.ContainsKey($key)

                It "Get-TargetResource: Contains Key: $($key)" {
                    $getContainsKey | Should -Be $true
                }

                if ($getContainsKey)
                {
                    if ($getResult.ContainsKey($key))
                    {
                        switch ((Get-Command Get-TargetResource).Parameters[$key].ParameterType)
                        {
                            ([System.String[]])
                            {
                                $getValueMatchesForKey = Compare-ArrayContent -Array1 $getResult[$key] -Array2 $ExpectedGetResults[$key]
                            }
                            ([System.Management.Automation.PSCredential])
                            {
                                $getValueMatchesForKey = $getResult[$key].UserName -like $ExpectedGetResults[$key].UserName
                            }
                            default
                            {
                                $getValueMatchesForKey = ($getResult[$key] -eq $ExpectedGetResults[$key])
                            }
                        }
                    }
                    else
                    {
                        $getValueMatchesForKey = $false
                    }

                    It "Get-TargetResource: Value Matches for Key: $($key)" {
                        $getValueMatchesForKey | Should -Be $true
                    }
                }
            }
        }

        # Test the Test-TargetResource results
        It 'Test-TargetResource' {
            $testResult | Should -Be $ExpectedTestResult
        }

        if ($addedVerbose)
        {
            $Params.Remove('Verbose')
        }
    }
}

<#
    .SYNOPSIS
        Runs Get-TargetTesource, or takes the results of a previous
        Get-TargetResource execution, and performs common tests against the
        results. The function must be provided either the results from a
        previous Get-TargetResource execution, or the parameters to send to
        Get-TargetResource, but not both. If neither parameter is specified,
        or both parameters are specified, the function will throw an exception.
 
    .PARAMETER GetResult
        The results of a previous Get-TargetResource execution.
 
    .PARAMETER GetTargetResourceParams
        The parameters that should be passed to Get-TargetResource.
#>

function Test-CommonGetTargetResourceFunctionality
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [System.Collections.Hashtable]
        $GetResult,

        [Parameter()]
        [System.Collections.Hashtable]
        $GetTargetResourceParams
    )

    if (($GetResult.Count -eq 0 -and $GetTargetResourceParams.Count -eq 0) -or ($GetResult.Count -gt 0 -and $GetTargetResourceParams.Count -gt 0))
    {
        throw 'Either the GetResult or GetTargetResourceParams parameters must be specified with non-empty hashtables, but not both.'
    }

    if ($GetResult.Count -eq 0)
    {
        $GetResult = Get-TargetResource @GetTargetResourceParams
    }

    It 'Should return a hashtable of properties' {
        $GetResult | Should -Be -Not $null
    }

    $getTargetResourceCommand = Get-Command Get-TargetResource

    It 'Only 1 Get-TargetResource function should be loaded' {
        $getTargetResourceCommand.Count -eq 1 | Should -Be $true
    }

    if ($getTargetResourceCommand.Count -eq 1)
    {
        foreach ($getTargetResourceParam in $getTargetResourceCommand.Parameters.Keys | Where-Object -FilterScript {$GetResult.ContainsKey($_)})
        {
            $getResultMemberType = '$null'

            if ($null -ne ($GetResult[$getTargetResourceParam]))
            {
                $getResultMemberType = $GetResult[$getTargetResourceParam].GetType().ToString()
            }

            It "Should return a value of type '$($getTargetResourceCommand.Parameters[$getTargetResourceParam].ParameterType.ToString())' for hashtable member '$getTargetResourceParam'. Actual return type: '$getResultMemberType'" {
                ($getTargetResourceCommand.Parameters[$getTargetResourceParam].ParameterType.ToString()) -eq $getResultMemberType | Should -Be $true
            }
        }
    }
}

function Test-ArrayContentsEqual
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [System.Collections.Hashtable]
        $TestParams,

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

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

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

        [Parameter()]
        [System.String]
        $ItLabel
    )

    Context $ContextLabel {
        [System.Collections.Hashtable] $getResult = Get-TargetResource @TestParams

        It $ItLabel {
            Compare-ArrayContent -Array1 $DesiredArrayContents -Array2 $getResult."$($GetResultParameterName)" -IgnoreCase | Should Be $true
        }
    }
}

function Test-Array2ContainsArray1
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [System.Collections.Hashtable]
        $TestParams,

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

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

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

        [Parameter()]
        [System.String]
        $ItLabel
    )

    Context $ContextLabel {
        [System.Collections.Hashtable] $getResult = Get-TargetResource @TestParams

        It $ItLabel {
            Test-ArrayElementsInSecondArray -Array1 $DesiredArrayContents -Array2 $getResult."$GetResultParameterName" -IgnoreCase | Should Be $true
        }
    }
}

# Creates a test OAB for DSC, or sees if it exists. If it is created or exists, return the name of the OAB.
function Get-TestOfflineAddressBook
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $ShellCredentials
    )

    [System.String] $testOabName = 'Offline Address Book (DSC Test)'

    Get-RemoteExchangeSession -Credential $ShellCredentials -CommandsToLoad '*-OfflineAddressBook'

    if ($null -eq (Get-OfflineAddressBook -Identity $testOabName -ErrorAction SilentlyContinue))
    {
        Write-Verbose -Message "Test OAB does not exist. Creating OAB with name '$testOabName'."

        $testOab = New-OfflineAddressBook -Name $testOabName -AddressLists '\'

        if ($null -eq $testOab)
        {
            throw 'Failed to create test OAB.'
        }
    }

    return $testOabName
}

# Removes the test DAG if it exists, and any associated databases
function Initialize-TestForDAG
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [System.String[]]
        $ServerName,

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

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

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

    Write-Verbose -Message 'Cleaning up test DAG and related resources'

    Get-RemoteExchangeSession -Credential $ShellCredentials -CommandsToLoad '*-MailboxDatabase',`
                                                                                  '*-DatabaseAvailabilityGroup',`
                                                                                  'Remove-DatabaseAvailabilityGroupServer',`
                                                                                  'Get-MailboxDatabaseCopyStatus',`
                                                                                  'Remove-MailboxDatabaseCopy'

    $existingDB = Get-MailboxDatabase -Identity "$($DatabaseName)" -Status -ErrorAction SilentlyContinue

    # First remove the test database copies
    Remove-CopiesOfTestDatabase -DatabaseName $DatabaseName

    # Now remove the actual DB's
    Remove-TestDatabase -DatabaseName $DatabaseName -ServerName $ServerName

    # Last remove the test DAG
    $dag = Get-DatabaseAvailabilityGroup -Identity "$($DAGName)" -ErrorAction SilentlyContinue

    if ($null -ne $dag)
    {
        Set-DatabaseAvailabilityGroup -Identity "$($DAGName)" -DatacenterActivationMode Off

        foreach ($server in $dag.Servers)
        {
            Remove-DatabaseAvailabilityGroupServer -MailboxServer "$($server.Name)" -Identity "$($DAGName)" -Confirm:$false
        }

        Remove-DatabaseAvailabilityGroup -Identity "$($DAGName)" -Confirm:$false
    }

    if ($null -ne (Get-DatabaseAvailabilityGroup -Identity "$($DAGName)" -ErrorAction SilentlyContinue))
    {
        throw 'Failed to remove test DAG'
    }

    # Disable the DAG computer account
    $compAccount = Get-ADComputer -Identity $DAGName -ErrorAction SilentlyContinue

    if ($null -ne $compAccount -and $compAccount.Enabled -eq $true)
    {
        $compAccount | Disable-ADAccount
    }

    Write-Verbose -Message 'Finished cleaning up test DAG and related resources'
}

<#
    .SYNOPSIS
        Removes all copies of the given Mailbox Database that are not currently
        mounted.
 
    .PARAMETER DatabaseName
        The name of the Database to remove copies from.
#>

function Remove-CopiesOfTestDatabase
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [System.String]
        $DatabaseName
    )

    $existingDB = Get-MailboxDatabase -Identity "$($DatabaseName)" -Status -ErrorAction SilentlyContinue

    # First remove the test database copies
    if ($null -ne $existingDB)
    {
        Get-MailboxDatabaseCopyStatus -Identity "$($DatabaseName)" | Where-Object -FilterScript {
            $_.Status -notlike 'Mounted'
        } | Remove-MailboxDatabaseCopy -Confirm:$false
    }
}

<#
    .SYNOPSIS
        Removes the specified Mailbox Database, as well as associated database
        files from the specified Servers.
 
    .PARAMETER ServerName
        The servers to remove database files from.
 
    .PARAMETER DatabaseName
        The name of the Database to remove.
#>

function Remove-TestDatabase
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [System.String[]]
        $ServerName,

        [Parameter()]
        [System.String]
        $DatabaseName
    )

    Get-MailboxDatabase | Where-Object -FilterScript {
        $_.Name -like "$($DatabaseName)"
    } | Remove-MailboxDatabase -Confirm:$false

    # Remove the files
    foreach ($server in $ServerName)
    {
        Get-ChildItem -LiteralPath "\\$($server)\c`$\Program Files\Microsoft\Exchange Server\V15\Mailbox\$($DatabaseName)" `
                      -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -Confirm:$false -ErrorAction SilentlyContinue
    }
}

<#
    .SYNOPSIS
        Prompts for credentials to use for Exchange tests and returns the
        credentials as a PSCredential object. Only prompts for credentials
        on the first call to the function.
#>

function Get-TestCredential
{
    # Suppressing this rule so that Exchange credentials can be re-used across multiple test scripts
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')]
    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCredential])]
    param()

    if ($null -eq $Global:TestCredential)
    {
        [PSCredential] $Global:TestCredential = Get-Credential -Message 'Enter credentials for connecting a Remote PowerShell session to Exchange'
    }

    return $Global:TestCredential
}

<#
    .SYNOPSIS
        Gets all configured Accepted Domains, and returns the Domain name of
        the first retrieved Accepted Domain. Throws an exception if no
        Accepted Domains are configured.
#>

function Get-TestAcceptedDomainName
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param()

    [System.Object[]] $acceptedDomains = Get-AcceptedDomain

    if ($acceptedDomains.Count -gt 0)
    {
        return $acceptedDomains[0].DomainName.ToString()
    }
    else
    {
        throw 'One or more Accepted Domains must be configured for tests to function.'
    }
}

<#
    .SYNOPSIS
        Returns a Mailbox object corresponding to a DSC Test Mailbox. Creates
        the Mailbox if it does not already exist.
#>

function Get-DSCTestMailbox
{
    [CmdletBinding()]
    [OutputType([Microsoft.Exchange.Data.Directory.Management.Mailbox])]
    param()

    $testMailboxName = 'DSCTestMailbox'

    $testDomain = Get-TestAcceptedDomainName
    $testCreds = Get-TestCredential

    $testMailbox = Get-Mailbox $testMailboxName -ErrorAction SilentlyContinue
    $primarySMTP = "$testMailboxName@$testDomain"
    $secondarySMTP = "$($testMailboxName)2@$testDomain"
    [System.Object[]] $dbsOnServer = Get-MailboxDatabase -Server $env:COMPUTERNAME -ErrorAction SilentlyContinue

    $changedMailbox = $false

    # Create the test mailbox if it doesn't exist
    if ($null -eq $testMailbox)
    {
        Write-Verbose -Message "Creating test mailbox: $testMailboxName"

        $newMailboxParams = @{
            Name               = $testMailboxName
            PrimarySmtpAddress = $primarySMTP
            UserPrincipalName  = $primarySMTP
            Password           = $testCreds.Password
        }

        if ($dbsOnServer.Count -gt 0)
        {
            $newMailboxParams.Add('Database',$dbsOnServer[0].Name)
        }

        $testMailbox = New-Mailbox @newMailboxParams

        if ($null -eq $testMailbox)
        {
            throw 'Failed to create test mailbox'
        }
    }

    # Set the test mailbox primary SMTP if not correct
    if ($testMailbox.PrimarySmtpAddress.Address -notlike $primarySMTP)
    {
        Write-Verbose -Message "Changing primary SMTP on test mailbox: $testMailboxName"

        $testMailbox | Set-Mailbox -PrimarySmtpAddress $primarySMTP

        $changedMailbox = $true
    }

    # Add the secondary SMTP if necessary
    if (($testMailbox.EmailAddresses | Where-Object {$_.AddressString -like $secondarySMTP}).Count -eq 0)
    {
        Write-Verbose -Message "Adding secondary SMTP on test mailbox: $testMailboxName"

        $testMailbox | Set-Mailbox -EmailAddresses @{add=$secondarySMTP}

        $changedMailbox = $true
    }

    # Get the mailbox one more time so we have updated properties on it
    if ($changedMailbox)
    {
        $testMailbox = Get-Mailbox $testMailboxName
    }

    return $testMailbox
}

<#
    .SYNOPSIS
        Returns a MailUser object corresponding to a DSC Test MailUser. Creates
        the MailUser if it does not already exist.
#>

function Get-DSCTestMailUser
{
    [CmdletBinding()]
    [OutputType([Microsoft.Exchange.Data.Directory.Management.MailUser])]
    param()

    $testMailUserName = 'DSCTestMailUser'

    $testMailUser = Get-MailUser $testMailUserName -ErrorAction SilentlyContinue
    $primarySMTP = "$testMailUserName@contoso.local"

    $changedMailUser = $false

    # Create the test MailUser if it doesn't exist
    if ($null -eq $testMailUser)
    {
        Write-Verbose -Message "Creating test mail user: $testMailUserName"

        $newMailUserParams = @{
            Name                 = $testMailUserName
            ExternalEmailAddress = $primarySMTP
        }

        $testMailUser = New-MailUser @newMailUserParams

        if ($null -eq $testMailUser)
        {
            throw 'Failed to create test MailUser'
        }
    }

    # Set the test MailUser primary SMTP if not correct
    if ($testMailUser.ExternalEmailAddress.AddressString -notlike $primarySMTP)
    {
        Write-Verbose -Message "Changing ExternalEmailAddress on test mail user: $testMailboxName"

        $testMailUser | Set-MailUser -ExternalEmailAddress $primarySMTP

        $changedMailUser = $true
    }

    # Get the MailUser one more time so we have updated properties on it
    if ($changedMailUser)
    {
        $testMailUser = Get-MailUser $testMailUserName
    }

    return $testMailUser
}

<#
    .SYNOPSIS
        Returns a MailContact object corresponding to a DSC Test MailContact.
        Creates the MailContact if it does not already exist.
#>

function Get-DSCTestMailContact
{
    [CmdletBinding()]
    [OutputType([Microsoft.Exchange.Data.Directory.Management.MailContact])]
    param()

    $testMailContactName = 'DSCTestMailContact'

    $testMailContact = Get-MailContact $testMailContactName -ErrorAction SilentlyContinue
    $primarySMTP = "$testMailContactName@contoso.local"

    $changedMailContact = $false

    # Create the test MailContact if it doesn't exist
    if ($null -eq $testMailContact)
    {
        Write-Verbose -Message "Creating test mail contact: $testMailContactName"

        $newMailContactParams = @{
            Name                 = $testMailContactName
            ExternalEmailAddress = $primarySMTP
        }

        $testMailContact = New-MailContact @newMailContactParams

        if ($null -eq $testMailContact)
        {
            throw 'Failed to create test MailContact'
        }
    }

    # Set the test MailContact primary SMTP if not correct
    if ($testMailContact.ExternalEmailAddress.AddressString -notlike $primarySMTP)
    {
        Write-Verbose -Message "Changing ExternalEmailAddress on test mail contact: $testMailContactName"

        $testMailContact | Set-MailContact -ExternalEmailAddress $primarySMTP

        $changedMailContact = $true
    }

    # Get the MailContact one more time so we have updated properties on it
    if ($changedMailContact)
    {
        $testMailContact = Get-MailContact $testMailContactName
    }

    return $testMailContact
}

<#
    .SYNOPSIS
        Returns the FQDN of the first domain controller discovered using
        Get-ADDomainController.
#>

function Get-TestDomainController
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param()

    $dcToTestAgainst = ''

    [System.Object[]] $foundDCs = Get-ADDomainController

    if ($foundDCs.Count -gt 0)
    {
        [System.String] $dcToTestAgainst = $foundDCs[0].HostName
    }

    return $dcToTestAgainst
}

Export-ModuleMember -Function *