Skip to content

Commit 65096b4

Browse files
committed
fix stale references being returned after bundling
1 parent da443c1 commit 65096b4

File tree

3 files changed

+181
-0
lines changed

3 files changed

+181
-0
lines changed

bundler/bundler_ref_rewrite_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"testing"
1313
"testing/fstest"
1414

15+
"github.com/pb33f/libopenapi"
1516
"github.com/pb33f/libopenapi/datamodel"
1617
"github.com/pb33f/libopenapi/index"
1718
"github.com/stretchr/testify/assert"
@@ -306,6 +307,117 @@ paths:
306307
assertNoFilePathRefs(t, result.Bytes)
307308
}
308309

310+
func TestBundleDocumentComposedWithOrigins_SchemaProxyGetReferenceUsesBundledRef(t *testing.T) {
311+
tmpDir := t.TempDir()
312+
313+
topSpec := `openapi: 3.1.0
314+
info:
315+
title: Bundle Ref Getter Test
316+
version: 1.0.0
317+
paths:
318+
/:
319+
get:
320+
operationId: getRoot
321+
responses:
322+
'400':
323+
$ref: "#/components/responses/BadRequest"
324+
'500':
325+
$ref: "./shared.yaml#/components/responses/InternalServerError"
326+
components:
327+
responses:
328+
BadRequest:
329+
$ref: "./shared.yaml#/components/responses/BadRequest"
330+
schemas:
331+
Error:
332+
type: object
333+
properties:
334+
wrong:
335+
type: string
336+
`
337+
338+
sharedSpec := `openapi: 3.1.0
339+
components:
340+
responses:
341+
BadRequest:
342+
description: Bad Request
343+
content:
344+
application/json:
345+
schema:
346+
$ref: "#/components/schemas/Error"
347+
InternalServerError:
348+
description: Internal Server Error
349+
content:
350+
application/json:
351+
schema:
352+
$ref: "#/components/schemas/InternalServerError"
353+
schemas:
354+
Error:
355+
type: object
356+
properties:
357+
message:
358+
type: string
359+
InternalServerError:
360+
type: object
361+
properties:
362+
message:
363+
type: string
364+
`
365+
366+
topFile := filepath.Join(tmpDir, "top.yaml")
367+
sharedFile := filepath.Join(tmpDir, "shared.yaml")
368+
require.NoError(t, os.WriteFile(topFile, []byte(topSpec), 0644))
369+
require.NoError(t, os.WriteFile(sharedFile, []byte(sharedSpec), 0644))
370+
371+
config := datamodel.NewDocumentConfiguration()
372+
config.BasePath = tmpDir
373+
config.SpecFilePath = topFile
374+
config.ExtractRefsSequentially = true
375+
376+
spec, err := os.ReadFile(topFile)
377+
require.NoError(t, err)
378+
379+
doc, err := libopenapi.NewDocumentWithConfiguration(spec, config)
380+
require.NoError(t, err)
381+
382+
model, err := doc.BuildV3Model()
383+
require.NoError(t, err)
384+
385+
result, err := BundleDocumentComposedWithOrigins(&model.Model, nil)
386+
require.NoError(t, err)
387+
require.NotNil(t, result)
388+
389+
bundledStr := string(result.Bytes)
390+
assert.Contains(t, bundledStr, "#/components/schemas/Error__shared")
391+
assert.Contains(t, bundledStr, "#/components/schemas/InternalServerError")
392+
393+
op := model.Model.Paths.PathItems.GetOrZero("/").Get
394+
require.NotNil(t, op)
395+
require.NotNil(t, op.Responses)
396+
397+
badRequest := op.Responses.Codes.GetOrZero("400")
398+
require.NotNil(t, badRequest)
399+
badRequestSchema := badRequest.Content.GetOrZero("application/json").Schema
400+
require.NotNil(t, badRequestSchema)
401+
402+
internalError := op.Responses.Codes.GetOrZero("500")
403+
require.NotNil(t, internalError)
404+
internalErrorSchema := internalError.Content.GetOrZero("application/json").Schema
405+
require.NotNil(t, internalErrorSchema)
406+
407+
assert.Equal(t, "#/components/schemas/Error__shared", badRequestSchema.GetReference())
408+
assert.Equal(t, "#/components/schemas/InternalServerError", internalErrorSchema.GetReference())
409+
410+
badRequestOrigin := result.Origins[badRequestSchema.GetReference()]
411+
require.NotNil(t, badRequestOrigin)
412+
assert.Equal(t, sharedFile, badRequestOrigin.OriginalFile)
413+
assert.Equal(t, "#/components/schemas/Error", badRequestOrigin.OriginalRef)
414+
415+
internalErrorOrigin := result.Origins[internalErrorSchema.GetReference()]
416+
require.NotNil(t, internalErrorOrigin)
417+
assert.Equal(t, sharedFile, internalErrorOrigin.OriginalFile)
418+
assert.Equal(t, "#/components/schemas/InternalServerError", internalErrorOrigin.OriginalRef)
419+
}
420+
309421
// TestBundlerComposedWithOrigins_AbsolutePathRefReuse ensures absolute-path refs
310422
// that point at inline-required content are replaced with the inlined node content.
311423
func TestBundlerComposedWithOrigins_AbsolutePathRefReuse(t *testing.T) {

datamodel/high/base/schema_proxy.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,11 @@ func (sp *SchemaProxy) GetReference() string {
313313
if sp.refStr != "" {
314314
return sp.refStr
315315
}
316+
if refNode := sp.GetReferenceNode(); refNode != nil {
317+
if refValNode := utils.GetRefValueNode(refNode); refValNode != nil {
318+
return refValNode.Value
319+
}
320+
}
316321
return sp.schema.GetValue().GetReference()
317322
}
318323

datamodel/high/base/schema_proxy_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,26 @@ func TestSchemaProxy_GetReference(t *testing.T) {
150150
assert.Equal(t, refNode, sp.GetReferenceNode())
151151
}
152152

153+
func TestSchemaProxy_GetReference_PrefersLiveRefNodeValue(t *testing.T) {
154+
refNode := utils.CreateRefNode("#/components/schemas/MySchema")
155+
156+
ref := low.Reference{}
157+
ref.SetReference("#/components/schemas/MySchema", refNode)
158+
159+
sp := &SchemaProxy{
160+
schema: &low.NodeReference[*lowbase.SchemaProxy]{
161+
Value: &lowbase.SchemaProxy{
162+
Reference: ref,
163+
},
164+
},
165+
}
166+
167+
refNode.Content[1].Value = "#/components/schemas/MySchema__shared"
168+
169+
assert.Equal(t, "#/components/schemas/MySchema__shared", sp.GetReference())
170+
assert.Equal(t, refNode, sp.GetReferenceNode())
171+
}
172+
153173
func TestSchemaProxy_IsReference_Nil(t *testing.T) {
154174
var sp *SchemaProxy
155175
assert.False(t, sp.IsReference())
@@ -1526,6 +1546,50 @@ func TestCreateSchemaProxyRefWithSchema_InlinePreservedRef(t *testing.T) {
15261546
require.GreaterOrEqual(t, len(node.Content), 4) // $ref key+val + description key+val
15271547
}
15281548

1549+
func TestSchemaProxy_MarshalYAMLInline_CircularReference_MatchesAbsoluteBasePath(t *testing.T) {
1550+
const ymlComponents = `components:
1551+
schemas:
1552+
Ten:
1553+
type: object`
1554+
1555+
var idxNode yaml.Node
1556+
err := yaml.Unmarshal([]byte(ymlComponents), &idxNode)
1557+
require.NoError(t, err)
1558+
1559+
idx := index.NewSpecIndexWithConfig(&idxNode, index.CreateOpenAPIIndexConfig())
1560+
idx.SetAbsolutePath(filepath.Join(t.TempDir(), "spec.yaml"))
1561+
idx.SetCircularReferences([]*index.CircularReferenceResult{
1562+
{
1563+
LoopPoint: &index.Reference{
1564+
Definition: "#/components/schemas/NotTen",
1565+
FullDefinition: idx.GetSpecAbsolutePath(),
1566+
},
1567+
},
1568+
})
1569+
1570+
refNode := utils.CreateRefNode("#/components/schemas/Ten")
1571+
1572+
lowProxy := new(lowbase.SchemaProxy)
1573+
err = lowProxy.Build(context.Background(), nil, refNode, idx)
1574+
require.NoError(t, err)
1575+
1576+
sp := NewSchemaProxy(&low.NodeReference[*lowbase.SchemaProxy]{
1577+
Value: lowProxy,
1578+
ValueNode: refNode,
1579+
})
1580+
1581+
rendered, err := sp.MarshalYAMLInline()
1582+
require.Error(t, err)
1583+
require.NotNil(t, rendered)
1584+
assert.Contains(t, err.Error(), "circular reference")
1585+
1586+
node, ok := rendered.(*yaml.Node)
1587+
require.True(t, ok)
1588+
require.Len(t, node.Content, 2)
1589+
assert.Equal(t, "$ref", node.Content[0].Value)
1590+
assert.Equal(t, "#/components/schemas/Ten", node.Content[1].Value)
1591+
}
1592+
15291593
func TestCreateSchemaProxyRefWithSchema_CircularRefSafe(t *testing.T) {
15301594
// Verify inline rendering doesn't panic when rendered Schema has a non-nil low-level
15311595
const ymlComponents = `components:

0 commit comments

Comments
 (0)