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
112 changes: 112 additions & 0 deletions bundler/bundler_ref_rewrite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"testing"
"testing/fstest"

"github.com/pb33f/libopenapi"
"github.com/pb33f/libopenapi/datamodel"
"github.com/pb33f/libopenapi/index"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -306,6 +307,117 @@ paths:
assertNoFilePathRefs(t, result.Bytes)
}

func TestBundleDocumentComposedWithOrigins_SchemaProxyGetReferenceUsesBundledRef(t *testing.T) {
tmpDir := t.TempDir()

topSpec := `openapi: 3.1.0
info:
title: Bundle Ref Getter Test
version: 1.0.0
paths:
/:
get:
operationId: getRoot
responses:
'400':
$ref: "#/components/responses/BadRequest"
'500':
$ref: "./shared.yaml#/components/responses/InternalServerError"
components:
responses:
BadRequest:
$ref: "./shared.yaml#/components/responses/BadRequest"
schemas:
Error:
type: object
properties:
wrong:
type: string
`

sharedSpec := `openapi: 3.1.0
components:
responses:
BadRequest:
description: Bad Request
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
InternalServerError:
description: Internal Server Error
content:
application/json:
schema:
$ref: "#/components/schemas/InternalServerError"
schemas:
Error:
type: object
properties:
message:
type: string
InternalServerError:
type: object
properties:
message:
type: string
`

topFile := filepath.Join(tmpDir, "top.yaml")
sharedFile := filepath.Join(tmpDir, "shared.yaml")
require.NoError(t, os.WriteFile(topFile, []byte(topSpec), 0644))
require.NoError(t, os.WriteFile(sharedFile, []byte(sharedSpec), 0644))

config := datamodel.NewDocumentConfiguration()
config.BasePath = tmpDir
config.SpecFilePath = topFile
config.ExtractRefsSequentially = true

spec, err := os.ReadFile(topFile)
require.NoError(t, err)

doc, err := libopenapi.NewDocumentWithConfiguration(spec, config)
require.NoError(t, err)

model, err := doc.BuildV3Model()
require.NoError(t, err)

result, err := BundleDocumentComposedWithOrigins(&model.Model, nil)
require.NoError(t, err)
require.NotNil(t, result)

bundledStr := string(result.Bytes)
assert.Contains(t, bundledStr, "#/components/schemas/Error__shared")
assert.Contains(t, bundledStr, "#/components/schemas/InternalServerError")

op := model.Model.Paths.PathItems.GetOrZero("/").Get
require.NotNil(t, op)
require.NotNil(t, op.Responses)

badRequest := op.Responses.Codes.GetOrZero("400")
require.NotNil(t, badRequest)
badRequestSchema := badRequest.Content.GetOrZero("application/json").Schema
require.NotNil(t, badRequestSchema)

internalError := op.Responses.Codes.GetOrZero("500")
require.NotNil(t, internalError)
internalErrorSchema := internalError.Content.GetOrZero("application/json").Schema
require.NotNil(t, internalErrorSchema)

assert.Equal(t, "#/components/schemas/Error__shared", badRequestSchema.GetReference())
assert.Equal(t, "#/components/schemas/InternalServerError", internalErrorSchema.GetReference())

badRequestOrigin := result.Origins[badRequestSchema.GetReference()]
require.NotNil(t, badRequestOrigin)
assert.Equal(t, sharedFile, badRequestOrigin.OriginalFile)
assert.Equal(t, "#/components/schemas/Error", badRequestOrigin.OriginalRef)

internalErrorOrigin := result.Origins[internalErrorSchema.GetReference()]
require.NotNil(t, internalErrorOrigin)
assert.Equal(t, sharedFile, internalErrorOrigin.OriginalFile)
assert.Equal(t, "#/components/schemas/InternalServerError", internalErrorOrigin.OriginalRef)
}

// TestBundlerComposedWithOrigins_AbsolutePathRefReuse ensures absolute-path refs
// that point at inline-required content are replaced with the inlined node content.
func TestBundlerComposedWithOrigins_AbsolutePathRefReuse(t *testing.T) {
Expand Down
5 changes: 5 additions & 0 deletions datamodel/high/base/schema_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,11 @@ func (sp *SchemaProxy) GetReference() string {
if sp.refStr != "" {
return sp.refStr
}
if refNode := sp.GetReferenceNode(); refNode != nil {
if refValNode := utils.GetRefValueNode(refNode); refValNode != nil {
return refValNode.Value
}
}
return sp.schema.GetValue().GetReference()
}

Expand Down
64 changes: 64 additions & 0 deletions datamodel/high/base/schema_proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,26 @@ func TestSchemaProxy_GetReference(t *testing.T) {
assert.Equal(t, refNode, sp.GetReferenceNode())
}

func TestSchemaProxy_GetReference_PrefersLiveRefNodeValue(t *testing.T) {
refNode := utils.CreateRefNode("#/components/schemas/MySchema")

ref := low.Reference{}
ref.SetReference("#/components/schemas/MySchema", refNode)

sp := &SchemaProxy{
schema: &low.NodeReference[*lowbase.SchemaProxy]{
Value: &lowbase.SchemaProxy{
Reference: ref,
},
},
}

refNode.Content[1].Value = "#/components/schemas/MySchema__shared"

assert.Equal(t, "#/components/schemas/MySchema__shared", sp.GetReference())
assert.Equal(t, refNode, sp.GetReferenceNode())
}

func TestSchemaProxy_IsReference_Nil(t *testing.T) {
var sp *SchemaProxy
assert.False(t, sp.IsReference())
Expand Down Expand Up @@ -1526,6 +1546,50 @@ func TestCreateSchemaProxyRefWithSchema_InlinePreservedRef(t *testing.T) {
require.GreaterOrEqual(t, len(node.Content), 4) // $ref key+val + description key+val
}

func TestSchemaProxy_MarshalYAMLInline_CircularReference_MatchesAbsoluteBasePath(t *testing.T) {
const ymlComponents = `components:
schemas:
Ten:
type: object`

var idxNode yaml.Node
err := yaml.Unmarshal([]byte(ymlComponents), &idxNode)
require.NoError(t, err)

idx := index.NewSpecIndexWithConfig(&idxNode, index.CreateOpenAPIIndexConfig())
idx.SetAbsolutePath(filepath.Join(t.TempDir(), "spec.yaml"))
idx.SetCircularReferences([]*index.CircularReferenceResult{
{
LoopPoint: &index.Reference{
Definition: "#/components/schemas/NotTen",
FullDefinition: idx.GetSpecAbsolutePath(),
},
},
})

refNode := utils.CreateRefNode("#/components/schemas/Ten")

lowProxy := new(lowbase.SchemaProxy)
err = lowProxy.Build(context.Background(), nil, refNode, idx)
require.NoError(t, err)

sp := NewSchemaProxy(&low.NodeReference[*lowbase.SchemaProxy]{
Value: lowProxy,
ValueNode: refNode,
})

rendered, err := sp.MarshalYAMLInline()
require.Error(t, err)
require.NotNil(t, rendered)
assert.Contains(t, err.Error(), "circular reference")

node, ok := rendered.(*yaml.Node)
require.True(t, ok)
require.Len(t, node.Content, 2)
assert.Equal(t, "$ref", node.Content[0].Value)
assert.Equal(t, "#/components/schemas/Ten", node.Content[1].Value)
}

func TestCreateSchemaProxyRefWithSchema_CircularRefSafe(t *testing.T) {
// Verify inline rendering doesn't panic when rendered Schema has a non-nil low-level
const ymlComponents = `components:
Expand Down
Loading