tests/Analyze.IacGap.Tests.ps1
|
#Requires -Version 7.0 #Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } <# Pester tests for src/analyze/Get-ScoutIacGap.ps1 — Bicep/IaC gap detection (ADO Story AB#325). Builds synthetic collect-shaped resources and throwaway template files under $env:TEMP — no Azure connection, no network calls. #> BeforeAll { $script:Root = Split-Path $PSScriptRoot -Parent . "$script:Root/src/analyze/Get-ScoutIacGap.ps1" } Describe 'Get-ScoutIacGap -- graceful degradation with no -TemplatePath' { BeforeAll { $script:Resources = @( [pscustomobject]@{ Type = 'Microsoft.Network/virtualNetworks'; Name = 'vnet-hub'; ResourceGroup = 'rg-connectivity' } ) } It 'returns HasTemplates = $false when -TemplatePath is omitted' { $result = Get-ScoutIacGap -CollectData $script:Resources $result.HasTemplates | Should -BeFalse $result.Message | Should -Not -BeNullOrEmpty $result.Unmanaged | Should -BeNullOrEmpty } It 'returns HasTemplates = $false when -TemplatePath does not exist' { $result = Get-ScoutIacGap -CollectData $script:Resources -TemplatePath (Join-Path $env:TEMP "ScoutIacGapMissing_$([guid]::NewGuid().ToString('N'))") $result.HasTemplates | Should -BeFalse } It 'returns HasTemplates = $false when -TemplatePath has no .bicep/.json files' { $emptyFolder = Join-Path $env:TEMP "ScoutIacGapEmpty_$([guid]::NewGuid().ToString('N'))" New-Item -ItemType Directory -Path $emptyFolder -Force | Out-Null try { 'not a template' | Out-File (Join-Path $emptyFolder 'notes.txt') $result = Get-ScoutIacGap -CollectData $script:Resources -TemplatePath $emptyFolder $result.HasTemplates | Should -BeFalse } finally { Remove-Item $emptyFolder -Recurse -Force -ErrorAction SilentlyContinue } } It 'does not throw' { { Get-ScoutIacGap -CollectData $script:Resources } | Should -Not -Throw } } Describe 'Get-ScoutIacGap -- unmanaged vs matched detection' { BeforeAll { $script:TemplateFolder = Join-Path $env:TEMP "ScoutIacGapTemplates_$([guid]::NewGuid().ToString('N'))" New-Item -ItemType Directory -Path $script:TemplateFolder -Force | Out-Null # Bicep template declaring vnet-hub (a literal, resolvable name) @" resource vnetHub 'Microsoft.Network/virtualNetworks@2023-05-01' = { name: 'vnet-hub' location: 'eastus' properties: {} } "@ | Out-File (Join-Path $script:TemplateFolder 'network.bicep') -Encoding utf8 # ARM JSON template declaring stgprod01 (a literal, resolvable name) @' { "resources": [ { "type": "Microsoft.Storage/storageAccounts", "apiVersion": "2023-01-01", "name": "stgprod01", "properties": {} }, { "type": "Microsoft.KeyVault/vaults", "apiVersion": "2023-07-01", "name": "[parameters('kvName')]", "properties": {} } ] } '@ | Out-File (Join-Path $script:TemplateFolder 'storage.json') -Encoding utf8 # discovered resources: vnet-hub (templated), stgprod01 (templated), # kv-prod01 (type present in templates but name is parameter-driven/unresolved), # vm-app03 (type not present in templates at all -- fully unmanaged) $script:Resources = @( [pscustomobject]@{ Type = 'Microsoft.Network/virtualNetworks'; Name = 'vnet-hub'; ResourceGroup = 'rg-connectivity' } [pscustomobject]@{ Type = 'Microsoft.Storage/storageAccounts'; Name = 'stgprod01'; ResourceGroup = 'rg-workload1' } [pscustomobject]@{ Type = 'Microsoft.KeyVault/vaults'; Name = 'kv-prod01'; ResourceGroup = 'rg-workload1' } [pscustomobject]@{ Type = 'Microsoft.Compute/virtualMachines'; Name = 'vm-app03'; ResourceGroup = 'rg-sandbox' } ) $script:Result = Get-ScoutIacGap -CollectData $script:Resources -TemplatePath $script:TemplateFolder } AfterAll { Remove-Item $script:TemplateFolder -Recurse -Force -ErrorAction SilentlyContinue } It 'has templates and processed both template files' { $script:Result.HasTemplates | Should -BeTrue $script:Result.TemplateFileCount | Should -Be 2 } It 'matches the templated, literally-named resources and does not flag them' { @($script:Result.Matched | Where-Object Name -eq 'vnet-hub').Count | Should -Be 1 @($script:Result.Matched | Where-Object Name -eq 'stgprod01').Count | Should -Be 1 @($script:Result.Unmanaged | Where-Object Name -eq 'vnet-hub').Count | Should -Be 0 @($script:Result.Unmanaged | Where-Object Name -eq 'stgprod01').Count | Should -Be 0 } It 'flags the fully-unmanaged resource (type absent from every template) as High severity' { $vm = $script:Result.Unmanaged | Where-Object Name -eq 'vm-app03' $vm | Should -Not -BeNullOrEmpty $vm.TypePresentInTemplates | Should -BeFalse $vm.Severity | Should -Be 'High' } It 'flags the parameter-driven-name resource as Medium severity (type present, name unresolved)' { $kv = $script:Result.Unmanaged | Where-Object Name -eq 'kv-prod01' $kv | Should -Not -BeNullOrEmpty $kv.TypePresentInTemplates | Should -BeTrue $kv.Severity | Should -Be 'Medium' } It 'does not report templated-but-missing entries unless requested' { $script:Result.TemplatedButMissing | Should -BeNullOrEmpty } } Describe 'Get-ScoutIacGap -- -IncludeTemplatedButMissing' { BeforeAll { $script:TemplateFolder = Join-Path $env:TEMP "ScoutIacGapMissing2_$([guid]::NewGuid().ToString('N'))" New-Item -ItemType Directory -Path $script:TemplateFolder -Force | Out-Null @" resource vnetHub 'Microsoft.Network/virtualNetworks@2023-05-01' = { name: 'vnet-never-deployed' properties: {} } "@ | Out-File (Join-Path $script:TemplateFolder 'network.bicep') -Encoding utf8 $script:Resources = @( [pscustomobject]@{ Type = 'Microsoft.Network/virtualNetworks'; Name = 'vnet-hub'; ResourceGroup = 'rg-connectivity' } ) } AfterAll { Remove-Item $script:TemplateFolder -Recurse -Force -ErrorAction SilentlyContinue } It 'reports the templated-but-undeployed resource only when the switch is passed' { $withoutSwitch = Get-ScoutIacGap -CollectData $script:Resources -TemplatePath $script:TemplateFolder $withoutSwitch.TemplatedButMissing | Should -BeNullOrEmpty $withSwitch = Get-ScoutIacGap -CollectData $script:Resources -TemplatePath $script:TemplateFolder -IncludeTemplatedButMissing $withSwitch.TemplatedButMissing.Count | Should -Be 1 $withSwitch.TemplatedButMissing[0].Name | Should -Be 'vnet-never-deployed' } } Describe 'Get-ScoutIacGap -- consumes the nested collect.json shape directly' { BeforeAll { $script:TemplateFolder = Join-Path $env:TEMP "ScoutIacGapNested_$([guid]::NewGuid().ToString('N'))" New-Item -ItemType Directory -Path $script:TemplateFolder -Force | Out-Null @" resource vnetHub 'Microsoft.Network/virtualNetworks@2023-05-01' = { name: 'vnet-hub' properties: {} } "@ | Out-File (Join-Path $script:TemplateFolder 'network.bicep') -Encoding utf8 $script:SampleCollectPath = Join-Path $script:Root 'tests/datadump/sample-collect.json' $script:Collect = Get-Content $script:SampleCollectPath -Raw | ConvertFrom-Json } AfterAll { Remove-Item $script:TemplateFolder -Recurse -Force -ErrorAction SilentlyContinue } It 'flattens networking.virtualNetworks from the real sample-collect.json fixture' { $result = Get-ScoutIacGap -CollectData $script:Collect -TemplatePath $script:TemplateFolder $result.DiscoveredCount | Should -BeGreaterThan 0 @($result.Matched | Where-Object Name -eq 'vnet-hub').Count | Should -Be 1 @($result.Unmanaged | Where-Object Name -eq 'vnet-spoke1').Count | Should -Be 1 } } |