tests/Collect.ArmChildResources.Tests.ps1
|
#Requires -Version 7.0 #Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } <# Unit and contract tests for Get-ScoutArmChildResource. Invoke-AzRestMethod is replaced with a deterministic local function throughout. No Azure context, credentials, tenant identifiers, or network access are used. #> Describe 'Get-ScoutArmChildResource' { BeforeAll { $script:RepoRoot = Split-Path -Parent $PSScriptRoot . (Join-Path $script:RepoRoot 'src/collect/Get-ScoutArmChildResource.ps1') function Get-TestParent { param( [string]$Type, [string]$Name, [string]$Kind, [hashtable]$Properties = @{} ) [PSCustomObject]@{ id = "/subscriptions/sub-test/resourceGroups/rg-test/providers/$Type/$Name" type = $Type name = $Name kind = $Kind subscriptionId = 'sub-test' resourceGroup = 'rg-test' location = 'eastus' properties = [PSCustomObject]$Properties } } $script:Parents = @( Get-TestParent -Type 'microsoft.machinelearningservices/workspaces' -Name 'ml-standard' -Kind 'Default' Get-TestParent -Type 'microsoft.machinelearningservices/workspaces' -Name 'ml-hub' -Kind 'Hub' Get-TestParent -Type 'microsoft.machinelearningservices/workspaces' -Name 'ml-project' -Kind 'Project' Get-TestParent -Type 'microsoft.cognitiveservices/accounts' -Name 'openai-one' -Kind 'OpenAI' Get-TestParent -Type 'microsoft.search/searchservices' -Name 'search-one' Get-TestParent -Type 'microsoft.desktopvirtualization/applicationgroups' -Name 'remoteapps-one' ` -Properties @{ applicationGroupType = 'RemoteApp' } Get-TestParent -Type 'microsoft.desktopvirtualization/applicationgroups' -Name 'desktop-one' ` -Properties @{ applicationGroupType = 'Desktop' } Get-TestParent -Type 'microsoft.insights/components' -Name 'appi-one' Get-TestParent -Type 'microsoft.operationalinsights/workspaces' -Name 'law-one' ) function Get-ArmChildResponseStub { $script:ArmCalls = [System.Collections.Generic.List[string]]::new() function global:Invoke-AzRestMethod { param( [string]$Path, [string]$Method, [Parameter(ValueFromRemainingArguments)]$Rest ) $null = $Method, $Rest $script:ArmCalls.Add($Path) $Payload = switch -Regex ($Path) { '/data/[^/]+/versions' { @{ value = @(@{ id = "$Path/version-one"; name = 'version-one'; properties = @{ dataType = 'uri_file' } }) } break } '/models/[^/]+/versions' { @{ value = @(@{ id = "$Path/version-one"; name = 'version-one'; properties = @{ flavors = @{ python = @{} } } }) } break } '/onlineEndpoints/[^/]+/deployments' { @{ value = @(@{ name = 'blue' }, @{ name = 'green' }) } break } '/batchEndpoints/[^/]+/deployments' { @{ value = @(@{ name = 'batch-deployment' }) } break } '/onlineEndpoints\?' { @{ value = @(@{ id = "$Path/endpoint-online"; name = 'endpoint-online'; type = 'Microsoft.MachineLearningServices/onlineEndpoints'; properties = @{ authMode = 'aadToken' } }) } break } '/batchEndpoints\?' { @{ value = @(@{ id = "$Path/endpoint-batch"; name = 'endpoint-batch'; type = 'Microsoft.MachineLearningServices/batchEndpoints'; properties = @{ authMode = 'aadToken' } }) } break } '/exportconfiguration\?' { @(@{ id = "$Path/export-one"; name = 'export-one'; ExportId = 'export-one' }) break } '/ProactiveDetectionConfigs\?' { @(@{ id = "$Path/rule-one"; name = 'rule-one'; Enabled = $true }) break } '/WorkItemConfigs\?' { @{ value = @(@{ id = "$Path/workitem-one"; name = 'workitem-one'; ConfigDisplayName = 'Work item' }) } break } default { $Leaf = (($Path -split '\?')[0] -split '/')[-1] @{ value = @(@{ id = "$(($Path -split '\?')[0])/$Leaf-one" name = "$Leaf-one" type = "Test.Provider/$Leaf" properties = @{ state = 'Ready' } }) } } } [PSCustomObject]@{ StatusCode = 200 Content = ($Payload | ConvertTo-Json -Depth 12 -Compress) } } } Get-ArmChildResponseStub } AfterAll { Remove-Item -Path Function:\Invoke-AzRestMethod -Force -ErrorAction SilentlyContinue } BeforeEach { Get-ArmChildResponseStub } Describe 'Get-ScoutArmChildResource - complete fourteen-dataset contract' { It 'emits all fourteen synthetic types in canonical order' { $Rows = @(Get-ScoutArmChildResource -Resources $script:Parents) $Types = @($Rows.TYPE | Select-Object -Unique) $Types | Should -Be @( 'AZSC/ARMChild/MLComputes' 'AZSC/ARMChild/MLDatasets' 'AZSC/ARMChild/MLDatastores' 'AZSC/ARMChild/MLEndpoints' 'AZSC/ARMChild/MLModels' 'AZSC/ARMChild/MLPipelines' 'AZSC/ARMChild/OpenAIDeployments' 'AZSC/ARMChild/SearchIndexes' 'AZSC/ARMChild/AVDApplications' 'AZSC/ARMChild/AppInsightsContinuousExport' 'AZSC/ARMChild/AppInsightsProactiveDetection' 'AZSC/ARMChild/AppInsightsWorkItems' 'AZSC/ARMChild/LAWorkspaceLinkedServices' 'AZSC/ARMChild/LAWorkspaceSavedSearches' ) $Rows.Count | Should -Be 15 -Because 'MLEndpoints emits one online and one batch endpoint' } It 'preserves raw child properties and stamps parent linkage on every row' { $Rows = @(Get-ScoutArmChildResource -Resources $script:Parents) foreach ($Row in $Rows) { $Row.id | Should -Not -BeNullOrEmpty $Row.name | Should -Not -BeNullOrEmpty $Row.PARENTID | Should -Match '^/subscriptions/sub-test/' $Row.PARENTTYPE | Should -Not -BeNullOrEmpty $Row.PARENTNAME | Should -Not -BeNullOrEmpty $Row.subscriptionId | Should -Be 'sub-test' $Row.RESOURCEGROUP | Should -Be 'rg-test' $Row.AZSC.Dataset | Should -Be ($Row.TYPE -replace '^AZSC/ARMChild/', '') $Row.AZSC.ParentId | Should -Be $Row.PARENTID $Row.AZSC.SourceType | Should -Not -BeNullOrEmpty } } It 'excludes ML Hub/Project workspaces and non-RemoteApp AVD groups before calling ARM' { $null = Get-ScoutArmChildResource -Resources $script:Parents ($script:ArmCalls -join "`n") | Should -Not -Match 'ml-hub' ($script:ArmCalls -join "`n") | Should -Not -Match 'ml-project' ($script:ArmCalls -join "`n") | Should -Not -Match 'desktop-one' ($script:ArmCalls -join "`n") | Should -Match 'ml-standard' ($script:ArmCalls -join "`n") | Should -Match 'remoteapps-one' } It 'captures latest ML versions and endpoint deployment counts as metadata' { $Rows = @(Get-ScoutArmChildResource -Resources $script:Parents) ($Rows | Where-Object TYPE -eq 'AZSC/ARMChild/MLDatasets').AZSC.LatestVersion.name | Should -Be 'version-one' ($Rows | Where-Object TYPE -eq 'AZSC/ARMChild/MLModels').AZSC.LatestVersion.name | Should -Be 'version-one' $Online = $Rows | Where-Object { $_.TYPE -eq 'AZSC/ARMChild/MLEndpoints' -and $_.AZSC.EndpointType -eq 'onlineEndpoints' } $Batch = $Rows | Where-Object { $_.TYPE -eq 'AZSC/ARMChild/MLEndpoints' -and $_.AZSC.EndpointType -eq 'batchEndpoints' } $Online.AZSC.DeploymentCount | Should -Be 2 $Batch.AZSC.DeploymentCount | Should -Be 1 } It 'uses the API versions and special query clauses from the current collectors' { $null = Get-ScoutArmChildResource -Resources $script:Parents $Calls = $script:ArmCalls -join "`n" $Calls | Should -Match '/computes\?api-version=2023-04-01' $Calls | Should -Match '/data\?api-version=2023-04-01' $Calls | Should -Match '/versions\?api-version=2023-04-01&\$orderby=createdTime desc&\$top=1' $Calls | Should -Match '/jobs\?api-version=2023-04-01&\$filter=jobType eq ''Pipeline''' $Calls | Should -Match '/deployments\?api-version=2023-05-01' $Calls | Should -Match '/indexes\?api-version=2023-11-01' $Calls | Should -Match '/applications\?api-version=2022-09-09' $Calls | Should -Match '/exportconfiguration\?api-version=2015-05-01' $Calls | Should -Match '/ProactiveDetectionConfigs\?api-version=2018-05-01-preview' $Calls | Should -Match '/linkedServices\?api-version=2020-08-01' $Calls | Should -Match '/savedSearches\?api-version=2020-08-01' $Calls | Should -Not -Match 'listQueryKeys' -Because 'the current SearchIndexes collector constructs that URI but never calls it' } } Describe 'Get-ScoutArmChildResource - selection, ordering and resilience' { It 'calls only requested datasets and still uses canonical dataset order' { $Rows = @(Get-ScoutArmChildResource -Resources $script:Parents -Dataset @('SearchIndexes', 'MLComputes')) $Rows.TYPE | Should -Be @( 'AZSC/ARMChild/MLComputes' 'AZSC/ARMChild/SearchIndexes' ) $script:ArmCalls.Count | Should -Be 2 } It 'makes no ARM calls when no matching parents exist' { $Rows = @(Get-ScoutArmChildResource -Resources @( Get-TestParent -Type 'microsoft.storage/storageaccounts' -Name 'storage-one' )) $Rows | Should -BeNullOrEmpty $script:ArmCalls.Count | Should -Be 0 } It 'contains one failed child collection and continues with the next dataset' { function global:Invoke-AzRestMethod { param( [string]$Path, [string]$Method, [Parameter(ValueFromRemainingArguments)]$Rest ) $null = $Method, $Rest if ($Path -match '/computes\?') { throw 'simulated ARM failure' } [PSCustomObject]@{ StatusCode = 200 Content = (@{ value = @(@{ id = "$Path/child-one"; name = 'child-one'; type = 'Test.Provider/child'; properties = @{} }) } | ConvertTo-Json -Depth 8 -Compress) } } $Warnings = @() $Rows = @(Get-ScoutArmChildResource -Resources $script:Parents ` -Dataset @('MLComputes', 'MLDatastores') -WarningVariable Warnings) @($Rows | Where-Object TYPE -eq 'AZSC/ARMChild/MLComputes') | Should -BeNullOrEmpty @($Rows | Where-Object TYPE -eq 'AZSC/ARMChild/MLDatastores').Count | Should -Be 1 $Warnings.Count | Should -Be 1 $Warnings[0].Message | Should -Match 'MLComputes' $Warnings[0].Message | Should -Match 'simulated ARM failure' } It 'accepts both ARM value envelopes and bare arrays without changing child shape' { $Rows = @(Get-ScoutArmChildResource -Resources $script:Parents ` -Dataset @('AppInsightsContinuousExport', 'AppInsightsWorkItems')) @($Rows | Where-Object TYPE -eq 'AZSC/ARMChild/AppInsightsContinuousExport').Count | Should -Be 1 @($Rows | Where-Object TYPE -eq 'AZSC/ARMChild/AppInsightsWorkItems').Count | Should -Be 1 ($Rows | Where-Object TYPE -eq 'AZSC/ARMChild/AppInsightsContinuousExport').ExportId | Should -Be 'export-one' ($Rows | Where-Object TYPE -eq 'AZSC/ARMChild/AppInsightsWorkItems').ConfigDisplayName | Should -Be 'Work item' } } } |