Private/Runtime/Tests/ConvertTo-OpenApiQueryParameter.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 (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -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'
}

AfterAll {
    Remove-Module -Name tcs.openapi -Force -ErrorAction SilentlyContinue
}

Describe 'ConvertTo-OpenApiQueryParameter' {
    # OpenAPI 3.0.3 "Style Examples" table (https://spec.openapis.org/oas/v3.0.3#style-examples), query styles.
    # The table shows spaceDelimited/pipeDelimited values without the name; in a query they follow 'color='.
    It '<Style> explode=<Explode> renders <Kind> as <Expected>' -ForEach @(
        @{ Style = 'form'; Explode = $false; Kind = 'empty'; Expected = 'color=' }
        @{ Style = 'form'; Explode = $false; Kind = 'string'; Expected = 'color=blue' }
        @{ Style = 'form'; Explode = $false; Kind = 'array'; Expected = 'color=blue,black,brown' }
        @{ Style = 'form'; Explode = $false; Kind = 'object'; Expected = 'color=R,100,G,200,B,150' }
        @{ Style = 'form'; Explode = $true; Kind = 'empty'; Expected = 'color=' }
        @{ Style = 'form'; Explode = $true; Kind = 'string'; Expected = 'color=blue' }
        @{ Style = 'form'; Explode = $true; Kind = 'array'; Expected = 'color=blue&color=black&color=brown' }
        @{ Style = 'form'; Explode = $true; Kind = 'object'; Expected = 'R=100&G=200&B=150' }
        @{ Style = 'spaceDelimited'; Explode = $false; Kind = 'array'; Expected = 'color=blue%20black%20brown' }
        @{ Style = 'spaceDelimited'; Explode = $false; Kind = 'object'; Expected = 'color=R%20100%20G%20200%20B%20150' }
        @{ Style = 'pipeDelimited'; Explode = $false; Kind = 'array'; Expected = 'color=blue|black|brown' }
        @{ Style = 'pipeDelimited'; Explode = $false; Kind = 'object'; Expected = 'color=R|100|G|200|B|150' }
        @{ Style = 'deepObject'; Explode = $true; Kind = 'object'; Expected = 'color%5BR%5D=100&color%5BG%5D=200&color%5BB%5D=150' }  # the spec shows color[R]=100; brackets are percent-encoded so both editions send the same query
    ) {
        $values = @{
            empty  = $null
            string = 'blue'
            array  = @('blue', 'black', 'brown')
            object = [ordered]@{ R = 100; G = 200; B = 150 }
        }
        InModuleScope -ModuleName tcs.openapi -Parameters @{ Style = $Style; Explode = $Explode; Value = $values[$Kind]; Expected = $Expected } {
            (ConvertTo-OpenApiQueryParameter -Name 'color' -Value $Value -Style $Style -Explode:$Explode) -join '&' | Should -BeExactly $Expected
        }
    }

    It 'treats an empty string and an empty array as empty' {
        InModuleScope -ModuleName tcs.openapi {
            (ConvertTo-OpenApiQueryParameter -Name 'q' -Value '') -join '&' | Should -BeExactly 'q='
            (ConvertTo-OpenApiQueryParameter -Name 'q' -Value @()) -join '&' | Should -BeExactly 'q='
        }
    }

    It 'percent-encodes reserved characters unless allowReserved is set' {
        InModuleScope -ModuleName tcs.openapi {
            (ConvertTo-OpenApiQueryParameter -Name 'filter' -Value 'a/b:c d&e') -join '&' | Should -BeExactly 'filter=a%2Fb%3Ac%20d%26e'
            (ConvertTo-OpenApiQueryParameter -Name 'filter' -Value 'a/b:c d' -AllowReserved) -join '&' | Should -BeExactly 'filter=a/b:c%20d'
        }
    }

    It 'encodes non-ASCII text as UTF-8' {
        InModuleScope -ModuleName tcs.openapi {
            (ConvertTo-OpenApiQueryParameter -Name 'name' -Value ([string][char]0x00E9)) -join '&' | Should -BeExactly 'name=%C3%A9'
        }
    }

    It 'writes booleans in lower case, dates as ISO 8601 and numbers with the invariant culture' {
        InModuleScope -ModuleName tcs.openapi {
            (ConvertTo-OpenApiQueryParameter -Name 'on' -Value $false) -join '&' | Should -BeExactly 'on=false'
            $date = New-Object System.DateTime -ArgumentList 2024, 5, 6, 7, 8, 9, ([System.DateTimeKind]::Utc)
            (ConvertTo-OpenApiQueryParameter -Name 'since' -Value $date) -join '&' | Should -BeExactly 'since=2024-05-06T07%3A08%3A09.0000000Z'
            (ConvertTo-OpenApiQueryParameter -Name 'n' -Value 1.5) -join '&' | Should -BeExactly 'n=1.5'
        }
    }

    It 'nests deepObject values and repeats arrays' {
        InModuleScope -ModuleName tcs.openapi {
            $value = [ordered]@{ a = [ordered]@{ b = 1 }; tags = @('x', 'y') }
            (ConvertTo-OpenApiQueryParameter -Name 'f' -Value $value -Style 'deepObject' -Explode) -join '&' | Should -BeExactly 'f%5Ba%5D%5Bb%5D=1&f%5Btags%5D=x&f%5Btags%5D=y'
        }
    }

    It 'uses the form style for a scalar with a delimited style' {
        InModuleScope -ModuleName tcs.openapi {
            (ConvertTo-OpenApiQueryParameter -Name 'c' -Value 'blue' -Style 'pipeDelimited') -join '&' | Should -BeExactly 'c=blue'
        }
    }
}