Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions datamodel/high/v3/components_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,146 @@ paths: {}
assert.Contains(t, logOutput, "\"section\":\"schemas\"")
}

func TestComponents_BuildComponentValueReferences(t *testing.T) {
low.ClearHashCache()
tmpDir := t.TempDir()

spec := `openapi: 3.1.0
info:
title: Test API
version: 1.0.0
components:
parameters:
ExternalParam:
$ref: "./params.yaml#/ExternalParam"
LocalTargetParam:
name: local
in: query
schema:
type: string
LocalParamRef:
$ref: "#/components/parameters/LocalTargetParam"
responses:
ExternalResponse:
$ref: "./responses.yaml#/ExternalResponse"
headers:
ExternalHeader:
$ref: "./headers.yaml#/ExternalHeader"
requestBodies:
ExternalBody:
$ref: "./request-bodies.yaml#/ExternalBody"
paths: {}
`

params := `ExternalParam:
name: tenant
in: header
required: true
schema:
type: string
`

responses := `ExternalResponse:
description: external response
content:
application/json:
schema:
type: object
`

headers := `ExternalHeader:
description: external header
schema:
type: string
`

requestBodies := `ExternalBody:
description: external body
required: true
content:
application/json:
schema:
type: object
`

require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "params.yaml"), []byte(params), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "responses.yaml"), []byte(responses), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "headers.yaml"), []byte(headers), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "request-bodies.yaml"), []byte(requestBodies), 0o644))

info, err := datamodel.ExtractSpecInfo([]byte(spec))
require.NoError(t, err)

cfg := &datamodel.DocumentConfiguration{
BasePath: tmpDir,
AllowFileReferences: true,
}
lowDoc, err := v3.CreateDocumentFromConfig(info, cfg)
require.NoError(t, err)

doc := NewDocument(lowDoc)
require.NotNil(t, doc.Components)

externalParam := doc.Components.Parameters.GetOrZero("ExternalParam")
require.NotNil(t, externalParam)
assert.Equal(t, "tenant", externalParam.Name)
assert.Equal(t, "header", externalParam.In)
require.NotNil(t, externalParam.Required)
assert.True(t, *externalParam.Required)
assert.True(t, externalParam.GoLow().IsReference())
assert.Equal(t, "./params.yaml#/ExternalParam", externalParam.GoLow().GetReference())
lowExternalParam := low.FindItemInOrderedMap[*v3.Parameter]("ExternalParam", doc.Components.GoLow().Parameters.Value)
require.NotNil(t, lowExternalParam)
assert.True(t, lowExternalParam.IsReference())
assert.Equal(t, "./params.yaml#/ExternalParam", lowExternalParam.GetReference())

localParam := doc.Components.Parameters.GetOrZero("LocalParamRef")
require.NotNil(t, localParam)
assert.Equal(t, "local", localParam.Name)
assert.Equal(t, "query", localParam.In)
assert.True(t, localParam.GoLow().IsReference())
assert.Equal(t, "#/components/parameters/LocalTargetParam", localParam.GoLow().GetReference())
lowLocalParam := low.FindItemInOrderedMap[*v3.Parameter]("LocalParamRef", doc.Components.GoLow().Parameters.Value)
require.NotNil(t, lowLocalParam)
assert.True(t, lowLocalParam.IsReference())
assert.Equal(t, "#/components/parameters/LocalTargetParam", lowLocalParam.GetReference())

externalResponse := doc.Components.Responses.GetOrZero("ExternalResponse")
require.NotNil(t, externalResponse)
assert.Equal(t, "external response", externalResponse.Description)
assert.NotNil(t, externalResponse.Content.GetOrZero("application/json"))
assert.True(t, externalResponse.GoLow().IsReference())
assert.Equal(t, "./responses.yaml#/ExternalResponse", externalResponse.GoLow().GetReference())
lowExternalResponse := low.FindItemInOrderedMap[*v3.Response]("ExternalResponse", doc.Components.GoLow().Responses.Value)
require.NotNil(t, lowExternalResponse)
assert.True(t, lowExternalResponse.IsReference())
assert.Equal(t, "./responses.yaml#/ExternalResponse", lowExternalResponse.GetReference())

