tests/Collect.TenantWideResources.Tests.ps1
|
#Requires -Version 7.0 #Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } BeforeAll { $script:RepoRoot = Split-Path $PSScriptRoot -Parent . (Join-Path $script:RepoRoot 'src/collect/ConvertTo-ScoutManagementGroupHierarchy.ps1') . (Join-Path $script:RepoRoot 'src/collect/Get-ScoutTenantWideResource.ps1') function Get-TestEnvelope { param( [Parameter(Mandatory)] [object[]]$Resources, [Parameter(Mandatory)] [string]$Type ) @($Resources | Where-Object type -eq $Type)[0] } function Get-TestManagementGroupTree { $Subscription = [PSCustomObject]@{ Type = '/subscriptions' Name = 'sub-1' DisplayName = 'Production' } $Child = [PSCustomObject]@{ Type = '/providers/Microsoft.Management/managementGroups' Name = 'platform' DisplayName = 'Platform' ParentName = 'root' ParentDisplayName = 'Tenant Root' TenantId = 'tenant-1' Children = @() } [PSCustomObject]@{ Type = '/providers/Microsoft.Management/managementGroups' Name = 'root' DisplayName = 'Tenant Root' TenantId = 'tenant-1' Children = @($Child, $Subscription) } } } Describe 'ConvertTo-ScoutManagementGroupHierarchy' { It 'flattens the tree in deterministic pre-order with depth, path, and direct-child counts' { $Rows = @(ConvertTo-ScoutManagementGroupHierarchy -Root (Get-TestManagementGroupTree)) @($Rows).Count | Should -Be 2 @($Rows.'Management Group ID') | Should -Be @('root', 'platform') $Rows[0].'Full Path' | Should -Be 'Tenant Root' $Rows[0].'Hierarchy Depth' | Should -Be 0 $Rows[0].'Parent Management Group' | Should -Be 'Tenant Root' $Rows[0].'Direct Child MGs' | Should -Be 1 $Rows[0].'Direct Subscriptions' | Should -Be 1 $Rows[0].'Direct Subscription Names' | Should -Be 'Production' $Rows[1].'Display Name' | Should -Be ' Platform' $Rows[1].'Full Path' | Should -Be 'Tenant Root / Platform' $Rows[1].'Hierarchy Depth' | Should -Be 1 $Rows[1].'Parent Management Group' | Should -Be 'Tenant Root' } It 'accepts missing Children as an empty child collection under StrictMode' { $RootWithoutChildren = [PSCustomObject]@{ Name = 'root' DisplayName = 'Root' TenantId = 'tenant-1' } $Rows = @(ConvertTo-ScoutManagementGroupHierarchy -Root $RootWithoutChildren) @($Rows).Count | Should -Be 1 $Rows[0].'Direct Child MGs' | Should -Be 0 $Rows[0].'Direct Subscriptions' | Should -Be 0 } } Describe 'Get-ScoutTenantWideResource - integration contract' { BeforeEach { Mock Get-AzRoleDefinition { @( [PSCustomObject]@{ Id = 'role-1'; Name = 'Custom Operator'; IsCustom = $true } ) } Mock Get-AzContext { [PSCustomObject]@{ Tenant = [PSCustomObject]@{ Id = 'tenant-1' } } } Mock Get-AzManagementGroup { @() } Mock Get-AzManagementGroup { Get-TestManagementGroupTree } -ParameterFilter { $GroupId -eq 'tenant-1' -and $Expand -and $Recurse } } It 'returns exactly four typed envelopes in stable order' { $Resources = @(Get-ScoutTenantWideResource -ApiResources @()) @($Resources.type) | Should -Be @( 'AZSC/Management/RoleDefinition' 'AZSC/Management/ManagementGroup' 'AZSC/Management/PolicyDefinition' 'AZSC/Management/PolicySetDefinition' ) foreach ($Resource in $Resources) { $Resource.PSObject.Properties.Name | Should -Contain 'properties' $Resource.properties.GetType() | Should -Be ([object[]]) } } It 'surfaces already-collected policy payloads without making another policy call' { $Policy1 = [PSCustomObject]@{ id = 'policy-1'; name = 'one' } $Policy2 = [PSCustomObject]@{ id = 'policy-2'; name = 'two' } $Set1 = [PSCustomObject]@{ id = 'set-1'; name = 'initiative' } $ApiResources = @( [PSCustomObject]@{ PolicyDefinitions = @($Policy1) PolicySetDefinitions = @($Set1) } [PSCustomObject]@{ PolicyDefinitions = @($Policy2) PolicySetDefinitions = $null } ) $Resources = @(Get-ScoutTenantWideResource -ApiResources $ApiResources) $Policies = Get-TestEnvelope -Resources $Resources -Type 'AZSC/Management/PolicyDefinition' $PolicySets = Get-TestEnvelope -Resources $Resources -Type 'AZSC/Management/PolicySetDefinition' @($Policies.properties.id) | Should -Be @('policy-1', 'policy-2') @($PolicySets.properties.id) | Should -Be @('set-1') $Source = Get-Content -LiteralPath ( Join-Path $script:RepoRoot 'src/collect/Get-ScoutTenantWideResource.ps1' ) -Raw $Source | Should -Not -Match 'Get-AzPolicyDefinition|Get-AzPolicySetDefinition' } It 'queries custom roles once and preserves the returned objects' { $Resources = @(Get-ScoutTenantWideResource -ApiResources @()) $Roles = Get-TestEnvelope -Resources $Resources -Type 'AZSC/Management/RoleDefinition' @($Roles.properties).Count | Should -Be 1 $Roles.properties[0].Name | Should -Be 'Custom Operator' Should -Invoke Get-AzRoleDefinition -Times 1 -Exactly -ParameterFilter { $Custom -and $ErrorAction -eq 'Stop' } } It 'uses one tenant-root recursive expansion when it succeeds' { $Resources = @(Get-ScoutTenantWideResource -ApiResources @()) $Groups = Get-TestEnvelope -Resources $Resources -Type 'AZSC/Management/ManagementGroup' @($Groups.properties).Count | Should -Be 2 Should -Invoke Get-AzManagementGroup -Times 1 -Exactly -ParameterFilter { $GroupId -eq 'tenant-1' -and $Expand -and $Recurse } Should -Invoke Get-AzManagementGroup -Times 0 -Exactly -ParameterFilter { [string]::IsNullOrWhiteSpace([string]$GroupId) } } } Describe 'Get-ScoutTenantWideResource - management-group fallback' { BeforeEach { Mock Get-AzRoleDefinition { @() } Mock Get-AzContext { [PSCustomObject]@{ Tenant = [PSCustomObject]@{ Id = 'tenant-1' } } } Mock Get-AzManagementGroup { @( [PSCustomObject]@{ Name = 'root' } [PSCustomObject]@{ Name = 'child' } ) } -ParameterFilter { [string]::IsNullOrWhiteSpace([string]$GroupId) } Mock Get-AzManagementGroup { Get-TestManagementGroupTree } -ParameterFilter { $GroupId -eq 'root' } Mock Get-AzManagementGroup { [PSCustomObject]@{ Name = 'child' DisplayName = 'Child' ParentName = 'root' ParentDisplayName = 'Tenant Root' TenantId = 'tenant-1' Children = @() } } -ParameterFilter { $GroupId -eq 'child' } Mock Get-AzManagementGroup { throw 'tenant root is not readable' } -ParameterFilter { $GroupId -eq 'tenant-1' } } It 'enumerates, expands every visible group by id, and keeps roots so subtrees are not duplicated' { $Resources = @(Get-ScoutTenantWideResource -ApiResources @()) $Groups = Get-TestEnvelope -Resources $Resources -Type 'AZSC/Management/ManagementGroup' @($Groups.properties.'Management Group ID') | Should -Be @('root', 'platform') Should -Invoke Get-AzManagementGroup -Times 1 -Exactly -ParameterFilter { [string]::IsNullOrWhiteSpace([string]$GroupId) } Should -Invoke Get-AzManagementGroup -Times 1 -Exactly -ParameterFilter { $GroupId -eq 'root' } Should -Invoke Get-AzManagementGroup -Times 1 -Exactly -ParameterFilter { $GroupId -eq 'child' } } } Describe 'Get-ScoutTenantWideResource - permission degradation' { BeforeEach { Mock Get-AzContext { [PSCustomObject]@{ Tenant = [PSCustomObject]@{ Id = 'tenant-1' } } } Mock Get-AzRoleDefinition { throw 'role read forbidden' } Mock Get-AzManagementGroup { throw 'management group read forbidden' } } It 'returns empty affected envelopes with warnings and preserves policy output' { $ApiResources = @( [PSCustomObject]@{ PolicyDefinitions = @([PSCustomObject]@{ id = 'policy-1' }) PolicySetDefinitions = @() } ) $Warnings = @() $Resources = @( Get-ScoutTenantWideResource -ApiResources $ApiResources -WarningVariable +Warnings ) @((Get-TestEnvelope -Resources $Resources -Type 'AZSC/Management/RoleDefinition').properties).Count | Should -Be 0 @((Get-TestEnvelope -Resources $Resources -Type 'AZSC/Management/ManagementGroup').properties).Count | Should -Be 0 @((Get-TestEnvelope -Resources $Resources -Type 'AZSC/Management/PolicyDefinition').properties).Count | Should -Be 1 @($Warnings).Count | Should -Be 2 ($Warnings -join ' ') | Should -Match 'custom role definitions could not be read' ($Warnings -join ' ') | Should -Match 'management groups could not be read' } } |