tests/ExchangeOrganization.Tests.ps1

#Requires -Module Pester
#
# Tests for Get-MsecExchangeTransportRule and Get-MsecExchangeOrganizationSetting.
#
# SCL -1 is "skip all filtering", which the number does not say. BypassesFiltering does.
#
# A rule can be Enabled and inert, because Mode Audit does not act. IsActive needs both.
#
# There are FOUR redirect properties, not one. Checking RedirectMessageTo alone misses
# BlindCopyTo, CopyTo and AddToRecipients - three ways to copy mail elsewhere.
#
# There are TWO auto-forwarding controls and both must be closed. Fixing one is the common
# half-fix, so they belong on the same row.

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

    $exoStubs = @{
        'Get-ConnectionInformation'        = { [CmdletBinding()] param() }
        'Get-AcceptedDomain'               = { [CmdletBinding()] param() }
        'Get-TransportRule'                = { [CmdletBinding()] param() }
        'Get-TransportConfig'              = { [CmdletBinding()] param() }
        'Get-RemoteDomain'                 = { [CmdletBinding()] param() }
        'Get-AdminAuditLogConfig'          = { [CmdletBinding()] param() }
        'Get-InboundConnector'             = { [CmdletBinding()] param() }
        'Get-OutboundConnector'            = { [CmdletBinding()] param() }
        'Get-HostedOutboundSpamFilterPolicy' = { [CmdletBinding()] param() }
    }
    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-MsecExchangeTransportRule' {

    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' }) }
        }
    }

    It 'says in words that SCL -1 means filtering is skipped' {
        $rows = InModuleScope msec {
            Mock Get-TransportRule -MockWith {
                @(
                    [pscustomobject]@{ Name = 'Bypass'; State = 'Enabled'; Mode = 'Enforce'; SetSCL = -1 }
                    [pscustomobject]@{ Name = 'Normal'; State = 'Enabled'; Mode = 'Enforce'; SetSCL = $null }
                )
            }
            @(Get-MsecExchangeTransportRule)
        }
        ($rows | Where-Object Name -eq 'Bypass').BypassesFiltering | Should -BeTrue
        ($rows | Where-Object Name -eq 'Normal').BypassesFiltering | Should -BeFalse
    }

    It 'treats an Enabled rule in Audit mode as not active' {
        $rows = InModuleScope msec {
            Mock Get-TransportRule -MockWith {
                @(
                    [pscustomobject]@{ Name = 'Enforcing'; State = 'Enabled';  Mode = 'Enforce' }
                    [pscustomobject]@{ Name = 'Auditing';  State = 'Enabled';  Mode = 'Audit' }
                    [pscustomobject]@{ Name = 'Off';       State = 'Disabled'; Mode = 'Enforce' }
                )
            }
            @(Get-MsecExchangeTransportRule)
        }
        # Enabled is not enough: an Audit-mode rule is live and does nothing.
        ($rows | Where-Object Name -eq 'Enforcing').IsActive | Should -BeTrue
        ($rows | Where-Object Name -eq 'Auditing').IsActive  | Should -BeFalse
        ($rows | Where-Object Name -eq 'Off').IsActive       | Should -BeFalse
    }

    It 'checks all four ways a rule can send mail elsewhere' {
        $rows = InModuleScope msec {
            Mock Get-TransportRule -MockWith {
                @(
                    [pscustomobject]@{ Name = 'R'; State = 'Enabled'; Mode = 'Enforce'; RedirectMessageTo = @('a@contoso.com') }
                    [pscustomobject]@{ Name = 'B'; State = 'Enabled'; Mode = 'Enforce'; BlindCopyTo       = @('b@evil.example') }
                    [pscustomobject]@{ Name = 'C'; State = 'Enabled'; Mode = 'Enforce'; CopyTo            = @('c@contoso.com') }
                    [pscustomobject]@{ Name = 'A'; State = 'Enabled'; Mode = 'Enforce'; AddToRecipients   = @('d@contoso.com') }
                    [pscustomobject]@{ Name = 'N'; State = 'Enabled'; Mode = 'Enforce' }
                )
            }
            @(Get-MsecExchangeTransportRule)
        }
        foreach ($n in 'R', 'B', 'C', 'A') {
            ($rows | Where-Object Name -eq $n).RedirectsMail | Should -BeTrue -Because "$n sets one of the four"
        }
        ($rows | Where-Object Name -eq 'N').RedirectsMail | Should -BeFalse
        # Only the one outside an accepted domain is named.
        ($rows | Where-Object Name -eq 'B').ExternalRecipients | Should -Contain 'b@evil.example'
        ($rows | Where-Object Name -eq 'C').ExternalRecipients | Should -BeNullOrEmpty
    }

    It 'nulls ExternalRecipients when accepted domains could not be read' {
        $rows = InModuleScope msec {
            Mock Get-AcceptedDomain -MockWith { throw 'denied' }
            Mock Get-TransportRule -MockWith {
                @([pscustomobject]@{ Name = 'R'; State = 'Enabled'; Mode = 'Enforce'; BlindCopyTo = @('x@elsewhere.example') })
            }
            @(Get-MsecExchangeTransportRule -WarningAction SilentlyContinue)
        }
        # Not an empty list, which would read as "checked, all internal".
        $rows[0].ExternalRecipients | Should -BeNullOrEmpty
        $rows[0].RedirectsMail      | Should -BeTrue
    }

    It 'returns only bypassing or redirecting rules with -RiskyOnly' {
        $rows = InModuleScope msec {
            Mock Get-TransportRule -MockWith {
                @(
                    [pscustomobject]@{ Name = 'Bypass'; State = 'Enabled'; Mode = 'Enforce'; SetSCL = -1 }
                    [pscustomobject]@{ Name = 'Plain';  State = 'Enabled'; Mode = 'Enforce' }
                )
            }
            @(Get-MsecExchangeTransportRule -RiskyOnly)
        }
        $rows.Count   | Should -Be 1
        $rows[0].Name | Should -Be 'Bypass'
    }
}

