Tests/Unit/Invoke-SPCGraphBatch.Tests.ps1
|
#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } BeforeAll { . (Join-Path $PSScriptRoot '../../Private/Invoke-SPCGraphBatch.ps1') } Describe 'Invoke-SPCGraphBatch Unit Tests' { Context 'Normal Operation' { It 'returns all sub-responses on successful batch request' { Mock Invoke-RestMethod { return [PSCustomObject]@{ responses = @( [PSCustomObject]@{ id = '1'; status = 200; body = 'OK' }, [PSCustomObject]@{ id = '2'; status = 200; body = 'OK' } ) } } $requests = [System.Collections.Generic.List[hashtable]]::new() $requests.Add(@{ id = '1'; method = 'GET'; url = '/users/1' }) $requests.Add(@{ id = '2'; method = 'GET'; url = '/users/2' }) $res = Invoke-SPCGraphBatch -Requests $requests -AccessToken 'fake-token' $res.Count | Should -Be 2 $res[0].status | Should -Be 200 $res[1].status | Should -Be 200 } } Context 'Envelope 429 Throttling' { It 'retries envelope 429 responses using Retry-After header or backoff and succeeds' { $script:callCount = 0 Mock Start-Sleep {} Mock Invoke-RestMethod { $script:callCount++ if ($script:callCount -eq 1) { $resp = [System.Net.HttpWebResponse]::new() # Throw exception with StatusCode 429 $ex = [System.Management.Automation.MethodInvocationException]::new("429 Throttled") $fakeResponse = [PSCustomObject]@{ StatusCode = 429 Headers = @{ 'Retry-After' = '1' } } $ex | Add-Member -MemberType NoteProperty -Name 'Response' -Value $fakeResponse -Force throw $ex } return [PSCustomObject]@{ responses = @([PSCustomObject]@{ id = '1'; status = 200; body = 'OK' }) } } $requests = [System.Collections.Generic.List[hashtable]]::new() $requests.Add(@{ id = '1'; method = 'GET'; url = '/users/1' }) $res = Invoke-SPCGraphBatch -Requests $requests -AccessToken 'fake-token' $res.Count | Should -Be 1 $res[0].status | Should -Be 200 $script:callCount | Should -Be 2 } It 'throws ERR-429 when envelope 429 retries exhaust max attempts' { Mock Start-Sleep {} Mock Invoke-RestMethod { $ex = [System.Management.Automation.MethodInvocationException]::new("429 Throttled") $fakeResponse = [PSCustomObject]@{ StatusCode = 429 } $ex | Add-Member -MemberType NoteProperty -Name 'Response' -Value $fakeResponse -Force throw $ex } $requests = [System.Collections.Generic.List[hashtable]]::new() $requests.Add(@{ id = '1'; method = 'GET'; url = '/users/1' }) { Invoke-SPCGraphBatch -Requests $requests -AccessToken 'fake-token' } | Should -Throw -ExpectedMessage "*ERR-429*" } } Context 'Inner Sub-request 429 Throttling' { It 'retries inner 429 sub-requests and collects non-429 results across attempts' { $script:batchAttempt = 0 Mock Start-Sleep {} Mock Invoke-RestMethod { $script:batchAttempt++ if ($script:batchAttempt -eq 1) { # First attempt: req 1 gets 200, req 2 gets 429 with Retry-After header return [PSCustomObject]@{ responses = @( [PSCustomObject]@{ id = '1'; status = 200; body = 'User1' }, [PSCustomObject]@{ id = '2'; status = 429; headers = @{ 'Retry-After' = '2' }; body = 'Throttled' } ) } } else { # Second attempt for req 2: gets 200 return [PSCustomObject]@{ responses = @( [PSCustomObject]@{ id = '2'; status = 200; body = 'User2' } ) } } } $requests = [System.Collections.Generic.List[hashtable]]::new() $requests.Add(@{ id = '1'; method = 'GET'; url = '/users/1' }) $requests.Add(@{ id = '2'; method = 'GET'; url = '/users/2' }) $res = Invoke-SPCGraphBatch -Requests $requests -AccessToken 'fake-token' $res.Count | Should -Be 2 $script:batchAttempt | Should -Be 2 $req1Res = $res | Where-Object { $_.id -eq '1' } $req2Res = $res | Where-Object { $_.id -eq '2' } $req1Res.status | Should -Be 200 $req2Res.status | Should -Be 200 } } Context 'AC-12 Security Guardrails' { It 'ensures no secrets or credentials appear in verbose or information streams' { Mock Start-Sleep {} Mock Invoke-RestMethod { return [PSCustomObject]@{ responses = @([PSCustomObject]@{ id = '1'; status = 200; body = 'OK' }) } } $requests = [System.Collections.Generic.List[hashtable]]::new() $requests.Add(@{ id = '1'; method = 'GET'; url = '/users/1' }) $verboseStream = [System.Collections.Generic.List[string]]::new() $infoStream = [System.Collections.Generic.List[string]]::new() Invoke-SPCGraphBatch -Requests $requests -AccessToken 'fake-token-value' -Verbose 4>&1 6>&1 | ForEach-Object { if ($_ -is [System.Management.Automation.VerboseRecord]) { $verboseStream.Add($_.Message) } elseif ($_ -is [System.Management.Automation.InformationRecord]) { $infoStream.Add($_.MessageData) } } foreach ($msg in $verboseStream) { $msg | Should -Not -Match 'password|secret|pfx|credential' } foreach ($msg in $infoStream) { $msg | Should -Not -Match 'password|secret|pfx|credential' } } } } |