tests/Get-MsecExchangeMailbox.Tests.ps1

#Requires -Module Pester
#
# Tests for Get-MsecExchangeMailbox.
#
# Every trap here is a value that would read as a measurement when nothing was measured:
#
# A Recipient forward cannot be judged external without resolving the recipient, so
# IsForwardingExternal is $null - NOT $false, which would claim it stays inside the tenant.
#
# The per-mailbox SMTP AUTH setting is usually $null, meaning "inherit the tenant default".
# Read as a boolean that is false, so an unresolved value reports SMTP AUTH disabled on every
# mailbox that never set it - the protocol that bypasses MFA, reported safe by default.
#
# A mailbox with no CAS record has no protocols to report. Measured live: five Bookings
# (SchedulingMailbox) mailboxes, which genuinely have none.

BeforeAll {
    $script:StubbedExoCommands = @()
    Import-Module (Join-Path $PSScriptRoot '..' 'Msec.psm1') -Force -ErrorAction Stop

    # ExchangeOnlineManagement is not a dependency of msec, and is absent from some CI images.
    # Pester's Mock needs the command to EXIST; stubs carry real parameter names so any
    # -ParameterFilter binds the way it would against the real cmdlet.
    $exoStubs = @{
        'Get-ConnectionInformation' = { [CmdletBinding()] param() }
        'Get-AcceptedDomain'        = { [CmdletBinding()] param() }
        'Get-TransportConfig'       = { [CmdletBinding()] param() }
        'Get-EXOCasMailbox'         = { [CmdletBinding()] param($ResultSize) }
        'Get-EXOMailbox'            = { [CmdletBinding()] param($RecipientTypeDetails, $PropertySets, $ResultSize) }
    }
    foreach ($exoCmd in $exoStubs.Keys) {
        if (-not (Get-Command $exoCmd -ErrorAction SilentlyContinue)) {
            Set-Item "function:global:$exoCmd" -Value $exoStubs[$exoCmd] -Force
            $script:StubbedExoCommands += $exoCmd
        }
    }
}
AfterAll {
    foreach ($exoCmd in $script:StubbedExoCommands) {
        Remove-Item "function:global:$exoCmd" -ErrorAction SilentlyContinue
    }
    Remove-Module msec -Force -ErrorAction SilentlyContinue
}

