functions/github/Invoke-GitHubRestMethod.Tests.ps1

$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'
. "$here\$sut"

Describe "Invoke-GitHubRestMethod" {

    $mockSplat = @{
        uri = "https://api.github.com/repos/does/notexist"
        verb = "GET"
        body = @{}
        token = "MOCK_TOKEN"
    }

    Context "When invoking with valid parameters" {
        Mock Invoke-RestMethod {
            return @{ StatusCode = 200 }
        }

        It "Should return a successful response" {
            $result = Invoke-GitHubRestMethod @mockSplat

            Assert-MockCalled Invoke-RestMethod -Exactly 1
            $result | Should -Not -BeNullOrEmpty
            $result.StatusCode | Should -Be 200
        }
    }

    Context "When encountering an error status code" {
        Mock Invoke-RestMethod {
            $errorDetails = '{"code": 1, "message": "BadRequest", "more_info": "", "status": 400}'
            $statusCode = 400
            $response = New-Object System.Net.Http.HttpResponseMessage $statusCode
            $exception = New-Object Microsoft.PowerShell.Commands.HttpResponseException "$statusCode ($($response.ReasonPhrase))", $response
            $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidOperation
            $errorID = 'WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand'
            $targetObject = $null
            $errorRecord = New-Object Management.Automation.ErrorRecord $exception, $errorID, $errorCategory, $targetObject
            $errorRecord.ErrorDetails = $errorDetails
            throw $errorRecord
        }

        It "Should throw an exception" {
            { Invoke-GitHubRestMethod @mockSplat } | Should -Throw
        }
    }

    Context "When encountering an ignored error status code" {
        Mock Invoke-RestMethod {
            $errorDetails = '{"code": 1, "message": "BadRequest", "more_info": "", "status": 400}'
            $statusCode = 400
            $response = New-Object System.Net.Http.HttpResponseMessage $statusCode
            $exception = New-Object Microsoft.PowerShell.Commands.HttpResponseException "$statusCode ($($response.ReasonPhrase))", $response
            $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidOperation
            $errorID = 'WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand'
            $targetObject = $null
            $errorRecord = New-Object Management.Automation.ErrorRecord $exception, $errorID, $errorCategory, $targetObject
            $errorRecord.ErrorDetails = $errorDetails
            throw $errorRecord
        }

        It "Should not throw an exception" {
            { Invoke-GitHubRestMethod @mockSplat -HttpErrorStatusCodesToIgnore @(400) } | Should -Not -Throw
        }

        It "Should return null" {
            $result = Invoke-GitHubRestMethod @mockSplat -HttpErrorStatusCodesToIgnore @(400)
            $result | Should -BeNullOrEmpty
        }
    }

    Context "When rate limit is exceeded and given a 'Retry-After' response header" {
        $script:pesterHasRetried = $false
        Mock Invoke-RestMethod {
            try {
                if (!$pesterHasRetried) {
                    $errorDetails = '{"code": 1, "message": "BadRequest", "more_info": "", "status": 429}'
                    $statusCode = 429
                    $response = New-Object System.Net.Http.HttpResponseMessage $statusCode
                    $response.Headers.Add('Retry-After', '1')
                    $exception = New-Object Microsoft.PowerShell.Commands.HttpResponseException "$statusCode ($($response.ReasonPhrase))", $response
                    $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidOperation
                    $errorID = 'WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand'
                    $targetObject = $null
                    $errorRecord = New-Object Management.Automation.ErrorRecord $exception, $errorID, $errorCategory, $targetObject
                    $errorRecord.ErrorDetails = $errorDetails
                    throw $errorRecord
                }
                else {
                    return @{ StatusCode = 200 }
                }
            }
            finally {
                $script:pesterHasRetried = $true
            } 
        }

        It "Should wait for the period specified in the Retry-After header" {

            # Act
            $result = Invoke-GitHubRestMethod @mockSplat

            # Assert
            $result | Should -Not -BeNullOrEmpty
            $result.StatusCode | Should -Be 200
            Assert-MockCalled Invoke-RestMethod -Exactly 2
        }
    }

    Context "When a rate limit quota is exhausted" {
        $script:pesterHasRetried = $false
        Mock Invoke-RestMethod {
            try {
                if (!$pesterHasRetried) {
                    $errorDetails = '{"code": 1, "message": "BadRequest", "more_info": "", "status": 429}'
                    $statusCode = 429
                    $response = New-Object System.Net.Http.HttpResponseMessage $statusCode
                    $response.Headers.Add('X-RateLimit-Reset', [datetime]::UtcNow.AddSeconds(1).ToFileTimeUtc())
                    $response.Headers.Add('X-RateLimit-Remaining', "0")
                    $exception = New-Object Microsoft.PowerShell.Commands.HttpResponseException "$statusCode ($($response.ReasonPhrase))", $response
                    $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidOperation
                    $errorID = 'WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand'
                    $targetObject = $null
                    $errorRecord = New-Object Management.Automation.ErrorRecord $exception, $errorID, $errorCategory, $targetObject
                    $errorRecord.ErrorDetails = $errorDetails
                    throw $errorRecord
                }
                else {
                    return @{ StatusCode = 200 }
                }
            }
            finally {
                $script:pesterHasRetried = $true
            } 
        }

        It "Should wait until the time speciied in the 'X-RateLimit-Reset' response header" {

            # Act
            $result = Invoke-GitHubRestMethod @mockSplat

            # Assert
            $result | Should -Not -BeNullOrEmpty
            $result.StatusCode | Should -Be 200
            Assert-MockCalled Invoke-RestMethod -Exactly 2
        }
    }

    Context "When rate limit is exceeded with no retry context" {
        $script:pesterRetryCount = 0
        Mock Invoke-RestMethod {
            try {
                if ($pesterRetryCount -lt 3) {
                    $errorDetails = '{"code": 1, "message": "BadRequest", "more_info": "", "status": 429}'
                    $statusCode = 429
                    $response = New-Object System.Net.Http.HttpResponseMessage $statusCode
                    $exception = New-Object Microsoft.PowerShell.Commands.HttpResponseException "$statusCode ($($response.ReasonPhrase))", $response
                    $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidOperation
                    $errorID = 'WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand'
                    $targetObject = $null
                    $errorRecord = New-Object Management.Automation.ErrorRecord $exception, $errorID, $errorCategory, $targetObject
                    $errorRecord.ErrorDetails = $errorDetails
                    throw $errorRecord
                }
                else {
                    return @{ StatusCode = 200 }
                }
            }
            finally {
                $script:pesterRetryCount++
            } 
        }

        It "Should implement an exponential back-off strategy" {

            # Act
            $startTime = Get-Date
            $result = Invoke-GitHubRestMethod @mockSplat -InitialBackOffSeconds 1

            # Assert
            $result | Should -Not -BeNullOrEmpty
            $result.StatusCode | Should -Be 200
            $stopTime = Get-Date
            $elapsedTime = $stopTime - $startTime
            $elapsedTime.TotalSeconds | Should -BeGreaterThan 4
        }
    }
}