Private/Document/Tests/Resolve-OpenApiSchemaReference.Tests.ps1
|
BeforeAll { Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '../../../tcs.openapi.psd1') -Force } Describe 'Resolve-OpenApiSchemaReference' { BeforeAll { # Created inside the module so the script block runs in module scope $script:newContext = InModuleScope tcs.openapi { { param([string]$Schemas) Get-OpenApiNormalizationContext -Root (ConvertFrom-OpenApiJson -Text ('{"components":{"schemas":' + $Schemas + '}}')) -SourceVersion '3.0.3' } } } It 'normalises the target once and returns the cached object afterwards' { InModuleScope tcs.openapi -Parameters @{ NewContext = $script:newContext } { param($NewContext) $context = & $NewContext '{"Pet":{"type":"object"}}' $first = Resolve-OpenApiSchemaReference -Context $context -Reference '#/components/schemas/Pet' -Pointer '/a' $second = Resolve-OpenApiSchemaReference -Context $context -Reference '#/components/schemas/Pet' -Pointer '/b' [object]::ReferenceEquals($first, $second) | Should -BeTrue $first.RefName | Should -Be 'Pet' } } It 'resolves #/definitions refs and escaped names' { InModuleScope tcs.openapi -Parameters @{ NewContext = $script:newContext } { param($NewContext) $context = & $NewContext '{"a/b~c":{"type":"string"},"Pet":{"type":"object"}}' (Resolve-OpenApiSchemaReference -Context $context -Reference '#/components/schemas/a~1b~0c' -Pointer '').RefName | Should -BeExactly 'a/b~c' (Resolve-OpenApiSchemaReference -Context $context -Reference '#/definitions/Pet' -Pointer '').RefName | Should -Be 'Pet' } } It 'marks a circular reference Recursive, does not expand it again and reports OA022 once' { InModuleScope tcs.openapi -Parameters @{ NewContext = $script:newContext } { param($NewContext) $context = & $NewContext '{"Node":{"type":"object","properties":{"next":{"$ref":"#/components/schemas/Node"},"children":{"type":"array","items":{"$ref":"#/components/schemas/Node"}}}}}' $node = Resolve-OpenApiSchemaReference -Context $context -Reference '#/components/schemas/Node' -Pointer '/components/schemas/Node' $node.Recursive | Should -BeFalse $node.Properties['next'].Recursive | Should -BeTrue $node.Properties['next'].RefName | Should -Be 'Node' $node.Properties['next'].Properties | Should -BeNullOrEmpty $node.Properties['children'].Items.Recursive | Should -BeTrue @($context.Findings | Where-Object Code -EQ 'OA022').Count | Should -Be 1 $context.Findings[0].Severity | Should -Be 'Information' $context.Findings[0].Pointer | Should -BeExactly '/components/schemas/Node' $context.SchemaStack.Count | Should -Be 0 } } It 'handles indirect cycles (A -> B -> A)' { InModuleScope tcs.openapi -Parameters @{ NewContext = $script:newContext } { param($NewContext) $context = & $NewContext '{"A":{"properties":{"b":{"$ref":"#/components/schemas/B"}}},"B":{"properties":{"a":{"$ref":"#/components/schemas/A"}}}}' $a = Resolve-OpenApiSchemaReference -Context $context -Reference '#/components/schemas/A' -Pointer '' $a.Properties['b'].RefName | Should -Be 'B' $a.Properties['b'].Recursive | Should -BeFalse $b = Resolve-OpenApiSchemaReference -Context $context -Reference '#/components/schemas/B' -Pointer '' $b.RefName | Should -Be 'B' $b.Properties['a'].RefName | Should -Be 'A' $b.Properties['a'].Recursive | Should -BeTrue $b.Properties['a'].Type | Should -Be 'object' @($context.Findings | Where-Object Code -EQ 'OA022').Pointer | Should -Be @('/components/schemas/A') } } It 'returns reference stubs for nested refs to named schemas and the full schema otherwise' { InModuleScope tcs.openapi -Parameters @{ NewContext = $script:newContext } { param($NewContext) $context = & $NewContext '{"Owner":{"type":"object","description":"o","properties":{"pet":{"$ref":"#/components/schemas/Pet"},"pets":{"type":"array","items":{"$ref":"#/components/schemas/Pet"}}}},"Pet":{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}}}' $owner = Resolve-OpenApiSchemaReference -Context $context -Reference '#/components/schemas/Owner' -Pointer '' $stub = $owner.Properties['pet'] $stub.RefName | Should -Be 'Pet' $stub.Type | Should -Be 'object' $stub.Properties | Should -BeNullOrEmpty $stub.Required.Count | Should -Be 0 $stub.Recursive | Should -BeFalse [object]::ReferenceEquals($stub, $owner.Properties['pets'].Items) | Should -BeTrue $full = Resolve-OpenApiSchemaReference -Context $context -Reference '#/components/schemas/Pet' -Pointer '' $full.Properties['id'].Type | Should -Be 'string' $full.Required | Should -Be @('id') [object]::ReferenceEquals($full, $stub) | Should -BeFalse } } It 'returns the full schema for a nested ref with -Full' { InModuleScope tcs.openapi -Parameters @{ NewContext = $script:newContext } { param($NewContext) $context = & $NewContext '{"Pet":{"type":"object","properties":{"id":{"type":"string"}}}}' [void]$context.SchemaStack.Add('/somewhere') (Resolve-OpenApiSchemaReference -Context $context -Reference '#/components/schemas/Pet' -Pointer '').Properties | Should -BeNullOrEmpty (Resolve-OpenApiSchemaReference -Context $context -Reference '#/components/schemas/Pet' -Pointer '' -Full).Properties.Count | Should -Be 1 } } It 'keeps the serialised size linear for heavily shared references' { InModuleScope tcs.openapi -Parameters @{ NewContext = $script:newContext } { param($NewContext) # S0 -> S1, S1 x2 -> ... -> S30: fully expanded this would be 2^30 nodes $parts = foreach ($index in 0..29) { '"S{0}":{{"type":"object","properties":{{"a":{{"$ref":"#/components/schemas/S{1}"}},"b":{{"$ref":"#/components/schemas/S{1}"}}}}}}' -f $index, ($index + 1) } $context = & $NewContext ('{' + ($parts -join ',') + ',"S30":{"type":"string"}}') $s0 = Resolve-OpenApiSchemaReference -Context $context -Reference '#/components/schemas/S0' -Pointer '' ($s0 | ConvertTo-Json -Depth 100 -Compress).Length | Should -BeLessThan 5000 } } It 'gives an alias its own name without renaming the target' { InModuleScope tcs.openapi -Parameters @{ NewContext = $script:newContext } { param($NewContext) $context = & $NewContext '{"Pet":{"type":"object"},"Animal":{"$ref":"#/components/schemas/Pet"}}' (Resolve-OpenApiSchemaReference -Context $context -Reference '#/components/schemas/Animal' -Pointer '').RefName | Should -Be 'Animal' (Resolve-OpenApiSchemaReference -Context $context -Reference '#/components/schemas/Pet' -Pointer '').RefName | Should -Be 'Pet' } } It 'reports external refs as OA020 and counts them' { InModuleScope tcs.openapi -Parameters @{ NewContext = $script:newContext } { param($NewContext) $context = & $NewContext '{}' $schema = Resolve-OpenApiSchemaReference -Context $context -Reference 'common.json#/Pet' -Pointer '/x/schema' $schema.Type | Should -BeNullOrEmpty $context.ExternalRefHits | Should -Be 1 $context.Findings[0].Code | Should -Be 'OA020' $context.Findings[0].Severity | Should -Be 'Error' $context.Findings[0].Pointer | Should -BeExactly '/x/schema' } } It 'counts external refs again when a cached schema that contains one is reused' { InModuleScope tcs.openapi -Parameters @{ NewContext = $script:newContext } { param($NewContext) $context = & $NewContext '{"W":{"properties":{"x":{"$ref":"http://example.com/x.json"}}}}' $null = Resolve-OpenApiSchemaReference -Context $context -Reference '#/components/schemas/W' -Pointer '' $context.ExternalRefHits | Should -Be 1 $null = Resolve-OpenApiSchemaReference -Context $context -Reference '#/components/schemas/W' -Pointer '' $context.ExternalRefHits | Should -Be 2 } } It 'reports unresolved local refs as OA021' { InModuleScope tcs.openapi -Parameters @{ NewContext = $script:newContext } { param($NewContext) $context = & $NewContext '{}' $null = Resolve-OpenApiSchemaReference -Context $context -Reference '#/components/schemas/Missing' -Pointer '/p' $context.Findings[0].Code | Should -Be 'OA021' $context.Findings[0].Severity | Should -Be 'Error' $context.ExternalRefHits | Should -Be 0 } } } |