Private/Generator/Tests/ConvertTo-OpenApiGenFunction.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
    $RepoRoot = Split-Path -Path (Split-Path -Path $ModuleRoot -Parent) -Parent
    . (Join-Path -Path $RepoRoot -ChildPath 'tests/Helpers/New-TestOpenApiModel.ps1')
}

Describe 'ConvertTo-OpenApiGenFunction' {
    BeforeAll {
        function ConvertTo-TestFunction {
            param($Operation, [string]$Prefix = 'Shop', [string]$ResponseTypeName, [string]$HelpUri)
            InModuleScope tcs.openapi -Parameters @{ Operation = $Operation; Prefix = $Prefix; ResponseTypeName = $ResponseTypeName; HelpUri = $HelpUri } {
                param($Operation, $Prefix, $ResponseTypeName, $HelpUri)
                $name = Get-OpenApiGenCommandName -Operation $Operation -NounPrefix $Prefix
                $model = Get-OpenApiGenParameterModel -Operation $Operation -BaseNoun $name.BaseNoun
                $template = (Get-OpenApiGenTemplate -Path (Join-Path -Path $script:TcsOpenApiModuleRoot -ChildPath 'Templates'))['Function.ps1']
                $text = ConvertTo-OpenApiGenFunction -CommandName $name -Operation $Operation -ParameterModel $model -ResponseTypeName $ResponseTypeName -Template $template -HelpUri $HelpUri
                [pscustomobject]@{ Name = $name.Name; Text = $text; Check = (Test-OpenApiGenFunction -Text $text -FunctionName $name.Name) }
            }
        }
        $orderSchema = New-TestSchema -Type object -Required @('item') -Properties ([ordered]@{
                item  = New-TestSchema -Type string
                count = New-TestSchema -Type integer -Format int32
            })
    }

    It 'renders a function that parses and binds' {
        $operation = New-TestOperation -OperationId 'getOrder' -Method GET -Path '/orders/{orderId}' -Parameters @((New-TestParameter -Name 'orderId' -In path))
        $result = ConvertTo-TestFunction -Operation $operation
        $result.Check.IsValid | Should -BeTrue
        $result.Check.ParameterNames | Should -Contain 'OrderId'
        $result.Text | Should -Match '(?m)^function Get-ShopOrder \{$'
        $result.Text | Should -Match "Invoke-OpenApiRequest @tcsRequest"
        $result.Text | Should -Match "\`$script:TcsOpenApiOperations\['getOrder'\]"
        $result.Text | Should -Match "'OrderId' = @\('path', 'orderId'\)"
        $result.Text | Should -Match "\[Alias\('Id'\)\]"
    }

    It 'uses ShouldProcess for <Method> with ConfirmImpact <Impact>' -TestCases @(
        @{ Method = 'POST'; Impact = 'Medium' }, @{ Method = 'PUT'; Impact = 'Medium' }, @{ Method = 'PATCH'; Impact = 'Medium' }, @{ Method = 'DELETE'; Impact = 'High' }
    ) {
        param($Method, $Impact)
        $result = ConvertTo-TestFunction -Operation (New-TestOperation -OperationId 'doThing' -Method $Method -Path '/things/{id}' -Parameters @((New-TestParameter -Name 'id' -In path)))
        $result.Text | Should -Match "SupportsShouldProcess = \`$true, ConfirmImpact = '$Impact'"
        $result.Text | Should -Match "ShouldProcess\(\`$tcsTarget, '$Method'\)"
        $result.Check.ParameterNames | Should -Contain 'WhatIf'
    }

    It 'does not use ShouldProcess for <Method>' -TestCases @(@{ Method = 'GET' }, @{ Method = 'HEAD' }, @{ Method = 'OPTIONS' }) {
        param($Method)
        $result = ConvertTo-TestFunction -Operation (New-TestOperation -OperationId 'readThing' -Method $Method -Path '/things')
        $result.Text | Should -Not -Match 'ShouldProcess'
        $result.Check.ParameterNames | Should -Not -Contain 'WhatIf'
    }

    It 'uses ShouldProcess for a state-changing verb on GET' {
        (ConvertTo-TestFunction -Operation (New-TestOperation -OperationId 'resetCache' -Method GET -Path '/cache')).Text | Should -Match 'SupportsShouldProcess'
    }

    It 'does not use ShouldProcess for <Verb> from <OperationId> on <Method>' -TestCases @(
        @{ OperationId = 'queryMetrics'; Method = 'POST'; Verb = 'Get' }
        @{ OperationId = 'searchOrders'; Method = 'POST'; Verb = 'Get' }
        @{ OperationId = 'getReport'; Method = 'PUT'; Verb = 'Get' }
        @{ OperationId = 'validateOrder'; Method = 'POST'; Verb = 'Test' }
        @{ OperationId = 'checkAddress'; Method = 'PATCH'; Verb = 'Test' }
    ) {
        param($OperationId, $Method, $Verb)
        $result = ConvertTo-TestFunction -Operation (New-TestOperation -OperationId $OperationId -Method $Method -Path '/things')
        $result.Name | Should -BeLike "$Verb-*"
        $result.Text | Should -Not -Match 'ShouldProcess'
        $result.Check.ParameterNames | Should -Not -Contain 'WhatIf'
        $result.Check.ParameterNames | Should -Not -Contain 'Confirm'
    }

    It 'uses ShouldProcess for <Verb> from <OperationId> on <Method>' -TestCases @(
        @{ OperationId = 'runReport'; Method = 'POST'; Verb = 'Invoke' }
        @{ OperationId = 'sendMessage'; Method = 'POST'; Verb = 'Send' }
        @{ OperationId = 'exportOrders'; Method = 'POST'; Verb = 'Export' }
        @{ OperationId = 'getRidOfOrder'; Method = 'DELETE'; Verb = 'Get' }
    ) {
        param($OperationId, $Method, $Verb)
        $result = ConvertTo-TestFunction -Operation (New-TestOperation -OperationId $OperationId -Method $Method -Path '/things')
        $result.Name | Should -BeLike "$Verb-*"
        $result.Text | Should -Match 'SupportsShouldProcess'
        $result.Check.ParameterNames | Should -Contain 'WhatIf'
    }

    It 'renders a flattened body with two parameter sets' {
        $operation = New-TestOperation -OperationId 'createOrder' -Method POST -Path '/orders' -RequestBody (New-TestRequestBody -Required -Content @((New-TestMediaType -ContentType 'application/json' -Schema $orderSchema)))
        $result = ConvertTo-TestFunction -Operation $operation
        $result.Check.IsValid | Should -BeTrue
        $result.Check.ParameterSets | Should -Be @('Parameters', 'Body')
        $result.Text | Should -Match "DefaultParameterSetName = 'Parameters'"
        $result.Text | Should -Match "if \(\`$PSCmdlet.ParameterSetName -eq 'Body'\)"
        $result.Text | Should -Match "\`$tcsRequest\['ContentType'\] = 'application/json'"
        $result.Text | Should -Match '(?m)^ else \{$'
    }

    It 'sends a flattened optional body only when a body parameter is bound' {
        $operation = New-TestOperation -OperationId 'createOrder' -Method POST -Path '/orders' -RequestBody (New-TestRequestBody -Content @((New-TestMediaType -ContentType 'application/json' -Schema $orderSchema)))
        (ConvertTo-TestFunction -Operation $operation).Text | Should -Match "elseif \(\`$tcsValues\['body'\].Count -gt 0\)"
    }

    It 'adds OutputType when the response type is known' {
        (ConvertTo-TestFunction -Operation (New-TestOperation -OperationId 'getOrder' -Method GET -Path '/o') -ResponseTypeName 'Shop.Order').Text | Should -Match "\[OutputType\('Shop.Order'\)\]"
        (ConvertTo-TestFunction -Operation (New-TestOperation -OperationId 'getOrder' -Method GET -Path '/o')).Text | Should -Not -Match 'OutputType'
    }

    It 'writes help: synopsis, description, parameters, a runnable example, link and deprecation' {
        $operation = New-TestOperation -OperationId 'getOrder' -Method GET -Path '/orders/{orderId}' -Summary 'Gets an order.' -Description 'Returns one order.' -ExternalDocsUrl 'https://docs.example.com/orders' -Deprecated -Parameters @(
            (New-TestParameter -Name 'orderId' -In path -Description 'The order id.' -Example 'o-1'),
            (New-TestParameter -Name 'expand' -In query -Required -Schema (New-TestSchema -Type array -Items (New-TestSchema -Type string)))
        )
        $result = ConvertTo-TestFunction -Operation $operation
        $result.Text | Should -Match '\.SYNOPSIS\s+Gets an order\.'
        $result.Text | Should -Match '\.DESCRIPTION\s+Returns one order\.'
        $result.Text | Should -Match 'This operation is deprecated\.'
        $result.Text | Should -Match '\.PARAMETER OrderId\s+The order id\.'
        $result.Text | Should -Match '\.PARAMETER Expand\s+The `expand` query parameter\.'
        $result.Text | Should -Match '\.PARAMETER Raw'
        $result.Text | Should -Match "\.EXAMPLE\s+Get-ShopOrder -OrderId 'o-1' -Expand @\('example'\)"
        $result.Text | Should -Match '\.LINK\s+https://docs.example.com/orders'
        $result.Text | Should -Match 'Calls the `getOrder` operation \(`GET /orders/\{orderId\}`\)\.'
        $result.Text | Should -Match '\.NOTES\s+Operation: `getOrder` \(`GET /orders/\{orderId\}`\)'
        $example = [regex]::Match($result.Text, '\.EXAMPLE\s+(.+)').Groups[1].Value
        $tokens = $null
        $errors = $null
        [void][System.Management.Automation.Language.Parser]::ParseInput($example, [ref]$tokens, [ref]$errors)
        $errors.Count | Should -Be 0
    }

    It 'uses the method and path when there is no summary or description' {
        (ConvertTo-TestFunction -Operation (New-TestOperation -OperationId 'getOrder' -Method GET -Path '/o')).Text | Should -Match '\.SYNOPSIS\s+Calls `GET /o`\.'
    }

    It 'turns HTML and Markdown from the document into plain text that MDX accepts' {
        $operation = New-TestOperation -OperationId 'listHosts' -Method GET -Path '/hosts/{site}' -Summary '**List** hosts' -Description 'Lists hosts. <br/><br/>**Note**: see [the docs](https://docs.example.com/h) for `userData` and {placeholders}.' -Parameters @(
            (New-TestParameter -Name 'site' -In path -Required -Description 'The site, as <b>name</b> or List<string> of ids.')
        )
        $text = (ConvertTo-TestFunction -Operation $operation).Text
        $text | Should -Match '\.SYNOPSIS\s+List hosts\s'
        $text | Should -Match '(?m)^\s+Note: see the docs \(https://docs.example.com/h\) for `userData` and `\{placeholders\}`\.$'
        $text | Should -Match 'The site, as name or `List<string>` of ids\.'
        $text | Should -Not -Match '<br|\*\*'
        $help = [regex]::Match($text, '(?s)<#(.*?)#>').Groups[1].Value
        [regex]::Replace($help, '`[^`]*`', '') | Should -Not -Match '[<{}]'
    }

    It 'keeps the developer note out of the help and gives every example a description' {
        $operation = New-TestOperation -OperationId 'getOrder' -Method GET -Path '/orders/{orderId}' -Parameters @((New-TestParameter -Name 'orderId' -In path -Required -Example 'o-1'))
        $text = (ConvertTo-TestFunction -Operation $operation).Text
        $help = [regex]::Match($text, '(?s)<#(.*?)#>').Groups[1].Value
        $help | Should -Not -Match 'Do not edit'
        $text | Should -Match '(?m)^\s+process \{\n\s+# Generated by tcs\.openapi\. Do not edit: put changes in Overrides\.ps1\.'
        $help | Should -Match "\.EXAMPLE\n\s+Get-ShopOrder -OrderId 'o-1'\n\n\s+Calls ``GET /orders/\{orderId\}`` with example values for the required parameters\."
    }

    It 'adds an -All example to pageable commands' {
        $operation = New-TestOperation -OperationId 'listOrders' -Method GET -Path '/orders' -Paging @{ Kind = 'nextLink'; ItemsProperty = 'value'; NextLinkProperty = 'nextLink' }
        $text = (ConvertTo-TestFunction -Operation $operation).Text
        @([regex]::Matches($text, '\.EXAMPLE')).Count | Should -Be 2
        $text | Should -Match '\.EXAMPLE\n\s+Get-ShopOrder -All\n\n\s+Requests the following pages as well and returns the items of every page\.'
    }

    It 'links to -HelpUri first and sets HelpUri' {
        $operation = New-TestOperation -OperationId 'getOrder' -Method GET -Path '/o' -ExternalDocsUrl 'https://api.example.com/docs/getOrder'
        $text = (ConvertTo-TestFunction -Operation $operation -HelpUri 'https://docs.example.com/shop/Get-ShopOrder').Text
        $text | Should -Match "\[CmdletBinding\(HelpUri = 'https://docs.example.com/shop/Get-ShopOrder'\)\]"
        $text | Should -Match '\.LINK\s+https://docs.example.com/shop/Get-ShopOrder\s+\.LINK\s+https://api.example.com/docs/getOrder'
        (ConvertTo-TestFunction -Operation $operation).Text | Should -Not -Match 'HelpUri'
    }

    It 'passes only the locations the operation has' {
        $operation = New-TestOperation -OperationId 'getOrder' -Method GET -Path '/o' -Parameters @((New-TestParameter -Name 'q' -In query), (New-TestParameter -Name 'c' -In cookie))
        $text = (ConvertTo-TestFunction -Operation $operation).Text
        $text | Should -Match "QueryParameters"
        $text | Should -Match "CookieParameters"
        $text | Should -Not -Match "PathParameters"
        $text | Should -Not -Match "HeaderParameters"
    }

    It 'adds a suppression for password parameters and plural-looking nouns' {
        $schema = New-TestSchema -Type object -Properties ([ordered]@{ password = New-TestSchema -Type string })
        $text = (ConvertTo-TestFunction -Operation (New-TestOperation -OperationId 'setPassword' -Method PUT -Path '/p' -RequestBody (New-TestRequestBody -Content @((New-TestMediaType -ContentType 'application/json' -Schema $schema))))).Text
        $text | Should -Match "SuppressMessageAttribute\('PSAvoidUsingPlainTextForPassword'"
        (ConvertTo-TestFunction -Operation (New-TestOperation -OperationId 'getMetadata' -Method GET -Path '/m')).Text | Should -Match "SuppressMessageAttribute\('PSUseSingularNouns'"
    }

    It 'escapes quotes from the document in code and help' {
        $operation = New-TestOperation -OperationId "get'Odd" -Method GET -Path "/it's" -Summary 'Has #> in it' -Parameters @((New-TestParameter -Name "o'k" -In query))
        $result = ConvertTo-TestFunction -Operation $operation
        $result.Check.IsValid | Should -BeTrue
        $result.Text | Should -Match "\['get''Odd'\]"
    }
}