tests/Analytics.Module.Tests.ps1
|
#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } #Requires -Modules ImportExcel <# .SYNOPSIS Pester tests for all Analytics inventory modules. .DESCRIPTION Tests both Processing and Reporting phases for each Analytics module using synthetic mock data. No live Azure authentication is required. .NOTES Author: AzureScout Contributors Version: 1.0.0 Created: 2026-02-25 #> # =================================================================== # DISCOVERY-TIME # =================================================================== $AnalyticsPath = Join-Path (Split-Path -Parent $PSScriptRoot) 'Modules' 'Public' 'InventoryModules' 'Analytics' $AnalyticsModules = @( @{ Name = 'Databricks'; File = 'Databricks.ps1'; Type = 'microsoft.databricks/workspaces'; Worksheet = 'Databricks' } @{ Name = 'DataExplorerCluster'; File = 'DataExplorerCluster.ps1'; Type = 'microsoft.kusto/clusters'; Worksheet = 'Data Explorer Clusters' } @{ Name = 'EvtHub'; File = 'EvtHub.ps1'; Type = 'microsoft.eventhub/namespaces'; Worksheet = 'Event Hubs' } @{ Name = 'Purview'; File = 'Purview.ps1'; Type = 'microsoft.purview/accounts'; Worksheet = 'Purview' } @{ Name = 'Streamanalytics'; File = 'Streamanalytics.ps1'; Type = 'microsoft.streamanalytics/streamingjobs'; Worksheet = 'Stream Analytics Jobs' } @{ Name = 'Synapse'; File = 'Synapse.ps1'; Type = 'microsoft.synapse/workspaces'; Worksheet = 'Synapse' } ) # =================================================================== # EXECUTION-TIME SETUP # =================================================================== BeforeAll { $script:ModuleRoot = Split-Path -Parent $PSScriptRoot $script:AnalyticsPath = Join-Path $script:ModuleRoot 'Modules' 'Public' 'InventoryModules' 'Analytics' $script:TempDir = Join-Path $env:TEMP 'AZSC_AnalyticsTests' if (Test-Path $script:TempDir) { Remove-Item $script:TempDir -Recurse -Force } New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null function New-MockAnalyticsResource { param([string]$Id, [string]$Name, [string]$Type, [string]$Kind = '', [string]$Location = 'eastus', [string]$RG = 'rg-analytics', [string]$SubscriptionId = 'sub-00000001', [object]$Props, [object]$SKU = $null) [PSCustomObject]@{ id = $Id NAME = $Name TYPE = $Type KIND = $Kind LOCATION = $Location RESOURCEGROUP = $RG subscriptionId = $SubscriptionId tags = [PSCustomObject]@{} PROPERTIES = $Props SKU = $SKU } } $script:MockResources = @() # --- Databricks --- $script:MockResources += New-MockAnalyticsResource -Id '/subscriptions/sub-00000001/resourceGroups/rg-analytics/providers/microsoft.databricks/workspaces/dbw-prod' ` -Name 'dbw-prod' -Type 'microsoft.databricks/workspaces' ` -SKU ([PSCustomObject]@{ name = 'premium'; tier = 'Premium' }) ` -Props ([PSCustomObject]@{ createdDateTime = '2025-06-15T10:00:00Z' parameters = [PSCustomObject]@{ enableNoPublicIp = [PSCustomObject]@{ value = 'False' } customVirtualNetworkId = [PSCustomObject]@{ value = '/subscriptions/sub-00000001/resourceGroups/rg-net/providers/Microsoft.Network/virtualNetworks/vnet-dbw/subnets/private' } storageAccountName = [PSCustomObject]@{ value = 'sadbwprod01' } storageAccountSkuName = [PSCustomObject]@{ value = 'Standard_LRS' } requireInfrastructureEncryption = [PSCustomObject]@{ value = $true } prepareEncryption = [PSCustomObject]@{ value = $false } customPrivateSubnetName = [PSCustomObject]@{ value = 'private-subnet' } customPublicSubnetName = [PSCustomObject]@{ value = 'public-subnet' } } managedResourceGroupId = '/subscriptions/sub-00000001/resourceGroups/rg-dbw-managed/providers/foo/bar' workspaceUrl = 'adb-123456.azuredatabricks.net' }) # --- Data Explorer Cluster --- $script:MockResources += New-MockAnalyticsResource -Id '/subscriptions/sub-00000001/resourceGroups/rg-analytics/providers/microsoft.kusto/clusters/dec-prod' ` -Name 'dec-prod' -Type 'microsoft.kusto/clusters' ` -SKU ([PSCustomObject]@{ name = 'Standard_D13_v2'; capacity = 2 }) ` -Props ([PSCustomObject]@{ state = 'Running'; stateReason = '' virtualNetworkConfiguration = [PSCustomObject]@{ subnetid = '/subscriptions/sub-00000001/resourceGroups/rg-net/providers/Microsoft.Network/virtualNetworks/vnet-kusto/subnets/kusto-subnet' dataManagementPublicIpId = '/subscriptions/sub-00000001/resourceGroups/rg-net/providers/Microsoft.Network/publicIPAddresses/pip-kusto-dm' enginePublicIpId = '/subscriptions/sub-00000001/resourceGroups/rg-net/providers/Microsoft.Network/publicIPAddresses/pip-kusto-engine' } trustedExternalTenants = @([PSCustomObject]@{ value = '*' }) optimizedAutoscale = [PSCustomObject]@{ isEnabled = 'true'; minimum = 2; maximum = 10 } enableDiskEncryption = $true; enableStreamingIngest = $true uri = 'https://dec-prod.eastus.kusto.windows.net' dataIngestionUri = 'https://ingest-dec-prod.eastus.kusto.windows.net' }) # --- Event Hub --- $script:MockResources += New-MockAnalyticsResource -Id '/subscriptions/sub-00000001/resourceGroups/rg-analytics/providers/microsoft.eventhub/namespaces/eh-prod' ` -Name 'eh-prod' -Type 'microsoft.eventhub/namespaces' ` -SKU ([PSCustomObject]@{ name = 'Standard'; capacity = 2 }) ` -Props ([PSCustomObject]@{ createdAt = '2025-03-10T08:30:00Z'; status = 'Active' zoneRedundant = $true; disablelocalauth = $false isAutoInflateEnabled = $true; maximumThroughputUnits = 20 kafkaEnabled = $true; minimumtlsversion = '1.2' serviceBusEndpoint = 'https://eh-prod.servicebus.windows.net:443/' }) # --- Purview --- $script:MockResources += New-MockAnalyticsResource -Id '/subscriptions/sub-00000001/resourceGroups/rg-analytics/providers/microsoft.purview/accounts/purv-prod' ` -Name 'purv-prod' -Type 'microsoft.purview/accounts' ` -Props ([PSCustomObject]@{ cloudConnectors = @([PSCustomObject]@{ id = 'cc1' }) privateEndpointConnections = @([PSCustomObject]@{ id = 'pe1' }, [PSCustomObject]@{ id = 'pe2' }) createdAt = '2025-01-20T14:00:00Z' sku = [PSCustomObject]@{ name = 'Standard'; capacity = 1 } friendlyName = 'Production Purview' managedResourceGroupName = 'rg-purview-managed' managedResources = [PSCustomObject]@{ storageAccount = '/subscriptions/sub-00000001/resourceGroups/rg-purview/providers/Microsoft.Storage/storageAccounts/sapurvprod' eventHubNamespace = '/subscriptions/sub-00000001/resourceGroups/rg-purview/providers/Microsoft.EventHub/namespaces/ehpurvprod' } publicNetworkAccess = 'Enabled' createdBy = 'admin@contoso.com' }) # --- Stream Analytics (needs both cluster and job resources) --- $script:MockResources += New-MockAnalyticsResource -Id '/subscriptions/sub-00000001/resourceGroups/rg-analytics/providers/microsoft.streamanalytics/clusters/sac-prod' ` -Name 'sac-prod' -Type 'microsoft.streamanalytics/clusters' ` -SKU ([PSCustomObject]@{ name = 'Default'; capacity = 36 }) ` -Props ([PSCustomObject]@{ capacityallocated = 36; capacityassigned = 12 createddate = '2025-02-01T10:00:00Z' }) $script:MockResources += New-MockAnalyticsResource -Id '/subscriptions/sub-00000001/resourceGroups/rg-analytics/providers/microsoft.streamanalytics/streamingjobs/saj-transform' ` -Name 'saj-transform' -Type 'microsoft.streamanalytics/streamingjobs' ` -Props ([PSCustomObject]@{ cluster = [PSCustomObject]@{ id = '/subscriptions/sub-00000001/resourceGroups/rg-analytics/providers/microsoft.streamanalytics/clusters/sac-prod' } createdDate = '2025-04-01T09:00:00Z' sku = [PSCustomObject]@{ name = 'Standard' } compatibilityLevel = '1.2' jobstorageaccount = [PSCustomObject]@{ accountname = 'sajstorage01'; authenticationmode = 'ConnectionString' } contentStoragePolicy = 'SystemAccount' dataLocale = 'en-US' eventsLateArrivalMaxDelayInSeconds = 5 eventsOutOfOrderMaxDelayInSeconds = 0 eventsOutOfOrderPolicy = 'Adjust' jobState = 'Running'; jobType = 'Cloud' lastOutputEventTime = '2025-06-01T12:00:00Z' outputStartTime = '2025-04-01T10:00:00Z' outputErrorPolicy = 'Stop' }) # --- Synapse --- $script:MockResources += New-MockAnalyticsResource -Id '/subscriptions/sub-00000001/resourceGroups/rg-analytics/providers/microsoft.synapse/workspaces/syn-prod' ` -Name 'syn-prod' -Type 'microsoft.synapse/workspaces' ` -Props ([PSCustomObject]@{ publicNetworkAccess = 'Enabled' privateEndpointConnections = @([PSCustomObject]@{ id = 'pe1' }) encryption = [PSCustomObject]@{ doubleEncryptionEnabled = $true } trustedServiceBypassEnabled = $true sqlAdministratorLogin = 'sqladmin' extraProperties = [PSCustomObject]@{ IsScopeEnabled = $true; WorkspaceType = 'Normal' } managedVirtualNetworkSettings = [PSCustomObject]@{ preventDataExfiltration = $true } managedVirtualNetwork = 'default' managedResourceGroupName = 'rg-synapse-managed' }) } AfterAll { if (Test-Path $script:TempDir) { Remove-Item $script:TempDir -Recurse -Force } } # =================================================================== # TESTS # =================================================================== Describe 'Analytics Module Files Exist' { It 'Analytics module folder exists' { $script:AnalyticsPath | Should -Exist } It '<Name> module file exists' -ForEach $AnalyticsModules { Join-Path $script:AnalyticsPath $File | Should -Exist } } Describe 'Analytics Module Processing Phase — <Name>' -ForEach $AnalyticsModules { BeforeAll { $script:ModFile = Join-Path $script:AnalyticsPath $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 'Analytics Module Reporting Phase — <Name>' -ForEach $AnalyticsModules { BeforeAll { $script:ModFile = Join-Path $script:AnalyticsPath $File $script:ResType = $Type $script:XlsxFile = Join-Path $script:TempDir ("Analytics_{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'" } } } |