externalHeader := doc.Components.Headers.GetOrZero("ExternalHeader")
require.NotNil(t, externalHeader)
assert.Equal(t, "external header", externalHeader.Description)
assert.NotNil(t, externalHeader.Schema)
assert.True(t, externalHeader.GoLow().IsReference())
assert.Equal(t, "./headers.yaml#/ExternalHeader", externalHeader.GoLow().GetReference())
lowExternalHeader := low.FindItemInOrderedMap[*v3.Header]("ExternalHeader", doc.Components.GoLow().Headers.Value)
require.NotNil(t, lowExternalHeader)
assert.True(t, lowExternalHeader.IsReference())
assert.Equal(t, "./headers.yaml#/ExternalHeader", lowExternalHeader.GetReference())

externalBody := doc.Components.RequestBodies.GetOrZero("ExternalBody")
require.NotNil(t, externalBody)
assert.Equal(t, "external body", externalBody.Description)
require.NotNil(t, externalBody.Required)
assert.True(t, *externalBody.Required)
assert.NotNil(t, externalBody.Content.GetOrZero("application/json"))
assert.True(t, externalBody.GoLow().IsReference())
assert.Equal(t, "./request-bodies.yaml#/ExternalBody", externalBody.GoLow().GetReference())
lowExternalBody := low.FindItemInOrderedMap[*v3.RequestBody]("ExternalBody", doc.Components.GoLow().RequestBodies.Value)
require.NotNil(t, lowExternalBody)
assert.True(t, lowExternalBody.IsReference())
assert.Equal(t, "./request-bodies.yaml#/ExternalBody", lowExternalBody.GetReference())
}