Describe 'Get-MsecExchangeMailbox' {

    BeforeEach {
        InModuleScope msec {
            $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} }
            Mock Get-ConnectionInformation -MockWith {
                [pscustomobject]@{ ConnectionUri = 'https://outlook.office365.com'; TenantID = 't'; AppId = 'c'; ConnectionId = 'x' }
            }
            Mock Get-AcceptedDomain  -MockWith { @([pscustomobject]@{ DomainName = 'contoso.com' }) }
            Mock Get-TransportConfig -MockWith { [pscustomobject]@{ SmtpClientAuthenticationDisabled = $true } }
            Mock Get-EXOCasMailbox   -MockWith {
                @([pscustomobject]@{
                    ExternalDirectoryObjectId = 'oid-1'; PopEnabled = $true; ImapEnabled = $false
                    ActiveSyncEnabled = $true; OWAEnabled = $true
                    SmtpClientAuthenticationDisabled = $null      # inherits the tenant
                })
            }
        }
    }

    It 'separates a raw SMTP forward from one naming a recipient object' {
        $rows = InModuleScope msec {
            Mock Get-EXOMailbox -MockWith {
                @(
                    [pscustomobject]@{ ExternalDirectoryObjectId = 'oid-1'; UserPrincipalName = 'a@contoso.com'
                                       ForwardingSmtpAddress = 'smtp:thief@evil.example'; DeliverToMailboxAndForward = $false }
                    [pscustomobject]@{ ExternalDirectoryObjectId = 'oid-2'; UserPrincipalName = 'b@contoso.com'
                                       ForwardingAddress = 'Ticketing system'; DeliverToMailboxAndForward = $true }
                    [pscustomobject]@{ ExternalDirectoryObjectId = 'oid-3'; UserPrincipalName = 'c@contoso.com' }
                )
            }
            @(Get-MsecExchangeMailbox)
        }

        $a = $rows | Where-Object UserPrincipalName -eq 'a@contoso.com'
        $a.ForwardingKind       | Should -Be 'SmtpAddress'
        $a.ForwardingTarget     | Should -Be 'thief@evil.example'   # the smtp: prefix is stripped
        $a.IsForwardingExternal | Should -BeTrue
        # No copy kept: the mail leaves and the owner cannot notice.
        $a.DeliverToMailboxAndForward | Should -BeFalse

        $b = $rows | Where-Object UserPrincipalName -eq 'b@contoso.com'
        $b.ForwardingKind | Should -Be 'Recipient'
        # NOT $false: judging it needs a recipient lookup this command does not do.
        $b.IsForwardingExternal | Should -BeNullOrEmpty

        $c = $rows | Where-Object UserPrincipalName -eq 'c@contoso.com'
        $c.ForwardingKind             | Should -Be 'None'
        $c.DeliverToMailboxAndForward | Should -BeNullOrEmpty
    }

    It 'treats a forward inside an accepted domain as internal' {
        $row = InModuleScope msec {
            Mock Get-EXOMailbox -MockWith {
                @([pscustomobject]@{ ExternalDirectoryObjectId = 'oid-1'; UserPrincipalName = 'a@contoso.com'
                                     ForwardingSmtpAddress = 'smtp:colleague@contoso.com'; DeliverToMailboxAndForward = $true })
            }
            @(Get-MsecExchangeMailbox)
        }
        $row[0].IsForwardingExternal | Should -BeFalse
    }

    It 'nulls IsForwardingExternal when the accepted domains could not be read' {
        $row = InModuleScope msec {
            Mock Get-AcceptedDomain -MockWith { throw 'denied' }
            Mock Get-EXOMailbox -MockWith {
                @([pscustomobject]@{ ExternalDirectoryObjectId = 'oid-1'; UserPrincipalName = 'a@contoso.com'
                                     ForwardingSmtpAddress = 'smtp:someone@elsewhere.example' })
            }
            @(Get-MsecExchangeMailbox -WarningAction SilentlyContinue)
        }
        # "not checked" must not render as "stays inside the tenant".
        $row[0].IsForwardingExternal | Should -BeNullOrEmpty
    }

    It 'resolves SMTP AUTH from the tenant when the mailbox does not set it' {
        $row = InModuleScope msec {
            Mock Get-EXOMailbox -MockWith {
                @([pscustomobject]@{ ExternalDirectoryObjectId = 'oid-1'; UserPrincipalName = 'a@contoso.com' })
            }
            @(Get-MsecExchangeMailbox)
        }
        $row[0].SmtpAuthEnabled | Should -BeFalse
        $row[0].SmtpAuthSource  | Should -Be 'Tenant'
    }

    It 'prefers the mailbox setting over the tenant default, and says which it used' {
        $row = InModuleScope msec {
            Mock Get-EXOCasMailbox -MockWith {
                @([pscustomobject]@{ ExternalDirectoryObjectId = 'oid-1'
                                     SmtpClientAuthenticationDisabled = $false })   # explicitly ON
            }
            Mock Get-EXOMailbox -MockWith {
                @([pscustomobject]@{ ExternalDirectoryObjectId = 'oid-1'; UserPrincipalName = 'a@contoso.com' })
            }
            @(Get-MsecExchangeMailbox)
        }
        # The tenant says disabled; this mailbox overrides it. SMTP AUTH bypasses MFA, so the
        # override is the answer that matters.
        $row[0].SmtpAuthEnabled | Should -BeTrue
        $row[0].SmtpAuthSource  | Should -Be 'Mailbox'
    }

    It 'nulls the protocol columns for a mailbox with no CAS record' {
        $row = InModuleScope msec {
            Mock Get-EXOCasMailbox -MockWith { @() }
            Mock Get-EXOMailbox -MockWith {
                @([pscustomobject]@{ ExternalDirectoryObjectId = 'oid-9'; UserPrincipalName = 'bookings@contoso.com'
                                     RecipientTypeDetails = 'SchedulingMailbox' })
            }
            @(Get-MsecExchangeMailbox)
        }
        # Bookings mailboxes genuinely have none - $false would claim they were checked and off.
        $row[0].PopEnabled      | Should -BeNullOrEmpty
        $row[0].ImapEnabled     | Should -BeNullOrEmpty
        $row[0].SmtpAuthEnabled | Should -BeNullOrEmpty
    }

    It 'returns only forwarding mailboxes with -ForwardingOnly' {
        $rows = InModuleScope msec {
            Mock Get-EXOMailbox -MockWith {
                @(
                    [pscustomobject]@{ ExternalDirectoryObjectId = 'oid-1'; UserPrincipalName = 'a@contoso.com'
                                       ForwardingSmtpAddress = 'smtp:x@y.example' }
                    [pscustomobject]@{ ExternalDirectoryObjectId = 'oid-2'; UserPrincipalName = 'b@contoso.com' }
                )
            }
            @(Get-MsecExchangeMailbox -ForwardingOnly)
        }
        $rows.Count | Should -Be 1
        $rows[0].UserPrincipalName | Should -Be 'a@contoso.com'
    }
}