Tests/GitHubGraphQl.Tests.ps1

# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

<#
.Synopsis
   Tests for GitHubGraphQl.ps1 module
#>


[CmdletBinding()]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '',
    Justification = 'Suppress false positives in Pester code blocks')]
param()

BeforeAll {
    # This is common test code setup logic for all Pester test files
    $moduleRootPath = Split-Path -Path $PSScriptRoot -Parent
    . (Join-Path -Path $moduleRootPath -ChildPath 'Tests\Common.ps1')
}

Describe 'GitHubCore/Invoke-GHGraphQl' {
    BeforeAll {
        $Description = 'description'
        $AccessToken = ''
        $TelemetryEventName = $null
        $TelemetryProperties = @{}
        $TelemetryExceptionBucket = $null

        Mock -CommandName Invoke-UpdateCheck -ModuleName $script:moduleName
    }

    Context 'When a valid query is specified' {
        BeforeAll {
            $testBody = '{ "query": "query login { viewer { login } }" }'
        }

        BeforeEach {
            $invokeGHGraphQLParms = @{
                Body = $testBody
            }
            $result = Invoke-GHGraphQl @invokeGHGraphQLParms
        }

        It 'Should return the expected result' {

            $result.data.viewer.login | Should -Be $script:ownerName
        }

        It 'Should call the expected mocks' {
            Assert-MockCalled -CommandName Invoke-UpdateCheck `
                -ModuleName $script:moduleName `
                -Exactly -Times 1
        }
    }

    Context 'When there is a Web/HTTP Request exception in Invoke-WebRequest' {
        BeforeAll {
            $testHostName = 'invalidhostname'
            $testBody = 'testBody'

            if ($PSVersionTable.PSEdition -eq 'Core')
            {
                # The exception message varies per platform. We could special-case it, but the exact message
                # may change over time and the module itself doesn't care about the specific message.
                # We'll just do a best-case match.
                # Windows: "No such host is known. ($($testHostName):443)"
                # Mac: "nodename nor servname provided, or not known ($($testHostName):443)"
                # Linux: "Resource temporarily unavailable ($($testHostName):443)"
                $exceptionMessage = "*$testHostName*"
                $categoryInfo = 'InvalidOperation'
                $targetName = "*$testHostName*"
            }
            else
            {
                $exceptionMessage = "The remote name could not be resolved: '$testHostName'"
                $categoryInfo = 'NotSpecified'
                $targetName = $testBody
            }

            Mock -CommandName Get-GitHubConfiguration -ModuleName $script:moduleName `
                -ParameterFilter { $Name -eq 'ApiHostName' } `
                -MockWith { 'invalidhostname' }
        }

        It 'Should throw the correct exception' {
            $invokeGHGraphQLParms = @{
                Body = $testBody
            }
            { Invoke-GHGraphQl @invokeGHGraphQlParms } |
            Should -Throw

            $Error[0].Exception.Message | Should -BeLike $exceptionMessage
            $Error[0].CategoryInfo.Category | Should -Be $categoryInfo
            $Error[0].CategoryInfo.TargetName | Should -BeLike $targetName
            $Error[0].FullyQualifiedErrorId | Should -BeLike '*Invoke-GHGraphQl'
        }
    }

    Context 'When there is a Web/HTTP Response exception in Invoke-WebRequest' {
        Context 'When there is invalid JSON in the request body' {
            BeforeAll {
                $testBody = 'InvalidJson'

                if ($PSVersionTable.PSEdition -eq 'Core')
                {
                    $exceptionMessage1 = '*Response status code does not indicate success: 400 (Bad Request)*'
                }
                else
                {
                    $exceptionMessage1 = '*The remote server returned an error: (400) Bad Request*'
                }

                $exceptionMessage2 = '*Problems parsing JSON | https://docs.github.com/graphql*'
            }

            It 'Should throw the correct exception' {
                $invokeGHGraphQLParms = @{
                    Body = $testBody
                }
                { Invoke-GHGraphQl @invokeGHGraphQlParms } |
                Should -Throw

                $Error[0].Exception.Message | Should -BeLike $exceptionMessage1
                $Error[0].Exception.Message | Should -BeLike $exceptionMessage2
                $Error[0].Exception.Message | Should -BeLike '*RequestId:*'
                $Error[0].CategoryInfo.Category | Should -Be 'InvalidOperation'
                $Error[0].CategoryInfo.TargetName | Should -Be $testBody
                $Error[0].FullyQualifiedErrorId | Should -BeLike '*Invoke-GHGraphQl'
            }
        }

        Context 'When the query user is not authenticated' {
            BeforeAll {
                $testBody = '{ "query": "query login { viewer { login } }" }'

                if ($PSVersionTable.PSEdition -eq 'Core')
                {
                    $exceptionMessage1 = '*Response status code does not indicate success: 401 (Unauthorized)*'
                }
                else
                {
                    $exceptionMessage1 = '*The remote server returned an error: (401) Unauthorized*'
                }

                $exceptionMessage2 = '*This endpoint requires you to be authenticated.*'

                Mock -CommandName Get-AccessToken -ModuleName $script:moduleName
            }

            It 'Should throw the correct exception' {
                $invokeGHGraphQLParms = @{
                    Body = $testBody
                }
                { Invoke-GHGraphQl @invokeGHGraphQlParms } |
                Should -Throw

                $Error[0].Exception.Message | Should -BeLike $exceptionMessage1
                $Error[0].Exception.Message | Should -BeLike $exceptionMessage2
                $Error[0].Exception.Message | Should -BeLike '*RequestId:*'
                $Error[0].CategoryInfo.Category | Should -Be 'InvalidOperation'
                $Error[0].CategoryInfo.TargetName | Should -Be $testBody
                $Error[0].FullyQualifiedErrorId | Should -BeLike '*Invoke-GHGraphQl'
            }
        }
    }

    Context 'When there is an other exception in Invoke-WebRequest' {
        BeforeAll {
            $testWebRequestTimeoutSec = 'invalid'
            $testBody = 'testBody'

            Mock -CommandName Get-GitHubConfiguration -ModuleName $script:moduleName `
                -ParameterFilter { $Name -eq 'WebRequestTimeoutSec' } `
                -MockWith { 'invalid' }
        }

        It 'Should throw the correct exception' {
            $invokeGHGraphQLParms = @{
                Body = $testBody
            }
            { Invoke-GHGraphQl @invokeGHGraphQlParms } |
            Should -Throw

            $Error[0].CategoryInfo.Category | Should -Be 'InvalidArgument'
            $Error[0].CategoryInfo.TargetName | Should -Be $testBody
            $Error[0].FullyQualifiedErrorId | Should -BeLike 'CannotConvertArgumentNoMessage*'
        }
    }

    Context 'When the GraphQl JSON Query is Invalid' {
        BeforeAll {
            $invalidQuery = 'InvalidQuery'
            $testBody = "{ ""query"":""$invalidQuery"" }"
        }

        It 'Should throw the correct exception' {
            $invokeGHGraphQLParms = @{
                Body = $testBody
            }
            { Invoke-GHGraphQl @invokeGHGraphQlParms } | Should -Throw

            $Error[0].Exception.Message | Should -BeLike "*Parse error on ""$invalidQuery""*"
            $Error[0].Exception.Message | Should -BeLike '*RequestId:*'
            $Error[0].CategoryInfo.Category | Should -Be 'NotSpecified'
            $Error[0].CategoryInfo.TargetName | Should -Be $testBody
            $Error[0].FullyQualifiedErrorId | Should -BeLike '*Invoke-GHGraphQl'
        }
    }

    Context 'When the GraphQl JSON query returns an error of ''NOT_FOUND''' {
        BeforeAll {
            $testOwner = 'microsoft'
            $testRepo = 'nonexisting-repo'
            $testQuery = "query repo { repository(name: \""$testRepo\"", owner: \""$testOwner\"") { id } }"
            $testBody = "{ ""query"": ""$testQuery"" }"
        }

        It 'Should throw the correct exception' {
            $invokeGHGraphQLParms = @{
                Body = $testBody
            }
            { Invoke-GHGraphQl @invokeGHGraphQlParms } | Should -Throw

            $Error[0].Exception.Message | Should -BeLike "*Could not resolve to a Repository with the name '$testOwner/$testRepo'*"
            $Error[0].Exception.Message | Should -BeLike '*RequestId:*'
            $Error[0].CategoryInfo.Category | Should -Be 'ObjectNotFound'
            $Error[0].CategoryInfo.TargetName | Should -Be $testBody
            $Error[0].FullyQualifiedErrorId | Should -Be 'NOT_FOUND,Invoke-GHGraphQl'
        }
    }
}

AfterAll {
    if (Test-Path -Path $script:originalConfigFile -PathType Leaf)
    {
        # Restore the user's configuration to its pre-test state
        Restore-GitHubConfiguration -Path $script:originalConfigFile
        $script:originalConfigFile = $null
    }
}