Private/Document/Tests/ConvertTo-OpenApiSchema.Tests.ps1

BeforeAll {
    Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '../../../tcs.openapi.psd1') -Force
}

Describe 'ConvertTo-OpenApiSchema' {
    BeforeAll {
        # Created inside the module so the script block runs in module scope
        $script:convert = InModuleScope tcs.openapi {
            {
                param([string]$Schema, [string]$Components = '{}')
                $root = ConvertFrom-OpenApiJson -Text ('{"components":{"schemas":' + $Components + '}}')
                $context = Get-OpenApiNormalizationContext -Root $root -SourceVersion '3.1.0'
                $result = ConvertTo-OpenApiSchema -Context $context -Node (ConvertFrom-OpenApiJson -Text $Schema) -Pointer '/s'
                [pscustomobject]@{ Schema = $result; Context = $context }
            }
        }
    }

    It 'copies scalar keywords' {
        InModuleScope tcs.openapi -Parameters @{ Convert = $script:convert } {
            param($Convert)
            $schema = (& $Convert '{"type":"string","format":"date-time","description":"d","title":"t","pattern":"^a","minLength":1,"maxLength":5,"default":"x","example":"e","enum":["x","y"],"deprecated":true}').Schema
            $schema.Type | Should -Be 'string'
            $schema.Format | Should -Be 'date-time'
            $schema.Description | Should -Be 'd'
            $schema.Title | Should -Be 't'
            $schema.Pattern | Should -Be '^a'
            $schema.MinLength | Should -Be 1
            $schema.MaxLength | Should -Be 5
            $schema.Default | Should -Be 'x'
            $schema.Example | Should -Be 'e'
            $schema.Enum | Should -Be @('x', 'y')
            $schema.Deprecated | Should -BeTrue
            $schema.Nullable | Should -BeFalse
        }
    }

    It 'maps 3.0 nullable' {
        InModuleScope tcs.openapi -Parameters @{ Convert = $script:convert } {
            param($Convert)
            (& $Convert '{"type":"string","nullable":true}').Schema.Nullable | Should -BeTrue
        }
    }

    It 'maps 3.1 type arrays with null to Type + Nullable' {
        InModuleScope tcs.openapi -Parameters @{ Convert = $script:convert } {
            param($Convert)
            $result = & $Convert '{"type":["string","null"]}'
            $result.Schema.Type | Should -Be 'string'
            $result.Schema.Nullable | Should -BeTrue
            $result.Context.Findings.Count | Should -Be 0
        }
    }

    It 'maps type null to an untyped nullable schema' {
        InModuleScope tcs.openapi -Parameters @{ Convert = $script:convert } {
            param($Convert)
            $schema = (& $Convert '{"type":"null"}').Schema
            $schema.Type | Should -BeNullOrEmpty
            $schema.Nullable | Should -BeTrue
        }
    }

    It 'reports several non-null types as OA031 and leaves Type null' {
        InModuleScope tcs.openapi -Parameters @{ Convert = $script:convert } {
            param($Convert)
            $result = & $Convert '{"type":["string","integer","null"]}'
            $result.Schema.Type | Should -BeNullOrEmpty
            $result.Schema.Nullable | Should -BeTrue
            $result.Context.Findings[0].Code | Should -Be 'OA031'
            $result.Context.Findings[0].Severity | Should -Be 'Warning'
            $result.Context.Findings[0].Pointer | Should -BeExactly '/s/type'
        }
    }

    It 'keeps const and takes the first of 3.1 examples' {
        InModuleScope tcs.openapi -Parameters @{ Convert = $script:convert } {
            param($Convert)
            $schema = (& $Convert '{"const":"fixed","examples":["a","b"]}').Schema
            $schema.Const | Should -Be 'fixed'
            $schema.Example | Should -Be 'a'
        }
    }

    It 'normalises items, properties, required and readOnly/writeOnly' {
        InModuleScope tcs.openapi -Parameters @{ Convert = $script:convert } {
            param($Convert)
            $schema = (& $Convert '{"type":"object","required":["id"],"properties":{"id":{"type":"string","readOnly":true},"pw":{"type":"string","writeOnly":true},"tags":{"type":"array","items":{"type":"string"}}}}').Schema
            @($schema.Properties.Keys) | Should -Be @('id', 'pw', 'tags')
            $schema.Required | Should -Be @('id')
            $schema.Properties['id'].ReadOnly | Should -BeTrue
            $schema.Properties['pw'].WriteOnly | Should -BeTrue
            $schema.Properties['tags'].Items.Type | Should -Be 'string'
        }
    }

    It 'keeps property names that differ only in case' {
        InModuleScope tcs.openapi -Parameters @{ Convert = $script:convert } {
            param($Convert)
            $schema = (& $Convert '{"type":"object","properties":{"Id":{"type":"string"},"id":{"type":"integer"}}}').Schema
            $schema.Properties['Id'].Type | Should -Be 'string'
            $schema.Properties['id'].Type | Should -Be 'integer'
        }
    }

    It 'maps additionalProperties as a boolean or a schema' {
        InModuleScope tcs.openapi -Parameters @{ Convert = $script:convert } {
            param($Convert)
            (& $Convert '{"type":"object","additionalProperties":false}').Schema.AdditionalProperties | Should -BeFalse
            (& $Convert '{"type":"object","additionalProperties":{"type":"integer"}}').Schema.AdditionalProperties.Type | Should -Be 'integer'
            (& $Convert '{"type":"object"}').Schema.AdditionalProperties | Should -BeNullOrEmpty
        }
    }

    It 'infers object and array types when type is missing' {
        InModuleScope tcs.openapi -Parameters @{ Convert = $script:convert } {
            param($Convert)
            (& $Convert '{"properties":{"a":{}}}').Schema.Type | Should -Be 'object'
            (& $Convert '{"additionalProperties":true}').Schema.Type | Should -Be 'object'
            (& $Convert '{"items":{"type":"string"}}').Schema.Type | Should -Be 'array'
            (& $Convert '{"description":"any"}').Schema.Type | Should -BeNullOrEmpty
        }
    }

    It 'merges allOf into Properties and Required and keeps AllOf' {
        InModuleScope tcs.openapi -Parameters @{ Convert = $script:convert } {
            param($Convert)
            $components = '{"Base":{"type":"object","required":["id"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}}}'
            $schema = (& $Convert '{"allOf":[{"$ref":"#/components/schemas/Base"},{"required":["extra"],"properties":{"extra":{"type":"integer"}}}],"properties":{"name":{"type":"string","description":"own wins"}}}' $components).Schema
            $schema.Type | Should -Be 'object'
            @($schema.Properties.Keys) | Should -Be @('id', 'name', 'extra')
            $schema.Properties['name'].Description | Should -Be 'own wins'
            $schema.Required | Should -Be @('id', 'extra')
            $schema.AllOf.Count | Should -Be 2
            $schema.AllOf[0].RefName | Should -Be 'Base'
        }
    }

    It 'takes Type, Format and Enum from a single allOf member and makes it nullable with nullable: true' {
        InModuleScope tcs.openapi -Parameters @{ Convert = $script:convert } {
            param($Convert)
            $schema = (& $Convert '{"allOf":[{"$ref":"#/components/schemas/Color"}],"nullable":true}' '{"Color":{"type":"string","format":"color","enum":["red","blue"]}}').Schema
            $schema.Type | Should -Be 'string'
            $schema.Format | Should -Be 'color'
            $schema.Enum | Should -Be @('red', 'blue')
            $schema.Nullable | Should -BeTrue
            $schema.RefName | Should -BeNullOrEmpty
        }
    }

    It 'keeps oneOf/anyOf members and the discriminator' {
        InModuleScope tcs.openapi -Parameters @{ Convert = $script:convert } {
            param($Convert)
            $schema = (& $Convert '{"oneOf":[{"type":"string"},{"type":"integer"}],"anyOf":[{"type":"boolean"}],"discriminator":{"propertyName":"kind","mapping":{"a":"#/components/schemas/A"}}}').Schema
            $schema.OneOf.Count | Should -Be 2
            $schema.OneOf[1].Type | Should -Be 'integer'
            $schema.AnyOf[0].Type | Should -Be 'boolean'
            $schema.Discriminator.PropertyName | Should -Be 'kind'
            $schema.Discriminator.Mapping['a'] | Should -Be '#/components/schemas/A'
            $schema.AllOf | Should -BeNullOrEmpty
        }
    }

    It 'resolves a $ref and sets RefName' {
        InModuleScope tcs.openapi -Parameters @{ Convert = $script:convert } {
            param($Convert)
            $schema = (& $Convert '{"$ref":"#/components/schemas/Pet"}' '{"Pet":{"type":"object","properties":{"id":{"type":"string"}}}}').Schema
            $schema.RefName | Should -Be 'Pet'
            $schema.Properties['id'].Type | Should -Be 'string'
        }
    }

    It 'applies description and nullable next to a $ref to a copy' {
        InModuleScope tcs.openapi -Parameters @{ Convert = $script:convert } {
            param($Convert)
            $root = ConvertFrom-OpenApiJson -Text '{"components":{"schemas":{"Pet":{"type":"object","description":"orig"}}}}'
            $context = Get-OpenApiNormalizationContext -Root $root
            $plain = ConvertTo-OpenApiSchema -Context $context -Node (ConvertFrom-OpenApiJson -Text '{"$ref":"#/components/schemas/Pet"}') -Pointer '/a'
            $copy = ConvertTo-OpenApiSchema -Context $context -Node (ConvertFrom-OpenApiJson -Text '{"$ref":"#/components/schemas/Pet","description":"new","nullable":true}') -Pointer '/b'
            $copy.Description | Should -Be 'new'
            $copy.Nullable | Should -BeTrue
            $copy.RefName | Should -Be 'Pet'
            $plain.Description | Should -Be 'orig'
            $plain.Nullable | Should -BeFalse
        }
    }

    It 'turns boolean schemas into blank schemas and ignores non-objects' {
        InModuleScope tcs.openapi -Parameters @{ Convert = $script:convert } {
            param($Convert)
            (& $Convert 'true').Schema.PSObject.TypeNames | Should -Contain 'Tcs.OpenApi.Schema'
            (& $Convert '"text"').Schema | Should -BeNullOrEmpty
            $context = Get-OpenApiNormalizationContext -Root $null
            ConvertTo-OpenApiSchema -Context $context -Node $null -Pointer '' | Should -BeNullOrEmpty
        }
    }

    It 'uses the first schema of tuple-style items' {
        InModuleScope tcs.openapi -Parameters @{ Convert = $script:convert } {
            param($Convert)
            (& $Convert '{"type":"array","items":[{"type":"integer"},{"type":"string"}]}').Schema.Items.Type | Should -Be 'integer'
        }
    }

    It 'reads only the node itself with -Shallow' {
        InModuleScope tcs.openapi {
            $context = Get-OpenApiNormalizationContext -Root (ConvertFrom-OpenApiJson -Text '{"components":{"schemas":{"X":{"type":"string"}}}}')
            $shallow = ConvertTo-OpenApiSchema -Context $context -Pointer '' -Shallow -Node (ConvertFrom-OpenApiJson -Text '{"description":"d","properties":{"a":{"$ref":"#/components/schemas/X"}},"allOf":[{"$ref":"#/components/schemas/X"}],"items":{"type":"string"}}')
            $shallow.Description | Should -Be 'd'
            $shallow.Type | Should -Be 'object'
            $shallow.Properties | Should -BeNullOrEmpty
            $shallow.AllOf | Should -BeNullOrEmpty
            $shallow.Items | Should -BeNullOrEmpty
            (ConvertTo-OpenApiSchema -Context $context -Pointer '' -Shallow -Node (ConvertFrom-OpenApiJson -Text '{"items":{}}')).Type | Should -Be 'array'
            (ConvertTo-OpenApiSchema -Context $context -Pointer '' -Shallow -Node (ConvertFrom-OpenApiJson -Text '{"$ref":"#/components/schemas/X"}')).Type | Should -BeNullOrEmpty
            $context.SchemaCache.Count | Should -Be 0
        }
    }

    It 'merges allOf members that are reference stubs from their full schema' {
        InModuleScope tcs.openapi {
            $root = ConvertFrom-OpenApiJson -Text '{"components":{"schemas":{"Base":{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}},"Derived":{"allOf":[{"$ref":"#/components/schemas/Base"}],"properties":{"extra":{"type":"integer"}}}}}}'
            $context = Get-OpenApiNormalizationContext -Root $root
            $derived = Resolve-OpenApiSchemaReference -Context $context -Reference '#/components/schemas/Derived' -Pointer ''
            $derived.AllOf[0].Properties | Should -BeNullOrEmpty
            @($derived.Properties.Keys) | Should -Be @('id', 'extra')
            $derived.Required | Should -Be @('id')
        }
    }
}