tests/Get-MsecEntraCrossTenantAccess.Tests.ps1

BeforeAll {
    Import-Module (Join-Path $PSScriptRoot '..' 'Msec.psd1') -Force

    # Defined here rather than in the Describe body: a variable set at discovery time is gone
    # by the time an It runs, which reads as the command returning nothing.
    #
    # The default TRUSTS MFA and the partners mostly do not override it - which is the shape
    # that exposes the inherit-vs-false defect. p-explicit opts out deliberately.
    $script:Default = @{
        inboundTrust = @{
            isMfaAccepted = $true
            isCompliantDeviceAccepted = $false
            isHybridAzureADJoinedDeviceAccepted = $false
        }
        b2bCollaborationInbound  = @{ usersAndGroups = @{ accessType = 'allowed' } }
        b2bCollaborationOutbound = @{ usersAndGroups = @{ accessType = 'allowed' } }
    }

    $script:Partners = @(
        # No inboundTrust at all - Graph returns null, meaning INHERIT.
        @{ tenantId = 'p-inherit' }

        # Explicitly opts out of the default's MFA trust.
        @{ tenantId = 'p-explicit'
           inboundTrust = @{ isMfaAccepted = $false
                             isCompliantDeviceAccepted = $false
                             isHybridAzureADJoinedDeviceAccepted = $false }
           b2bCollaborationInbound = @{ usersAndGroups = @{ accessType = 'blocked' } } }
    )
}

