From 6b2f2baeaff0f1e6e1a4e91e45f5408f67509f50 Mon Sep 17 00:00:00 2001 From: quobix Date: Sun, 1 Nov 2026 12:03:35 -0500 Subject: [PATCH 1/2] fix: resolve relative refs against a root BaseURL An empty base path made filepath.Abs resolve refs against the working directory, so refs failed and the local path was sent to the remote server. Co-Authored-By: Claude Opus 5.5 --- datamodel/low/extraction_functions.go | 7 ++- document_root_base_url_test.go | 82 +++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 document_root_base_url_test.go diff --git a/datamodel/low/extraction_functions.go b/datamodel/low/extraction_functions.go index 7ef955f7..7260b9be 100644 --- a/datamodel/low/extraction_functions.go +++ b/datamodel/low/extraction_functions.go @@ -313,7 +313,12 @@ func LocateRefNodeWithContext(ctx context.Context, root *yaml.Node, idx *index.S // check for a config baseURL and use that if it exists. if idx.GetConfig().BaseURL != nil { u := *idx.GetConfig().BaseURL - abs, _ := filepath.Abs(utils.CheckPathOverlap(u.Path, rv, string(os.PathSeparator))) + // an empty base path would otherwise resolve against the working directory. + basePath := u.Path + if basePath == "" { + basePath = string(os.PathSeparator) + } + abs, _ := filepath.Abs(utils.CheckPathOverlap(basePath, rv, string(os.PathSeparator))) u.Path = utils.ReplaceWindowsDriveWithLinuxPath(abs) rv = u.String() } diff --git a/document_root_base_url_test.go b/document_root_base_url_test.go new file mode 100644 index 00000000..fa64b2c9 --- /dev/null +++ b/document_root_base_url_test.go @@ -0,0 +1,82 @@ +// Copyright 2026 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package libopenapi + +import ( + "net/http" + "net/http/httptest" + "net/url" + "os" + "strings" + "sync" + "testing" + + "github.com/pb33f/libopenapi/datamodel" +) + +func TestRootBaseURLDoesNotSendWorkingDirectoryToRemoteServer(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + var mu sync.Mutex + var requested []string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mu.Lock() + requested = append(requested, r.URL.Path) + mu.Unlock() + if r.URL.Path == "/schemas/thing.yaml" { + _, _ = w.Write([]byte("type: object\n")) + return + } + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + base, err := url.Parse(server.URL) + if err != nil { + t.Fatal(err) + } + spec := []byte(`openapi: 3.1.0 +info: + title: root base + version: 1.0.0 +paths: + /thing: + get: + responses: + "200": + description: ok + content: + application/json: + schema: + $ref: ./schemas/thing.yaml +`) + doc, err := NewDocumentWithConfiguration(spec, &datamodel.DocumentConfiguration{ + BaseURL: base, + AllowRemoteReferences: true, + }) + if err != nil { + t.Fatal(err) + } + model, err := doc.BuildV3Model() + if err != nil { + t.Fatal(err) + } + + operation := model.Model.Paths.PathItems.GetOrZero("/thing").Get + mediaType := operation.Responses.Codes.GetOrZero("200").Content.GetOrZero("application/json") + if schema := mediaType.Schema.Schema(); schema == nil { + t.Fatalf("schema did not resolve: %v", mediaType.Schema.GetBuildError()) + } + + mu.Lock() + defer mu.Unlock() + for _, path := range requested { + if strings.HasPrefix(path, cwd) { + t.Fatalf("working directory was sent to the remote server: %s", path) + } + } +} From 1c90e0b8b994a0044416f06cce2c5fe4708c2b32 Mon Sep 17 00:00:00 2001 From: quobix Date: Sun, 1 Nov 2026 13:24:14 -0500 Subject: [PATCH 2/2] test: cover the root BaseURL path in datamodel/low Coverage is measured per package, so the root package test did not count. Co-Authored-By: Claude Opus 5.5 --- datamodel/low/extraction_functions_test.go | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/datamodel/low/extraction_functions_test.go b/datamodel/low/extraction_functions_test.go index 0718f93f..6ebd3d5a 100644 --- a/datamodel/low/extraction_functions_test.go +++ b/datamodel/low/extraction_functions_test.go @@ -1868,6 +1868,36 @@ func TestLocateRefNode_CurrentPathKey_Path_URL(t *testing.T) { assert.NotNil(t, c) } +func TestLocateRefNode_RootBaseURL_IgnoresWorkingDirectory(t *testing.T) { + no := yaml.Node{ + Kind: yaml.MappingNode, + Content: []*yaml.Node{ + { + Kind: yaml.ScalarNode, + Value: "$ref", + }, + { + Kind: yaml.ScalarNode, + Value: "yazzy.yaml", + }, + }, + } + + cwd, err := os.Getwd() + require.NoError(t, err) + + cf := index.CreateClosedAPIIndexConfig() + u, _ := url.Parse("https://herbs-and-coffee-in-the-fall.com") + cf.BaseURL = u + idx := index.NewSpecIndexWithConfig(&no, cf) + n, i, e, c := LocateRefNodeWithContext(context.Background(), &no, idx) + assert.Nil(t, n) + assert.NotNil(t, i) + assert.NotNil(t, c) + require.Error(t, e) + assert.NotContains(t, e.Error(), filepath.ToSlash(cwd)) +} + func TestLocateRefNode_CurrentPathKey_DeeperPath_URL(t *testing.T) { no := yaml.Node{ Kind: yaml.MappingNode,