func TestComponents_warnPreservedComponentMapRefs_Guards(t *testing.T) {
var nilComp *Components
nilComp.warnPreservedComponentMapRefs()
Expand Down
200 changes: 200 additions & 0 deletions datamodel/low/v3/component_value_reference_coverage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// Copyright 2022-2026 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT

package v3

import (
"context"
"testing"

"github.com/pb33f/libopenapi/datamodel/low"
"github.com/pb33f/libopenapi/index"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.yaml.in/yaml/v4"
)

type refAwareLowModel interface {
Build(context.Context, *yaml.Node, *yaml.Node, *index.SpecIndex) error
GetKeyNode() *yaml.Node
GetReference() string
GetReferenceNode() *yaml.Node
GetRootNode() *yaml.Node
IsReference() bool
}

type scalarRootLowModel interface {
Build(context.Context, *yaml.Node, *yaml.Node, *index.SpecIndex) error
GetKeyNode() *yaml.Node
GetNodes() map[int][]*yaml.Node
GetRootNode() *yaml.Node
}

func TestComponentValueReferenceBuilders_PreserveRootRef(t *testing.T) {
ref := "./components.yaml#/shared/Thing"
keyNode, rootNode := componentValueRefNode(t, ref)

tests := []struct {
name string
build func(*testing.T, *yaml.Node, *yaml.Node) refAwareLowModel
}{
{
name: "Callback",
build: func(t *testing.T, keyNode, rootNode *yaml.Node) refAwareLowModel {
return buildRootRefModel(t, keyNode, rootNode, &Callback{})
},
},
{
name: "Header",
build: func(t *testing.T, keyNode, rootNode *yaml.Node) refAwareLowModel {
return buildRootRefModel(t, keyNode, rootNode, &Header{})
},
},
{
name: "Link",
build: func(t *testing.T, keyNode, rootNode *yaml.Node) refAwareLowModel {
return buildRootRefModel(t, keyNode, rootNode, &Link{})
},
},
{
name: "Parameter",
build: func(t *testing.T, keyNode, rootNode *yaml.Node) refAwareLowModel {
return buildRootRefModel(t, keyNode, rootNode, &Parameter{})
},
},
{
name: "PathItem",
build: func(t *testing.T, keyNode, rootNode *yaml.Node) refAwareLowModel {
return buildRootRefModel(t, keyNode, rootNode, &PathItem{})
},
},
{
name: "RequestBody",
build: func(t *testing.T, keyNode, rootNode *yaml.Node) refAwareLowModel {
return buildRootRefModel(t, keyNode, rootNode, &RequestBody{})
},
},
{
name: "Response",
build: func(t *testing.T, keyNode, rootNode *yaml.Node) refAwareLowModel {
return buildRootRefModel(t, keyNode, rootNode, &Response{})
},
},
{
name: "SecurityScheme",
build: func(t *testing.T, keyNode, rootNode *yaml.Node) refAwareLowModel {
return buildRootRefModel(t, keyNode, rootNode, &SecurityScheme{})
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
model := test.build(t, keyNode, rootNode)

assert.True(t, model.IsReference())
assert.Equal(t, ref, model.GetReference())
assert.Same(t, rootNode, model.GetReferenceNode())
assert.Same(t, rootNode, model.GetRootNode())
assert.Same(t, keyNode, model.GetKeyNode())
})
}
}

func TestScalarRootBuilders_RetainScalarNode(t *testing.T) {
rootNode := scalarRootNode(t, "scalar-root")

tests := []struct {
name string
build func(*testing.T, *yaml.Node) scalarRootLowModel
}{
{
name: "Link",
build: func(t *testing.T, rootNode *yaml.Node) scalarRootLowModel {
return buildScalarRootModel(t, rootNode, &Link{})
},
},
{
name: "MediaType",
build: func(t *testing.T, rootNode *yaml.Node) scalarRootLowModel {
return buildScalarRootModel(t, rootNode, &MediaType{})
},
},
{
name: "Parameter",
build: func(t *testing.T, rootNode *yaml.Node) scalarRootLowModel {
return buildScalarRootModel(t, rootNode, &Parameter{})
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
model := test.build(t, rootNode)
nodes := model.GetNodes()

assert.Same(t, rootNode, model.GetRootNode())
assert.Nil(t, model.GetKeyNode())
require.Len(t, nodes[rootNode.Line], 1)
assert.Same(t, rootNode, nodes[rootNode.Line][0])
assert.Equal(t, "scalar-root", nodes[rootNode.Line][0].Value)
})
}
}

func TestPathItem_Build_IgnoresUnknownScalarFields(t *testing.T) {
yml := `summary: supported metadata
purge: disabled
get:
description: supported operation`

var root yaml.Node
require.NoError(t, yaml.Unmarshal([]byte(yml), &root))
idx := index.NewSpecIndex(&root)
rootNode := root.Content[0]

var pathItem PathItem
require.NoError(t, low.BuildModel(rootNode, &pathItem))
require.NoError(t, pathItem.Build(context.Background(), nil, rootNode, idx))

require.NotNil(t, pathItem.Get.Value)
assert.Equal(t, "supported operation", pathItem.Get.Value.Description.Value)
assert.Nil(t, pathItem.AdditionalOperations.Value)
}

func componentValueRefNode(t *testing.T, ref string) (*yaml.Node, *yaml.Node) {
t.Helper()

var root yaml.Node
yml := "thing:\n $ref: '" + ref + "'\n"
require.NoError(t, yaml.Unmarshal([]byte(yml), &root))
require.NotEmpty(t, root.Content)
require.Len(t, root.Content[0].Content, 2)
return root.Content[0].Content[0], root.Content[0].Content[1]
}

func scalarRootNode(t *testing.T, value string) *yaml.Node {
t.Helper()

var root yaml.Node
require.NoError(t, yaml.Unmarshal([]byte(value), &root))
require.NotEmpty(t, root.Content)
return root.Content[0]
}

func buildRootRefModel[T refAwareLowModel](t *testing.T, keyNode, rootNode *yaml.Node, model T) T {
t.Helper()

idx := index.NewSpecIndexWithConfig(rootNode, &index.SpecIndexConfig{SkipExternalRefResolution: true})
require.NoError(t, low.BuildModel(rootNode, model))
require.NoError(t, model.Build(context.Background(), keyNode, rootNode, idx))
return model
}

func buildScalarRootModel[T scalarRootLowModel](t *testing.T, rootNode *yaml.Node, model T) T {
t.Helper()

require.NoError(t, low.BuildModel(rootNode, model))
require.NoError(t, model.Build(context.Background(), nil, rootNode, nil))
return model
}
Loading
Loading