Describe 'Get-MsecEntraCrossTenantAccess' {

    BeforeEach {
        InModuleScope msec { Mock Assert-MsecSession {} }
    }

    It 'resolves an inherited setting to the default rather than reporting false' {
        # THE DEFECT THE COMMAND EXISTS TO PREVENT. Graph returns inboundTrust as null on a
        # partner with no override; null renders as blank or False in any table, so a tenant
        # whose default trusts MFA would be reported as trusting nothing.
        InModuleScope msec -Parameters @{ D = $script:Default; P = $script:Partners } {
            param($D, $P)
            Mock Invoke-MsecGraphRequest {
                if ($Path -like '*crossTenantAccessPolicy/default*') { return $D }
                if ($Path -like '*partners*') { return $P }
                return @{ displayName = 'Partner Inc' }
            }
            $row = Get-MsecEntraCrossTenantAccess | Where-Object TenantId -eq 'p-inherit'
            $row.TrustsMfa   | Should -BeTrue
            $row.IsInherited | Should -BeTrue
        }
    }

    It 'keeps an explicit false even when the default is true' {
        # The opposite error: resolving everything to the default would erase a deliberate
        # opt-out, which is a real decision someone made about a specific partner.
        InModuleScope msec -Parameters @{ D = $script:Default; P = $script:Partners } {
            param($D, $P)
            Mock Invoke-MsecGraphRequest {
                if ($Path -like '*crossTenantAccessPolicy/default*') { return $D }
                if ($Path -like '*partners*') { return $P }
                return @{ displayName = 'Partner Inc' }
            }
            $row = Get-MsecEntraCrossTenantAccess | Where-Object TenantId -eq 'p-explicit'
            $row.TrustsMfa   | Should -BeFalse
            $row.IsInherited | Should -BeFalse
        }
    }

    It 'returns the default as a row of its own' {
        # The partners list describes the organisations someone once configured. The default
        # governs everyone else - on a measured tenant that was every CRO with actual guests.
        InModuleScope msec -Parameters @{ D = $script:Default; P = $script:Partners } {
            param($D, $P)
            Mock Invoke-MsecGraphRequest {
                if ($Path -like '*crossTenantAccessPolicy/default*') { return $D }
                if ($Path -like '*partners*') { return $P }
                return @{ displayName = 'Partner Inc' }
            }
            $rows = Get-MsecEntraCrossTenantAccess
            $rows[0].Scope       | Should -Be 'Default'
            $rows[0].TrustsMfa   | Should -BeTrue
            $rows[0].IsInherited | Should -BeFalse
            @($rows).Count       | Should -Be 3
        }
    }

    It 'treats a missing accessType as allowed, not as unknown' {
        # An entry that was never narrowed has no accessType. Blank would read as 'unknown'
        # for what is in fact the ordinary B2B default.
        InModuleScope msec {
            Mock Invoke-MsecGraphRequest {
                if ($Path -like '*crossTenantAccessPolicy/default*') {
                    return @{ inboundTrust = @{ isMfaAccepted = $false } }
                }
                if ($Path -like '*partners*') { return @(@{ tenantId = 'p1' }) }
                return @{ displayName = 'X' }
            }
            (Get-MsecEntraCrossTenantAccess).InboundB2B | Should -Not -Contain ''
            (Get-MsecEntraCrossTenantAccess)[0].InboundB2B | Should -Be 'allowed'
        }
    }

    It '-BlockedOnly returns the partners barred from collaborating' {
        InModuleScope msec -Parameters @{ D = $script:Default; P = $script:Partners } {
            param($D, $P)
            Mock Invoke-MsecGraphRequest {
                if ($Path -like '*crossTenantAccessPolicy/default*') { return $D }
                if ($Path -like '*partners*') { return $P }
                return @{ displayName = 'Partner Inc' }
            }
            (Get-MsecEntraCrossTenantAccess -BlockedOnly).TenantId | Should -Be @('p-explicit')
        }
    }

    It '-TrustingMfaOnly uses the effective value, so it includes inheritors' {
        # An inheriting partner DOES trust MFA when the default does. Filtering on the raw
        # field would silently omit exactly the rows that matter.
        InModuleScope msec -Parameters @{ D = $script:Default; P = $script:Partners } {
            param($D, $P)
            Mock Invoke-MsecGraphRequest {
                if ($Path -like '*crossTenantAccessPolicy/default*') { return $D }
                if ($Path -like '*partners*') { return $P }
                return @{ displayName = 'Partner Inc' }
            }
            $t = Get-MsecEntraCrossTenantAccess -TrustingMfaOnly
            $t.TenantId | Should -Contain 'p-inherit'
            $t.TenantId | Should -Not -Contain 'p-explicit'
        }
    }

    It 'reports the tenant id when the name cannot be resolved' {
        # An unnamed partner is still a partner; a blank would read as no partner at all.
        InModuleScope msec -Parameters @{ D = $script:Default } {
            param($D)
            Mock Invoke-MsecGraphRequest {
                if ($Path -like '*crossTenantAccessPolicy/default*') { return $D }
                if ($Path -like '*partners*') { return @(@{ tenantId = 'unknown-tenant' }) }
                throw 'tenant not found'
            }
            $row = Get-MsecEntraCrossTenantAccess | Where-Object Scope -eq 'Partner'
            $row.TenantName | Should -Be 'unknown-tenant'
        }
    }

    It 'resolves each tenant name once' {
        InModuleScope msec -Parameters @{ D = $script:Default } {
            param($D)
            Mock Invoke-MsecGraphRequest {
                if ($Path -like '*crossTenantAccessPolicy/default*') { return $D }
                if ($Path -like '*partners*') { return @(@{ tenantId = 'dup' }, @{ tenantId = 'dup' }) }
                return @{ displayName = 'Dup Inc' }
            }
            Get-MsecEntraCrossTenantAccess | Out-Null
            Should -Invoke Invoke-MsecGraphRequest -Times 1 -Exactly -ParameterFilter {
                $Path -like "*findTenantInformationByTenantId(tenantId='dup')*"
            }
        }
    }

    It 'names the missing permission when Graph refuses' {
        InModuleScope msec {
            Mock Invoke-MsecGraphRequest { throw 'Response status code does not indicate success: 403 (Forbidden).' }
            { Get-MsecEntraCrossTenantAccess } | Should -Throw '*Policy.Read.All*'
        }
    }

    It 'emits the documented type so the table view applies' {
        InModuleScope msec -Parameters @{ D = $script:Default; P = $script:Partners } {
            param($D, $P)
            Mock Invoke-MsecGraphRequest {
                if ($Path -like '*crossTenantAccessPolicy/default*') { return $D }
                if ($Path -like '*partners*') { return $P }
                return @{ displayName = 'Partner Inc' }
            }
            (Get-MsecEntraCrossTenantAccess)[0].PSObject.TypeNames |
                Should -Contain 'MsecEntraCrossTenantAccess'
        }
    }
}