tests/Get-MsecEntraAuthenticationMethod.Tests.ps1

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

    # Defined here, not 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 rather than as a test bug.
    $policy = @{
        authenticationMethodConfigurations = @(
            @{ id = 'Fido2'; state = 'enabled'; isSelfServiceRegistrationAllowed = $true
               includeTargets = @(@{ id = 'g-break-glass'; targetType = 'group' }) }
            @{ id = 'MicrosoftAuthenticator'; state = 'enabled'
               includeTargets = @(@{ id = 'all_users'; targetType = 'group' })
               excludeTargets = @(@{ id = 'g-break-glass'; targetType = 'group' }) }
            @{ id = 'Sms'; state = 'enabled'
               includeTargets = @(@{ id = 'all_users'; targetType = 'group' }) }
            @{ id = 'TemporaryAccessPass'; state = 'disabled'
               includeTargets = @(@{ id = 'all_users'; targetType = 'group' }) }
            @{ id = 'X509Certificate'; state = 'disabled'
               includeTargets = @(@{ id = 'all_users'; targetType = 'group' }) }
        )
    }
}

Describe 'Get-MsecEntraAuthenticationMethod' {

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

    It 'reports an enabled method scoped to one group as NOT available to all users' {
        # The entire point of the command. Fido2 reads 'enabled' and a report that stopped at
        # the state column would credit this tenant with having FIDO2, when two accounts can
        # use it. State and reach are different questions and the command answers both.
        InModuleScope msec -Parameters @{ P = $policy } {
            param($P)
            Mock Invoke-MsecGraphRequest {
                if ($Path -like '*authenticationMethodsPolicy*') { return $P }
                return @{ displayName = 'Emergency-Access-Accounts' }
            }
            $fido = Get-MsecEntraAuthenticationMethod | Where-Object Method -eq 'Fido2'
            $fido.State               | Should -Be 'enabled'
            $fido.AvailableToAllUsers | Should -BeFalse
            $fido.IncludeTarget       | Should -Be @('Emergency-Access-Accounts')
        }
    }

    It 'treats the all_users sentinel as everyone without looking it up as a group' {
        # all_users is a built-in token, not a directory object: a lookup 404s. Resolving it to
        # the literal string 'all_users' would also read as a group name in the output.
        InModuleScope msec -Parameters @{ P = $policy } {
            param($P)
            Mock Invoke-MsecGraphRequest {
                if ($Path -like '*authenticationMethodsPolicy*') { return $P }
                if ($Path -like '*all_users*') { throw 'Request_ResourceNotFound' }
                return @{ displayName = 'Emergency-Access-Accounts' }
            }
            $sms = Get-MsecEntraAuthenticationMethod | Where-Object Method -eq 'Sms'
            $sms.AvailableToAllUsers | Should -BeTrue
            $sms.IncludeTarget       | Should -Be @('All users')
        }
    }

    It 'does not call AvailableToAllUsers true for a disabled method targeted at everyone' {
        # TemporaryAccessPass ships targeted at all users and switched off. Reading the target
        # alone would report it as available tenant-wide; reading the state alone would lose
        # that it is ready to turn on. Both are needed, and AvailableToAllUsers is the AND.
        InModuleScope msec -Parameters @{ P = $policy } {
            param($P)
            Mock Invoke-MsecGraphRequest {
                if ($Path -like '*authenticationMethodsPolicy*') { return $P }
                return @{ displayName = 'x' }
            }
            $tap = Get-MsecEntraAuthenticationMethod | Where-Object Method -eq 'TemporaryAccessPass'
            $tap.State               | Should -Be 'disabled'
            $tap.AvailableToAllUsers | Should -BeFalse
            $tap.IncludeTarget       | Should -Be @('All users')
        }
    }

    It 'returns disabled methods so "turned off" stays distinct from "does not exist"' {
        InModuleScope msec -Parameters @{ P = $policy } {
            param($P)
            Mock Invoke-MsecGraphRequest {
                if ($Path -like '*authenticationMethodsPolicy*') { return $P }
                return @{ displayName = 'x' }
            }
            $all = Get-MsecEntraAuthenticationMethod
            $all.Count | Should -Be 5
            ($all | Where-Object State -eq 'disabled').Method |
                Should -Be @('TemporaryAccessPass', 'X509Certificate')
        }
    }

    It 'classifies phishing resistance by Microsoft''s list, disabled methods included' {
        InModuleScope msec -Parameters @{ P = $policy } {
            param($P)
            Mock Invoke-MsecGraphRequest {
                if ($Path -like '*authenticationMethodsPolicy*') { return $P }
                return @{ displayName = 'x' }
            }
            $all = Get-MsecEntraAuthenticationMethod
            ($all | Where-Object IsPhishingResistant).Method |
                Should -Be @('Fido2', 'X509Certificate')
        }
    }

    It '-PhishableOnly returns enabled non-resistant methods and excludes disabled ones' {
        # A disabled weak method is not a way in, so it must not appear in the list of ways in.
        InModuleScope msec -Parameters @{ P = $policy } {
            param($P)
            Mock Invoke-MsecGraphRequest {
                if ($Path -like '*authenticationMethodsPolicy*') { return $P }
                return @{ displayName = 'x' }
            }
            (Get-MsecEntraAuthenticationMethod -PhishableOnly).Method |
                Should -Be @('MicrosoftAuthenticator', 'Sms')
        }
    }

    It '-EnabledOnly drops the disabled methods' {
        InModuleScope msec -Parameters @{ P = $policy } {
            param($P)
            Mock Invoke-MsecGraphRequest {
                if ($Path -like '*authenticationMethodsPolicy*') { return $P }
                return @{ displayName = 'x' }
            }
            (Get-MsecEntraAuthenticationMethod -EnabledOnly).Method |
                Should -Be @('Fido2', 'MicrosoftAuthenticator', 'Sms')
        }
    }

    It 'reports a group id when the name cannot be resolved, rather than a blank' {
        # A target that cannot be named is still a target. Blanking it would read as "no
        # restriction", which inverts the meaning.
        InModuleScope msec {
            Mock Invoke-MsecGraphRequest {
                if ($Path -like '*authenticationMethodsPolicy*') {
                    return @{ authenticationMethodConfigurations = @(
                        @{ id = 'Sms'; state = 'enabled'
                           includeTargets = @(@{ id = 'deleted-group-id'; targetType = 'group' }) }) }
                }
                throw 'Request_ResourceNotFound'
            }
            $sms = Get-MsecEntraAuthenticationMethod
            $sms.IncludeTarget       | Should -Be @('deleted-group-id')
            $sms.AvailableToAllUsers | Should -BeFalse
        }
    }

    It 'resolves each distinct group once however many methods target it' {
        InModuleScope msec -Parameters @{ P = $policy } {
            param($P)
            Mock Invoke-MsecGraphRequest {
                if ($Path -like '*authenticationMethodsPolicy*') { return $P }
                return @{ displayName = 'Emergency-Access-Accounts' }
            }
            Get-MsecEntraAuthenticationMethod | Out-Null
            # g-break-glass is targeted twice - Fido2 includes it, MicrosoftAuthenticator
            # excludes it - and all_users costs no call at all, so the whole policy comes to
            # exactly one group lookup.
            Should -Invoke Invoke-MsecGraphRequest -Times 1 -Exactly -ParameterFilter {
                $Path -like '*groups/g-break-glass*'
            }
        }
    }
    It 'names the missing permission when Graph refuses the policy' {
        InModuleScope msec {
            Mock Invoke-MsecGraphRequest { throw 'Response status code does not indicate success: 403 (Forbidden).' }
            { Get-MsecEntraAuthenticationMethod } | Should -Throw '*Policy.Read.All*'
        }
    }

    It 'emits the documented type so the table view applies' {
        InModuleScope msec -Parameters @{ P = $policy } {
            param($P)
            Mock Invoke-MsecGraphRequest {
                if ($Path -like '*authenticationMethodsPolicy*') { return $P }
                return @{ displayName = 'x' }
            }
            (Get-MsecEntraAuthenticationMethod)[0].PSObject.TypeNames |
                Should -Contain 'MsecEntraAuthenticationMethod'
        }
    }
}