Files
libopenapi/index/resolver_test.go
quobix 8717b3cd33 An enormous amount of surgery on the low level model.
Every `Build()` method now requires a `context.Context`. This is so the rolodex knows where to resolve from when locating relative links. Without knowing where we are, there is no way to resolve anything. This new mechanism allows the model to recurse across as many files as required to locate references, without loosing track of where we are in the process.

Signed-off-by: quobix <dave@quobix.com>
2023-10-23 15:04:34 -04:00

665 lines
18 KiB
Go

package index
import (
"errors"
"fmt"
"github.com/pb33f/libopenapi/datamodel"
"github.com/pb33f/libopenapi/utils"
"net/http"
"net/url"
"os"
"testing"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
)
func TestNewResolver(t *testing.T) {
assert.Nil(t, NewResolver(nil))
}
func Benchmark_ResolveDocumentStripe(b *testing.B) {
baseDir := "../test_specs/stripe.yaml"
resolveFile, _ := os.ReadFile(baseDir)
var rootNode yaml.Node
_ = yaml.Unmarshal(resolveFile, &rootNode)
for n := 0; n < b.N; n++ {
cf := CreateOpenAPIIndexConfig()
rolo := NewRolodex(cf)
rolo.SetRootNode(&rootNode)
indexedErr := rolo.IndexTheRolodex()
assert.Len(b, utils.UnwrapErrors(indexedErr), 3)
}
}
func TestResolver_ResolveComponents_CircularSpec(t *testing.T) {
circular, _ := os.ReadFile("../test_specs/circular-tests.yaml")
var rootNode yaml.Node
_ = yaml.Unmarshal(circular, &rootNode)
cf := CreateClosedAPIIndexConfig()
cf.AvoidCircularReferenceCheck = true
rolo := NewRolodex(cf)
rolo.SetRootNode(&rootNode)
indexedErr := rolo.IndexTheRolodex()
assert.NoError(t, indexedErr)
rolo.Resolve()
assert.Len(t, rolo.GetCaughtErrors(), 3)
_, err := yaml.Marshal(rolo.GetRootIndex().GetResolver().resolvedRoot)
assert.NoError(t, err)
}
func TestResolver_CheckForCircularReferences(t *testing.T) {
circular, _ := os.ReadFile("../test_specs/circular-tests.yaml")
var rootNode yaml.Node
_ = yaml.Unmarshal(circular, &rootNode)
cf := CreateClosedAPIIndexConfig()
rolo := NewRolodex(cf)
rolo.SetRootNode(&rootNode)
indexedErr := rolo.IndexTheRolodex()
assert.Error(t, indexedErr)
assert.Len(t, utils.UnwrapErrors(indexedErr), 3)
rolo.CheckForCircularReferences()
assert.Len(t, rolo.GetCaughtErrors(), 3)
assert.Len(t, rolo.GetRootIndex().GetResolver().GetResolvingErrors(), 3)
assert.Len(t, rolo.GetRootIndex().GetResolver().GetCircularErrors(), 3)
}
func TestResolver_CheckForCircularReferences_CatchArray(t *testing.T) {
circular := []byte(`openapi: 3.0.0
components:
schemas:
ProductCategory:
type: "object"
properties:
name:
type: "string"
children:
type: "array"
items:
$ref: "#/components/schemas/ProductCategory"
description: "Array of sub-categories in the same format."
required:
- "name"
- "children"`)
var rootNode yaml.Node
_ = yaml.Unmarshal(circular, &rootNode)
idx := NewSpecIndexWithConfig(&rootNode, CreateClosedAPIIndexConfig())
resolver := NewResolver(idx)
assert.NotNil(t, resolver)
circ := resolver.CheckForCircularReferences()
assert.Len(t, circ, 1)
assert.Len(t, resolver.GetResolvingErrors(), 1) // infinite loop is a resolving error.
assert.Len(t, resolver.GetCircularErrors(), 1)
assert.True(t, resolver.GetCircularErrors()[0].IsArrayResult)
_, err := yaml.Marshal(resolver.resolvedRoot)
assert.NoError(t, err)
}
func TestResolver_CheckForCircularReferences_IgnoreArray(t *testing.T) {
circular := []byte(`openapi: 3.0.0
components:
schemas:
ProductCategory:
type: "object"
properties:
name:
type: "string"
children:
type: "array"
items:
$ref: "#/components/schemas/ProductCategory"
description: "Array of sub-categories in the same format."
required:
- "name"
- "children"`)
var rootNode yaml.Node
_ = yaml.Unmarshal(circular, &rootNode)
idx := NewSpecIndexWithConfig(&rootNode, CreateClosedAPIIndexConfig())
resolver := NewResolver(idx)
assert.NotNil(t, resolver)
resolver.IgnoreArrayCircularReferences()
circ := resolver.CheckForCircularReferences()
assert.Len(t, circ, 0)
assert.Len(t, resolver.GetResolvingErrors(), 0)
assert.Len(t, resolver.GetCircularErrors(), 0)
_, err := yaml.Marshal(resolver.resolvedRoot)
assert.NoError(t, err)
}
func TestResolver_CheckForCircularReferences_IgnorePoly_Any(t *testing.T) {
circular := []byte(`openapi: 3.0.0
components:
schemas:
ProductCategory:
type: "object"
properties:
name:
type: "string"
children:
type: "object"
anyOf:
- $ref: "#/components/schemas/ProductCategory"
description: "Array of sub-categories in the same format."
required:
- "name"
- "children"`)
var rootNode yaml.Node
_ = yaml.Unmarshal(circular, &rootNode)
idx := NewSpecIndexWithConfig(&rootNode, CreateClosedAPIIndexConfig())
resolver := NewResolver(idx)
assert.NotNil(t, resolver)
resolver.IgnorePolymorphicCircularReferences()
circ := resolver.CheckForCircularReferences()
assert.Len(t, circ, 0)
assert.Len(t, resolver.GetResolvingErrors(), 0)
assert.Len(t, resolver.GetCircularErrors(), 0)
_, err := yaml.Marshal(resolver.resolvedRoot)
assert.NoError(t, err)
}
func TestResolver_CheckForCircularReferences_IgnorePoly_All(t *testing.T) {
circular := []byte(`openapi: 3.0.0
components:
schemas:
ProductCategory:
type: "object"
properties:
name:
type: "string"
children:
type: "object"
allOf:
- $ref: "#/components/schemas/ProductCategory"
description: "Array of sub-categories in the same format."
required:
- "name"
- "children"`)
var rootNode yaml.Node
_ = yaml.Unmarshal(circular, &rootNode)
idx := NewSpecIndexWithConfig(&rootNode, CreateClosedAPIIndexConfig())
resolver := NewResolver(idx)
assert.NotNil(t, resolver)
resolver.IgnorePolymorphicCircularReferences()
circ := resolver.CheckForCircularReferences()
assert.Len(t, circ, 0)
assert.Len(t, resolver.GetResolvingErrors(), 0)
assert.Len(t, resolver.GetCircularErrors(), 0)
_, err := yaml.Marshal(resolver.resolvedRoot)
assert.NoError(t, err)
}
func TestResolver_CheckForCircularReferences_IgnorePoly_One(t *testing.T) {
circular := []byte(`openapi: 3.0.0
components:
schemas:
ProductCategory:
type: "object"
properties:
name:
type: "string"
children:
type: "object"
oneOf:
- $ref: "#/components/schemas/ProductCategory"
description: "Array of sub-categories in the same format."
required:
- "name"
- "children"`)
var rootNode yaml.Node
_ = yaml.Unmarshal(circular, &rootNode)
idx := NewSpecIndexWithConfig(&rootNode, CreateClosedAPIIndexConfig())
resolver := NewResolver(idx)
assert.NotNil(t, resolver)
resolver.IgnorePolymorphicCircularReferences()
circ := resolver.CheckForCircularReferences()
assert.Len(t, circ, 0)
assert.Len(t, resolver.GetResolvingErrors(), 0)
assert.Len(t, resolver.GetCircularErrors(), 0)
_, err := yaml.Marshal(resolver.resolvedRoot)
assert.NoError(t, err)
}
func TestResolver_CheckForCircularReferences_CatchPoly_Any(t *testing.T) {
circular := []byte(`openapi: 3.0.0
components:
schemas:
ProductCategory:
type: "object"
properties:
name:
type: "string"
children:
type: "object"
anyOf:
- $ref: "#/components/schemas/ProductCategory"
description: "Array of sub-categories in the same format."
required:
- "name"
- "children"`)
var rootNode yaml.Node
_ = yaml.Unmarshal(circular, &rootNode)
idx := NewSpecIndexWithConfig(&rootNode, CreateClosedAPIIndexConfig())
resolver := NewResolver(idx)
assert.NotNil(t, resolver)
circ := resolver.CheckForCircularReferences()
assert.Len(t, circ, 0)
assert.Len(t, resolver.GetResolvingErrors(), 0) // not an infinite loop if poly.
assert.Len(t, resolver.GetCircularErrors(), 1)
assert.Equal(t, "anyOf", resolver.GetCircularErrors()[0].PolymorphicType)
_, err := yaml.Marshal(resolver.resolvedRoot)
assert.NoError(t, err)
}
func TestResolver_CheckForCircularReferences_CatchPoly_All(t *testing.T) {
circular := []byte(`openapi: 3.0.0
components:
schemas:
ProductCategory:
type: "object"
properties:
name:
type: "string"
children:
type: "object"
allOf:
- $ref: "#/components/schemas/ProductCategory"
description: "Array of sub-categories in the same format."
required:
- "name"
- "children"`)
var rootNode yaml.Node
_ = yaml.Unmarshal(circular, &rootNode)
idx := NewSpecIndexWithConfig(&rootNode, CreateClosedAPIIndexConfig())
resolver := NewResolver(idx)
assert.NotNil(t, resolver)
circ := resolver.CheckForCircularReferences()
assert.Len(t, circ, 0)
assert.Len(t, resolver.GetResolvingErrors(), 0) // not an infinite loop if poly.
assert.Len(t, resolver.GetCircularErrors(), 1)
assert.Equal(t, "allOf", resolver.GetCircularErrors()[0].PolymorphicType)
assert.True(t, resolver.GetCircularErrors()[0].IsPolymorphicResult)
_, err := yaml.Marshal(resolver.resolvedRoot)
assert.NoError(t, err)
}
func TestResolver_CircularReferencesRequiredValid(t *testing.T) {
circular, _ := os.ReadFile("../test_specs/swagger-valid-recursive-model.yaml")
var rootNode yaml.Node
_ = yaml.Unmarshal(circular, &rootNode)
idx := NewSpecIndexWithConfig(&rootNode, CreateClosedAPIIndexConfig())
resolver := NewResolver(idx)
assert.NotNil(t, resolver)
circ := resolver.CheckForCircularReferences()
assert.Len(t, circ, 0)
_, err := yaml.Marshal(resolver.resolvedRoot)
assert.NoError(t, err)
}
func TestResolver_CircularReferencesRequiredInvalid(t *testing.T) {
circular, _ := os.ReadFile("../test_specs/swagger-invalid-recursive-model.yaml")
var rootNode yaml.Node
_ = yaml.Unmarshal(circular, &rootNode)
idx := NewSpecIndexWithConfig(&rootNode, CreateClosedAPIIndexConfig())
resolver := NewResolver(idx)
assert.NotNil(t, resolver)
circ := resolver.CheckForCircularReferences()
assert.Len(t, circ, 2)
_, err := yaml.Marshal(resolver.resolvedRoot)
assert.NoError(t, err)
}
func TestResolver_DeepJourney(t *testing.T) {
var journey []*Reference
for f := 0; f < 200; f++ {
journey = append(journey, nil)
}
idx := NewSpecIndexWithConfig(nil, CreateClosedAPIIndexConfig())
resolver := NewResolver(idx)
assert.Nil(t, resolver.extractRelatives(nil, nil, nil, nil, journey, false))
}
func TestResolver_ResolveComponents_Stripe_NoRolodex(t *testing.T) {
baseDir := "../test_specs/stripe.yaml"
resolveFile, _ := os.ReadFile(baseDir)
var stripeRoot yaml.Node
_ = yaml.Unmarshal(resolveFile, &stripeRoot)
info, _ := datamodel.ExtractSpecInfoWithDocumentCheck(resolveFile, true)
cf := CreateOpenAPIIndexConfig()
cf.SpecInfo = info
idx := NewSpecIndexWithConfig(&stripeRoot, cf)
resolver := NewResolver(idx)
assert.NotNil(t, resolver)
circ := resolver.CheckForCircularReferences()
assert.Len(t, circ, 3)
_, err := yaml.Marshal(resolver.resolvedRoot)
assert.NoError(t, err)
}
func TestResolver_ResolveComponents_Stripe(t *testing.T) {
baseDir := "../test_specs/stripe.yaml"
resolveFile, _ := os.ReadFile(baseDir)
var stripeRoot yaml.Node
_ = yaml.Unmarshal(resolveFile, &stripeRoot)
info, _ := datamodel.ExtractSpecInfoWithDocumentCheck(resolveFile, true)
cf := CreateOpenAPIIndexConfig()
cf.SpecInfo = info
cf.AvoidCircularReferenceCheck = true
rolo := NewRolodex(cf)
rolo.SetRootNode(&stripeRoot)
indexedErr := rolo.IndexTheRolodex()
assert.NoError(t, indexedErr)
// after resolving, the rolodex will have errors.
rolo.Resolve()
assert.Len(t, rolo.GetCaughtErrors(), 3)
assert.Len(t, rolo.GetRootIndex().GetResolver().GetNonPolymorphicCircularErrors(), 3)
assert.Len(t, rolo.GetRootIndex().GetResolver().GetPolymorphicCircularErrors(), 0)
}
func TestResolver_ResolveComponents_BurgerShop(t *testing.T) {
mixedref, _ := os.ReadFile("../test_specs/burgershop.openapi.yaml")
var rootNode yaml.Node
_ = yaml.Unmarshal(mixedref, &rootNode)
idx := NewSpecIndexWithConfig(&rootNode, CreateClosedAPIIndexConfig())
resolver := NewResolver(idx)
assert.NotNil(t, resolver)
circ := resolver.Resolve()
assert.Len(t, circ, 0)
}
func TestResolver_ResolveComponents_PolyNonCircRef(t *testing.T) {
yml := `paths:
/hey:
get:
responses:
"200":
$ref: '#/components/schemas/crackers'
components:
schemas:
cheese:
description: cheese
anyOf:
items:
$ref: '#/components/schemas/crackers'
crackers:
description: crackers
allOf:
- $ref: '#/components/schemas/tea'
tea:
description: tea`
var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode)
idx := NewSpecIndexWithConfig(&rootNode, CreateClosedAPIIndexConfig())
resolver := NewResolver(idx)
assert.NotNil(t, resolver)
circ := resolver.CheckForCircularReferences()
assert.Len(t, circ, 0)
}
func TestResolver_ResolveComponents_PolyCircRef(t *testing.T) {
yml := `openapi: 3.1.0
components:
schemas:
cheese:
description: cheese
anyOf:
- $ref: '#/components/schemas/crackers'
crackers:
description: crackers
anyOf:
- $ref: '#/components/schemas/cheese'
tea:
description: tea`
var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode)
idx := NewSpecIndexWithConfig(&rootNode, CreateClosedAPIIndexConfig())
resolver := NewResolver(idx)
assert.NotNil(t, resolver)
_ = resolver.CheckForCircularReferences()
resolver.circularReferences[0].IsInfiniteLoop = true // override
assert.Len(t, idx.GetCircularReferences(), 1)
assert.Len(t, resolver.GetPolymorphicCircularErrors(), 1)
assert.Equal(t, 2, idx.GetCircularReferences()[0].LoopIndex)
}
func TestResolver_ResolveComponents_Missing(t *testing.T) {
yml := `paths:
/hey:
get:
responses:
"200":
$ref: '#/components/schemas/crackers'
components:
schemas:
cheese:
description: cheese
properties:
thang:
$ref: '#/components/schemas/crackers'
crackers:
description: crackers
properties:
butter:
$ref: 'go home, I am drunk'`
var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode)
idx := NewSpecIndexWithConfig(&rootNode, CreateClosedAPIIndexConfig())
resolver := NewResolver(idx)
assert.NotNil(t, resolver)
err := resolver.Resolve()
assert.Len(t, err, 1)
assert.Equal(t, "cannot resolve reference `go home, I am drunk`, it's missing: $go home, I am drunk [18:11]", err[0].Error())
}
func TestResolver_ResolveComponents_MixedRef(t *testing.T) {
mixedref, _ := os.ReadFile("../test_specs/mixedref-burgershop.openapi.yaml")
var rootNode yaml.Node
_ = yaml.Unmarshal(mixedref, &rootNode)
// create a test server.
server := test_buildMixedRefServer()
defer server.Close()
// create a new config that allows local and remote to be mixed up.
cf := CreateOpenAPIIndexConfig()
cf.AvoidBuildIndex = true
cf.AllowRemoteLookup = true
cf.AvoidCircularReferenceCheck = true
cf.BasePath = "../test_specs"
// setting this baseURL will override the base
cf.BaseURL, _ = url.Parse(server.URL)
// create a new rolodex
rolo := NewRolodex(cf)
// set the rolodex root node to the root node of the spec.
rolo.SetRootNode(&rootNode)
// create a new remote fs and set the config for indexing.
remoteFS, _ := NewRemoteFSWithRootURL(server.URL)
remoteFS.SetIndexConfig(cf)
// set our remote handler func
c := http.Client{}
remoteFS.RemoteHandlerFunc = c.Get
// configure the local filesystem.
fsCfg := LocalFSConfig{
BaseDirectory: cf.BasePath,
FileFilters: []string{"burgershop.openapi.yaml"},
DirFS: os.DirFS(cf.BasePath),
}
// create a new local filesystem.
fileFS, err := NewLocalFSWithConfig(&fsCfg)
assert.NoError(t, err)
// add file systems to the rolodex
rolo.AddLocalFS(cf.BasePath, fileFS)
rolo.AddRemoteFS(server.URL, remoteFS)
// index the rolodex.
indexedErr := rolo.IndexTheRolodex()
assert.NoError(t, indexedErr)
rolo.Resolve()
index := rolo.GetRootIndex
resolver := index().GetResolver()
assert.Len(t, resolver.GetCircularErrors(), 0)
assert.Equal(t, 2, resolver.GetIndexesVisited())
// in v0.8.2 a new check was added when indexing, to prevent re-indexing the same file multiple times.
assert.Equal(t, 6, resolver.GetRelativesSeen())
assert.Equal(t, 5, resolver.GetJourneysTaken())
assert.Equal(t, 7, resolver.GetReferenceVisited())
}
func TestResolver_ResolveComponents_k8s(t *testing.T) {
k8s, _ := os.ReadFile("../test_specs/k8s.json")
var rootNode yaml.Node
_ = yaml.Unmarshal(k8s, &rootNode)
idx := NewSpecIndexWithConfig(&rootNode, CreateClosedAPIIndexConfig())
resolver := NewResolver(idx)
assert.NotNil(t, resolver)
circ := resolver.Resolve()
assert.Len(t, circ, 0)
}
// Example of how to resolve the Stripe OpenAPI specification, and check for circular reference errors
func ExampleNewResolver() {
// create a yaml.Node reference as a root node.
var rootNode yaml.Node
// load in the Stripe OpenAPI spec (lots of polymorphic complexity in here)
stripeBytes, _ := os.ReadFile("../test_specs/stripe.yaml")
// unmarshal bytes into our rootNode.
_ = yaml.Unmarshal(stripeBytes, &rootNode)
// create a new spec index (resolver depends on it)
indexConfig := CreateClosedAPIIndexConfig()
idx := NewSpecIndexWithConfig(&rootNode, indexConfig)
// create a new resolver using the index.
resolver := NewResolver(idx)
// resolve the document, if there are circular reference errors, they are returned/
// WARNING: this is a destructive action and the rootNode will be PERMANENTLY altered and cannot be unresolved
circularErrors := resolver.Resolve()
// The Stripe API has a bunch of circular reference problems, mainly from polymorphism.
// So let's print them out.
//
fmt.Printf("There are %d circular reference errors, %d of them are polymorphic errors, %d are not",
len(circularErrors), len(resolver.GetPolymorphicCircularErrors()), len(resolver.GetNonPolymorphicCircularErrors()))
// Output: There are 3 circular reference errors, 0 of them are polymorphic errors, 3 are not
}
func ExampleResolvingError() {
re := ResolvingError{
ErrorRef: errors.New("je suis une erreur"),
Node: &yaml.Node{
Line: 5,
Column: 21,
},
Path: "#/definitions/JeSuisUneErreur",
CircularReference: &CircularReferenceResult{},
}
fmt.Printf("%s", re.Error())
// Output: je suis une erreur: #/definitions/JeSuisUneErreur [5:21]
}