tests/Compute.Module.Tests.ps1
|
#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } #Requires -Modules ImportExcel <# .SYNOPSIS Pester tests for all Compute inventory modules. .DESCRIPTION Tests both Processing and Reporting phases for each Compute module using synthetic mock data. No live Azure authentication is required. .NOTES Author: AzureScout Contributors Version: 1.0.0 Created: 2026-02-24 Phase: 15.3, 19.2, 19.4 — Compute / AVD / VM Enhancement Testing #> # =================================================================== # DISCOVERY-TIME: module spec table (outside BeforeAll for -ForEach) # =================================================================== $ComputePath = Join-Path (Split-Path -Parent $PSScriptRoot) 'Modules' 'Public' 'InventoryModules' 'Compute' $ComputeModules = @( @{ Name = 'VirtualMachine'; File = 'VirtualMachine.ps1'; Type = 'microsoft.compute/virtualmachines'; Worksheet = 'Virtual Machines' } @{ Name = 'VirtualMachineScaleSet'; File = 'VirtualMachineScaleSet.ps1'; Type = 'microsoft.compute/virtualmachinescalesets'; Worksheet = 'VM Scale Sets' } @{ Name = 'VMDisk'; File = 'VMDisk.ps1'; Type = 'microsoft.compute/disks'; Worksheet = 'Disks' } @{ Name = 'AvailabilitySets'; File = 'AvailabilitySets.ps1'; Type = 'microsoft.compute/availabilitysets'; Worksheet = 'Availability Sets' } @{ Name = 'CloudServices'; File = 'CloudServices.ps1'; Type = 'microsoft.classiccompute/domainnames'; Worksheet = 'Cloud Services' } @{ Name = 'AVDApplicationGroups'; File = 'AVDApplicationGroups.ps1'; Type = 'microsoft.desktopvirtualization/applicationgroups'; Worksheet = 'AVD Application Groups' } @{ Name = 'AVDWorkspaces'; File = 'AVDWorkspaces.ps1'; Type = 'microsoft.desktopvirtualization/workspaces'; Worksheet = 'AVD Workspaces' } @{ Name = 'AVDSessionHosts'; File = 'AVDSessionHosts.ps1'; Type = 'microsoft.desktopvirtualization/hostpools/sessionhosts'; Worksheet = 'AVD Session Hosts' } @{ Name = 'AVDScalingPlans'; File = 'AVDScalingPlans.ps1'; Type = 'microsoft.desktopvirtualization/scalingplans'; Worksheet = 'AVD Scaling Plans' } @{ Name = 'AVDApplications'; File = 'AVDApplications.ps1'; Type = 'microsoft.desktopvirtualization/applicationgroups'; Worksheet = 'AVD Applications' } @{ Name = 'VMWare'; File = 'VMWare.ps1'; Type = 'Microsoft.AVS/privateClouds'; Worksheet = 'VMWare' } @{ Name = 'VMOperationalData'; File = 'VMOperationalData.ps1'; Type = 'microsoft.compute/virtualmachines'; Worksheet = 'VM Operational' } @{ Name = 'AVDAzureLocal'; File = 'AVDAzureLocal.ps1'; Type = 'microsoft.hybridcompute/machines'; Worksheet = 'AVD Azure Local' } ) # AVD Host Pools needs its own spec (Worksheet name differs from plan) $AVDHostPoolSpec = @{ Name = 'AVD'; File = 'AVD.ps1'; Type = 'microsoft.desktopvirtualization/hostpools'; Worksheet = 'AVD Host Pools' } # =================================================================== # EXECUTION-TIME SETUP # =================================================================== BeforeAll { $script:ModuleRoot = Split-Path -Parent $PSScriptRoot $script:ComputePath = Join-Path $script:ModuleRoot 'Modules' 'Public' 'InventoryModules' 'Compute' $script:TempDir = Join-Path $env:TEMP 'AZSC_ComputeTests' if (Test-Path $script:TempDir) { Remove-Item $script:TempDir -Recurse -Force } New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null function New-MockVM { param([string]$Id, [string]$Name, [string]$Location = 'eastus', [string]$SubscriptionId = 'sub-00000001', [string]$RG = 'rg-test', [object]$Props) [PSCustomObject]@{ id = $Id NAME = $Name TYPE = 'microsoft.compute/virtualmachines' LOCATION = $Location RESOURCEGROUP = $RG subscriptionId = $SubscriptionId KIND = '' tags = [PSCustomObject]@{} PROPERTIES = $Props } } function New-MockResource { param([string]$Id, [string]$Name, [string]$Type, [string]$Location = 'eastus', [string]$SubscriptionId = 'sub-00000001', [string]$RG = 'rg-test', [object]$Props, [string]$Kind = '', [string]$ManagedBy = '') [PSCustomObject]@{ id = $Id NAME = $Name TYPE = $Type LOCATION = $Location RESOURCEGROUP = $RG subscriptionId = $SubscriptionId KIND = $Kind tags = [PSCustomObject]@{} PROPERTIES = $Props MANAGEDBY = $ManagedBy } } function Invoke-ComputeModule { param([string]$ModuleFile, [string]$Task, [object]$Resources = $null, [object]$SmaResources = $null, [string]$File = $null, [string]$TableStyle = 'Light20') $content = Get-Content -Path $ModuleFile -Raw $sb = [ScriptBlock]::Create($content) Invoke-Command -ScriptBlock $sb -ArgumentList $null, $null, $null, $Resources, $null, $Task, $File, $SmaResources, $TableStyle, $null } # ── Mock resources ──────────────────────────────────────────────── $script:MockResources = @() # Virtual Machine $script:MockResources += New-MockVM -Id '/sub/sub-00000001/vm/vm01' -Name 'vm-prod-01' -Props ([PSCustomObject]@{ storageProfile = [PSCustomObject]@{ osDisk = [PSCustomObject]@{ osType = 'Windows'; diskSizeGB = 128 } dataDisks = @() imageReference = [PSCustomObject]@{ publisher = 'MicrosoftWindowsServer'; offer = 'WindowsServer'; sku = '2022-Datacenter'; version = 'latest' } } hardwareProfile = [PSCustomObject]@{ vmSize = 'Standard_D4s_v3' } osProfile = [PSCustomObject]@{ computerName = 'vm-prod-01'; adminUsername = 'azureuser' } networkProfile = [PSCustomObject]@{ networkInterfaces = @(@{ id = '/sub/sub-00000001/nic/nic01' }) } provisioningState = 'Succeeded' powerState = 'PowerState/running' timeCreated = '2025-06-15T08:30:00Z' }) # VM Disk $script:MockResources += New-MockResource -Id '/subscriptions/sub-00000001/resourceGroups/rg-test/providers/microsoft.compute/disks/disk-os-01' -Name 'disk-os-01' -Type 'microsoft.compute/disks' -ManagedBy '/subscriptions/sub-00000001/resourceGroups/rg-test/providers/Microsoft.Compute/virtualMachines/vm-prod-01' -Props ([PSCustomObject]@{ diskSizeGB = 128; osType = 'Windows'; diskState = 'Attached'; hyperVGeneration = 'V2' provisioningState = 'Succeeded'; timeCreated = '2026-01-01T00:00:00Z' creationData = [PSCustomObject]@{ createOption = 'FromImage' } encryption = [PSCustomObject]@{ type = 'EncryptionAtRestWithPlatformKey' } sku = [PSCustomObject]@{ name = 'Premium_LRS' } }) # NIC (needed by VirtualMachine module) $script:MockResources += New-MockResource -Id '/sub/sub-00000001/nic/nic01' -Name 'nic01' -Type 'microsoft.network/networkinterfaces' -Props ([PSCustomObject]@{ ipConfigurations = @(@{ properties = [PSCustomObject]@{ subnet = [PSCustomObject]@{ id = '/vnet/vnet1/subnets/default' }; privateIPAddress = '10.0.0.4'; publicIPAddress = $null } }) enableAcceleratedNetworking = $true; enableIPForwarding = $false virtualMachine = [PSCustomObject]@{ id = '/sub/sub-00000001/vm/vm01' } }) # VM Scale Set $script:MockResources += New-MockResource -Id '/sub/sub-00000001/vmss/vmss01' -Name 'vmss-web' -Type 'microsoft.compute/virtualmachinescalesets' -Props ([PSCustomObject]@{ sku = [PSCustomObject]@{ name = 'Standard_D2s_v3'; capacity = 3; tier = 'Standard' } upgradePolicy = [PSCustomObject]@{ mode = 'Automatic' } provisioningState = 'Succeeded' singlePlacementGroup = $true orchestrationMode = 'Uniform' timeCreated = '2025-07-01T12:00:00Z' }) # Availability Set $script:MockResources += New-MockResource -Id '/sub/sub-00000001/as/as01' -Name 'as-web' -Type 'microsoft.compute/availabilitysets' -Props ([PSCustomObject]@{ platformUpdateDomainCount = 5; platformFaultDomainCount = 3 virtualMachines = @(@{ id = '/vm/vm01' }) sku = [PSCustomObject]@{ name = 'Aligned' } }) # Cloud Services $script:MockResources += New-MockResource -Id '/sub/sub-00000001/cs/cs01' -Name 'cs-legacy' -Type 'microsoft.classiccompute/domainnames' -Props ([PSCustomObject]@{ status = 'Running'; label = 'Legacy Cloud Service'; deploymentSlot = 'Production' }) # AVD Host Pool $script:MockResources += New-MockResource -Id '/sub/sub-00000001/hp/hp01' -Name 'hp-prod' -Type 'microsoft.desktopvirtualization/hostpools' -Props ([PSCustomObject]@{ hostPoolType = 'Pooled'; loadBalancerType = 'BreadthFirst'; maxSessionLimit = 10 validationEnvironment = $false; startVMOnConnect = $true registrationInfo = [PSCustomObject]@{ expirationTime = '9999-12-31T23:59:59Z' } preferredAppGroupType = 'Desktop' }) # AVD Application Group $script:MockResources += New-MockResource -Id '/sub/sub-00000001/ag/ag01' -Name 'ag-desktop' -Type 'microsoft.desktopvirtualization/applicationgroups' -Props ([PSCustomObject]@{ applicationGroupType = 'Desktop'; hostPoolArmPath = '/hp/hp01' workspaceArmPath = '/ws/ws01'; friendlyName = 'Desktop Apps' }) # AVD Application Group (RemoteApp) — needed by AVDApplications module $script:MockResources += New-MockResource ` -Id '/subscriptions/sub-00000001/resourceGroups/rg-avd/providers/Microsoft.DesktopVirtualization/applicationGroups/ag-remoteapp' ` -Name 'ag-remoteapp' -Type 'microsoft.desktopvirtualization/applicationgroups' -Props ([PSCustomObject]@{ applicationGroupType = 'RemoteApp'; hostPoolArmPath = '/hp/hp01' workspaceArmPath = '/ws/ws01'; friendlyName = 'Remote Apps' }) # AVD Workspace $script:MockResources += New-MockResource -Id '/sub/sub-00000001/avdws/avdws01' -Name 'avdws-prod' -Type 'microsoft.desktopvirtualization/workspaces' -Props ([PSCustomObject]@{ applicationGroupReferences = @('/ag/ag01'); friendlyName = 'Production Workspace' publicNetworkAccess = 'Enabled' }) # AVD Session Host $script:MockResources += New-MockResource -Id '/sub/sub-00000001/hp/hp01/sessionhosts/vm01' -Name 'hp01/vm01.corp.com' -Type 'microsoft.desktopvirtualization/hostpools/sessionhosts' -Props ([PSCustomObject]@{ status = 'Available'; agentVersion = '1.0.5780'; osVersion = 'Windows Server 2022' sessions = 2; allowNewSession = $true; assignedUser = $null lastHeartBeat = '2026-02-24T10:00:00Z'; updateState = 'Succeeded' resourceId = '/sub/sub-00000001/vm/vm01' }) # AVD Session Host (Azure Local / hybridCompute) — needed by AVDAzureLocal module $script:MockResources += New-MockResource ` -Id '/subscriptions/sub-00000001/resourceGroups/rg-avd/providers/Microsoft.DesktopVirtualization/hostPools/hp01/sessionHosts/arcvm01' ` -Name 'hp01/arcvm01.corp.com' -Type 'microsoft.desktopvirtualization/hostpools/sessionhosts' -Props ([PSCustomObject]@{ status = 'Available'; agentVersion = '1.0.5780'; osVersion = 'Windows Server 2022' sessions = 1; allowNewSession = $true; assignedUser = 'testuser@corp.com' lastHeartBeat = '2026-02-24T10:00:00Z'; updateState = 'Succeeded' resourceId = '/subscriptions/sub-00000001/resourceGroups/rg-avd/providers/Microsoft.hybridCompute/machines/arcvm01' }) # Arc machine tagged as AVD session host — needed by AVDAzureLocal module $script:MockResources += [PSCustomObject]@{ id = '/subscriptions/sub-00000001/resourceGroups/rg-avd/providers/Microsoft.HybridCompute/machines/arcvm01' NAME = 'arcvm01' TYPE = 'microsoft.hybridcompute/machines' LOCATION = 'eastus' RESOURCEGROUP = 'rg-avd' subscriptionId = 'sub-00000001' KIND = '' tags = [PSCustomObject]@{ AvdSessionHost = 'true' } PROPERTIES = [PSCustomObject]@{ status = 'Connected' agentversion = '1.0.5780' osVersion = 'Windows Server 2022' lastStatusChange = '2026-02-24T10:00:00Z' } MANAGEDBY = '' } # AVD Scaling Plan $script:MockResources += New-MockResource -Id '/sub/sub-00000001/sp/sp01' -Name 'sp-weekday' -Type 'microsoft.desktopvirtualization/scalingplans' -Props ([PSCustomObject]@{ hostPoolReferences = @(@{ hostPoolArmPath = '/hp/hp01'; scalingPlanEnabled = $true }) schedules = @(@{ name = 'Weekday'; daysOfWeek = @('Monday','Tuesday') }) timeZone = 'Eastern Standard Time' exclusionTag = 'ExcludeScaling' }) # VMWare Private Cloud $script:MockResources += New-MockResource -Id '/subscriptions/sub-00000001/resourceGroups/rg-avs/providers/Microsoft.AVS/privateClouds/avs-prod' -Name 'avs-prod' -Type 'Microsoft.AVS/privateClouds' -Props ([PSCustomObject]@{ sku = [PSCustomObject]@{ name = 'AV36' } availability = [PSCustomObject]@{ strategy = 'SingleZone'; zone = 1 } circuit = [PSCustomObject]@{ expressRouteID = '/subscriptions/sub-00000001/resourceGroups/rg-net/providers/Microsoft.Network/expressRouteCircuits/er-avs' } encryption = [PSCustomObject]@{ status = 'Enabled' } externalCloudLinks = @() identitySources = @() internet = 'Disabled' managementCluster = [PSCustomObject]@{ clusterSize = 3 } managementNetwork = '10.0.0.0/22' networkBlock = '10.0.0.0/22' provisioningNetwork = '10.0.4.0/24' vmotionNetwork = '10.0.8.0/24' endpoints = [PSCustomObject]@{ hcxCloudManager = 'https://hcx.avs-prod.azure.com'; nsxtManager = 'https://nsx.avs-prod.azure.com'; vcsa = 'https://vcsa.avs-prod.azure.com' } }) # Mock Invoke-AzRestMethod for AVDApplications and VMOperationalData function Invoke-AzRestMethod { param([string]$Path, [string]$Method = 'GET') $mockResponse = @{ value = @() } if ($Path -match '/applicationgroups/.+/applications\?') { $mockResponse = @{ value = @(@{ name = 'Calculator'; properties = @{ friendlyName = 'Calculator'; description = 'Windows Calculator'; applicationType = 'InBuilt'; filePath = 'C:\Windows\system32\calc.exe'; commandLineSetting = 'DoNotAllow'; commandLineArguments = ''; iconPath = 'C:\Windows\system32\calc.exe'; showInPortal = $true } }) } } elseif ($Path -match 'patchAssessmentResults') { $mockResponse = @{ value = @(); properties = @{ startDateTime = '2025-06-01T00:00:00Z'; availablePatchCountByClassification = @{ critical = 0; security = 1; updateRollUp = 2 }; lastModifiedDateTime = '2025-06-01T12:00:00Z' } } } [PSCustomObject]@{ Content = ($mockResponse | ConvertTo-Json -Depth 10); StatusCode = 200 } } } AfterAll { if (Test-Path $script:TempDir) { Remove-Item $script:TempDir -Recurse -Force } } # =================================================================== # TESTS # =================================================================== Describe 'Compute Module Files Exist' { It 'Compute module folder exists' { $script:ComputePath | Should -Exist } It '<Name> module file exists' -ForEach ($ComputeModules + @($AVDHostPoolSpec)) { Join-Path $script:ComputePath $File | Should -Exist } } Describe 'Compute Module Processing Phase — <Name>' -ForEach $ComputeModules { BeforeAll { $script:ModFile = Join-Path $script:ComputePath $File $script:ResType = $Type } It 'Processing returns results when matching resources are present' { $matchedResources = $script:MockResources | Where-Object { $_.TYPE -eq $script:ResType } if ($matchedResources) { $content = Get-Content -Path $script:ModFile -Raw $sb = [ScriptBlock]::Create($content) $result = Invoke-Command -ScriptBlock $sb -ArgumentList $null, $null, $null, $script:MockResources, $null, 'Processing', $null, $null, 'Light20', $null $result | Should -Not -BeNullOrEmpty } else { Set-ItResult -Skipped -Because "No mock resource of type '$script:ResType'" } } It 'Processing does not throw when given an empty resource list' { $content = Get-Content -Path $script:ModFile -Raw $sb = [ScriptBlock]::Create($content) { Invoke-Command -ScriptBlock $sb -ArgumentList $null, $null, $null, @(), $null, 'Processing', $null, $null, 'Light20', $null } | Should -Not -Throw } } Describe 'Compute Module Reporting Phase — <Name>' -ForEach $ComputeModules { BeforeAll { $script:ModFile = Join-Path $script:ComputePath $File $script:ResType = $Type $script:WsName = $Worksheet $script:XlsxFile = Join-Path $script:TempDir ("Compute_{0}_{1}.xlsx" -f $Name, [System.IO.Path]::GetRandomFileName()) $matchedResources = $script:MockResources | Where-Object { $_.TYPE -eq $script:ResType } if ($matchedResources) { $content = Get-Content -Path $script:ModFile -Raw $sb = [ScriptBlock]::Create($content) $script:ProcessedData = Invoke-Command -ScriptBlock $sb -ArgumentList $null, $null, $null, $script:MockResources, $null, 'Processing', $null, $null, 'Light20', $null } else { $script:ProcessedData = $null } } It 'Reporting phase does not throw' { if ($script:ProcessedData) { $content = Get-Content -Path $script:ModFile -Raw $sb = [ScriptBlock]::Create($content) { Invoke-Command -ScriptBlock $sb -ArgumentList $null, $null, $null, $null, $null, 'Reporting', $script:XlsxFile, $script:ProcessedData, 'Light20', $null } | Should -Not -Throw } else { Set-ItResult -Skipped -Because "No mock resource of type '$script:ResType'" } } It 'Excel file is created' { if ($script:ProcessedData) { $script:XlsxFile | Should -Exist } else { Set-ItResult -Skipped -Because "No mock resource of type '$script:ResType'" } } } Describe 'AVD SessionHosts — Processing produces expected fields' { It 'Produces Host Pool, Session Host, Status, Arc Enabled, Azure Local, Agent Version fields' { $modFile = Join-Path $script:ComputePath 'AVDSessionHosts.ps1' $content = Get-Content -Path $modFile -Raw $sb = [ScriptBlock]::Create($content) $result = Invoke-Command -ScriptBlock $sb -ArgumentList $null, $null, $null, $script:MockResources, $null, 'Processing', $null, $null, 'Light20', $null $result | Should -Not -BeNullOrEmpty $row = $result | Select-Object -First 1 $row.Keys | Should -Contain 'Host Pool' $row.Keys | Should -Contain 'Session Host' $row.Keys | Should -Contain 'Status' $row.Keys | Should -Contain 'Arc Enabled' $row.Keys | Should -Contain 'Azure Local' $row.Keys | Should -Contain 'Agent Version' } } Describe 'VirtualMachine — Processing produces required columns' { It 'Processing returns rows with OS, SKU and VM Name fields' { $modFile = Join-Path $script:ComputePath 'VirtualMachine.ps1' $content = Get-Content -Path $modFile -Raw $sb = [ScriptBlock]::Create($content) $result = Invoke-Command -ScriptBlock $sb -ArgumentList $null, $null, $null, $script:MockResources, $null, 'Processing', $null, $null, 'Light20', $null $result | Should -Not -BeNullOrEmpty } } |