tests/PermissionAudit.Tests.ps1

#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' }

<#
.SYNOPSIS
    Pester tests for the -PermissionAudit switch and Invoke-AZSCPermissionAudit function.
 
.DESCRIPTION
    Validates:
      - The -PermissionAudit and -IncludeEntraPermissions switches exist on Invoke-AzureScout
      - Invoke-AZSCPermissionAudit function signature (parameters, OutputFormat ValidateSet)
      - Return object structure when function is invoked with no Azure context
      - IncludeEntraPermissions switch is exposed and documented in the function
    No live Azure authentication is required.
 
.NOTES
    Author: AzureScout Contributors
    Version: 1.0.0
    Created: 2026-02-24
    Phase: 20 — Permission Audit Feature
#>


BeforeAll {
    $script:ModuleRoot      = Split-Path -Parent $PSScriptRoot
    $script:InvokeScript    = Join-Path $script:ModuleRoot 'Modules' 'Public' 'PublicFunctions' 'Invoke-AzureScout.ps1'
    $script:AuditScript     = Join-Path $script:ModuleRoot 'Modules' 'Private' 'Main' 'Invoke-AZTIPermissionAudit.ps1'

    # Dot-source both scripts to inspect function metadata
    . $script:InvokeScript
    . $script:AuditScript

    $script:InvokeCmd = Get-Command -Name Invoke-AzureScout         -ErrorAction SilentlyContinue
    $script:AuditCmd  = Get-Command -Name Invoke-AZSCPermissionAudit -ErrorAction SilentlyContinue
}

# ===================================================================
# Switch parameters on Invoke-AzureScout
# ===================================================================
Describe 'PermissionAudit switches on Invoke-AzureScout' {
    It 'Invoke-AzureScout function is available' {
        $script:InvokeCmd | Should -Not -BeNullOrEmpty
    }

    It 'PermissionAudit switch parameter exists' {
        $script:InvokeCmd.Parameters.ContainsKey('PermissionAudit') | Should -BeTrue
    }

    It 'PermissionAudit is a [switch] type' {
        $script:InvokeCmd.Parameters['PermissionAudit'].ParameterType | Should -Be ([System.Management.Automation.SwitchParameter])
    }

    It 'PermissionAudit has alias "AuditPermissions"' {
        $aliases = $script:InvokeCmd.Parameters['PermissionAudit'].Aliases
        $aliases | Should -Contain 'AuditPermissions'
    }

    It 'PermissionAudit has alias "CheckPermissions"' {
        $aliases = $script:InvokeCmd.Parameters['PermissionAudit'].Aliases
        $aliases | Should -Contain 'CheckPermissions'
    }

    It 'IncludeEntraPermissions switch parameter exists' {
        $script:InvokeCmd.Parameters.ContainsKey('IncludeEntraPermissions') | Should -BeTrue
    }

    It 'IncludeEntraPermissions is a [switch] type' {
        $script:InvokeCmd.Parameters['IncludeEntraPermissions'].ParameterType | Should -Be ([System.Management.Automation.SwitchParameter])
    }

    It 'IncludeEntraPermissions has alias "EntraAudit"' {
        $aliases = $script:InvokeCmd.Parameters['IncludeEntraPermissions'].Aliases
        $aliases | Should -Contain 'EntraAudit'
    }

    It 'IncludeEntraPermissions has alias "CheckEntraPermissions"' {
        $aliases = $script:InvokeCmd.Parameters['IncludeEntraPermissions'].Aliases
        $aliases | Should -Contain 'CheckEntraPermissions'
    }
}

# ===================================================================
# Invoke-AZSCPermissionAudit function signature
# ===================================================================
Describe 'Invoke-AZSCPermissionAudit — Function Signature' {
    It 'Invoke-AZSCPermissionAudit function is available after dot-source' {
        $script:AuditCmd | Should -Not -BeNullOrEmpty
    }

    It 'IncludeEntraPermissions switch parameter exists' {
        $script:AuditCmd.Parameters.ContainsKey('IncludeEntraPermissions') | Should -BeTrue
    }

    It 'IncludeEntraPermissions is a [switch] type' {
        $script:AuditCmd.Parameters['IncludeEntraPermissions'].ParameterType | Should -Be ([System.Management.Automation.SwitchParameter])
    }

    It 'TenantID parameter exists' {
        $script:AuditCmd.Parameters.ContainsKey('TenantID') | Should -BeTrue
    }

    It 'TenantID is a [string] type' {
        $script:AuditCmd.Parameters['TenantID'].ParameterType | Should -Be ([string])
    }

    It 'OutputFormat parameter exists' {
        $script:AuditCmd.Parameters.ContainsKey('OutputFormat') | Should -BeTrue
    }

    It 'OutputFormat ValidateSet contains "Console"' {
        $vs = $script:AuditCmd.Parameters['OutputFormat'].Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
        $vs.ValidValues | Should -Contain 'Console'
    }

    It 'OutputFormat ValidateSet contains "Markdown"' {
        $vs = $script:AuditCmd.Parameters['OutputFormat'].Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
        $vs.ValidValues | Should -Contain 'Markdown'
    }

    It 'OutputFormat ValidateSet contains "AsciiDoc"' {
        $vs = $script:AuditCmd.Parameters['OutputFormat'].Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
        $vs.ValidValues | Should -Contain 'AsciiDoc'
    }

    It 'OutputFormat ValidateSet contains "Json"' {
        $vs = $script:AuditCmd.Parameters['OutputFormat'].Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
        $vs.ValidValues | Should -Contain 'Json'
    }

    It 'OutputFormat ValidateSet contains "All"' {
        $vs = $script:AuditCmd.Parameters['OutputFormat'].Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
        $vs.ValidValues | Should -Contain 'All'
    }

    It 'OutputFormat default value is "Console"' {
        $default = $script:AuditCmd.Parameters['OutputFormat'].DefaultValue
        if ($null -eq $default) {
            Set-ItResult -Skipped -Because 'Default not accessible via reflection'
        } else {
            $default | Should -Be 'Console'
        }
    }

    It 'ReportDir parameter exists' {
        $script:AuditCmd.Parameters.ContainsKey('ReportDir') | Should -BeTrue
    }

    It 'SubscriptionID parameter exists' {
        $script:AuditCmd.Parameters.ContainsKey('SubscriptionID') | Should -BeTrue
    }

    It 'SubscriptionID accepts an array of strings' {
        $script:AuditCmd.Parameters['SubscriptionID'].ParameterType | Should -Be ([string[]])
    }
}