Describe 'Get-MsecExchangeOrganizationSetting' {

    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-HostedOutboundSpamFilterPolicy -MockWith { @([pscustomobject]@{ Name = 'Default'; IsDefault = $true; AutoForwardingMode = 'On' }) }
            Mock Get-RemoteDomain        -MockWith { @([pscustomobject]@{ Name = 'Default'; DomainName = '*'; AutoForwardEnabled = $true }) }
            Mock Get-TransportConfig     -MockWith { [pscustomobject]@{ SmtpClientAuthenticationDisabled = $true } }
            Mock Get-AdminAuditLogConfig -MockWith { [pscustomobject]@{ UnifiedAuditLogIngestionEnabled = $true; AdminAuditLogEnabled = $true } }
            Mock Get-AcceptedDomain      -MockWith { @([pscustomobject]@{ DomainName = 'contoso.com' }) }
            Mock Get-InboundConnector    -MockWith { @() }
            Mock Get-OutboundConnector   -MockWith { @() }
            Mock Get-TransportRule       -MockWith { @([pscustomobject]@{ Name = 'B'; State = 'Enabled'; Mode = 'Enforce'; SetSCL = -1 }) }
        }
    }

    It 'puts both auto-forwarding controls on the same row' {
        $row = InModuleScope msec { Get-MsecExchangeOrganizationSetting }
        # Closing one and leaving the other is the usual half-fix.
        $row.AutoForwardingMode             | Should -Be 'On'
        $row.RemoteDomainAutoForwardEnabled | Should -BeTrue
    }

    It 'reports SMTP AUTH in the positive, inverting the raw property' {
        $row = InModuleScope msec { Get-MsecExchangeOrganizationSetting }
        # Raw property is SmtpClientAuthenticationDisabled = $true.
        $row.SmtpAuthEnabledTenantWide | Should -BeFalse
    }

    It 'counts only the bypass rules that are actually active' {
        $row = InModuleScope msec {
            Mock Get-TransportRule -MockWith {
                @(
                    [pscustomobject]@{ Name = 'Live'; State = 'Enabled';  Mode = 'Enforce'; SetSCL = -1 }
                    [pscustomobject]@{ Name = 'Off';  State = 'Disabled'; Mode = 'Enforce'; SetSCL = -1 }
                )
            }
            Get-MsecExchangeOrganizationSetting
        }
        $row.FilterBypassRuleCount       | Should -Be 2
        $row.ActiveFilterBypassRuleCount | Should -Be 1
    }

    It 'nulls one column rather than failing the whole row when a lookup is refused' {
        $row = InModuleScope msec {
            Mock Get-AdminAuditLogConfig -MockWith { throw 'access denied' }
            Get-MsecExchangeOrganizationSetting -WarningAction SilentlyContinue
        }
        # A partial answer about tenant posture beats none, as long as the gap shows as a gap.
        $row.UnifiedAuditLogEnabled | Should -BeNullOrEmpty
        $row.AutoForwardingMode     | Should -Be 'On'
    }
}