tests/Get-MsecEntraAccessPackage.Tests.ps1

#Requires -Module Pester
#
# Tests for Get-MsecEntraAccessPackage.
#
# The traps here are all about a control reading as present when it is absent, or absent when
# it is simply unconfigurable:
#
# APPROVAL AND EXPIRY ARE DIFFERENT CONTROLS. Approval governs who gets in, expiry governs how
# long they stay. Measured on one tenant: all 77 policies required approval and 69 never
# expired, including 25 packages granting directory administrator roles. Reading the approval
# column alone calls that fully governed.
#
# noExpiration MUST RENDER AS 'never', NOT BLANK. An empty expiry cell reads as "not
# configured yet" and invites assuming a default applies. It IS the configuration.
#
# A PACKAGE WITH NO POLICY HAS ApprovalRequired $null, NOT $false. "Nobody can request this"
# and "anyone gets it unapproved" are opposite findings and must not share a rendering.
#
# THE NESTED EXPAND IS LOAD-BEARING. With $expand=role,scope the scope is named "Root" on
# every package, because the scope is the extent within the resource rather than the resource.
# Only role($expand=resource) names the group the package actually grants.

BeforeAll {
    Import-Module (Join-Path $PSScriptRoot '..' 'Msec.psm1') -Force -ErrorAction Stop
}
AfterAll { Remove-Module msec -Force -ErrorAction SilentlyContinue }

