Files
libopenapi/index/spec_index_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

1385 lines
39 KiB
Go

// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT
package index
import (
"fmt"
"github.com/pb33f/libopenapi/utils"
"log"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
"os"
"os/exec"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
)
func TestSpecIndex_ExtractRefsStripe(t *testing.T) {
stripe, _ := os.ReadFile("../test_specs/stripe.yaml")
var rootNode yaml.Node
_ = yaml.Unmarshal(stripe, &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
assert.Len(t, index.allRefs, 385)
assert.Equal(t, 537, len(index.allMappedRefs))
combined := index.GetAllCombinedReferences()
assert.Equal(t, 537, len(combined))
assert.Len(t, index.rawSequencedRefs, 1972)
assert.Equal(t, 246, index.pathCount)
assert.Equal(t, 402, index.operationCount)
assert.Equal(t, 537, index.schemaCount)
assert.Equal(t, 0, index.globalTagsCount)
assert.Equal(t, 0, index.globalLinksCount)
assert.Equal(t, 0, index.componentParamCount)
assert.Equal(t, 143, index.operationParamCount)
assert.Equal(t, 88, index.componentsInlineParamDuplicateCount)
assert.Equal(t, 55, index.componentsInlineParamUniqueCount)
assert.Equal(t, 1516, index.enumCount)
assert.Len(t, index.GetAllEnums(), 1516)
assert.Len(t, index.GetPolyAllOfReferences(), 0)
assert.Len(t, index.GetPolyOneOfReferences(), 275)
assert.Len(t, index.GetPolyAnyOfReferences(), 553)
assert.Len(t, index.GetAllReferenceSchemas(), 1972)
assert.NotNil(t, index.GetRootServersNode())
assert.Len(t, index.GetAllRootServers(), 1)
// not required, but flip the circular result switch on and off.
assert.False(t, index.AllowCircularReferenceResolving())
index.SetAllowCircularReferenceResolving(true)
assert.True(t, index.AllowCircularReferenceResolving())
// simulate setting of circular references, also pointless but needed for coverage.
assert.Nil(t, index.GetCircularReferences())
index.SetCircularReferences([]*CircularReferenceResult{new(CircularReferenceResult)})
assert.Len(t, index.GetCircularReferences(), 1)
assert.Len(t, index.GetRefsByLine(), 537)
assert.Len(t, index.GetLinesWithReferences(), 1972)
assert.Len(t, index.GetAllExternalDocuments(), 0)
}
func TestSpecIndex_Asana(t *testing.T) {
asana, _ := os.ReadFile("../test_specs/asana.yaml")
var rootNode yaml.Node
_ = yaml.Unmarshal(asana, &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
assert.Len(t, index.allRefs, 152)
assert.Len(t, index.allMappedRefs, 171)
combined := index.GetAllCombinedReferences()
assert.Equal(t, 171, len(combined))
assert.Equal(t, 118, index.pathCount)
assert.Equal(t, 152, index.operationCount)
assert.Equal(t, 135, index.schemaCount)
assert.Equal(t, 26, index.globalTagsCount)
assert.Equal(t, 0, index.globalLinksCount)
assert.Equal(t, 30, index.componentParamCount)
assert.Equal(t, 107, index.operationParamCount)
assert.Equal(t, 8, index.componentsInlineParamDuplicateCount)
assert.Equal(t, 69, index.componentsInlineParamUniqueCount)
}
func TestSpecIndex_DigitalOcean(t *testing.T) {
do, _ := os.ReadFile("../test_specs/digitalocean.yaml")
var rootNode yaml.Node
_ = yaml.Unmarshal(do, &rootNode)
location := "https://raw.githubusercontent.com/digitalocean/openapi/main/specification"
baseURL, _ := url.Parse(location)
// create a new config that allows remote lookups.
cf := &SpecIndexConfig{}
cf.AvoidBuildIndex = true
cf.AllowRemoteLookup = true
cf.AvoidCircularReferenceCheck = true
cf.Logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelError,
}))
// setting this baseURL will override the base
cf.BaseURL = baseURL
// 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, _ := NewRemoteFSWithConfig(cf)
// create a handler that uses an env variable to capture any GITHUB_TOKEN in the OS ENV
// and inject it into the request header, so this does not fail when running lots of local tests.
if os.Getenv("GITHUB_TOKEN") != "" {
client := &http.Client{
Timeout: time.Second * 60,
}
remoteFS.SetRemoteHandlerFunc(func(url string) (*http.Response, error) {
request, _ := http.NewRequest(http.MethodGet, url, nil)
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", os.Getenv("GITHUB_TOKEN")))
return client.Do(request)
})
}
// add remote filesystem
rolo.AddRemoteFS(location, remoteFS)
// index the rolodex.
indexedErr := rolo.IndexTheRolodex()
assert.NoError(t, indexedErr)
files := remoteFS.GetFiles()
fileLen := len(files)
assert.Equal(t, 1646, fileLen)
assert.Len(t, remoteFS.GetErrors(), 0)
// check circular references
rolo.CheckForCircularReferences()
assert.Len(t, rolo.GetCaughtErrors(), 0)
assert.Len(t, rolo.GetIgnoredCircularReferences(), 0)
}
func TestSpecIndex_DigitalOcean_FullCheckoutLocalResolve(t *testing.T) {
// this is a full checkout of the digitalocean API repo.
tmp, _ := os.MkdirTemp("", "openapi")
cmd := exec.Command("git", "clone", "https://github.com/digitalocean/openapi", tmp)
defer os.RemoveAll(filepath.Join(tmp, "openapi"))
err := cmd.Run()
if err != nil {
log.Fatalf("cmd.Run() failed with %s\n", err)
}
spec, _ := filepath.Abs(filepath.Join(tmp, "specification", "DigitalOcean-public.v2.yaml"))
doLocal, _ := os.ReadFile(spec)
var rootNode yaml.Node
_ = yaml.Unmarshal(doLocal, &rootNode)
basePath := filepath.Join(tmp, "specification")
// create a new config that allows local and remote to be mixed up.
cf := CreateOpenAPIIndexConfig()
cf.AllowRemoteLookup = true
cf.AvoidCircularReferenceCheck = true
cf.BasePath = basePath
cf.Logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelError,
}))
// create a new rolodex
rolo := NewRolodex(cf)
// set the rolodex root node to the root node of the spec.
rolo.SetRootNode(&rootNode)
// configure the local filesystem.
fsCfg := LocalFSConfig{
BaseDirectory: cf.BasePath,
DirFS: os.DirFS(cf.BasePath),
Logger: cf.Logger,
}
// create a new local filesystem.
fileFS, fsErr := NewLocalFSWithConfig(&fsCfg)
assert.NoError(t, fsErr)
files := fileFS.GetFiles()
fileLen := len(files)
assert.Equal(t, 1691, fileLen)
rolo.AddLocalFS(basePath, fileFS)
rErr := rolo.IndexTheRolodex()
assert.NoError(t, rErr)
index := rolo.GetRootIndex()
assert.NotNil(t, index)
assert.Len(t, index.GetMappedReferencesSequenced(), 299)
assert.Len(t, index.GetMappedReferences(), 299)
assert.Len(t, fileFS.GetErrors(), 0)
// check circular references
rolo.CheckForCircularReferences()
assert.Len(t, rolo.GetCaughtErrors(), 0)
assert.Len(t, rolo.GetIgnoredCircularReferences(), 0)
}
func TestSpecIndex_DigitalOcean_LookupsNotAllowed(t *testing.T) {
do, _ := os.ReadFile("../test_specs/digitalocean.yaml")
var rootNode yaml.Node
_ = yaml.Unmarshal(do, &rootNode)
location := "https://raw.githubusercontent.com/digitalocean/openapi/main/specification"
baseURL, _ := url.Parse(location)
// create a new config that does not allow remote lookups.
cf := &SpecIndexConfig{}
cf.AvoidBuildIndex = true
cf.AvoidCircularReferenceCheck = true
cf.Logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelError,
}))
// setting this baseURL will override the base
cf.BaseURL = baseURL
// 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, _ := NewRemoteFSWithConfig(cf)
// add remote filesystem
rolo.AddRemoteFS(location, remoteFS)
// index the rolodex.
indexedErr := rolo.IndexTheRolodex()
assert.Error(t, indexedErr)
assert.Len(t, utils.UnwrapErrors(indexedErr), 291)
index := rolo.GetRootIndex()
files := remoteFS.GetFiles()
fileLen := len(files)
assert.Equal(t, 0, fileLen)
assert.Len(t, remoteFS.GetErrors(), 0)
// no lookups allowed, bits have not been set, so there should just be a bunch of errors.
assert.True(t, len(index.GetReferenceIndexErrors()) > 0)
}
func TestSpecIndex_BaseURLError(t *testing.T) {
do, _ := os.ReadFile("../test_specs/digitalocean.yaml")
var rootNode yaml.Node
_ = yaml.Unmarshal(do, &rootNode)
location := "https://githerbsandcoffeeandcode.com/fresh/herbs/for/you" // not gonna work bro.
baseURL, _ := url.Parse(location)
// create a new config that allows remote lookups.
cf := &SpecIndexConfig{}
cf.AvoidBuildIndex = true
cf.AllowRemoteLookup = true
cf.AvoidCircularReferenceCheck = true
cf.Logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelError,
}))
// setting this baseURL will override the base
cf.BaseURL = baseURL
// 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, _ := NewRemoteFSWithConfig(cf)
// add remote filesystem
rolo.AddRemoteFS(location, remoteFS)
// index the rolodex.
indexedErr := rolo.IndexTheRolodex()
assert.Error(t, indexedErr)
assert.Len(t, utils.UnwrapErrors(indexedErr), 291)
files := remoteFS.GetFiles()
fileLen := len(files)
assert.Equal(t, 0, fileLen)
assert.GreaterOrEqual(t, len(remoteFS.GetErrors()), 200)
}
func TestSpecIndex_k8s(t *testing.T) {
asana, _ := os.ReadFile("../test_specs/k8s.json")
var rootNode yaml.Node
_ = yaml.Unmarshal(asana, &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
assert.Len(t, index.allRefs, 558)
assert.Equal(t, 563, len(index.allMappedRefs))
combined := index.GetAllCombinedReferences()
assert.Equal(t, 563, len(combined))
assert.Equal(t, 436, index.pathCount)
assert.Equal(t, 853, index.operationCount)
assert.Equal(t, 563, index.schemaCount)
assert.Equal(t, 0, index.globalTagsCount)
assert.Equal(t, 58, index.operationTagsCount)
assert.Equal(t, 0, index.globalLinksCount)
assert.Equal(t, 0, index.componentParamCount)
assert.Equal(t, 36, index.operationParamCount)
assert.Equal(t, 26, index.componentsInlineParamDuplicateCount)
assert.Equal(t, 10, index.componentsInlineParamUniqueCount)
assert.Equal(t, 58, index.GetTotalTagsCount())
assert.Equal(t, 2524, index.GetRawReferenceCount())
}
func TestSpecIndex_PetstoreV2(t *testing.T) {
asana, _ := os.ReadFile("../test_specs/petstorev2.json")
var rootNode yaml.Node
_ = yaml.Unmarshal(asana, &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
assert.Len(t, index.allRefs, 6)
assert.Len(t, index.allMappedRefs, 6)
assert.Equal(t, 14, index.pathCount)
assert.Equal(t, 20, index.operationCount)
assert.Equal(t, 6, index.schemaCount)
assert.Equal(t, 3, index.globalTagsCount)
assert.Equal(t, 3, index.operationTagsCount)
assert.Equal(t, 0, index.globalLinksCount)
assert.Equal(t, 1, index.componentParamCount)
assert.Equal(t, 1, index.GetComponentParameterCount())
assert.Equal(t, 11, index.operationParamCount)
assert.Equal(t, 5, index.componentsInlineParamDuplicateCount)
assert.Equal(t, 6, index.componentsInlineParamUniqueCount)
assert.Equal(t, 3, index.GetTotalTagsCount())
assert.Equal(t, 2, len(index.GetSecurityRequirementReferences()))
}
func TestSpecIndex_XSOAR(t *testing.T) {
xsoar, _ := os.ReadFile("../test_specs/xsoar.json")
var rootNode yaml.Node
_ = yaml.Unmarshal(xsoar, &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
assert.Len(t, index.allRefs, 209)
assert.Equal(t, 85, index.pathCount)
assert.Equal(t, 88, index.operationCount)
assert.Equal(t, 245, index.schemaCount)
assert.Equal(t, 207, len(index.allMappedRefs))
assert.Equal(t, 0, index.globalTagsCount)
assert.Equal(t, 0, index.operationTagsCount)
assert.Equal(t, 0, index.globalLinksCount)
assert.Len(t, index.GetRootSecurityReferences(), 1)
assert.NotNil(t, index.GetRootSecurityNode())
}
func TestSpecIndex_PetstoreV3(t *testing.T) {
petstore, _ := os.ReadFile("../test_specs/petstorev3.json")
var rootNode yaml.Node
_ = yaml.Unmarshal(petstore, &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
assert.Len(t, index.allRefs, 7)
assert.Len(t, index.allMappedRefs, 7)
assert.Equal(t, 13, index.pathCount)
assert.Equal(t, 19, index.operationCount)
assert.Equal(t, 8, index.schemaCount)
assert.Equal(t, 3, index.globalTagsCount)
assert.Equal(t, 3, index.operationTagsCount)
assert.Equal(t, 0, index.globalLinksCount)
assert.Equal(t, 0, index.componentParamCount)
assert.Equal(t, 9, index.operationParamCount)
assert.Equal(t, 4, index.componentsInlineParamDuplicateCount)
assert.Equal(t, 5, index.componentsInlineParamUniqueCount)
assert.Equal(t, 3, index.GetTotalTagsCount())
assert.Equal(t, 90, index.GetAllDescriptionsCount())
assert.Equal(t, 19, index.GetAllSummariesCount())
assert.Len(t, index.GetAllDescriptions(), 90)
assert.Len(t, index.GetAllSummaries(), 19)
}
var mappedRefs = 15
func TestSpecIndex_BurgerShop(t *testing.T) {
burgershop, _ := os.ReadFile("../test_specs/burgershop.openapi.yaml")
var rootNode yaml.Node
_ = yaml.Unmarshal(burgershop, &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
assert.Len(t, index.allRefs, mappedRefs)
assert.Len(t, index.allMappedRefs, mappedRefs)
assert.Equal(t, mappedRefs, len(index.GetMappedReferences()))
assert.Equal(t, mappedRefs, len(index.GetMappedReferencesSequenced()))
assert.Equal(t, 6, index.pathCount)
assert.Equal(t, 6, index.GetPathCount())
assert.Equal(t, 6, len(index.GetAllComponentSchemas()))
assert.Equal(t, 56, len(index.GetAllSchemas()))
assert.Equal(t, 34, len(index.GetAllSequencedReferences()))
assert.NotNil(t, index.GetSchemasNode())
assert.NotNil(t, index.GetParametersNode())
assert.Equal(t, 5, index.operationCount)
assert.Equal(t, 5, index.GetOperationCount())
assert.Equal(t, 6, index.schemaCount)
assert.Equal(t, 6, index.GetComponentSchemaCount())
assert.Equal(t, 2, index.globalTagsCount)
assert.Equal(t, 2, index.GetGlobalTagsCount())
assert.Equal(t, 2, index.GetTotalTagsCount())
assert.Equal(t, 2, index.operationTagsCount)
assert.Equal(t, 2, index.GetOperationTagsCount())
assert.Equal(t, 3, index.globalLinksCount)
assert.Equal(t, 3, index.GetGlobalLinksCount())
assert.Equal(t, 1, index.globalCallbacksCount)
assert.Equal(t, 1, index.GetGlobalCallbacksCount())
assert.Equal(t, 2, index.componentParamCount)
assert.Equal(t, 2, index.GetComponentParameterCount())
assert.Equal(t, 4, index.operationParamCount)
assert.Equal(t, 4, index.GetOperationsParameterCount())
assert.Equal(t, 0, index.componentsInlineParamDuplicateCount)
assert.Equal(t, 0, index.GetInlineDuplicateParamCount())
assert.Equal(t, 2, index.componentsInlineParamUniqueCount)
assert.Equal(t, 2, index.GetInlineUniqueParamCount())
assert.Equal(t, 1, len(index.GetAllRequestBodies()))
assert.NotNil(t, index.GetRootNode())
assert.NotNil(t, index.GetGlobalTagsNode())
assert.NotNil(t, index.GetPathsNode())
assert.NotNil(t, index.GetDiscoveredReferences())
assert.Equal(t, 1, len(index.GetPolyReferences()))
assert.NotNil(t, index.GetOperationParameterReferences())
assert.Equal(t, 3, len(index.GetAllSecuritySchemes()))
assert.Equal(t, 2, len(index.GetAllParameters()))
assert.Equal(t, 1, len(index.GetAllResponses()))
assert.Equal(t, 2, len(index.GetInlineOperationDuplicateParameters()))
assert.Equal(t, 0, len(index.GetReferencesWithSiblings()))
assert.Equal(t, mappedRefs, len(index.GetAllReferences()))
assert.Equal(t, 0, len(index.GetOperationParametersIndexErrors()))
assert.Equal(t, 5, len(index.GetAllPaths()))
assert.Equal(t, 5, len(index.GetOperationTags()))
assert.Equal(t, 3, len(index.GetAllParametersFromOperations()))
}
func TestSpecIndex_GetAllParametersFromOperations(t *testing.T) {
yml := `openapi: 3.0.0
servers:
- url: http://localhost:8080
paths:
/test:
get:
parameters:
- name: action
in: query
schema:
type: string
- name: action
in: query
schema:
type: string`
var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
assert.Equal(t, 1, len(index.GetAllParametersFromOperations()))
assert.Equal(t, 1, len(index.GetOperationParametersIndexErrors()))
}
func TestSpecIndex_BurgerShop_AllTheComponents(t *testing.T) {
burgershop, _ := os.ReadFile("../test_specs/all-the-components.yaml")
var rootNode yaml.Node
_ = yaml.Unmarshal(burgershop, &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
assert.Equal(t, 1, len(index.GetAllHeaders()))
assert.Equal(t, 1, len(index.GetAllLinks()))
assert.Equal(t, 1, len(index.GetAllCallbacks()))
assert.Equal(t, 1, len(index.GetAllExamples()))
assert.Equal(t, 1, len(index.GetAllResponses()))
assert.Equal(t, 2, len(index.GetAllRootServers()))
assert.Equal(t, 2, len(index.GetAllOperationsServers()))
}
func TestSpecIndex_SwaggerResponses(t *testing.T) {
yml := `swagger: 2.0
responses:
niceResponse:
description: hi`
var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
assert.Equal(t, 1, len(index.GetAllResponses()))
}
func TestSpecIndex_NoNameParam(t *testing.T) {
yml := `paths:
/users/{id}:
parameters:
- in: path
name: id
- in: query
get:
parameters:
- in: path
name: id
- in: query`
var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
assert.Equal(t, 2, len(index.GetOperationParametersIndexErrors()))
}
func TestSpecIndex_NoRoot(t *testing.T) {
index := NewSpecIndex(nil)
refs := index.ExtractRefs(nil, nil, nil, 0, false, "")
docs := index.ExtractExternalDocuments(nil)
assert.Nil(t, docs)
assert.Nil(t, refs)
assert.Nil(t, index.FindComponent("nothing", nil))
assert.Equal(t, -1, index.GetOperationCount())
assert.Equal(t, -1, index.GetPathCount())
assert.Equal(t, -1, index.GetGlobalTagsCount())
assert.Equal(t, -1, index.GetOperationTagsCount())
assert.Equal(t, -1, index.GetTotalTagsCount())
assert.Equal(t, -1, index.GetOperationsParameterCount())
assert.Equal(t, -1, index.GetComponentParameterCount())
assert.Equal(t, -1, index.GetComponentSchemaCount())
assert.Equal(t, -1, index.GetGlobalLinksCount())
}
func test_buildMixedRefServer() *httptest.Server {
bs, _ := os.ReadFile("../test_specs/burgershop.openapi.yaml")
return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("Last-Modified", "Wed, 21 Oct 2015 07:28:00 GMT")
_, _ = rw.Write(bs)
}))
}
func TestSpecIndex_BurgerShopMixedRef(t *testing.T) {
// 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"
cf.Logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelError,
}))
// setting this baseURL will override the base
cf.BaseURL, _ = url.Parse(server.URL)
cFile := "../test_specs/mixedref-burgershop.openapi.yaml"
yml, _ := os.ReadFile(cFile)
var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode)
// 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()
rolo.BuildIndexes()
assert.NoError(t, indexedErr)
index := rolo.GetRootIndex()
rolo.CheckForCircularReferences()
assert.Len(t, index.allRefs, 5)
assert.Len(t, index.allMappedRefs, 5)
assert.Equal(t, 5, index.GetPathCount())
assert.Equal(t, 5, index.GetOperationCount())
assert.Equal(t, 1, index.GetComponentSchemaCount())
assert.Equal(t, 2, index.GetGlobalTagsCount())
assert.Equal(t, 3, index.GetTotalTagsCount())
assert.Equal(t, 2, index.GetOperationTagsCount())
assert.Equal(t, 0, index.GetGlobalLinksCount())
assert.Equal(t, 0, index.GetComponentParameterCount())
assert.Equal(t, 2, index.GetOperationsParameterCount())
assert.Equal(t, 1, index.GetInlineDuplicateParamCount())
assert.Equal(t, 1, index.GetInlineUniqueParamCount())
assert.Len(t, index.refErrors, 0)
assert.Len(t, index.GetCircularReferences(), 0)
}
func TestSpecIndex_TestEmptyBrokenReferences(t *testing.T) {
badref, _ := os.ReadFile("../test_specs/badref-burgershop.openapi.yaml")
var rootNode yaml.Node
_ = yaml.Unmarshal(badref, &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
assert.Equal(t, 5, index.GetPathCount())
assert.Equal(t, 5, index.GetOperationCount())
assert.Equal(t, 5, index.GetComponentSchemaCount())
assert.Equal(t, 2, index.GetGlobalTagsCount())
assert.Equal(t, 3, index.GetTotalTagsCount())
assert.Equal(t, 2, index.GetOperationTagsCount())
assert.Equal(t, 2, index.GetGlobalLinksCount())
assert.Equal(t, 0, index.GetComponentParameterCount())
assert.Equal(t, 2, index.GetOperationsParameterCount())
assert.Equal(t, 1, index.GetInlineDuplicateParamCount())
assert.Equal(t, 1, index.GetInlineUniqueParamCount())
assert.Len(t, index.refErrors, 6)
}
func TestTagsNoDescription(t *testing.T) {
yml := `tags:
- name: one
- name: two
- three: three`
var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
assert.Equal(t, 3, index.GetGlobalTagsCount())
}
func TestGlobalCallbacksNoIndexTest(t *testing.T) {
idx := new(SpecIndex)
assert.Equal(t, -1, idx.GetGlobalCallbacksCount())
}
func TestMultipleCallbacksPerOperationVerb(t *testing.T) {
yml := `components:
callbacks:
callbackA:
"{$request.query.queryUrl}":
post:
description: callbackAPost
get:
description: callbackAGet
callbackB:
"{$request.query.queryUrl}":
post:
description: callbackBPost
get:
description: callbackBGet
paths:
/pb33f/arriving-soon:
post:
callbacks:
callbackA:
$ref: '#/components/callbacks/CallbackA'
callbackB:
$ref: '#/components/callbacks/CallbackB'
get:
callbacks:
callbackB:
$ref: '#/components/callbacks/CallbackB'
callbackA:
$ref: '#/components/callbacks/CallbackA'`
var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
assert.Equal(t, 4, index.GetGlobalCallbacksCount())
}
func TestSpecIndex_ExtractComponentsFromRefs(t *testing.T) {
yml := `components:
schemas:
pizza:
properties:
something:
$ref: '#/components/\schemas/\something'
something:
description: something`
var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
assert.Len(t, index.GetReferenceIndexErrors(), 1)
}
func TestSpecIndex_FindComponent_WithACrazyAssPath(t *testing.T) {
yml := `paths:
/crazy/ass/references:
get:
parameters:
- name: a param
schema:
type: string
description: Show information about one architecture.
responses:
"200":
content:
application/xml; charset=utf-8:
schema:
example:
name: x86_64
description: OK. The request has succeeded.
"404":
content:
application/xml; charset=utf-8:
example:
code: unknown_architecture
summary: "Architecture does not exist: x999"
schema:
$ref: "#/paths/~1crazy~1ass~1references/get/parameters/0"
"400":
content:
application/xml; charset=utf-8:
example:
code: unknown_architecture
summary: "Architecture does not exist: x999"
schema:
$ref: "#/paths/~1crazy~1ass~1references/get/responses/404/content/application~1xml;%20charset=utf-8/schema"
description: Not Found.`
var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
assert.Equal(t, "#/paths/~1crazy~1ass~1references/get/parameters/0",
index.FindComponent("#/paths/~1crazy~1ass~1references/get/responses/404/content/application~1xml;%20charset=utf-8/schema", nil).Node.Content[1].Value)
assert.Equal(t, "a param",
index.FindComponent("#/paths/~1crazy~1ass~1references/get/parameters/0", nil).Node.Content[1].Value)
}
func TestSpecIndex_FindComponent(t *testing.T) {
yml := `components:
schemas:
pizza:
properties:
something:
$ref: '#/components/schemas/something'
something:
description: something`
var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
assert.Nil(t, index.FindComponent("I-do-not-exist", nil))
}
func TestSpecIndex_TestPathsNodeAsArray(t *testing.T) {
yml := `components:
schemas:
pizza:
properties:
something:
$ref: '#/components/schemas/something'
something:
description: something`
var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
assert.Nil(t, index.lookupRolodex(nil))
}
func TestSpecIndex_CheckBadURLRefNoRemoteAllowed(t *testing.T) {
yml := `openapi: 3.1.0
paths:
/cakes:
post:
parameters:
- $ref: 'httpsss://badurl'`
var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode)
c := CreateClosedAPIIndexConfig()
idx := NewSpecIndexWithConfig(&rootNode, c)
assert.Len(t, idx.refErrors, 1)
}
func TestSpecIndex_CheckIndexDiscoversNoComponentLocalFileReference(t *testing.T) {
c := []byte("name: time for coffee")
_ = os.WriteFile("coffee-time.yaml", c, 0o664)
defer os.Remove("coffee-time.yaml")
// create a new config that allows local and remote to be mixed up.
cf := CreateOpenAPIIndexConfig()
cf.AvoidCircularReferenceCheck = true
cf.BasePath = "."
// create a new rolodex
rolo := NewRolodex(cf)
// configure the local filesystem.
fsCfg := LocalFSConfig{
BaseDirectory: cf.BasePath,
FileFilters: []string{"coffee-time.yaml"},
DirFS: os.DirFS(cf.BasePath),
}
// create a new local filesystem.
fileFS, err := NewLocalFSWithConfig(&fsCfg)
assert.NoError(t, err)
yml := `openapi: 3.0.3
paths:
/cakes:
post:
parameters:
- $ref: 'coffee-time.yaml'`
var coffee yaml.Node
_ = yaml.Unmarshal([]byte(yml), &coffee)
// set the rolodex root node to the root node of the spec.
rolo.SetRootNode(&coffee)
rolo.AddLocalFS(cf.BasePath, fileFS)
rErr := rolo.IndexTheRolodex()
assert.NoError(t, rErr)
index := rolo.GetRootIndex()
assert.NotNil(t, index.GetAllParametersFromOperations()["/cakes"]["post"]["coffee-time.yaml"][0].Node)
}
func TestSpecIndex_lookupFileReference_MultiRes(t *testing.T) {
embie := []byte("naughty:\n - puppy: dog\n - puppy: naughty\npuppy:\n - naughty: puppy")
_ = os.WriteFile("embie.yaml", embie, 0o664)
defer os.Remove("embie.yaml")
// create a new config that allows local and remote to be mixed up.
cf := CreateOpenAPIIndexConfig()
cf.AvoidBuildIndex = true
cf.AvoidCircularReferenceCheck = true
cf.BasePath = "."
// create a new rolodex
rolo := NewRolodex(cf)
var myPuppy yaml.Node
_ = yaml.Unmarshal(embie, &myPuppy)
// set the rolodex root node to the root node of the spec.
rolo.SetRootNode(&myPuppy)
// configure the local filesystem.
fsCfg := LocalFSConfig{
BaseDirectory: cf.BasePath,
FileFilters: []string{"embie.yaml"},
DirFS: os.DirFS(cf.BasePath),
}
// create a new local filesystem.
fileFS, err := NewLocalFSWithConfig(&fsCfg)
assert.NoError(t, err)
rolo.AddLocalFS(cf.BasePath, fileFS)
rErr := rolo.IndexTheRolodex()
assert.NoError(t, rErr)
embieRoloFile, fErr := rolo.Open("embie.yaml")
assert.NoError(t, fErr)
assert.NotNil(t, embieRoloFile)
index := rolo.GetRootIndex()
//index.seenRemoteSources = make(map[string]*yaml.Node)
absoluteRef, _ := filepath.Abs("embie.yaml#/naughty")
fRef, _ := index.SearchIndexForReference(absoluteRef)
assert.NotNil(t, fRef)
}
func TestSpecIndex_lookupFileReference(t *testing.T) {
pup := []byte("good:\n - puppy: dog\n - puppy: forever-more")
var myPuppy yaml.Node
_ = yaml.Unmarshal(pup, &myPuppy)
_ = os.WriteFile("fox.yaml", pup, 0o664)
defer os.Remove("fox.yaml")
// create a new config that allows local and remote to be mixed up.
cf := CreateOpenAPIIndexConfig()
cf.AvoidBuildIndex = true
cf.AvoidCircularReferenceCheck = true
cf.BasePath = "."
// create a new rolodex
rolo := NewRolodex(cf)
// set the rolodex root node to the root node of the spec.
rolo.SetRootNode(&myPuppy)
// configure the local filesystem.
fsCfg := LocalFSConfig{
BaseDirectory: cf.BasePath,
FileFilters: []string{"fox.yaml"},
DirFS: os.DirFS(cf.BasePath),
}
// create a new local filesystem.
fileFS, err := NewLocalFSWithConfig(&fsCfg)
assert.NoError(t, err)
rolo.AddLocalFS(cf.BasePath, fileFS)
rErr := rolo.IndexTheRolodex()
assert.NoError(t, rErr)
fox, fErr := rolo.Open("fox.yaml")
assert.NoError(t, fErr)
assert.Equal(t, "fox.yaml", fox.Name())
assert.Equal(t, "good:\n - puppy: dog\n - puppy: forever-more", string(fox.GetContent()))
}
func TestSpecIndex_parameterReferencesHavePaths(t *testing.T) {
_ = os.WriteFile("paramour.yaml", []byte(`components:
parameters:
param3:
name: param3
in: query
schema:
type: string`), 0o664)
defer os.Remove("paramour.yaml")
// 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 = "."
yml := `paths:
/:
parameters:
- $ref: '#/components/parameters/param1'
- $ref: '#/components/parameters/param1'
- $ref: 'paramour.yaml#/components/parameters/param3'
get:
parameters:
- $ref: '#/components/parameters/param2'
- $ref: '#/components/parameters/param2'
- name: test
in: query
schema:
type: string
components:
parameters:
param1:
name: param1
in: query
schema:
type: string
param2:
name: param2
in: query
schema:
type: string`
var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode)
// create a new rolodex
rolo := NewRolodex(cf)
// set the rolodex root node to the root node of the spec.
rolo.SetRootNode(&rootNode)
// configure the local filesystem.
fsCfg := LocalFSConfig{
BaseDirectory: cf.BasePath,
FileFilters: []string{"paramour.yaml"},
DirFS: os.DirFS(cf.BasePath),
}
// create a new local filesystem.
fileFS, err := NewLocalFSWithConfig(&fsCfg)
assert.NoError(t, err)
// add file system
rolo.AddLocalFS(cf.BasePath, fileFS)
// index the rolodex.
indexedErr := rolo.IndexTheRolodex()
assert.NoError(t, indexedErr)
rolo.BuildIndexes()
index := rolo.GetRootIndex()
params := index.GetAllParametersFromOperations()
if assert.Contains(t, params, "/") {
if assert.Contains(t, params["/"], "top") {
if assert.Contains(t, params["/"]["top"], "#/components/parameters/param1") {
assert.Equal(t, "$.components.parameters.param1", params["/"]["top"]["#/components/parameters/param1"][0].Path)
}
if assert.Contains(t, params["/"]["top"], "paramour.yaml#/components/parameters/param3") {
assert.Equal(t, "$.components.parameters.param3", params["/"]["top"]["paramour.yaml#/components/parameters/param3"][0].Path)
}
}
if assert.Contains(t, params["/"], "get") {
if assert.Contains(t, params["/"]["get"], "#/components/parameters/param2") {
assert.Equal(t, "$.components.parameters.param2", params["/"]["get"]["#/components/parameters/param2"][0].Path)
}
if assert.Contains(t, params["/"]["get"], "test") {
assert.Equal(t, "$.paths./.get.parameters[2]", params["/"]["get"]["test"][0].Path)
}
}
}
}
func TestSpecIndex_serverReferencesHaveParentNodesAndPaths(t *testing.T) {
yml := `servers:
- url: https://api.example.com/v1
paths:
/:
servers:
- url: https://api.example.com/v2
get:
servers:
- url: https://api.example.com/v3`
var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
rootServers := index.GetAllRootServers()
for i, server := range rootServers {
assert.NotNil(t, server.ParentNode)
assert.Equal(t, fmt.Sprintf("$.servers[%d]", i), server.Path)
}
opServers := index.GetAllOperationsServers()
for path, ops := range opServers {
for op, servers := range ops {
for i, server := range servers {
assert.NotNil(t, server.ParentNode)
opPath := fmt.Sprintf(".%s", op)
if op == "top" {
opPath = ""
}
assert.Equal(t, fmt.Sprintf("$.paths.%s%s.servers[%d]", path, opPath, i), server.Path)
}
}
}
}
func TestSpecIndex_schemaComponentsHaveParentsAndPaths(t *testing.T) {
yml := `components:
schemas:
Pet:
type: object
Dog:
type: object`
var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
schemas := index.GetAllSchemas()
for _, schema := range schemas {
assert.NotNil(t, schema.ParentNode)
assert.Equal(t, fmt.Sprintf("$.components.schemas.%s", schema.Name), schema.Path)
}
}
func TestSpecIndex_ParamsWithDuplicateNamesButUniqueInTypes(t *testing.T) {
yml := `openapi: 3.1.0
info:
title: Test
version: 0.0.1
servers:
- url: http://localhost:35123
paths:
/example/{action}:
parameters:
- name: fastAction
in: path
required: true
schema:
type: string
- name: fastAction
in: query
required: true
schema:
type: string
get:
operationId: example
parameters:
- name: action
in: path
required: true
schema:
type: string
- name: action
in: query
required: true
schema:
type: string
responses:
"200":
description: OK`
var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode)
idx := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
assert.Len(t, idx.paramAllRefs, 4)
assert.Len(t, idx.paramInlineDuplicateNames, 2)
assert.Len(t, idx.operationParamErrors, 0)
assert.Len(t, idx.refErrors, 0)
}
func TestSpecIndex_ParamsWithDuplicateNamesAndSameInTypes(t *testing.T) {
yml := `openapi: 3.1.0
info:
title: Test
version: 0.0.1
servers:
- url: http://localhost:35123
paths:
/example/{action}:
parameters:
- name: fastAction
in: path
required: true
schema:
type: string
- name: fastAction
in: path
required: true
schema:
type: string
get:
operationId: example
parameters:
- name: action
in: path
required: true
schema:
type: string
- name: action
in: query
required: true
schema:
type: string
responses:
"200":
description: OK`
var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode)
idx := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
assert.Len(t, idx.paramAllRefs, 3)
assert.Len(t, idx.paramInlineDuplicateNames, 2)
assert.Len(t, idx.operationParamErrors, 1)
assert.Len(t, idx.refErrors, 0)
}
func TestSpecIndex_foundObjectsWithProperties(t *testing.T) {
yml := `paths:
/test:
get:
responses:
'200':
description: OK
content:
application/json:
type: object
properties:
test:
type: string
components:
schemas:
test:
type: object
properties:
test:
type: string
test2:
type: [object, null]
properties:
test:
type: string
test3:
type: object
additionalProperties: true`
var rootNode yaml.Node
yaml.Unmarshal([]byte(yml), &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
objects := index.GetAllObjectsWithProperties()
assert.Len(t, objects, 3)
}
// Example of how to load in an OpenAPI Specification and index it.
func ExampleNewSpecIndex() {
// define a rootNode to hold our raw spec AST.
var rootNode yaml.Node
// load in the stripe OpenAPI specification into bytes (it's pretty meaty)
stripeSpec, _ := os.ReadFile("../test_specs/stripe.yaml")
// unmarshal spec into our rootNode
_ = yaml.Unmarshal(stripeSpec, &rootNode)
// create a new specification index.
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
// print out some statistics
fmt.Printf("There are %d references\n"+
"%d paths\n"+
"%d operations\n"+
"%d component schemas\n"+
"%d reference schemas\n"+
"%d inline schemas\n"+
"%d inline schemas that are objects or arrays\n"+
"%d total schemas\n"+
"%d enums\n"+
"%d polymorphic references",
len(index.GetAllCombinedReferences()),
len(index.GetAllPaths()),
index.GetOperationCount(),
len(index.GetAllComponentSchemas()),
len(index.GetAllReferenceSchemas()),
len(index.GetAllInlineSchemas()),
len(index.GetAllInlineSchemaObjects()),
len(index.GetAllSchemas()),
len(index.GetAllEnums()),
len(index.GetPolyOneOfReferences())+len(index.GetPolyAnyOfReferences()))
// Output: There are 537 references
// 246 paths
// 402 operations
// 537 component schemas
// 1972 reference schemas
// 11749 inline schemas
// 2612 inline schemas that are objects or arrays
// 14258 total schemas
// 1516 enums
// 828 polymorphic references
}
func TestSpecIndex_GetAllPathsHavePathAndParent(t *testing.T) {
yml := `openapi: 3.1.0
info:
title: Test
version: 0.0.1
servers:
- url: http://localhost:35123
paths:
/test:
get:
responses:
"200":
description: OK
post:
responses:
"200":
description: OK
/test2:
delete:
responses:
"200":
description: OK
put:
responses:
"200":
description: OK`
var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode)
idx := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
paths := idx.GetAllPaths()
assert.Equal(t, "$.paths./test.get", paths["/test"]["get"].Path)
assert.Equal(t, 9, paths["/test"]["get"].ParentNode.Line)
assert.Equal(t, "$.paths./test.post", paths["/test"]["post"].Path)
assert.Equal(t, 13, paths["/test"]["post"].ParentNode.Line)
assert.Equal(t, "$.paths./test2.delete", paths["/test2"]["delete"].Path)
assert.Equal(t, 18, paths["/test2"]["delete"].ParentNode.Line)
assert.Equal(t, "$.paths./test2.put", paths["/test2"]["put"].Path)
assert.Equal(t, 22, paths["/test2"]["put"].ParentNode.Line)
}