Public/Tests/Invoke-OpenApiRequest.Tests.ps1

BeforeAll {
    $env:TCS_CONFIG_ROOT = Join-Path -Path $TestDrive -ChildPath 'config'
    $env:TCS_SKIP_UPDATE_CHECK = '1'
    $env:TCS_TELEMETRY_OPTOUT = '1'
    $ModuleRoot = Split-Path -Path $PSScriptRoot -Parent | Split-Path -Parent
    Import-Module -Name (Join-Path -Path $ModuleRoot -ChildPath 'tcs.openapi.psd1') -Force
    # Windows PowerShell 5.1 does not load System.Net.Http by default
    Add-Type -AssemblyName 'System.Net.Http'
    $RepoRoot = Split-Path -Path (Split-Path -Path $ModuleRoot -Parent) -Parent
    . (Join-Path -Path $RepoRoot -ChildPath 'tests/Helpers/TestHttpServer.ps1')
    $script:server = Start-TestHttpServer -Handler { param($Request) @{ Body = @{ method = $Request.Method; path = $Request.Path } } }
    Set-OpenApiContext -Service 'Direct' -BaseUri $script:server.BaseUri
}

AfterAll {
    $script:server.Stop()
    Remove-Module -Name tcs.openapi -Force -ErrorAction SilentlyContinue
}

Describe 'Invoke-OpenApiRequest' {
    BeforeEach {
        $script:server.Requests.Clear()
    }

    It 'accepts the operation metadata as a hashtable or a PSCustomObject' {
        $hashtable = @{ OperationId = 'a'; Method = 'get'; Path = '/h/{id}'; Parameters = @(@{ Name = 'id'; In = 'path' }); Security = @() }
        (Invoke-OpenApiRequest -Service 'Direct' -Operation $hashtable -PathParameters @{ id = 1 }).path | Should -Be '/h/1'
        $json = '{"OperationId":"b","Method":"DELETE","Path":"/o/{id}","Parameters":[{"Name":"id","In":"path","Style":null,"Explode":null,"AllowReserved":false}],"Security":[],"SecuritySchemes":{},"Paging":null,"ResponseTypeName":"Direct.Thing","PSTypeName":"Tcs.OpenApi.OperationMetadata"}'
        $object = $json | ConvertFrom-Json
        $result = Invoke-OpenApiRequest -Service 'Direct' -Operation $object -PathParameters @{ id = 'x' }
        $result.method | Should -Be 'DELETE'
        $result.path | Should -Be '/o/x'
        $result.PSObject.TypeNames[0] | Should -Be 'Direct.Thing'
    }

    It 'defaults to GET and sends a User-Agent and Accept header' {
        $null = Invoke-OpenApiRequest -Service 'Direct' -Operation @{ Path = '/x' }
        $script:server.Requests[0].Method | Should -Be 'GET'
        $script:server.Requests[0].Headers['User-Agent'] | Should -Match '^tcs\.openapi/'
        $script:server.Requests[0].Headers['Accept'] | Should -Match 'application/json'
    }

    It 'sends the operation response content types as Accept' {
        $null = Invoke-OpenApiRequest -Service 'Direct' -Operation @{ Path = '/x'; ResponseContentTypes = @('application/xml', 'text/csv') }
        $script:server.Requests[0].Headers['Accept'] | Should -Be 'application/xml, text/csv'
    }

    It 'reads a stream body once and never hands the caller''s stream to the HTTP layer' {
        # On .NET Framework HttpClient disposes the request content, so a stream passed through would be closed
        Mock -ModuleName tcs.openapi -CommandName New-OpenApiHttpContent -MockWith {
            New-Object System.Net.Http.ByteArrayContent -ArgumentList (, [byte[]]$Body)
        }
        $stream = New-Object System.IO.MemoryStream -ArgumentList (, [byte[]](1, 2, 3))
        $stream.Position = 2
        $null = Invoke-OpenApiRequest -Service 'Direct' -Operation @{ Path = '/x'; Method = 'PUT'; RequestContentTypes = @('application/octet-stream') } -Body $stream
        Should -Invoke -ModuleName tcs.openapi -CommandName New-OpenApiHttpContent -Times 1 -Exactly -ParameterFilter { $Body -is [byte[]] }
        $script:server.Requests[0].BodyBytes | Should -Be ([byte[]](1, 2, 3))
        $stream.CanRead | Should -BeTrue
        $stream.Position | Should -Be 2
        $stream.Dispose()
    }

    It 'takes the caller''s preferences and sends a read under $WhatIfPreference' {
        # A generated Get- command has no -WhatIf, so a session-wide $WhatIfPreference must not stop the engine
        # from copying the caller's preferences (Set-Variable honours -WhatIf) or from sending the request.
        # $WhatIfPreference reaches the module through the global scope, so it is set there
        function Invoke-TestRead {
            [CmdletBinding()]
            param()
            Invoke-OpenApiRequest -Service 'Direct' -Operation @{ Path = '/read' } -Cmdlet $PSCmdlet
        }
        $saved = $global:WhatIfPreference
        try {
            $global:WhatIfPreference = $true
            $output = @(Invoke-TestRead -Verbose 4>&1)
        }
        finally {
            $global:WhatIfPreference = $saved
        }
        $script:server.Requests.Count | Should -Be 1
        @($output | Where-Object -FilterScript { $_ -is [System.Management.Automation.VerboseRecord] }).Count | Should -BeGreaterThan 0
        ($output | Where-Object -FilterScript { $_ -isnot [System.Management.Automation.VerboseRecord] }).path | Should -Be '/read'
    }

    It 'writes an error when no service name is known' {
        { Invoke-OpenApiRequest -Operation @{ Path = '/x' } -ErrorAction Stop } | Should -Throw -ErrorId 'OpenApi.MissingService*'
    }

    It 'has comment-based help for every parameter' {
        $help = Get-Help -Name Invoke-OpenApiRequest -Full
        $help.Synopsis | Should -Not -BeNullOrEmpty
        @($help.Examples.Example).Count | Should -BeGreaterThan 0
        foreach ($name in 'Service', 'Operation', 'PathParameters', 'QueryParameters', 'HeaderParameters', 'CookieParameters', 'Body', 'ContentType', 'OutFile', 'All', 'Raw', 'Cmdlet') {
            ($help.Parameters.Parameter | Where-Object -FilterScript { $_.Name -eq $name }).Description | Should -Not -BeNullOrEmpty
        }
    }
}