Describe 'Get-MsecEntraAccessPackage' {
    BeforeEach {
        InModuleScope msec {
            $script:MsecSession = @{
                TenantId = 't'; ClientId = 'c'; KeyVaultName = 'kv'; KeyName = 'k'
                ThumbprintBytes = [byte[]](1..20); Tokens = @{}
            }
        }
    }

    It 'renders noExpiration as never, and a real duration as itself' {
        $rows = InModuleScope msec {
            Mock Invoke-MsecGraphRequest -MockWith {
                if ($Path -match 'accessPackages\?') {
                    [pscustomobject]@{ value = @([pscustomobject]@{ id = 'p1'; displayName = 'Admin Access'; isHidden = $false
                        catalog = [pscustomobject]@{ displayName = 'Privileged' } }) }
                }
                elseif ($Path -match 'assignmentPolicies') {
                    [pscustomobject]@{ value = @(
                        [pscustomobject]@{ id = 'pol1'; displayName = 'forever'; allowedTargetScope = 'specificDirectoryUsers'
                            expiration = [pscustomobject]@{ type = 'noExpiration' }
                            requestApprovalSettings = [pscustomobject]@{ isApprovalRequiredForAdd = $true }
                            accessPackage = [pscustomobject]@{ id = 'p1' } }
                        [pscustomobject]@{ id = 'pol2'; displayName = 'a year'; allowedTargetScope = 'allMemberUsers'
                            expiration = [pscustomobject]@{ type = 'afterDuration'; duration = 'P365D' }
                            requestApprovalSettings = [pscustomobject]@{ isApprovalRequiredForAdd = $true }
                            accessPackage = [pscustomobject]@{ id = 'p1' } }
                    ) }
                }
                else { [pscustomobject]@{ value = @() } }
            }
            @(Get-MsecEntraAccessPackage -WarningAction SilentlyContinue)
        }

        @($rows).Count | Should -Be 2
        ($rows | Where-Object Policy -eq 'forever').Expiration | Should -Be 'never'
        ($rows | Where-Object Policy -eq 'a year').Expiration  | Should -Be 'P365D'
        # Blank would read as "not configured yet" and invite assuming a default applies.
        ($rows | Where-Object Policy -eq 'forever').Expiration | Should -Not -BeNullOrEmpty
    }

    It 'leaves ApprovalRequired null for a package with no policy, not false' {
        $row = InModuleScope msec {
            Mock Invoke-MsecGraphRequest -MockWith {
                if ($Path -match 'accessPackages\?') {
                    [pscustomobject]@{ value = @([pscustomobject]@{ id = 'orphan'; displayName = 'No policy yet'
                        catalog = [pscustomobject]@{ displayName = 'General' } }) }
                }
                else { [pscustomobject]@{ value = @() } }
            }
            Get-MsecEntraAccessPackage -WarningAction SilentlyContinue
        }

        # The package still appears - it exists and could be given a policy tomorrow.
        $row.Package | Should -Be 'No policy yet'
        # $null, NOT $false: nobody can request it, which is the opposite of "no approval needed".
        $row.ApprovalRequired  | Should -BeNullOrEmpty
        $row.ApprovalRequired  | Should -Not -Be $false
        $row.Policy            | Should -BeNullOrEmpty
        $row.AllowedTargetScope | Should -BeNullOrEmpty
    }

    It 'names the granted resource via the nested expand, not the "Root" scope' {
        $captured = InModuleScope msec {
            $script:Paths = [System.Collections.Generic.List[string]]::new()
            Mock Invoke-MsecGraphRequest -MockWith {
                $script:Paths.Add([string]$Path)
                if ($Path -match 'accessPackages\?') {
                    [pscustomobject]@{ value = @([pscustomobject]@{ id = 'p1'; displayName = 'Auth Admin'
                        catalog = [pscustomobject]@{ displayName = 'Privileged' } }) }
                }
                elseif ($Path -match 'assignmentPolicies') { [pscustomobject]@{ value = @() } }
                elseif ($Path -match 'resourceRoleScopes') {
                    [pscustomobject]@{ value = @([pscustomobject]@{
                        role = [pscustomobject]@{ displayName = 'Member'; originSystem = 'AadGroup'
                                                  resource = [pscustomobject]@{ displayName = 'pim-entra-authentication-administrator' } }
                        scope = [pscustomobject]@{ displayName = 'Root' } }) }
                }
                else { [pscustomobject]@{ value = @() } }
            }
            $r = Get-MsecEntraAccessPackage -WarningAction SilentlyContinue
            [pscustomobject]@{ Row = $r; Paths = $script:Paths }
        }

        # The resource, not the scope. "Root/Member" names nothing and is the same on every package.
        $captured.Row.ResourceRole | Should -Contain 'pim-entra-authentication-administrator/Member [AadGroup]'
        ($captured.Paths -join ' ') | Should -Match 'expand=role\(\$expand=resource\)'
        # string[], so -contains tests it exactly rather than -like '*Administrator*'.
        $captured.Row.ResourceRole -is [array] | Should -BeTrue
    }

    It 'filters on expiry, approval and self-requestability independently' {
        $sets = InModuleScope msec {
            Mock Invoke-MsecGraphRequest -MockWith {
                if ($Path -match 'accessPackages\?') {
                    [pscustomobject]@{ value = @([pscustomobject]@{ id = 'p1'; displayName = 'Pkg'
                        catalog = [pscustomobject]@{ displayName = 'Cat' } }) }
                }
                elseif ($Path -match 'assignmentPolicies') {
                    [pscustomobject]@{ value = @(
                        [pscustomobject]@{ id = 'a'; displayName = 'named-approved-never'; allowedTargetScope = 'specificDirectoryUsers'
                            expiration = [pscustomobject]@{ type = 'noExpiration' }
                            requestApprovalSettings = [pscustomobject]@{ isApprovalRequiredForAdd = $true }
                            accessPackage = [pscustomobject]@{ id = 'p1' } }
                        [pscustomobject]@{ id = 'b'; displayName = 'open-unapproved-never'; allowedTargetScope = 'allMemberUsers'
                            expiration = [pscustomobject]@{ type = 'noExpiration' }
                            requestApprovalSettings = [pscustomobject]@{ isApprovalRequiredForAdd = $false }
                            accessPackage = [pscustomobject]@{ id = 'p1' } }
                        [pscustomobject]@{ id = 'c'; displayName = 'named-approved-90d'; allowedTargetScope = 'specificDirectoryUsers'
                            expiration = [pscustomobject]@{ type = 'afterDuration'; duration = 'P90D' }
                            requestApprovalSettings = [pscustomobject]@{ isApprovalRequiredForAdd = $true }
                            accessPackage = [pscustomobject]@{ id = 'p1' } }
                    ) }
                }
                else { [pscustomobject]@{ value = @() } }
            }
            [pscustomobject]@{
                All        = @(Get-MsecEntraAccessPackage -WarningAction SilentlyContinue)
                NoExpiry   = @(Get-MsecEntraAccessPackage -NoExpiry -WarningAction SilentlyContinue)
                NoApproval = @(Get-MsecEntraAccessPackage -NoApproval -WarningAction SilentlyContinue)
                SelfReq    = @(Get-MsecEntraAccessPackage -SelfRequestable -WarningAction SilentlyContinue)
            }
        }

        $sets.All.Count        | Should -Be 3
        $sets.NoExpiry.Count   | Should -Be 2
        $sets.NoApproval.Count | Should -Be 1
        $sets.NoApproval.Policy | Should -Be 'open-unapproved-never'
        $sets.SelfReq.Count    | Should -Be 1
        $sets.SelfReq.Policy   | Should -Be 'open-unapproved-never'
    }

    It 'names the catalogs that exist when an unknown one is asked for' {
        $w = InModuleScope msec {
            Mock Invoke-MsecGraphRequest -MockWith {
                if ($Path -match 'accessPackages\?') {
                    [pscustomobject]@{ value = @([pscustomobject]@{ id = 'p1'; displayName = 'Pkg'
                        catalog = [pscustomobject]@{ displayName = 'Privileged' } }) }
                }
                else { [pscustomobject]@{ value = @() } }
            }
            Get-MsecEntraAccessPackage -Catalog 'Nope' -WarningVariable wv -WarningAction SilentlyContinue | Out-Null
            $wv
        }
        ($w | Where-Object { "$_" -match "'Nope'" -and "$_" -match 'Privileged' }) | Should -Not -BeNullOrEmpty
    }

    It 'rewrites a 403 to name the permission' {
        InModuleScope msec {
            Mock Invoke-MsecGraphRequest -MockWith { throw '403 Forbidden' }
            { Get-MsecEntraAccessPackage } | Should -Throw '*EntitlementManagement.Read.All*'
        }
    }
}