Skip to content

Commit ab37c7b

Browse files
test: windows
1 parent aa189c0 commit ab37c7b

File tree

7 files changed

+266
-12
lines changed

7 files changed

+266
-12
lines changed

internal/cache/example_test.go renamed to cache/example_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package cache_test
33
import (
44
"fmt"
55

6-
"github.com/speakeasy-api/openapi/internal/cache"
6+
"github.com/speakeasy-api/openapi/cache"
77
"github.com/speakeasy-api/openapi/internal/utils"
88
"github.com/speakeasy-api/openapi/references"
99
)
File renamed without changes.

internal/cache/manager_test.go renamed to cache/manager_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,9 @@ func TestClearFieldCache_Success(t *testing.T) {
8787
assert.Equal(t, int64(0), stats.FieldCacheSize, "Field cache should be empty")
8888
}
8989

90+
// nolint:paralleltest
9091
func TestGetAllCacheStats_Success(t *testing.T) {
91-
t.Parallel()
92+
// Don't run in parallel since we're testing global cache state
9293

9394
// Clear all caches first
9495
ClearAllCaches()

internal/utils/references.go

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -183,19 +183,98 @@ func (rc *ReferenceClassification) joinURL(relative string) (string, error) {
183183
return resolvedURL.String(), nil
184184
}
185185

186-
// joinFilePath joins this file path reference with a relative path using Go's filepath package
186+
// joinFilePath joins this file path reference with a relative path using cross-platform path handling
187187
func (rc *ReferenceClassification) joinFilePath(relative string) (string, error) {
188188
// If relative path is absolute, return it as-is
189189
// Check for both OS-specific absolute paths and Unix-style absolute paths (for cross-platform compatibility)
190-
if filepath.IsAbs(relative) || strings.HasPrefix(relative, "/") {
190+
if filepath.IsAbs(relative) || strings.HasPrefix(relative, "/") || rc.isWindowsAbsolutePath(relative) {
191191
return relative, nil
192192
}
193193

194-
// For all relative paths, join them with the base directory
195-
// Use filepath.Join for proper path handling, then convert to forward slashes for OpenAPI/JSON Schema compatibility
196-
joined := filepath.Join(filepath.Dir(rc.Original), relative)
197-
// Convert backslashes to forward slashes for cross-platform compatibility in OpenAPI contexts
198-
return strings.ReplaceAll(joined, "\\", "/"), nil
194+
// Determine the path separator style from the original path
195+
isWindowsStyle := strings.Contains(rc.Original, "\\") && !strings.Contains(rc.Original, "/")
196+
197+
// Get the directory part of the original path using cross-platform logic
198+
var baseDir string
199+
if isWindowsStyle {
200+
// Handle Windows-style paths manually for cross-platform compatibility
201+
baseDir = rc.getWindowsDir()
202+
} else {
203+
// Use standard filepath.Dir for Unix-style paths
204+
baseDir = filepath.Dir(rc.Original)
205+
}
206+
207+
// Join the paths
208+
var joined string
209+
if isWindowsStyle {
210+
// Manual Windows-style path joining
211+
joined = rc.joinWindowsPaths(baseDir, relative)
212+
} else {
213+
// Use standard filepath.Join for Unix-style paths
214+
joined = filepath.Join(baseDir, relative)
215+
// Convert to forward slashes for OpenAPI/JSON Schema compatibility
216+
joined = strings.ReplaceAll(joined, "\\", "/")
217+
}
218+
219+
return joined, nil
220+
}
221+
222+
// getWindowsDir extracts the directory part from a Windows-style path
223+
func (rc *ReferenceClassification) getWindowsDir() string {
224+
path := rc.Original
225+
// Find the last backslash
226+
lastSlash := strings.LastIndex(path, "\\")
227+
if lastSlash == -1 {
228+
return "." // No directory separator found
229+
}
230+
return path[:lastSlash]
231+
}
232+
233+
// joinWindowsPaths joins Windows-style paths manually
234+
func (rc *ReferenceClassification) joinWindowsPaths(base, relative string) string {
235+
// Handle relative path navigation
236+
// Split by both forward and backward slashes to handle cross-platform relative paths
237+
var parts []string
238+
if strings.Contains(relative, "/") {
239+
// Unix-style path with forward slashes
240+
parts = strings.Split(relative, "/")
241+
} else {
242+
// Windows-style path with backslashes
243+
parts = strings.Split(relative, "\\")
244+
}
245+
246+
baseParts := strings.Split(base, "\\")
247+
248+
for _, part := range parts {
249+
switch part {
250+
case ".":
251+
// Current directory, do nothing
252+
continue
253+
case "..":
254+
// Parent directory, remove last part from base
255+
if len(baseParts) > 1 {
256+
baseParts = baseParts[:len(baseParts)-1]
257+
}
258+
default:
259+
// Regular path component
260+
baseParts = append(baseParts, part)
261+
}
262+
}
263+
264+
return strings.Join(baseParts, "\\")
265+
}
266+
267+
// isWindowsAbsolutePath checks if a path is a Windows absolute path (e.g., C:\path or \\server\share)
268+
func (rc *ReferenceClassification) isWindowsAbsolutePath(path string) bool {
269+
// Check for drive letter paths (C:\, D:\, etc.)
270+
if len(path) >= 3 && path[1] == ':' && (path[2] == '\\' || path[2] == '/') {
271+
return true
272+
}
273+
// Check for UNC paths (\\server\share)
274+
if strings.HasPrefix(path, "\\\\") {
275+
return true
276+
}
277+
return false
199278
}
200279

201280
// JoinReference is a convenience function that classifies the base reference and joins it with a relative reference.
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package utils
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
// TestWindowsStylePathJoining tests Windows-style path joining logic
11+
// This test simulates the Windows path behavior to verify our fixes
12+
func TestWindowsStylePathJoining_Success(t *testing.T) {
13+
t.Parallel()
14+
15+
tests := []struct {
16+
name string
17+
base string
18+
relative string
19+
expected string
20+
}{
21+
{
22+
name: "windows path with simple relative file",
23+
base: "C:\\path\\to\\schemas\\user.json",
24+
relative: "common.json",
25+
expected: "C:\\path\\to\\schemas\\common.json",
26+
},
27+
{
28+
name: "windows path with relative directory navigation",
29+
base: "C:\\path\\to\\schemas\\user.json",
30+
relative: "..\\base\\common.json",
31+
expected: "C:\\path\\to\\base\\common.json",
32+
},
33+
{
34+
name: "windows path with dot relative path",
35+
base: "C:\\path\\to\\schemas\\user.json",
36+
relative: ".\\common.json",
37+
expected: "C:\\path\\to\\schemas\\common.json",
38+
},
39+
{
40+
name: "windows path with absolute relative path",
41+
base: "C:\\path\\to\\schemas\\user.json",
42+
relative: "D:\\other\\path\\schema.json",
43+
expected: "D:\\other\\path\\schema.json",
44+
},
45+
{
46+
name: "windows path with fragment",
47+
base: "C:\\path\\to\\schema.json",
48+
relative: "#/definitions/User",
49+
expected: "C:\\path\\to\\schema.json#/definitions/User",
50+
},
51+
{
52+
name: "windows path with unix-style dot relative path",
53+
base: "C:\\path\\to\\schemas\\user.json",
54+
relative: "./common.json",
55+
expected: "C:\\path\\to\\schemas\\common.json",
56+
},
57+
{
58+
name: "windows path with unix-style relative directory navigation",
59+
base: "C:\\path\\to\\schemas\\user.json",
60+
relative: "../base/common.json",
61+
expected: "C:\\path\\to\\base\\common.json",
62+
},
63+
{
64+
name: "windows path with unix-style complex relative path",
65+
base: "D:\\a\\openapi\\openapi\\jsonschema\\oas3\\testdata\\resolve_test_main.yaml",
66+
relative: "./resolve_test_external.yaml",
67+
expected: "D:\\a\\openapi\\openapi\\jsonschema\\oas3\\testdata\\resolve_test_external.yaml",
68+
},
69+
{
70+
name: "windows UNC path joining",
71+
base: "\\\\server\\share\\path\\base.json",
72+
relative: "schema.json",
73+
expected: "\\\\server\\share\\path\\schema.json",
74+
},
75+
}
76+
77+
for _, tt := range tests {
78+
t.Run(tt.name, func(t *testing.T) {
79+
t.Parallel()
80+
81+
classification, err := ClassifyReference(tt.base)
82+
require.NoError(t, err)
83+
require.NotNil(t, classification)
84+
require.True(t, classification.IsFile, "Base should be classified as file path")
85+
86+
result, err := classification.JoinWith(tt.relative)
87+
require.NoError(t, err)
88+
assert.Equal(t, tt.expected, result)
89+
})
90+
}
91+
}
92+
93+
// TestWindowsStylePathJoinReference_Success tests the convenience function
94+
func TestWindowsStylePathJoinReference_Success(t *testing.T) {
95+
t.Parallel()
96+
97+
tests := []struct {
98+
name string
99+
base string
100+
relative string
101+
expected string
102+
}{
103+
{
104+
name: "windows path joining via convenience function",
105+
base: "C:\\path\\to\\base.json",
106+
relative: "schema.json",
107+
expected: "C:\\path\\to\\schema.json",
108+
},
109+
{
110+
name: "windows UNC path joining",
111+
base: "\\\\server\\share\\path\\base.json",
112+
relative: "schema.json",
113+
expected: "\\\\server\\share\\path\\schema.json",
114+
},
115+
}
116+
117+
for _, tt := range tests {
118+
t.Run(tt.name, func(t *testing.T) {
119+
t.Parallel()
120+
121+
result, err := JoinReference(tt.base, tt.relative)
122+
require.NoError(t, err)
123+
assert.Equal(t, tt.expected, result)
124+
})
125+
}
126+
}
127+
128+
// TestUnixStylePathJoining_Success tests that Unix-style paths still work correctly
129+
func TestUnixStylePathJoining_Success(t *testing.T) {
130+
t.Parallel()
131+
132+
tests := []struct {
133+
name string
134+
base string
135+
relative string
136+
expected string
137+
}{
138+
{
139+
name: "unix path with simple relative file",
140+
base: "/path/to/schemas/user.json",
141+
relative: "common.json",
142+
expected: "/path/to/schemas/common.json",
143+
},
144+
{
145+
name: "unix path with relative directory navigation",
146+
base: "/path/to/schemas/user.json",
147+
relative: "../base/common.json",
148+
expected: "/path/to/base/common.json",
149+
},
150+
{
151+
name: "unix path with dot relative path",
152+
base: "/path/to/schemas/user.json",
153+
relative: "./common.json",
154+
expected: "/path/to/schemas/common.json",
155+
},
156+
}
157+
158+
for _, tt := range tests {
159+
t.Run(tt.name, func(t *testing.T) {
160+
t.Parallel()
161+
162+
classification, err := ClassifyReference(tt.base)
163+
require.NoError(t, err)
164+
require.NotNil(t, classification)
165+
require.True(t, classification.IsFile, "Base should be classified as file path")
166+
167+
result, err := classification.JoinWith(tt.relative)
168+
require.NoError(t, err)
169+
assert.Equal(t, tt.expected, result)
170+
})
171+
}
172+
}

internal/utils/url_cache_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,10 @@ func TestURLCache_Clear(t *testing.T) {
129129
assert.Equal(t, int64(0), sizeAfter)
130130
}
131131

132+
// nolint:paralleltest
132133
func TestParseURLCached_Global(t *testing.T) {
133-
t.Parallel()
134+
// Don't run in parallel since we're testing global cache state
135+
134136
// Clear global cache before test
135137
ClearGlobalURLCache()
136138

references/resolution_cache_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,8 @@ func TestRefCache_Clear(t *testing.T) {
176176
assert.Equal(t, int64(0), stats.Size)
177177
}
178178

179+
//nolint:paralleltest // This test uses global cache and cannot be parallel
179180
func TestResolveAbsoluteReferenceCached_Global(t *testing.T) {
180-
t.Parallel()
181181
// Clear global cache before test
182182
ClearGlobalRefCache()
183183

@@ -203,8 +203,8 @@ func TestResolveAbsoluteReferenceCached_Global(t *testing.T) {
203203
ClearGlobalRefCache()
204204
}
205205

206+
//nolint:paralleltest // This test uses global cache and cannot be parallel
206207
func TestResolveAbsoluteReference_UsesCache(t *testing.T) {
207-
t.Parallel()
208208
// Clear global cache before test
209209
ClearGlobalRefCache()
210210

0 commit comments

Comments
 (0)