# ===================================================================
# No-auth graceful behavior
# ===================================================================
Describe 'Invoke-AZSCPermissionAudit — No Azure Context Behavior' {
    It 'Returns $null when no Azure context is present (no auth)' {
        # The function calls Get-AzContext internally; without auth it returns $null
        # and the function returns $null early with an error message — not a throw
        $result = Invoke-AZSCPermissionAudit -ErrorAction SilentlyContinue 2>$null
        # If no context, function should return null (not throw)
        # If somehow context IS present (CI with auth), this still validates the result type
        if ($null -ne $result) {
            $result | Should -BeOfType [PSCustomObject]
        } else {
            $result | Should -BeNullOrEmpty
        }
    }

    It 'Does not throw terminating error when Get-AzContext returns $null' {
        { Invoke-AZSCPermissionAudit -ErrorAction SilentlyContinue 2>$null } | Should -Not -Throw
    }
}

# ===================================================================
# Return object structure (source analysis)
# ===================================================================
Describe 'Invoke-AZSCPermissionAudit — Return Object Structure (Source Analysis)' {
    BeforeAll {
        $script:AuditSource = Get-Content -Path $script:AuditScript -Raw
    }

    It 'Source defines ArmAccess in the return object' {
        $script:AuditSource | Should -Match 'ArmAccess'
    }

    It 'Source defines GraphAccess in the return object' {
        $script:AuditSource | Should -Match 'GraphAccess'
    }

    It 'Source defines CallerAccount in the return object' {
        $script:AuditSource | Should -Match 'CallerAccount'
    }

    It 'Source defines OverallReadiness in the return object' {
        $script:AuditSource | Should -Match 'OverallReadiness'
    }

    It 'Source defines ArmDetails in the return object' {
        $script:AuditSource | Should -Match 'armDetails|ArmDetails'
    }

    It 'Source defines ProviderResults in the return object' {
        $script:AuditSource | Should -Match 'providerResults|ProviderResults'
    }

    It 'Source defines GraphDetails in the return object' {
        $script:AuditSource | Should -Match 'graphDetails|GraphDetails'
    }

    It 'Source defines Recommendations in the return object' {
        $script:AuditSource | Should -Match 'recommendations|Recommendations'
    }

    It 'OverallReadiness uses switch expression with Insufficient state' {
        $script:AuditSource | Should -Match 'Insufficient'
    }

    It 'OverallReadiness includes FullARM state' {
        $script:AuditSource | Should -Match 'FullARM'
    }

    It 'OverallReadiness includes FullARMAndEntra state' {
        $script:AuditSource | Should -Match 'FullARMAndEntra'
    }

    It 'Source has separate ARM path and Entra path (IncludeEntraPermissions guard)' {
        $script:AuditSource | Should -Match 'IncludeEntraPermissions'
    }
}

# ===================================================================
# Invoke-AzureScout source-level PermissionAudit routing
# ===================================================================
Describe 'PermissionAudit routing in Invoke-AzureScout source' {
    BeforeAll {
        $script:InvokeSource = Get-Content -Path $script:InvokeScript -Raw
    }

    It 'Invoke-AzureScout calls Invoke-AZSCPermissionAudit when -PermissionAudit is set' {
        $script:InvokeSource | Should -Match 'Invoke-AZSCPermissionAudit'
    }

    It 'Source passes IncludeEntraPermissions to the audit function' {
        $script:InvokeSource | Should -Match 'IncludeEntraPermissions'
    }

    It 'Source passes SubscriptionID to the audit function' {
        $script:InvokeSource | Should -Match 'Invoke-AZSCPermissionAudit[\s\S]*-SubscriptionID'
    }

    It 'Source has early-return or conditional block that skips inventory when PermissionAudit is used' {
        $script:InvokeSource | Should -Match 'PermissionAudit'
    }
}