Public/Tests/Import-OpenApiDocument.Tests.ps1

BeforeAll {
    Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '../../tcs.openapi.psd1') -Force
    $script:fixtures = (Resolve-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '../../../../tests/Fixtures')).ProviderPath
    $script:petstoreJson = Join-Path -Path $script:fixtures -ChildPath 'document-petstore-3.0.json'
    $script:petstoreYaml = Join-Path -Path $script:fixtures -ChildPath 'document-petstore-3.0.yaml'
}

Describe 'Import-OpenApiDocument' {
    Context 'Help' {
        It 'has a synopsis, a description, parameter help and an example' {
            $help = Get-Help -Name Import-OpenApiDocument -Full
            $help.Synopsis | Should -Not -BeNullOrEmpty
            $help.Description | Should -Not -BeNullOrEmpty
            @($help.Examples.Example).Count | Should -BeGreaterThan 0
            foreach ($name in 'Path', 'Uri', 'InputObject') {
                ($help.Parameters.Parameter | Where-Object Name -EQ $name).Description | Should -Not -BeNullOrEmpty
            }
        }
    }

    Context '-Path' {
        It 'returns the document model' {
            $model = Import-OpenApiDocument -Path $script:petstoreJson
            $model.PSObject.TypeNames | Should -Contain 'Tcs.OpenApi.Document'
            $model.Title | Should -Be 'Petstore'
            $model.Operations.Count | Should -Be 6
            $model.Operations[0].PSObject.TypeNames | Should -Contain 'Tcs.OpenApi.Operation'
        }

        It 'accepts files from the pipeline' {
            $model = Get-Item -Path $script:petstoreJson | Import-OpenApiDocument
            $model.Title | Should -Be 'Petstore'
        }

        It 'throws for a missing file' {
            { Import-OpenApiDocument -Path (Join-Path -Path $TestDrive -ChildPath 'missing.json') -ErrorAction Stop } | Should -Throw
        }
    }

    Context '-InputObject' {
        It 'reads a JSON string' {
            $text = Get-Content -Path $script:petstoreJson -Raw
            (Import-OpenApiDocument -InputObject $text).Title | Should -Be 'Petstore'
        }

        It 'throws for invalid JSON' {
            { Import-OpenApiDocument -InputObject '{"openapi":' -ErrorAction Stop } | Should -Throw -ExpectedMessage '*not valid JSON*'
        }

        It 'throws OpenApi.UnsupportedVersion (OA001) for an unsupported version' {
            $thrown = $null
            try {
                Import-OpenApiDocument -InputObject '{"openapi":"3.2.0","paths":{}}' -ErrorAction Stop
            }
            catch {
                $thrown = $_
            }
            $thrown.FullyQualifiedErrorId | Should -BeLike 'OpenApi.UnsupportedVersion*'
            $thrown.Exception.Message | Should -BeLike "*'3.2.0' is not supported*"
        }

        It 'returns the model for documents with other problems' {
            $model = Import-OpenApiDocument -InputObject '{"openapi":"3.0.0","info":{"title":"t","version":"1"}}'
            $model.Findings[0].Code | Should -Be 'OA002'
        }
    }

    Context '-Uri' {
        It 'downloads the document and resolves relative servers against the URL' {
            $bytes = [System.IO.File]::ReadAllBytes($script:petstoreJson)
            Mock -ModuleName tcs.openapi Invoke-WebRequest {
                [pscustomobject]@{ RawContentStream = New-Object -TypeName System.IO.MemoryStream -ArgumentList (, $bytes); Content = '' }
            }.GetNewClosure()
            $model = Import-OpenApiDocument -Uri 'https://petstore.example.com/spec/openapi.json'
            $model.Title | Should -Be 'Petstore'
            $model.Servers[1].Url | Should -Be 'https://petstore.example.com/v1'
            Should -Invoke -ModuleName tcs.openapi Invoke-WebRequest -Times 1
        }

        It 'throws when the download fails' {
            Mock -ModuleName tcs.openapi Invoke-WebRequest { throw 'Name resolution failed' }
            { Import-OpenApiDocument -Uri 'https://nowhere.example.com/openapi.json' -ErrorAction Stop } | Should -Throw -ExpectedMessage '*Name resolution failed*'
        }
    }

    Context 'YAML' {
        It 'reads a .yaml file that holds JSON without powershell-yaml' {
            Mock -ModuleName tcs.openapi Get-OpenApiYamlConverter { }
            $path = Join-Path -Path $TestDrive -ChildPath 'json-in.yaml'
            Copy-Item -Path $script:petstoreJson -Destination $path
            (Import-OpenApiDocument -Path $path).Title | Should -Be 'Petstore'
        }

        It 'throws a clear error when powershell-yaml is not available' {
            Mock -ModuleName tcs.openapi Get-OpenApiYamlConverter { }
            $thrown = $null
            try {
                Import-OpenApiDocument -Path $script:petstoreYaml -ErrorAction Stop
            }
            catch {
                $thrown = $_
            }
            $thrown.FullyQualifiedErrorId | Should -BeLike 'OpenApi.YamlNotSupported*'
            $thrown.Exception.Message | Should -BeLike '*Install-Module powershell-yaml*'
        }

        It 'uses ConvertFrom-Yaml when it is available and gives the same model as the JSON copy' {
            $seen = @{}
            $jsonText = Get-Content -Path $script:petstoreJson -Raw
            $parsed = & (Get-Module -Name tcs.openapi) { param($Text) ConvertFrom-OpenApiJson -Text $Text } $jsonText
            # Stand-in for powershell-yaml: records its arguments and returns the parsed equivalent of the YAML file
            New-Item -Path 'Function:\global:ConvertFrom-Yaml' -Value {
                param([string]$Yaml, [switch]$Ordered)
                $seen['Yaml'] = $Yaml
                $seen['Ordered'] = [bool]$Ordered
                $parsed
            }.GetNewClosure() -Force | Out-Null
            try {
                $fromYaml = Import-OpenApiDocument -Path $script:petstoreYaml
            }
            finally {
                Remove-Item -Path 'Function:\ConvertFrom-Yaml' -ErrorAction SilentlyContinue
            }
            $seen['Yaml'] | Should -BeExactly (Get-Content -Path $script:petstoreYaml -Raw)
            $seen['Ordered'] | Should -BeTrue
            $fromJson = Import-OpenApiDocument -Path $script:petstoreJson
            ($fromYaml | ConvertTo-Json -Depth 100 -Compress) | Should -BeExactly ($fromJson | ConvertTo-Json -Depth 100 -Compress)
        }
    }

    Context 'Swagger 2.0' {
        It 'returns an OpenAPI 3-shaped model' {
            $model = Import-OpenApiDocument -Path (Join-Path -Path $script:fixtures -ChildPath 'document-swagger-2.0.json')
            $model.SourceVersion | Should -Be '2.0'
            $model.Schemas['Pet'].RefName | Should -Be 'Pet'
            ($model.Operations | Where-Object OperationId -EQ 'addPet').RequestBody.Content[0].ContentType | Should -Be 'application/json'
        }
    }
}