modules/Azure/Infrastructure/Tests/Unit/InvokeAzureApiPagination.Tests.ps1
|
BeforeAll { Remove-Module Devolutions.CIEM -Force -ErrorAction SilentlyContinue Import-Module (Join-Path $PSScriptRoot '..' '..' '..' '..' '..' 'Devolutions.CIEM.psd1') function Initialize-InfrastructureTestDatabase { New-CIEMDatabase -Path "$TestDrive/ciem.db" InModuleScope Devolutions.CIEM { $script:DatabasePath = "$TestDrive/ciem.db" } $schemaPath = Join-Path $PSScriptRoot '..' '..' 'Data' 'azure_schema.sql' foreach ($statement in ((Get-Content $schemaPath -Raw) -split ';\s*\n' | Where-Object { $_.Trim() })) { Invoke-CIEMQuery -Query $statement.Trim() -AsNonQuery | Out-Null } } } Describe 'Invoke-AzureApi pagination handling' { BeforeEach { Remove-Item "$TestDrive/ciem.db" -Force -ErrorAction SilentlyContinue Initialize-InfrastructureTestDatabase } It 'Does not emit progress for a single page response' { InModuleScope Devolutions.CIEM { $script:AzureAuthContext = [CIEMAzureAuthContext]::new() $script:AzureAuthContext.IsConnected = $true $script:AzureAuthContext.GraphToken = 'graph-token' $script:progressCalls = [System.Collections.Generic.List[object]]::new() function Write-Progress { param($Activity, $Status, $CurrentOperation, [switch]$Completed) $script:progressCalls.Add([pscustomobject]@{ Completed = [bool]$Completed Status = $Status CurrentOperation = $CurrentOperation }) } function Invoke-RestMethod { param($Uri, $Method, $Headers, $ResponseHeadersVariable, $ErrorAction) [pscustomobject]@{ value = @( [pscustomobject]@{ id = 'one' } [pscustomobject]@{ id = 'two' } ) } } $result = @(Invoke-AzureApi -Api Graph -Path '/users?$select=id' -ResourceName 'Users' -ErrorAction Stop) $result | Should -HaveCount 2 @($script:progressCalls) | Should -HaveCount 0 } } It 'Emits progress for every page and completes the progress bar' { InModuleScope Devolutions.CIEM { $script:AzureAuthContext = [CIEMAzureAuthContext]::new() $script:AzureAuthContext.IsConnected = $true $script:AzureAuthContext.GraphToken = 'graph-token' $script:progressCalls = [System.Collections.Generic.List[object]]::new() $script:responses = @{ 'https://graph.microsoft.com/v1.0/users?$select=id' = [pscustomobject]@{ value = @([pscustomobject]@{ id = 'page-1' }) '@odata.nextLink' = 'https://graph.microsoft.com/v1.0/users?page=2' } 'https://graph.microsoft.com/v1.0/users?page=2' = [pscustomobject]@{ value = @([pscustomobject]@{ id = 'page-2' }) '@odata.nextLink' = 'https://graph.microsoft.com/v1.0/users?page=3' } 'https://graph.microsoft.com/v1.0/users?page=3' = [pscustomobject]@{ value = @([pscustomobject]@{ id = 'page-3' }) '@odata.nextLink' = $null } } function Write-Progress { param($Activity, $Status, $CurrentOperation, [switch]$Completed) $script:progressCalls.Add([pscustomobject]@{ Completed = [bool]$Completed Status = $Status CurrentOperation = $CurrentOperation }) } function Invoke-RestMethod { param($Uri, $Method, $Headers, $ResponseHeadersVariable, $ErrorAction) $script:responses[$Uri] } $result = @(Invoke-AzureApi -Api Graph -Path '/users?$select=id' -ResourceName 'Users' -ErrorAction Stop) $activeCalls = @($script:progressCalls | Where-Object { -not $_.Completed }) $completedCalls = @($script:progressCalls | Where-Object { $_.Completed }) $result | Should -HaveCount 3 $activeCalls | Should -HaveCount 3 $completedCalls | Should -HaveCount 1 } } It 'Throws when the same nextLink is returned twice in a row' { InModuleScope Devolutions.CIEM { $script:AzureAuthContext = [CIEMAzureAuthContext]::new() $script:AzureAuthContext.IsConnected = $true $script:AzureAuthContext.GraphToken = 'graph-token' $script:responses = @{ 'https://graph.microsoft.com/v1.0/users?$select=id' = [pscustomobject]@{ value = @([pscustomobject]@{ id = 'page-1' }) '@odata.nextLink' = 'https://graph.microsoft.com/v1.0/users?page=2' } 'https://graph.microsoft.com/v1.0/users?page=2' = [pscustomobject]@{ value = @([pscustomobject]@{ id = 'page-2' }) '@odata.nextLink' = 'https://graph.microsoft.com/v1.0/users?page=2' } } function Write-Progress { param($Activity, $Status, $CurrentOperation, [switch]$Completed) } function Invoke-RestMethod { param($Uri, $Method, $Headers, $ResponseHeadersVariable, $ErrorAction) $script:responses[$Uri] } { Invoke-AzureApi -Api Graph -Path '/users?$select=id' -ResourceName 'Users' -ErrorAction Stop } | Should -Throw '*pagination cycle*' } } It 'Stops cleanly when a page is empty even if nextLink is still present' { InModuleScope Devolutions.CIEM { $script:AzureAuthContext = [CIEMAzureAuthContext]::new() $script:AzureAuthContext.IsConnected = $true $script:AzureAuthContext.GraphToken = 'graph-token' $script:callUris = [System.Collections.Generic.List[string]]::new() $script:responses = @{ 'https://graph.microsoft.com/v1.0/users?$select=id' = [pscustomobject]@{ value = @([pscustomobject]@{ id = 'page-1' }) '@odata.nextLink' = 'https://graph.microsoft.com/v1.0/users?page=2' } 'https://graph.microsoft.com/v1.0/users?page=2' = [pscustomobject]@{ value = @() '@odata.nextLink' = 'https://graph.microsoft.com/v1.0/users?page=3' } } function Write-Progress { param($Activity, $Status, $CurrentOperation, [switch]$Completed) } function Invoke-RestMethod { param($Uri, $Method, $Headers, $ResponseHeadersVariable, $ErrorAction) $script:callUris.Add($Uri) $script:responses[$Uri] } $result = @(Invoke-AzureApi -Api Graph -Path '/users?$select=id' -ResourceName 'Users' -ErrorAction Stop) $result | Should -HaveCount 1 @($script:callUris) | Should -HaveCount 2 @($script:callUris) | Should -Not -Contain 'https://graph.microsoft.com/v1.0/users?page=3' } } } |