tests/Assessment.Collect.Tests.ps1
|
#Requires -Version 7.0 #Requires -Modules Pester #Requires -Modules Az.ResourceGraph <# Pester tests for src/collect/Invoke-Collect.ps1's parameter wiring: - `-Categories` actually filters which Resource Graph queries run (rather than the pre-fix behavior of always running the full ~25-query set regardless of the value passed). - `-ManagementGroupId`, when supplied, is threaded through to every `Search-AzGraph` call as `-ManagementGroup`; omitted entirely when not supplied (preserves tenant-wide behavior). Search-AzGraph is mocked throughout — no live Azure connection is made. #> BeforeAll { $root = Split-Path $PSScriptRoot -Parent Import-Module Az.ResourceGraph -ErrorAction Stop . "$root/src/collect/Invoke-Collect.ps1" } Describe 'Invoke-Collect -Categories query filtering' { BeforeEach { Mock Search-AzGraph { return @() } } It 'a specific category (Security) runs the queries it and its cross-domain rules need, and skips unrelated-domain queries' { Invoke-Collect -Categories @('Security') | Out-Null # In-scope for Security: its own domain (keyVaults) plus the networking/ # databases queries caf.security / waf.security filter by (nsgPublicInbound, # privateEndpoints, privateDnsZones, sqlServers) — see Invoke-Collect.ps1's # $queryCategories map. Should -Invoke Search-AzGraph -ParameterFilter { $Query -match 'microsoft\.keyvault/vaults' } -Times 1 Should -Invoke Search-AzGraph -ParameterFilter { $Query -match 'microsoft\.network/networksecuritygroups' } -Times 1 Should -Invoke Search-AzGraph -ParameterFilter { $Query -match 'microsoft\.sql/servers"' } -Times 1 # Out-of-scope for Security: Web/Containers/Analytics/AI/Hybrid/Integration/ # IoT-only domain queries should NOT run. Should -Invoke Search-AzGraph -ParameterFilter { $Query -match 'microsoft\.web/sites' } -Times 0 -Exactly Should -Invoke Search-AzGraph -ParameterFilter { $Query -match 'microsoft\.containerservice/managedclusters' } -Times 0 -Exactly Should -Invoke Search-AzGraph -ParameterFilter { $Query -match 'microsoft\.synapse/workspaces' } -Times 0 -Exactly Should -Invoke Search-AzGraph -ParameterFilter { $Query -match 'microsoft\.devices/iothubs' } -Times 0 -Exactly Should -Invoke Search-AzGraph -ParameterFilter { $Query -match 'microsoft\.hybridcompute/machines"' } -Times 0 -Exactly Should -Invoke Search-AzGraph -ParameterFilter { $Query -match 'microsoft\.eventhub/namespaces' } -Times 0 -Exactly } It 'the Security run collects meaningfully fewer resource types than the full/default run' { $script:callCount = 0 Mock Search-AzGraph { $script:callCount++; return @() } Invoke-Collect -Categories @('Security') | Out-Null $securityCalls = $script:callCount $script:callCount = 0 Mock Search-AzGraph { $script:callCount++; return @() } Invoke-Collect | Out-Null $fullCalls = $script:callCount $securityCalls | Should -BeLessThan $fullCalls } It '''*'' (the default) runs the full query set, including domain-only queries Security skips' { Invoke-Collect | Out-Null Should -Invoke Search-AzGraph -ParameterFilter { $Query -match 'microsoft\.web/sites' } -Times 1 Should -Invoke Search-AzGraph -ParameterFilter { $Query -match 'microsoft\.containerservice/managedclusters' } -Times 1 Should -Invoke Search-AzGraph -ParameterFilter { $Query -match 'microsoft\.keyvault/vaults' } -Times 1 } It 'an empty -Categories list runs the full query set (same as ''*'')' { Invoke-Collect -Categories @() | Out-Null Should -Invoke Search-AzGraph -ParameterFilter { $Query -match 'microsoft\.web/sites' } -Times 1 } It 'always runs the base subscriptions query regardless of category' { Invoke-Collect -Categories @('Security') | Out-Null Should -Invoke Search-AzGraph -ParameterFilter { $Query -match 'microsoft\.resources/subscriptions' } -Times 1 } } Describe 'Invoke-Collect tag aggregation (AB#367)' { BeforeEach { Mock Search-AzGraph { if ($Query -match 'microsoft\.resources/subscriptions') { return @( [pscustomobject]@{ id = 'sub-0001'; name = 'sub-prod-a'; state = 'Enabled'; tags = [pscustomobject]@{ Environment = 'Production'; CostCenter = 'CC-100' } } [pscustomobject]@{ id = 'sub-0002'; name = 'sub-prod-b'; state = 'Enabled'; tags = [pscustomobject]@{ Environment = 'Production' } } [pscustomobject]@{ id = 'sub-0003'; name = 'sub-dev'; state = 'Enabled'; tags = [pscustomobject]@{ Environment = 'Dev'; CostCenter = 'CC-200' } } [pscustomobject]@{ id = 'sub-0004'; name = 'sub-sbx'; state = 'Enabled'; tags = [pscustomobject]@{} } [pscustomobject]@{ id = 'sub-0005'; name = 'sub-notag'; state = 'Enabled'; tags = $null } ) } return @() } } It 'produces one entry per distinct tag key with deduplicated, sorted values across all subscriptions' { $result = Invoke-Collect -Categories @('*') # Exactly one entry per distinct KEY (Environment, CostCenter) -- not one # per subscription, and not a raw concatenation of per-subscription bags. $result.tags.Count | Should -Be 2 $envEntry = $result.tags | Where-Object { $_.key -eq 'Environment' } $envEntry | Should -Not -BeNullOrEmpty # sub-0001 and sub-0002 both tag Environment=Production -- the shared value # must collapse to one unique entry, not be double-counted. @($envEntry.values).Count | Should -Be 2 @($envEntry.values) | Should -Be @('Dev', 'Production') $ccEntry = $result.tags | Where-Object { $_.key -eq 'CostCenter' } $ccEntry | Should -Not -BeNullOrEmpty @($ccEntry.values).Count | Should -Be 2 @($ccEntry.values) | Should -Be @('CC-100', 'CC-200') } It 'does not error on subscriptions with empty ({}) or null tag bags' { { Invoke-Collect -Categories @('*') } | Should -Not -Throw } } Describe 'Invoke-Collect -ManagementGroupId scoping' { BeforeEach { Mock Search-AzGraph { return @() } } It 'passes -ManagementGroup to every Search-AzGraph call when an id is supplied' { Invoke-Collect -Categories @('Security') -ManagementGroupId 'contoso-root-mg' | Out-Null # Every call this run made should carry the management group -- none should # be missing it. Should -Invoke Search-AzGraph -ParameterFilter { $null -eq $ManagementGroup } -Times 0 -Exactly Should -Invoke Search-AzGraph -ParameterFilter { $ManagementGroup -eq 'contoso-root-mg' } -Times 1 } It 'omits -ManagementGroup entirely when no id is supplied (preserves tenant-wide scope)' { Invoke-Collect -Categories @('Security') | Out-Null Should -Invoke Search-AzGraph -ParameterFilter { $null -ne $ManagementGroup } -Times 0 -Exactly } } |