Skip to content

Commit 5404d3e

Browse files
committed
circular $ref expansion: fixed edge cases
* fixes go-openapi#94 * now expanded $ref are always contained in the resulting document. All circular $ref that used to resolve to a remote $ref now resolve as a json pointer inside the expanded document. * schema IDs are removed from the expanded spec: schemas expanded from some schema ID reference now refer to the new expanded root document. * fixed circular ID resolution Signed-off-by: Frederic BIDON <[email protected]>
1 parent efe8fb3 commit 5404d3e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+21618
-228
lines changed

circular_test.go

+145-46
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import (
44
"encoding/json"
55
"io/ioutil"
66
"net/http"
7+
"net/http/httptest"
8+
"os"
79
"path/filepath"
10+
"sort"
811
"testing"
912
"time"
1013

@@ -16,7 +19,8 @@ func TestExpandCircular_Issue3(t *testing.T) {
1619
jazon := expandThisOrDieTrying(t, "fixtures/expansion/overflow.json")
1720
require.NotEmpty(t, jazon)
1821

19-
// TODO: assert $ref
22+
// all $ref are in the root document
23+
assertRefInJSON(t, jazon, "#/definitions/")
2024
}
2125

2226
func TestExpandCircular_RefExpansion(t *testing.T) {
@@ -33,47 +37,41 @@ func TestExpandCircular_RefExpansion(t *testing.T) {
3337
schema := spec.Definitions["car"]
3438

3539
assert.NotPanics(t, func() {
36-
_, err := expandSchema(schema, []string{"#/definitions/car"}, resolver, basePath)
40+
_, err := expandSchema(schema, []string{"#/definitions/car"}, resolver, basePath, "/definitions/car")
3741
require.NoError(t, err)
3842
}, "Calling expand schema with circular refs, should not panic!")
3943
}
4044

41-
func TestExpandCircular_Spec2Expansion(t *testing.T) {
42-
// TODO: assert repeatable results (see commented section below)
43-
45+
func TestExpandCircular_Minimal(t *testing.T) {
4446
fixturePath := filepath.Join("fixtures", "expansion", "circular-minimal.json")
4547
jazon := expandThisOrDieTrying(t, fixturePath)
4648
require.NotEmpty(t, jazon)
4749

48-
// assert stripped $ref in result
4950
assert.NotContainsf(t, jazon, "circular-minimal.json#/",
5051
"expected %s to be expanded with stripped circular $ref", fixturePath)
5152

52-
fixturePath = "fixtures/expansion/circularSpec2.json"
53-
jazon = expandThisOrDieTrying(t, fixturePath)
54-
assert.NotEmpty(t, jazon)
55-
56-
assert.NotContainsf(t, jazon, "circularSpec.json#/",
57-
"expected %s to be expanded with stripped circular $ref", fixturePath)
58-
5953
/*
60-
61-
At the moment, the result of expanding circular references is not stable,
54+
At the moment, the result of expanding circular references is not stable (issue #93),
6255
when several cycles have intersections:
6356
the spec structure is randomly walked through and mutating as expansion is carried out.
6457
detected cycles in $ref are not necessarily the shortest matches.
6558
6659
This may result in different, functionally correct expanded specs (e.g. with same validations)
67-
68-
for i := 0; i < 1; i++ {
69-
bbb := expandThisOrDieTrying(t, fixturePath)
70-
t.Log(bbb)
71-
if !assert.JSONEqf(t, jazon, bbb, "on iteration %d, we should have stable expanded spec", i) {
72-
t.FailNow()
73-
return
74-
}
75-
}
7660
*/
61+
assertRefInJSON(t, jazon, "#/definitions/node") // NOTE: we are not sure which node definition is used
62+
}
63+
64+
func TestExpandCircular_Spec2Expansion(t *testing.T) {
65+
// assert stripped $ref in result
66+
67+
fixturePath := "fixtures/expansion/circularSpec2.json"
68+
jazon := expandThisOrDieTrying(t, fixturePath)
69+
assert.NotEmpty(t, jazon)
70+
71+
assert.NotContainsf(t, jazon, "circularSpec.json#/",
72+
"expected %s to be expanded with stripped circular $ref", fixturePath)
73+
74+
assertRefInJSON(t, jazon, "#/definitions/")
7775
}
7876

7977
func TestExpandCircular_MoreCircular(t *testing.T) {
@@ -89,22 +87,22 @@ func TestExpandCircular_MoreCircular(t *testing.T) {
8987
fixturePath := "fixtures/more_circulars/spec.json"
9088
jazon := expandThisOrDieTrying(t, fixturePath)
9189
require.NotEmpty(t, jazon)
92-
assertRefInJSON(t, jazon, "item.json#/item")
90+
assertRefInJSON(t, jazon, "#/responses/itemResponse/schema")
9391

9492
fixturePath = "fixtures/more_circulars/spec2.json"
9593
jazon = expandThisOrDieTrying(t, fixturePath)
9694
require.NotEmpty(t, jazon)
97-
assertRefInJSON(t, jazon, "item2.json#/item")
95+
assertRefInJSON(t, jazon, "#/responses/itemResponse/schema")
9896

9997
fixturePath = "fixtures/more_circulars/spec3.json"
10098
jazon = expandThisOrDieTrying(t, fixturePath)
10199
require.NotEmpty(t, jazon)
102-
assertRefInJSON(t, jazon, "item.json#/item")
100+
assertRefInJSON(t, jazon, "#/definitions/myItems")
103101

104102
fixturePath = "fixtures/more_circulars/spec4.json"
105103
jazon = expandThisOrDieTrying(t, fixturePath)
106104
require.NotEmpty(t, jazon)
107-
assertRefInJSON(t, jazon, "item4.json#/item")
105+
assertRefInJSON(t, jazon, "#/parameters/itemParameter/schema")
108106
}
109107

110108
func TestExpandCircular_Issue957(t *testing.T) {
@@ -175,30 +173,131 @@ func TestExpandCircular_RemoteCircularID(t *testing.T) {
175173
}()
176174
time.Sleep(100 * time.Millisecond)
177175

178-
fixturePath := "http://localhost:1234/tree"
179-
jazon := expandThisSchemaOrDieTrying(t, fixturePath)
176+
t.Run("CircularID", func(t *testing.T) {
177+
fixturePath := "http://localhost:1234/tree"
178+
jazon := expandThisSchemaOrDieTrying(t, fixturePath)
179+
180+
// all $ref are now in the single root
181+
assertRefInJSONRegexp(t, jazon, "(^#/definitions/node$)|(^#?$)") // root $ref should be '#' or ""
182+
183+
sch := new(Schema)
184+
require.NoError(t, json.Unmarshal([]byte(jazon), sch))
180185

181-
sch := new(Schema)
182-
require.NoError(t, json.Unmarshal([]byte(jazon), sch))
186+
// expand already expanded: this is not an idempotent operation: circular $ref
187+
// are expanded again until a (deeper) cycle is detected
188+
require.NoError(t, ExpandSchema(sch, nil, nil))
183189

184-
require.NotPanics(t, func() {
185-
assert.NoError(t, ExpandSchemaWithBasePath(sch, nil, &ExpandOptions{}))
190+
// expand already expanded
191+
require.NoError(t, ExpandSchema(sch, nil, nil))
192+
193+
// Empty base path fails:
194+
require.Error(t, ExpandSchemaWithBasePath(sch, nil, &ExpandOptions{}))
186195
})
187196

188-
fixturePath = "fixtures/more_circulars/with-id.json"
189-
jazon = expandThisOrDieTrying(t, fixturePath)
197+
t.Run("withID", func(t *testing.T) {
198+
const fixturePath = "fixtures/more_circulars/with-id.json"
199+
jazon := expandThisOrDieTrying(t, fixturePath)
200+
t.Log(jazon)
201+
202+
// cannot guarantee that the circular will always hook on the same $ref
203+
// but we can assert that thre is only one
204+
m := rex.FindAllStringSubmatch(jazon, -1)
205+
require.NotEmpty(t, m)
190206

191-
// cannot guarantee that the circular will always hook on the same $ref
192-
// but we can assert that there is only one
193-
m := rex.FindAllStringSubmatch(jazon, -1)
194-
require.NotEmpty(t, m)
207+
refs := make(map[string]struct{}, 5)
208+
for _, matched := range m {
209+
subMatch := matched[1]
210+
refs[subMatch] = struct{}{}
211+
}
212+
213+
// TODO(fred): the expansion is incorrect (it was already, with an undetected empty $ref)
214+
// require.Len(t, refs, 1)
215+
require.Len(t, refs, 1)
216+
})
217+
}
195218

196-
refs := make(map[string]struct{}, 5)
197-
for _, matched := range m {
198-
subMatch := matched[1]
199-
refs[subMatch] = struct{}{}
219+
func TestSortRefTracker(t *testing.T) {
220+
tracked := refTrackers{
221+
refTracker{Pointer: "/c/d/e"},
222+
refTracker{Pointer: "/definitions/x"},
223+
refTracker{Pointer: "/a/b/c/d"},
224+
refTracker{Pointer: "/b"},
225+
refTracker{Pointer: "/z"},
226+
refTracker{Pointer: "/definitions/a"},
200227
}
228+
sort.Sort(tracked)
229+
require.EqualValues(t, refTrackers{
230+
refTracker{Pointer: "/definitions/a"},
231+
refTracker{Pointer: "/definitions/x"},
232+
refTracker{Pointer: "/b"},
233+
refTracker{Pointer: "/z"},
234+
refTracker{Pointer: "/c/d/e"},
235+
refTracker{Pointer: "/a/b/c/d"},
236+
}, tracked)
237+
}
238+
239+
func TestRemoteExpandAzure(t *testing.T) {
240+
// local copy of : https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/network/resource-manager/Microsoft.Network/stable/2020-04-01/publicIpAddress.json
241+
server := httptest.NewServer(http.FileServer(http.Dir("fixtures/azure")))
242+
defer server.Close()
243+
244+
jazon := expandThisOrDieTrying(t, server.URL+"/publicIpAddress.json")
245+
246+
assertRefInJSONRegexp(t, jazon, `^(#/definitions/)|(#/paths/.+/get/default/schema/properties/error)|(\./examples/)`)
247+
}
248+
249+
func TestDocRef(t *testing.T) {
250+
doc := []byte(`{
251+
"description": "root pointer ref",
252+
"schema": {
253+
"properties": {
254+
"foo": {"$ref": "#"}
255+
},
256+
"additionalProperties": false
257+
}
258+
}`)
259+
var schema Schema
260+
261+
require.NoError(t, json.Unmarshal(doc, &schema))
262+
263+
// expand from root
264+
require.NoError(t, ExpandSchema(&schema, &schema, nil))
265+
266+
jazon, err := json.MarshalIndent(schema, "", " ")
267+
require.NoError(t, err)
268+
269+
assertRefInJSONRegexp(t, string(jazon), `(^#$)|(^$)`)
201270

202-
// TODO(fred): the expansion is incorrect (it was already, with an undetected empty $ref)
203-
// require.Len(t, refs, 1)
271+
// expand from self
272+
require.NoError(t, ExpandSchema(&schema, nil, nil))
273+
274+
jazon, err = json.MarshalIndent(schema, "", " ")
275+
require.NoError(t, err)
276+
277+
assertRefInJSONRegexp(t, string(jazon), `(^#$)|(^$)`)
278+
279+
// expand from file
280+
temp, err := ioutil.TempFile(".", "test_doc_ref*.json")
281+
require.NoError(t, err)
282+
283+
file := temp.Name()
284+
defer func() {
285+
_ = os.Remove(file)
286+
}()
287+
_, err = temp.Write(doc)
288+
require.NoError(t, err)
289+
require.NoError(t, temp.Close())
290+
291+
require.NoError(t, ExpandSchemaWithBasePath(&schema, nil, &ExpandOptions{RelativeBase: file}))
292+
293+
jazon, err = json.MarshalIndent(schema, "", " ")
294+
require.NoError(t, err)
295+
296+
assertRefInJSONRegexp(t, string(jazon), `(^#$)|(^$)`)
297+
298+
ref := RefSchema("#")
299+
require.NoError(t, ExpandSchema(ref, &schema, nil))
300+
jazon, err = json.MarshalIndent(ref, "", " ")
301+
require.NoError(t, err)
302+
t.Log(string(jazon))
204303
}

errors.go

+3
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,7 @@ var (
1515

1616
// ErrExpandUnsupportedType indicates that $ref expansion is attempted on some invalid type
1717
ErrExpandUnsupportedType = errors.New("expand: unsupported type. Input should be of type *Parameter or *Response")
18+
19+
// ErrInternalRef indicates an internal error with $ref track. Signal this as a bug.
20+
ErrInternalRef = errors.New("circular $ref: expected circular to be found in tracker")
1821
)

0 commit comments

Comments
 (0)