bashing through usecases and updating tests as we go.

so many things that can go wrong. have to catch them all.

Signed-off-by: quobix <dave@quobix.com>
This commit is contained in:
quobix
2023-10-19 15:18:33 -04:00
parent 054103b733
commit b295e8fd5c
11 changed files with 404 additions and 181 deletions

View File

@@ -189,6 +189,7 @@ func (index *SpecIndex) ExtractRefs(node, parent *yaml.Node, seenPath []string,
var p string var p string
uri := strings.Split(value, "#/") uri := strings.Split(value, "#/")
if strings.HasPrefix(value, "http") || filepath.IsAbs(value) { if strings.HasPrefix(value, "http") || filepath.IsAbs(value) {
if len(uri) == 2 { if len(uri) == 2 {
_, p = utils.ConvertComponentIdIntoFriendlyPathSearch(fmt.Sprintf("#/%s", uri[1])) _, p = utils.ConvertComponentIdIntoFriendlyPathSearch(fmt.Sprintf("#/%s", uri[1]))

View File

@@ -62,8 +62,17 @@ func (index *SpecIndex) FindComponent(componentId string, parent *yaml.Node) *Re
} }
} else { } else {
if !strings.Contains(componentId, "#") { if !strings.Contains(componentId, "#") {
// does it contain a file extension?
fileExt := filepath.Ext(componentId)
if fileExt != "" {
return index.lookupRolodex(uri) return index.lookupRolodex(uri)
} }
// root search
return index.FindComponentInRoot(componentId)
}
return index.FindComponentInRoot(fmt.Sprintf("#/%s", uri[0])) return index.FindComponentInRoot(fmt.Sprintf("#/%s", uri[0]))
} }
@@ -368,6 +377,7 @@ func (index *SpecIndex) lookupRolodex(uri []string) *Reference {
if index.specAbsolutePath != "" { if index.specAbsolutePath != "" {
if index.config.BaseURL != nil { if index.config.BaseURL != nil {
// consider the file remote.
//if strings.Contains(file, "../../") { //if strings.Contains(file, "../../") {
// extract the base path from the specAbsolutePath for this index. // extract the base path from the specAbsolutePath for this index.
@@ -384,8 +394,11 @@ func (index *SpecIndex) lookupRolodex(uri []string) *Reference {
//absoluteFileLocation = loc //absoluteFileLocation = loc
} else { } else {
panic("nooooooo")
absoluteFileLocation, _ = filepath.Abs(filepath.Join(index.config.BaseURL.Path, file)) // consider the file local
dir := filepath.Dir(index.config.SpecAbsolutePath)
absoluteFileLocation, _ = filepath.Abs(filepath.Join(dir, file))
} }
} else { } else {
absoluteFileLocation = file absoluteFileLocation = file

View File

@@ -43,7 +43,7 @@ func TestSpecIndex_CheckCircularIndex(t *testing.T) {
_ = yaml.Unmarshal([]byte(yml), &rootNode) _ = yaml.Unmarshal([]byte(yml), &rootNode)
cf := CreateOpenAPIIndexConfig() cf := CreateOpenAPIIndexConfig()
cf.AvoidBuildIndex = true cf.AvoidCircularReferenceCheck = true
cf.BasePath = "../test_specs" cf.BasePath = "../test_specs"
rolo := NewRolodex(cf) rolo := NewRolodex(cf)

View File

@@ -268,8 +268,8 @@ type SpecIndex struct {
enumCount int enumCount int
descriptionCount int descriptionCount int
summaryCount int summaryCount int
seenRemoteSources map[string]*yaml.Node //seenRemoteSources map[string]*yaml.Node
seenLocalSources map[string]*yaml.Node //seenLocalSources map[string]*yaml.Node
refLock sync.Mutex refLock sync.Mutex
componentLock sync.RWMutex componentLock sync.RWMutex
errorLock sync.RWMutex errorLock sync.RWMutex

View File

@@ -82,8 +82,8 @@ func boostrapIndexCollections(rootNode *yaml.Node, index *SpecIndex) {
index.securityRequirementRefs = make(map[string]map[string][]*Reference) index.securityRequirementRefs = make(map[string]map[string][]*Reference)
index.polymorphicRefs = make(map[string]*Reference) index.polymorphicRefs = make(map[string]*Reference)
index.refsWithSiblings = make(map[string]Reference) index.refsWithSiblings = make(map[string]Reference)
index.seenRemoteSources = make(map[string]*yaml.Node) //index.seenRemoteSources = make(map[string]*yaml.Node)
index.seenLocalSources = make(map[string]*yaml.Node) //index.seenLocalSources = make(map[string]*yaml.Node)
index.opServersRefs = make(map[string]map[string][]*Reference) index.opServersRefs = make(map[string]map[string][]*Reference)
index.httpClient = &http.Client{Timeout: time.Duration(5) * time.Second} index.httpClient = &http.Client{Timeout: time.Duration(5) * time.Second}
index.componentIndexChan = make(chan bool) index.componentIndexChan = make(chan bool)

View File

@@ -258,10 +258,6 @@ func (r *Rolodex) IndexTheRolodex() error {
return nil return nil
} }
// disable index building, it will need to be run after the rolodex indexed
// at a high level.
r.indexConfig.AvoidBuildIndex = true
var caughtErrors []error var caughtErrors []error
var indexBuildQueue []*SpecIndex var indexBuildQueue []*SpecIndex
@@ -373,6 +369,19 @@ func (r *Rolodex) IndexTheRolodex() error {
if r.rootNode != nil { if r.rootNode != nil {
// if there is a base path, then we need to set the root spec config to point to a theoretical root.yaml
// which does not exist, but is used to formulate the absolute path to root references correctly.
if r.indexConfig.BasePath != "" && r.indexConfig.BaseURL == nil {
basePath := r.indexConfig.BasePath
if !filepath.IsAbs(basePath) {
basePath, _ = filepath.Abs(basePath)
}
r.indexConfig.SpecAbsolutePath = filepath.Join(basePath, "root.yaml")
}
// todo: variation with no base path, but a base URL.
index := NewSpecIndexWithConfig(r.rootNode, r.indexConfig) index := NewSpecIndexWithConfig(r.rootNode, r.indexConfig)
resolver := NewResolver(index) resolver := NewResolver(index)
if r.indexConfig.IgnoreArrayCircularReferences { if r.indexConfig.IgnoreArrayCircularReferences {
@@ -382,9 +391,7 @@ func (r *Rolodex) IndexTheRolodex() error {
resolver.IgnorePolymorphicCircularReferences() resolver.IgnorePolymorphicCircularReferences()
} }
if !r.indexConfig.AvoidBuildIndex {
index.BuildIndex() index.BuildIndex()
}
if !r.indexConfig.AvoidCircularReferenceCheck { if !r.indexConfig.AvoidCircularReferenceCheck {
resolvingErrors := resolver.CheckForCircularReferences() resolvingErrors := resolver.CheckForCircularReferences()
@@ -393,10 +400,14 @@ func (r *Rolodex) IndexTheRolodex() error {
} }
} }
r.rootIndex = index r.rootIndex = index
if len(index.refErrors) > 0 {
caughtErrors = append(caughtErrors, index.refErrors...)
}
} }
r.indexingDuration = time.Since(started) r.indexingDuration = time.Since(started)
r.indexed = true r.indexed = true
r.caughtErrors = caughtErrors r.caughtErrors = caughtErrors
r.built = true
return errors.Join(caughtErrors...) return errors.Join(caughtErrors...)
} }

View File

@@ -17,6 +17,7 @@ import (
) )
type LocalFS struct { type LocalFS struct {
indexConfig *SpecIndexConfig
entryPointDirectory string entryPointDirectory string
baseDirectory string baseDirectory string
Files map[string]RolodexFile Files map[string]RolodexFile
@@ -28,7 +29,18 @@ func (l *LocalFS) GetFiles() map[string]RolodexFile {
return l.Files return l.Files
} }
func (l *LocalFS) GetErrors() []error {
return l.readingErrors
}
func (l *LocalFS) Open(name string) (fs.File, error) { func (l *LocalFS) Open(name string) (fs.File, error) {
if l.indexConfig != nil && !l.indexConfig.AllowFileLookup {
return nil, &fs.PathError{Op: "open", Path: name,
Err: fmt.Errorf("file lookup for '%s' not allowed, set the index configuration "+
"to AllowFileLookup to be true", name)}
}
if !filepath.IsAbs(name) { if !filepath.IsAbs(name) {
var absErr error var absErr error
name, absErr = filepath.Abs(filepath.Join(l.baseDirectory, name)) name, absErr = filepath.Abs(filepath.Join(l.baseDirectory, name))

View File

@@ -27,6 +27,7 @@ type RemoteFS struct {
rootURLParsed *url.URL rootURLParsed *url.URL
RemoteHandlerFunc RemoteURLHandler RemoteHandlerFunc RemoteURLHandler
Files syncmap.Map Files syncmap.Map
ProcessingFiles syncmap.Map
FetchTime int64 FetchTime int64
FetchChannel chan *RemoteFile FetchChannel chan *RemoteFile
remoteWg sync.WaitGroup remoteWg sync.WaitGroup
@@ -235,6 +236,10 @@ func (i *RemoteFS) GetFiles() map[string]RolodexFile {
return files return files
} }
func (i *RemoteFS) GetErrors() []error {
return i.remoteErrors
}
func (i *RemoteFS) seekRelatives(file *RemoteFile) { func (i *RemoteFS) seekRelatives(file *RemoteFile) {
extractedRefs := ExtractRefs(string(file.data)) extractedRefs := ExtractRefs(string(file.data))
@@ -288,6 +293,11 @@ func (i *RemoteFS) seekRelatives(file *RemoteFile) {
func (i *RemoteFS) Open(remoteURL string) (fs.File, error) { func (i *RemoteFS) Open(remoteURL string) (fs.File, error) {
if i.indexConfig != nil && !i.indexConfig.AllowRemoteLookup {
return nil, fmt.Errorf("remote lookup for '%s' is not allowed, please set "+
"AllowRemoteLookup to true as part of the index configuration", remoteURL)
}
remoteParsedURL, err := url.Parse(remoteURL) remoteParsedURL, err := url.Parse(remoteURL)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -298,6 +308,20 @@ func (i *RemoteFS) Open(remoteURL string) (fs.File, error) {
return r.(*RemoteFile), nil return r.(*RemoteFile), nil
} }
// if we're processing, we need to block and wait for the file to be processed
// try path first
if _, ok := i.ProcessingFiles.Load(remoteParsedURL.Path); ok {
i.logger.Debug("waiting for existing fetch to complete", "file", remoteURL, "remoteURL", remoteParsedURL.String())
for {
if wf, ko := i.Files.Load(remoteParsedURL.Path); ko {
return wf.(*RemoteFile), nil
}
}
}
// add to processing
i.ProcessingFiles.Store(remoteParsedURL.Path, true)
fileExt := ExtractFileType(remoteParsedURL.Path) fileExt := ExtractFileType(remoteParsedURL.Path)
if fileExt == UNSUPPORTED { if fileExt == UNSUPPORTED {
@@ -314,7 +338,7 @@ func (i *RemoteFS) Open(remoteURL string) (fs.File, error) {
} }
} }
i.logger.Debug("Loading remote file", "file", remoteURL, "remoteURL", remoteParsedURL.String()) i.logger.Debug("loading remote file", "file", remoteURL, "remoteURL", remoteParsedURL.String())
// no handler func? use the default client. // no handler func? use the default client.
if i.RemoteHandlerFunc == nil { if i.RemoteHandlerFunc == nil {
@@ -323,20 +347,32 @@ func (i *RemoteFS) Open(remoteURL string) (fs.File, error) {
response, clientErr := i.RemoteHandlerFunc(remoteParsedURL.String()) response, clientErr := i.RemoteHandlerFunc(remoteParsedURL.String())
if clientErr != nil { if clientErr != nil {
i.remoteErrors = append(i.remoteErrors, clientErr)
// remove from processing
i.ProcessingFiles.Delete(remoteParsedURL.Path)
if response != nil { if response != nil {
i.logger.Error("client error", "error", clientErr, "status", response.StatusCode) i.logger.Error("client error", "error", clientErr, "status", response.StatusCode)
} else { } else {
i.logger.Error("no response for request", "error", clientErr.Error()) i.logger.Error("client error, empty body", "error", clientErr.Error())
} }
return nil, clientErr return nil, clientErr
} }
responseBytes, readError := io.ReadAll(response.Body) responseBytes, readError := io.ReadAll(response.Body)
if readError != nil { if readError != nil {
// remove from processing
i.ProcessingFiles.Delete(remoteParsedURL.Path)
return nil, readError return nil, readError
} }
if response.StatusCode >= 400 { if response.StatusCode >= 400 {
// remove from processing
i.ProcessingFiles.Delete(remoteParsedURL.Path)
i.logger.Error("Unable to fetch remote document", i.logger.Error("Unable to fetch remote document",
"file", remoteParsedURL.Path, "status", response.StatusCode, "resp", string(responseBytes)) "file", remoteParsedURL.Path, "status", response.StatusCode, "resp", string(responseBytes))
return nil, fmt.Errorf("unable to fetch remote document: %s", string(responseBytes)) return nil, fmt.Errorf("unable to fetch remote document: %s", string(responseBytes))
@@ -344,6 +380,8 @@ func (i *RemoteFS) Open(remoteURL string) (fs.File, error) {
absolutePath, pathErr := filepath.Abs(remoteParsedURL.Path) absolutePath, pathErr := filepath.Abs(remoteParsedURL.Path)
if pathErr != nil { if pathErr != nil {
// remove from processing
i.ProcessingFiles.Delete(remoteParsedURL.Path)
return nil, pathErr return nil, pathErr
} }
@@ -394,11 +432,16 @@ func (i *RemoteFS) Open(remoteURL string) (fs.File, error) {
i.Files.Store(absolutePath, remoteFile) i.Files.Store(absolutePath, remoteFile)
if len(remoteFile.data) > 0 {
i.logger.Debug("successfully loaded file", "file", absolutePath) i.logger.Debug("successfully loaded file", "file", absolutePath)
}
i.seekRelatives(remoteFile) i.seekRelatives(remoteFile)
idx.BuildIndex() idx.BuildIndex()
// remove from processing
i.ProcessingFiles.Delete(remoteParsedURL.Path)
if !i.remoteRunning { if !i.remoteRunning {
return remoteFile, errors.Join(i.remoteErrors...) return remoteFile, errors.Join(i.remoteErrors...)
} else { } else {

View File

@@ -24,12 +24,27 @@ func (index *SpecIndex) SearchIndexForReferenceByReference(fullRef *Reference) *
if strings.HasPrefix(uri[0], "http") { if strings.HasPrefix(uri[0], "http") {
roloLookup = fullRef.FullDefinition roloLookup = fullRef.FullDefinition
} else { } else {
if filepath.IsAbs(uri[0]) {
roloLookup = uri[0]
} else {
if filepath.Ext(absPath) != "" {
absPath = filepath.Dir(absPath)
}
roloLookup, _ = filepath.Abs(filepath.Join(absPath, uri[0])) roloLookup, _ = filepath.Abs(filepath.Join(absPath, uri[0]))
} }
} }
}
ref = fmt.Sprintf("#/%s", uri[1]) ref = fmt.Sprintf("#/%s", uri[1])
} else { } else {
if filepath.IsAbs(uri[0]) {
roloLookup = uri[0]
} else {
if filepath.Ext(absPath) != "" {
absPath = filepath.Dir(absPath)
}
roloLookup, _ = filepath.Abs(filepath.Join(absPath, uri[0])) roloLookup, _ = filepath.Abs(filepath.Join(absPath, uri[0]))
}
ref = uri[0] ref = uri[0]
} }
@@ -65,6 +80,15 @@ func (index *SpecIndex) SearchIndexForReferenceByReference(fullRef *Reference) *
return s return s
} }
} }
// does component exist in the root?
node, _ := rFile.GetContentAsYAMLNode()
if node != nil {
found := idx.FindComponent(ref, node)
if found != nil {
return found
}
}
} }
} }

View File

@@ -430,10 +430,10 @@ func (index *SpecIndex) GetAllOperationsServers() map[string]map[string][]*Refer
return index.opServersRefs return index.opServersRefs
} }
// GetAllExternalIndexes will return all indexes for external documents //// GetAllExternalIndexes will return all indexes for external documents
func (index *SpecIndex) GetAllExternalIndexes() map[string]*SpecIndex { //func (index *SpecIndex) GetAllExternalIndexes() map[string]*SpecIndex {
return index.externalSpecIndex // return index.externalSpecIndex
} //}
// SetAllowCircularReferenceResolving will flip a bit that can be used by any consumers to determine if they want // SetAllowCircularReferenceResolving will flip a bit that can be used by any consumers to determine if they want
// to allow or disallow circular references to be resolved or visited // to allow or disallow circular references to be resolved or visited

View File

@@ -5,6 +5,7 @@ package index
import ( import (
"fmt" "fmt"
"github.com/pb33f/libopenapi/utils"
"log" "log"
"log/slog" "log/slog"
"net/http" "net/http"
@@ -64,7 +65,6 @@ func TestSpecIndex_ExtractRefsStripe(t *testing.T) {
assert.Len(t, index.GetRefsByLine(), 537) assert.Len(t, index.GetRefsByLine(), 537)
assert.Len(t, index.GetLinesWithReferences(), 1972) assert.Len(t, index.GetLinesWithReferences(), 1972)
assert.Len(t, index.GetAllExternalDocuments(), 0) assert.Len(t, index.GetAllExternalDocuments(), 0)
assert.Len(t, index.GetAllExternalIndexes(), 0)
} }
func TestSpecIndex_Asana(t *testing.T) { func TestSpecIndex_Asana(t *testing.T) {
@@ -97,7 +97,7 @@ func TestSpecIndex_DigitalOcean(t *testing.T) {
location := "https://raw.githubusercontent.com/digitalocean/openapi/main/specification" location := "https://raw.githubusercontent.com/digitalocean/openapi/main/specification"
baseURL, _ := url.Parse(location) baseURL, _ := url.Parse(location)
// create a new config that allows local and remote to be mixed up. // create a new config that allows remote lookups.
cf := &SpecIndexConfig{} cf := &SpecIndexConfig{}
cf.AvoidBuildIndex = true cf.AvoidBuildIndex = true
cf.AllowRemoteLookup = true cf.AllowRemoteLookup = true
@@ -121,7 +121,6 @@ func TestSpecIndex_DigitalOcean(t *testing.T) {
// create a handler that uses an env variable to capture any GITHUB_TOKEN in the OS ENV // 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. // and inject it into the request header, so this does not fail when running lots of local tests.
if os.Getenv("GITHUB_TOKEN") != "" { if os.Getenv("GITHUB_TOKEN") != "" {
client := &http.Client{ client := &http.Client{
Timeout: time.Second * 60, Timeout: time.Second * 60,
} }
@@ -130,7 +129,6 @@ func TestSpecIndex_DigitalOcean(t *testing.T) {
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", os.Getenv("GITHUB_TOKEN"))) request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", os.Getenv("GITHUB_TOKEN")))
return client.Do(request) return client.Do(request)
}) })
} }
// add remote filesystem // add remote filesystem
@@ -143,19 +141,7 @@ func TestSpecIndex_DigitalOcean(t *testing.T) {
files := remoteFS.GetFiles() files := remoteFS.GetFiles()
fileLen := len(files) fileLen := len(files)
assert.Equal(t, 1646, fileLen) assert.Equal(t, 1646, fileLen)
assert.Len(t, remoteFS.GetErrors(), 0)
//
//
//
//
//
//assert.NoError(t, indexedErr)
//
//index := rolo.GetRootIndex()
//rolo.CheckForCircularReferences()
//
//assert.Len(t, index.GetAllExternalIndexes(), 291)
//assert.NotNil(t, index)
} }
func TestSpecIndex_DigitalOcean_FullCheckoutLocalResolve(t *testing.T) { func TestSpecIndex_DigitalOcean_FullCheckoutLocalResolve(t *testing.T) {
@@ -163,69 +149,153 @@ func TestSpecIndex_DigitalOcean_FullCheckoutLocalResolve(t *testing.T) {
tmp, _ := os.MkdirTemp("", "openapi") tmp, _ := os.MkdirTemp("", "openapi")
cmd := exec.Command("git", "clone", "https://github.com/digitalocean/openapi", tmp) cmd := exec.Command("git", "clone", "https://github.com/digitalocean/openapi", tmp)
defer os.RemoveAll(filepath.Join(tmp, "openapi")) defer os.RemoveAll(filepath.Join(tmp, "openapi"))
err := cmd.Run() err := cmd.Run()
if err != nil { if err != nil {
log.Fatalf("cmd.Run() failed with %s\n", err) log.Fatalf("cmd.Run() failed with %s\n", err)
} }
spec, _ := filepath.Abs(filepath.Join(tmp, "specification", "DigitalOcean-public.v2.yaml")) spec, _ := filepath.Abs(filepath.Join(tmp, "specification", "DigitalOcean-public.v2.yaml"))
doLocal, _ := os.ReadFile(spec) doLocal, _ := os.ReadFile(spec)
var rootNode yaml.Node var rootNode yaml.Node
_ = yaml.Unmarshal(doLocal, &rootNode) _ = yaml.Unmarshal(doLocal, &rootNode)
config := CreateOpenAPIIndexConfig() basePath := filepath.Join(tmp, "specification")
config.BasePath = filepath.Join(tmp, "specification")
index := NewSpecIndexWithConfig(&rootNode, config) // 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 = basePath
// 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),
}
// create a new local filesystem.
fileFS, fsErr := NewLocalFSWithConfig(&fsCfg)
assert.NoError(t, fsErr)
files := fileFS.GetFiles()
fileLen := len(files)
assert.Equal(t, 1684, fileLen)
rolo.AddLocalFS(basePath, fileFS)
rErr := rolo.IndexTheRolodex()
assert.NoError(t, rErr)
index := rolo.GetRootIndex()
assert.NotNil(t, index) assert.NotNil(t, index)
assert.Len(t, index.GetAllExternalIndexes(), 296)
ref := index.SearchIndexForReference("resources/apps/apps_list_instanceSizes.yml") assert.Len(t, index.GetMappedReferencesSequenced(), 296)
assert.NotNil(t, ref) assert.Len(t, index.GetMappedReferences(), 296)
assert.Equal(t, "operationId", ref.Node.Content[0].Value) assert.Len(t, fileFS.GetErrors(), 0)
ref = index.SearchIndexForReference("examples/ruby/domains_create.yml")
assert.NotNil(t, ref)
assert.Equal(t, "lang", ref.Node.Content[0].Value)
ref = index.SearchIndexForReference("../../shared/responses/server_error.yml")
assert.NotNil(t, ref)
assert.Equal(t, "description", ref.Node.Content[0].Value)
ref = index.SearchIndexForReference("../models/options.yml")
assert.NotNil(t, ref)
} }
func TestSpecIndex_DigitalOcean_LookupsNotAllowed(t *testing.T) { func TestSpecIndex_DigitalOcean_LookupsNotAllowed(t *testing.T) {
asana, _ := os.ReadFile("../test_specs/digitalocean.yaml") do, _ := os.ReadFile("../test_specs/digitalocean.yaml")
var rootNode yaml.Node var rootNode yaml.Node
_ = yaml.Unmarshal(asana, &rootNode) _ = yaml.Unmarshal(do, &rootNode)
baseURL, _ := url.Parse("https://raw.githubusercontent.com/digitalocean/openapi/main/specification") location := "https://raw.githubusercontent.com/digitalocean/openapi/main/specification"
index := NewSpecIndexWithConfig(&rootNode, &SpecIndexConfig{ baseURL, _ := url.Parse(location)
BaseURL: baseURL,
}) // 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. // no lookups allowed, bits have not been set, so there should just be a bunch of errors.
assert.Len(t, index.GetAllExternalIndexes(), 0)
assert.True(t, len(index.GetReferenceIndexErrors()) > 0) assert.True(t, len(index.GetReferenceIndexErrors()) > 0)
} }
func TestSpecIndex_BaseURLError(t *testing.T) { func TestSpecIndex_BaseURLError(t *testing.T) {
asana, _ := os.ReadFile("../test_specs/digitalocean.yaml")
do, _ := os.ReadFile("../test_specs/digitalocean.yaml")
var rootNode yaml.Node var rootNode yaml.Node
_ = yaml.Unmarshal(asana, &rootNode) _ = yaml.Unmarshal(do, &rootNode)
// this should fail because the base url is not a valid url and digital ocean won't be able to resolve location := "https://githerbsandcoffeeandcode.com/fresh/herbs/for/you" // not gonna work bro.
// anything. baseURL, _ := url.Parse(location)
baseURL, _ := url.Parse("https://githerbs.com/fresh/herbs/for/you")
index := NewSpecIndexWithConfig(&rootNode, &SpecIndexConfig{ // create a new config that allows remote lookups.
BaseURL: baseURL, cf := &SpecIndexConfig{}
//AllowRemoteLookup: true, cf.AvoidBuildIndex = true
//AllowFileLookup: 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)
assert.Len(t, index.GetAllExternalIndexes(), 0)
} }
func TestSpecIndex_k8s(t *testing.T) { func TestSpecIndex_k8s(t *testing.T) {
@@ -494,13 +564,9 @@ func test_buildMixedRefServer() *httptest.Server {
bs, _ := os.ReadFile("../test_specs/burgershop.openapi.yaml") bs, _ := os.ReadFile("../test_specs/burgershop.openapi.yaml")
return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.URL.String() == "/daveshanley/vacuum/main/model/test_files/burgershop.openapi.yaml" {
rw.Header().Set("Last-Modified", "Wed, 21 Oct 2015 07:28:00 GMT") rw.Header().Set("Last-Modified", "Wed, 21 Oct 2015 07:28:00 GMT")
_, _ = rw.Write(bs) _, _ = rw.Write(bs)
return
}
_, _ = rw.Write([]byte(`OK`))
})) }))
} }
@@ -516,6 +582,9 @@ func TestSpecIndex_BurgerShopMixedRef(t *testing.T) {
cf.AllowRemoteLookup = true cf.AllowRemoteLookup = true
cf.AvoidCircularReferenceCheck = true cf.AvoidCircularReferenceCheck = true
cf.BasePath = "../test_specs" cf.BasePath = "../test_specs"
cf.Logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelError,
}))
// setting this baseURL will override the base // setting this baseURL will override the base
cf.BaseURL, _ = url.Parse(server.URL) cf.BaseURL, _ = url.Parse(server.URL)
@@ -720,7 +789,7 @@ func TestSpecIndex_FindComponent_WithACrazyAssPath(t *testing.T) {
index.FindComponent("#/paths/~1crazy~1ass~1references/get/parameters/0", nil).Node.Content[1].Value) index.FindComponent("#/paths/~1crazy~1ass~1references/get/parameters/0", nil).Node.Content[1].Value)
} }
func TestSpecIndex_FindComponenth(t *testing.T) { func TestSpecIndex_FindComponent(t *testing.T) {
yml := `components: yml := `components:
schemas: schemas:
pizza: pizza:
@@ -754,34 +823,34 @@ func TestSpecIndex_TestPathsNodeAsArray(t *testing.T) {
assert.Nil(t, index.lookupRolodex(nil)) assert.Nil(t, index.lookupRolodex(nil))
} }
func TestSpecIndex_lookupRemoteReference_SeenSourceSimulation_Error(t *testing.T) { //func TestSpecIndex_lookupRemoteReference_SeenSourceSimulation_Error(t *testing.T) {
index := new(SpecIndex) // index := new(SpecIndex)
index.seenRemoteSources = make(map[string]*yaml.Node) // index.seenRemoteSources = make(map[string]*yaml.Node)
index.seenRemoteSources["https://no-hope-for-a-dope.com"] = &yaml.Node{} // index.seenRemoteSources["https://no-hope-for-a-dope.com"] = &yaml.Node{}
_, _, err := index.lookupRemoteReference("https://no-hope-for-a-dope.com#/$.....#[;]something") // _, _, err := index.lookupRemoteReference("https://no-hope-for-a-dope.com#/$.....#[;]something")
assert.Error(t, err) // assert.Error(t, err)
} //}
func TestSpecIndex_lookupRemoteReference_SeenSourceSimulation_BadFind(t *testing.T) { //func TestSpecIndex_lookupRemoteReference_SeenSourceSimulation_BadFind(t *testing.T) {
index := new(SpecIndex) // index := new(SpecIndex)
index.seenRemoteSources = make(map[string]*yaml.Node) // index.seenRemoteSources = make(map[string]*yaml.Node)
index.seenRemoteSources["https://no-hope-for-a-dope.com"] = &yaml.Node{} // index.seenRemoteSources["https://no-hope-for-a-dope.com"] = &yaml.Node{}
a, b, err := index.lookupRemoteReference("https://no-hope-for-a-dope.com#/hey") // a, b, err := index.lookupRemoteReference("https://no-hope-for-a-dope.com#/hey")
assert.Error(t, err) // assert.Error(t, err)
assert.Nil(t, a) // assert.Nil(t, a)
assert.Nil(t, b) // assert.Nil(t, b)
} //}
// Discovered in issue https://github.com/pb33f/libopenapi/issues/37 // Discovered in issue https://github.com/pb33f/libopenapi/issues/37
func TestSpecIndex_lookupRemoteReference_NoComponent(t *testing.T) { //func TestSpecIndex_lookupRemoteReference_NoComponent(t *testing.T) {
index := new(SpecIndex) // index := new(SpecIndex)
index.seenRemoteSources = make(map[string]*yaml.Node) // index.seenRemoteSources = make(map[string]*yaml.Node)
index.seenRemoteSources["https://api.rest.sh/schemas/ErrorModel.json"] = &yaml.Node{} // index.seenRemoteSources["https://api.rest.sh/schemas/ErrorModel.json"] = &yaml.Node{}
a, b, err := index.lookupRemoteReference("https://api.rest.sh/schemas/ErrorModel.json") // a, b, err := index.lookupRemoteReference("https://api.rest.sh/schemas/ErrorModel.json")
assert.NoError(t, err) // assert.NoError(t, err)
assert.NotNil(t, a) // assert.NotNil(t, a)
assert.NotNil(t, b) // assert.NotNil(t, b)
} //}
// Discovered in issue https://github.com/daveshanley/vacuum/issues/225 // Discovered in issue https://github.com/daveshanley/vacuum/issues/225
func TestSpecIndex_lookupFileReference_NoComponent(t *testing.T) { func TestSpecIndex_lookupFileReference_NoComponent(t *testing.T) {
@@ -792,29 +861,13 @@ func TestSpecIndex_lookupFileReference_NoComponent(t *testing.T) {
_ = os.WriteFile("coffee-time.yaml", []byte("time: for coffee"), 0o664) _ = os.WriteFile("coffee-time.yaml", []byte("time: for coffee"), 0o664)
defer os.Remove("coffee-time.yaml") defer os.Remove("coffee-time.yaml")
index.seenRemoteSources = make(map[string]*yaml.Node) //index.seenRemoteSources = make(map[string]*yaml.Node)
a, b, err := index.lookupFileReference("coffee-time.yaml") a, b, err := index.lookupFileReference("coffee-time.yaml")
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, a) assert.NotNil(t, a)
assert.NotNil(t, b) assert.NotNil(t, b)
} }
func TestSpecIndex_CheckBadURLRef(t *testing.T) {
yml := `openapi: 3.1.0
paths:
/cakes:
post:
parameters:
- $ref: 'httpsss://badurl'`
var rootNode yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig())
assert.Len(t, index.refErrors, 2)
}
func TestSpecIndex_CheckBadURLRefNoRemoteAllowed(t *testing.T) { func TestSpecIndex_CheckBadURLRefNoRemoteAllowed(t *testing.T) {
yml := `openapi: 3.1.0 yml := `openapi: 3.1.0
paths: paths:
@@ -829,15 +882,34 @@ paths:
c := CreateClosedAPIIndexConfig() c := CreateClosedAPIIndexConfig()
idx := NewSpecIndexWithConfig(&rootNode, c) idx := NewSpecIndexWithConfig(&rootNode, c)
assert.Len(t, idx.refErrors, 2) assert.Len(t, idx.refErrors, 1)
assert.Equal(t, "remote lookups are not permitted, "+
"please set AllowRemoteLookup to true in the configuration", idx.refErrors[0].Error())
} }
func TestSpecIndex_CheckIndexDiscoversNoComponentLocalFileReference(t *testing.T) { func TestSpecIndex_CheckIndexDiscoversNoComponentLocalFileReference(t *testing.T) {
_ = os.WriteFile("coffee-time.yaml", []byte("name: time for coffee"), 0o664) c := []byte("name: time for coffee")
_ = os.WriteFile("coffee-time.yaml", c, 0o664)
defer os.Remove("coffee-time.yaml") 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 yml := `openapi: 3.0.3
paths: paths:
/cakes: /cakes:
@@ -845,24 +917,32 @@ paths:
parameters: parameters:
- $ref: 'coffee-time.yaml'` - $ref: 'coffee-time.yaml'`
var rootNode yaml.Node var coffee yaml.Node
_ = yaml.Unmarshal([]byte(yml), &rootNode) _ = yaml.Unmarshal([]byte(yml), &coffee)
index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig()) // 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) assert.NotNil(t, index.GetAllParametersFromOperations()["/cakes"]["post"]["coffee-time.yaml"][0].Node)
} }
func TestSpecIndex_lookupRemoteReference_SeenSourceSimulation_BadJSON(t *testing.T) { //func TestSpecIndex_lookupRemoteReference_SeenSourceSimulation_BadJSON(t *testing.T) {
index := NewSpecIndexWithConfig(nil, &SpecIndexConfig{ // index := NewSpecIndexWithConfig(nil, &SpecIndexConfig{
//AllowRemoteLookup: true, // //AllowRemoteLookup: true,
}) // })
index.seenRemoteSources = make(map[string]*yaml.Node) // index.seenRemoteSources = make(map[string]*yaml.Node)
a, b, err := index.lookupRemoteReference("https://google.com//logos/doodles/2022/labor-day-2022-6753651837109490.3-l.png#/hey") // a, b, err := index.lookupRemoteReference("https://google.com//logos/doodles/2022/labor-day-2022-6753651837109490.3-l.png#/hey")
assert.Error(t, err) // assert.Error(t, err)
assert.Nil(t, a) // assert.Nil(t, a)
assert.Nil(t, b) // assert.Nil(t, b)
} //}
func TestSpecIndex_lookupFileReference_BadFileName(t *testing.T) { func TestSpecIndex_lookupFileReference_BadFileName(t *testing.T) {
index := NewSpecIndexWithConfig(nil, CreateOpenAPIIndexConfig()) index := NewSpecIndexWithConfig(nil, CreateOpenAPIIndexConfig())
@@ -870,39 +950,79 @@ func TestSpecIndex_lookupFileReference_BadFileName(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
} }
func TestSpecIndex_lookupFileReference_SeenSourceSimulation_Error(t *testing.T) { //
index := NewSpecIndexWithConfig(nil, CreateOpenAPIIndexConfig()) //func TestSpecIndex_lookupFileReference_SeenSourceSimulation_Error(t *testing.T) {
index.seenRemoteSources = make(map[string]*yaml.Node) // index := NewSpecIndexWithConfig(nil, CreateOpenAPIIndexConfig())
index.seenRemoteSources["magic-money-file.json"] = &yaml.Node{} // index.seenRemoteSources = make(map[string]*yaml.Node)
_, _, err := index.lookupFileReference("magic-money-file.json#something") // index.seenRemoteSources["magic-money-file.json"] = &yaml.Node{}
assert.Error(t, err) // _, _, err := index.lookupFileReference("magic-money-file.json#something")
} // assert.Error(t, err)
//}
func TestSpecIndex_lookupFileReference_BadFile(t *testing.T) { //
index := NewSpecIndexWithConfig(nil, CreateOpenAPIIndexConfig()) //func TestSpecIndex_lookupFileReference_BadFile(t *testing.T) {
_, _, err := index.lookupFileReference("chickers.json#no-rice") // index := NewSpecIndexWithConfig(nil, CreateOpenAPIIndexConfig())
assert.Error(t, err) // _, _, err := index.lookupFileReference("chickers.json#no-rice")
} // assert.Error(t, err)
//}
func TestSpecIndex_lookupFileReference_BadFileDataRead(t *testing.T) { //
_ = os.WriteFile("chickers.yaml", []byte("broke: the: thing: [again]"), 0o664) //func TestSpecIndex_lookupFileReference_BadFileDataRead(t *testing.T) {
defer os.Remove("chickers.yaml") // _ = os.WriteFile("chickers.yaml", []byte("broke: the: thing: [again]"), 0o664)
var root yaml.Node // defer os.Remove("chickers.yaml")
index := NewSpecIndexWithConfig(&root, CreateOpenAPIIndexConfig()) // var root yaml.Node
_, _, err := index.lookupFileReference("chickers.yaml#no-rice") // index := NewSpecIndexWithConfig(&root, CreateOpenAPIIndexConfig())
assert.Error(t, err) // _, _, err := index.lookupFileReference("chickers.yaml#no-rice")
} // assert.Error(t, err)
//}
func TestSpecIndex_lookupFileReference_MultiRes(t *testing.T) { func TestSpecIndex_lookupFileReference_MultiRes(t *testing.T) {
_ = os.WriteFile("embie.yaml", []byte("naughty:\n - puppy: dog\n - puppy: naughty\npuppy:\n - naughty: puppy"), 0o664)
embie := []byte("naughty:\n - puppy: dog\n - puppy: naughty\npuppy:\n - naughty: puppy")
_ = os.WriteFile("embie.yaml", embie, 0o664)
defer os.Remove("embie.yaml") defer os.Remove("embie.yaml")
index := NewSpecIndexWithConfig(nil, CreateOpenAPIIndexConfig()) // create a new config that allows local and remote to be mixed up.
index.seenRemoteSources = make(map[string]*yaml.Node) cf := CreateOpenAPIIndexConfig()
k, doc, err := index.lookupFileReference("embie.yaml#/.naughty") 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) assert.NoError(t, err)
assert.NotNil(t, doc)
assert.Nil(t, k) 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) { func TestSpecIndex_lookupFileReference(t *testing.T) {
@@ -918,7 +1038,6 @@ func TestSpecIndex_lookupFileReference(t *testing.T) {
// create a new config that allows local and remote to be mixed up. // create a new config that allows local and remote to be mixed up.
cf := CreateOpenAPIIndexConfig() cf := CreateOpenAPIIndexConfig()
cf.AvoidBuildIndex = true cf.AvoidBuildIndex = true
cf.AllowRemoteLookup = true
cf.AvoidCircularReferenceCheck = true cf.AvoidCircularReferenceCheck = true
cf.BasePath = "." cf.BasePath = "."