tests/Assessment.PermissionAudit.Tests.ps1

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

<#
    Pester tests for src/assess/Test-ScoutPermission.ps1 -- the read-only
    permission pre-flight (ADO Story AB#5051).

    Focus: the $Manifest[$_] null-crash class found in a StrictMode sweep.
    `$Manifest[$_].Ingest -contains 'AzGovViz'` dots directly into whatever
    `$Manifest[$_]` returns -- a bracket lookup on a Hashtable/[ordered] map
    returns $null (not a throw) for a key that isn't present, but the
    IMMEDIATELY FOLLOWING `.Ingest` dot-access on that $null throws
    PropertyNotFoundException under Set-StrictMode -Version Latest. This is
    reachable whenever -Assessment names something that isn't actually a key
    in manifests/assessments.psd1 (a caller-supplied name never validated
    against the manifest, since Test-ScoutPermission has no ValidateSet on
    -Assessment and is a standalone, directly-callable function).

    No live Azure authentication is required -- Get-AzContext/Get-AzRoleAssignment
    are stubbed with local functions of the same name, which PowerShell resolves
    ahead of any same-named command from an Az module that may or may not be
    loaded in the test session.
#>


BeforeAll {
    $script:Root = Split-Path $PSScriptRoot -Parent

    # Stubs -- Test-ScoutPermission.ps1 never imports Az.Accounts/Az.Resources
    # itself (the caller is expected to already have an authenticated context),
    # so these local functions satisfy its two Az calls without any live
    # Azure connection or Az module dependency in this test.
    function Get-AzContext {
        [pscustomobject]@{
            Tenant  = [pscustomobject]@{ Id = '00000000-0000-0000-0000-000000000000' }
            Account = [pscustomobject]@{ Id = 'test-user@contoso.com' }
        }
    }
    function Get-AzRoleAssignment { param([string]$Scope, [string]$SignInName, [string]$ErrorAction) @() }

    . "$script:Root/src/assess/Test-ScoutPermission.ps1"
}

Describe 'Test-ScoutPermission -- unknown/missing manifest key crash class (StrictMode sweep)' {
    It 'does not throw when -Assessment names a value that is not a key in -Manifest at all' {
        $manifest = @{ LandingZone = @{ Ingest = @('AzGovViz') } }
        $results = Test-ScoutPermission -Assessment @('NotARealAssessmentName') -Manifest $manifest 6>$null
        @($results).Count | Should -BeGreaterThan 0
    }

    It 'does not throw when -Manifest itself is $null' {
        $results = Test-ScoutPermission -Assessment @('LandingZone') -Manifest $null 6>$null
        @($results).Count | Should -BeGreaterThan 0
    }

    It 'still detects the AzGovViz ingest requirement for an assessment that IS a valid manifest key' {
        $manifest = @{ LandingZone = @{ Ingest = @('AzGovViz') } }
        $results = Test-ScoutPermission -Assessment @('LandingZone') -Manifest $manifest 6>$null
        # @() wrap is load-bearing here too: a Where-Object match of zero items
        # collapses the bare expression to $null, and $null.Count is exactly the
        # crash class this whole test file exists to guard against.
        @($results | Where-Object Check -like 'Graph:*').Count | Should -BeGreaterThan 0
    }

    It 'reports no Graph checks when every named assessment is missing/unknown (no AzGovViz ingest found)' {
        $manifest = @{ LandingZone = @{ Ingest = @('AzGovViz') } }
        $results = Test-ScoutPermission -Assessment @('DoesNotExist1', 'DoesNotExist2') -Manifest $manifest 6>$null
        @($results | Where-Object Check -like 'Graph:*').Count | Should -Be 0
        # The ARM Reader @ MG root check always runs regardless.
        @($results | Where-Object Check -eq 'ARM Reader @ MG root').Count | Should -Be 1
    }
}