tests/Get-MsecTeamsPolicyAssignment.Tests.ps1

#Requires -Module Pester
#
# Tests for Get-MsecTeamsPolicyAssignment.
#
# The command exists for one reason: a policy list alone cannot tell a restrictive tenant from
# a tenant holding a restrictive policy that applies to nobody. So the behaviour worth pinning
# hardest is that a policy with ZERO holders is still returned - if it were omitted, the output
# would read exactly like the tenant where that policy does not exist.
#
# The second is that an unreadable user list reports null rather than 0. A failed read that
# prints "0 users" on every policy is both wrong and the most alarming possible misreading.
#
# Skipped wholesale without MicrosoftTeams installed: Pester's Mock requires the command being
# mocked to EXIST, and Get-Cs* only exist when that module is present. The CI images do not all
# carry it.

$script:HasTeams = $null -ne (Get-Module -ListAvailable MicrosoftTeams)

BeforeAll {
    $modulePath = Join-Path $PSScriptRoot '..' 'Msec.psm1'
    Import-Module $modulePath -Force -ErrorAction Stop
}

AfterAll {
    Remove-Module Msec -Force -ErrorAction SilentlyContinue
}

Describe 'Get-MsecTeamsPolicyAssignment' -Skip:(-not $script:HasTeams) {

    BeforeEach {
        InModuleScope Msec {
            # No session: the command must not try to self-connect during a unit test.
            $script:MsecSession = $null
            $script:MsecTeamsAsCurrentUser = $null
        }
    }

    It 'returns a policy that applies to nobody, because that is the finding' {
        $rows = InModuleScope Msec {
            Mock Get-CsTeamsMeetingPolicy -MockWith {
                @(
                    [pscustomobject]@{ Identity = 'Global' }
                    [pscustomobject]@{ Identity = 'Tag:RestrictedAnonymousAccess' }
                    [pscustomobject]@{ Identity = 'Tag:NeverAssigned' }
                )
            }
            Mock Get-CsOnlineUser -MockWith {
                @(
                    [pscustomobject]@{ UserPrincipalName = 'a@x.com'; AccountType = 'User'; TeamsMeetingPolicy = $null }
                    [pscustomobject]@{ UserPrincipalName = 'b@x.com'; AccountType = 'User'; TeamsMeetingPolicy = $null }
                    [pscustomobject]@{ UserPrincipalName = 'c@x.com'; AccountType = 'User'
                                       TeamsMeetingPolicy = [pscustomobject]@{ Name = 'RestrictedAnonymousAccess'; Authority = 'Host' } }
                )
            }
            Get-MsecTeamsPolicyAssignment -PolicyType Meeting
        }

        # The whole point: three policies in, three rows out.
        $rows.Count | Should -Be 3

        $never = $rows | Where-Object PolicyName -eq 'NeverAssigned'
        $never | Should -Not -BeNullOrEmpty
        $never.UserCount | Should -Be 0
    }

    It 'counts users with no explicit assignment toward Global' {
        $rows = InModuleScope Msec {
            Mock Get-CsTeamsMeetingPolicy -MockWith {
                @([pscustomobject]@{ Identity = 'Global' }, [pscustomobject]@{ Identity = 'Tag:Strict' })
            }
            Mock Get-CsOnlineUser -MockWith {
                @(
                    [pscustomobject]@{ UserPrincipalName = 'a@x.com'; TeamsMeetingPolicy = $null }
                    [pscustomobject]@{ UserPrincipalName = 'b@x.com'; TeamsMeetingPolicy = $null }
                    [pscustomobject]@{ UserPrincipalName = 'c@x.com'; TeamsMeetingPolicy = [pscustomobject]@{ Name = 'Strict' } }
                )
            }
            Get-MsecTeamsPolicyAssignment -PolicyType Meeting
        }

        # Teams reports "no assignment" as null, not as the string 'Global'.
        ($rows | Where-Object IsGlobal).UserCount | Should -Be 2
        ($rows | Where-Object PolicyName -eq 'Strict').UserCount | Should -Be 1
        ($rows | Where-Object IsGlobal).TotalUsers | Should -Be 3
    }

    It 'accepts a plain string assignment as well as a UserPolicyDefinition' {
        $rows = InModuleScope Msec {
            Mock Get-CsTeamsMeetingPolicy -MockWith { @([pscustomobject]@{ Identity = 'Tag:Strict' }) }
            Mock Get-CsOnlineUser -MockWith {
                # Older MicrosoftTeams versions hand back a bare string here.
                @([pscustomobject]@{ UserPrincipalName = 'a@x.com'; TeamsMeetingPolicy = 'Tag:Strict' })
            }
            Get-MsecTeamsPolicyAssignment -PolicyType Meeting
        }

        ($rows | Where-Object PolicyName -eq 'Strict').UserCount | Should -Be 1
    }

    It 'reports null, not zero, when the user list cannot be read' {
        $rows = InModuleScope Msec {
            Mock Get-CsTeamsMeetingPolicy -MockWith { @([pscustomobject]@{ Identity = 'Global' }) }
            Mock Get-CsOnlineUser -MockWith { throw 'Authorization has been denied for this request.' }
            Get-MsecTeamsPolicyAssignment -PolicyType Meeting -WarningAction SilentlyContinue
        }

        # 0 would claim the policy applies to no one. It is not a measurement at all.
        $rows.Count | Should -Be 1
        $rows[0].UserCount | Should -BeNullOrEmpty
        $rows[0].PercentOfUsers | Should -BeNullOrEmpty
        $rows[0].TotalUsers | Should -BeNullOrEmpty
        # The policy itself is still named, so the row is not silently dropped either.
        $rows[0].PolicyName | Should -Be 'Global'
    }

    It 'reports a policy area it could not list instead of omitting it' {
        $rows = InModuleScope Msec {
            Mock Get-CsTeamsMessagingPolicy -MockWith { throw 'Access denied' }
            Mock Get-CsOnlineUser -MockWith { @([pscustomobject]@{ UserPrincipalName = 'a@x.com'; TeamsMessagingPolicy = $null }) }
            Get-MsecTeamsPolicyAssignment -PolicyType Messaging -WarningAction SilentlyContinue
        }

        $rows.Count | Should -Be 1
        $rows[0].PolicyName | Should -Be 'Unreadable'
        $rows[0].UserCount | Should -BeNullOrEmpty
    }

    It 'returns only explicit assignments with -IncludeUser' {
        $rows = InModuleScope Msec {
            Mock Get-CsTeamsMeetingPolicy -MockWith { @([pscustomobject]@{ Identity = 'Global' }, [pscustomobject]@{ Identity = 'Tag:Strict' }) }
            Mock Get-CsOnlineUser -MockWith {
                @(
                    [pscustomobject]@{ UserPrincipalName = 'a@x.com'; AccountType = 'User'; TeamsMeetingPolicy = $null }
                    [pscustomobject]@{ UserPrincipalName = 'c@x.com'; AccountType = 'User'; TeamsMeetingPolicy = [pscustomobject]@{ Name = 'Strict' } }
                )
            }
            Get-MsecTeamsPolicyAssignment -PolicyType Meeting -IncludeUser
        }

        # The users on Global are the whole tenant and are not the exceptions anyone is after.
        $rows.Count | Should -Be 1
        $rows[0].UserPrincipalName | Should -Be 'c@x.com'
        $rows[0].PolicyName | Should -Be 'Strict'
    }

    It 'does not offer Federation or Client, which have no per-user assignment' {
        # They are tenant-wide configuration. Accepting them would imply a count exists.
        $valid = (Get-Command Get-MsecTeamsPolicyAssignment).Parameters['PolicyType'].Attributes |
                 Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] } |
                 Select-Object -ExpandProperty ValidValues

        $valid | Should -Not -Contain 'Federation'
        $valid | Should -Not -Contain 'Client'
        $valid | Should -Contain 'Meeting'
    }
}