1 package spec
2
3 import (
4 "os"
5 "path"
6 "path/filepath"
7 "runtime"
8 "strings"
9 "testing"
10
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"
13 )
14
15 const windowsOS = "windows"
16
17
18 var currentDriveLetter = getCurrentDrive()
19
20
21 func getCurrentDrive() string {
22 if runtime.GOOS != windowsOS {
23 return ""
24 }
25 p, err := filepath.Abs("/")
26 if err != nil {
27 panic(err)
28 }
29 if len(p) == 0 {
30 panic("current windows drive letter is empty")
31 }
32 return strings.ToLower(string(p[0]))
33 }
34
35 func TestNormalizer_NormalizeURI(t *testing.T) {
36 type testNormalizePathsTestCases []struct {
37 refPath string
38 base string
39 expOutput string
40 windows bool
41 nonWindows bool
42 }
43
44 testCases := func() testNormalizePathsTestCases {
45 return testNormalizePathsTestCases{
46 {
47
48 refPath: "",
49 base: "http://www.example.com/base/path/swagger.json",
50 expOutput: "http://www.example.com/base/path/swagger.json",
51 },
52 {
53
54 refPath: "#",
55 base: "http://www.example.com/base/path/swagger.json",
56 expOutput: "http://www.example.com/base/path/swagger.json",
57 },
58 {
59
60 refPath: "#/definitions/Pet",
61 base: "http://www.example.com/base/path/swagger.json",
62 expOutput: "http://www.example.com/base/path/swagger.json#/definitions/Pet",
63 },
64 {
65
66 refPath: "http://www.anotherexample.com/another/base/path/swagger.json#/definitions/Pet",
67 base: "http://www.example.com/base/path/swagger.json",
68 expOutput: "http://www.anotherexample.com/another/base/path/swagger.json#/definitions/Pet",
69 },
70 {
71
72 refPath: "another/base/path/swagger.json#/definitions/Pet",
73 base: "http://www.example.com/base/path/swagger.json",
74 expOutput: "http://www.example.com/base/path/another/base/path/swagger.json#/definitions/Pet",
75 },
76 {
77
78 refPath: "/another/base/path.json",
79 base: "/base/path.json",
80 expOutput: "/another/base/path.json",
81 },
82 {
83
84 refPath: "/another///base//path.json/",
85 base: "/base/path.json",
86 expOutput: "/another/base/path.json",
87 },
88 {
89
90 refPath: "",
91 base: "/base/path.json",
92 expOutput: "/base/path.json",
93 },
94 {
95
96 refPath: "/another/base/path.json#/definitions/Pet",
97 base: "/base/path.json",
98 expOutput: "/another/base/path.json#/definitions/Pet",
99 },
100 {
101
102 refPath: "another/base/path.json#/definitions/Pet",
103 base: "/base/path.json",
104 expOutput: "/base/another/base/path.json#/definitions/Pet",
105 },
106 {
107
108 refPath: "./another/base/path.json#/definitions/Pet",
109 base: "/base/path.json",
110 expOutput: "/base/another/base/path.json#/definitions/Pet",
111 },
112 {
113 refPath: "another/base/path.json#/definitions/Pet",
114 base: "file:///base/path.json",
115 expOutput: "file:///base/another/base/path.json#/definitions/Pet",
116 },
117 {
118 refPath: "/another/base/path.json#/definitions/Pet",
119 base: "https://www.example.com:8443//base/path.json",
120 expOutput: "https://www.example.com:8443/another/base/path.json#/definitions/Pet",
121 },
122 {
123
124 refPath: "/another/base/path.json#/definitions/Pet",
125 base: "https://www.example.com:8443//base/path.json?raw=true",
126 expOutput: "https://www.example.com:8443/another/base/path.json?raw=true#/definitions/Pet",
127 },
128 {
129
130 refPath: "https://origin.com/another/file.json?raw=true",
131 base: "https://www.example.com:8443//base/path.json?raw=true",
132 expOutput: "https://origin.com/another/file.json?raw=true",
133 },
134 {
135 refPath: "another/base/def.yaml#/definitions/Pet",
136 base: "file:///base/path.json",
137 expOutput: "file:///base/another/base/def.yaml#/definitions/Pet",
138 },
139 {
140 refPath: "",
141 base: "file:///base/path.json",
142 expOutput: "file:///base/path.json",
143 },
144 {
145 refPath: "#",
146 base: "file:///base/path.json",
147 expOutput: "file:///base/path.json",
148 },
149 {
150 refPath: "../other/another.json#/definitions/X",
151 base: "file:///base/path.json",
152 expOutput: "file:///other/another.json#/definitions/X",
153 },
154 {
155
156 refPath: "\x7f\x9a",
157 base: "file:///base/path.json",
158 expOutput: "file:///base/path.json",
159 },
160 {
161
162 refPath: `C:\another\base\path.json`,
163 base: `file:///c:/base/path.json`,
164 expOutput: `file:///c:/another/base/path.json`,
165 windows: true,
166 },
167 {
168
169 refPath: `C:\another\Base\path.json#/definitions/Pet`,
170 base: `file:///c:/base/path.json`,
171 expOutput: `file:///c:/another/base/path.json#/definitions/Pet`,
172 windows: true,
173 },
174 {
175
176 refPath: `another\base\path.json#/definitions/Pet`,
177 base: `file:///c:/base/path.json`,
178 expOutput: `file:///c:/base/another/base/path.json#/definitions/Pet`,
179 windows: true,
180 },
181 {
182
183 refPath: `.\another\base\path.json#/definitions/Pet`,
184 base: `file:///c:/base/path.json`,
185 expOutput: `file:///c:/base/another/base/path.json#/definitions/Pet`,
186 windows: true,
187 },
188 {
189 refPath: `\\host\share\another\base\path.json#/definitions/Pet`,
190 base: `file:///c:/base/path.json`,
191 expOutput: `file://host/share/another/base/path.json#/definitions/Pet`,
192 windows: true,
193 },
194 {
195
196 refPath: `file://E:\Base\sub\File.json`,
197 base: `file:///c:/base/path.json`,
198 expOutput: `file:///e:/base/sub/file.json`,
199 windows: true,
200 },
201 {
202
203
204 refPath: `Resources.yaml#/definitions/Pets`,
205 base: `file:///c:/base/Spec.json`,
206 expOutput: `file:///c:/base/Resources.yaml#/definitions/Pets`,
207 windows: true,
208 },
209 {
210
211 refPath: `Resources.yaml#/definitions/Pets`,
212 base: `file:///c:/base/Spec.json`,
213 expOutput: `file:///c:/base/Resources.yaml#/definitions/Pets`,
214 nonWindows: true,
215 },
216 {
217
218 refPath: `Resources.yaml#/definitions/Pets`,
219 base: `https://example.com//base/Spec.json`,
220 expOutput: `https://example.com/base/Resources.yaml#/definitions/Pets`,
221 },
222 {
223
224 refPath: `Resources.yaml#/definitions/Pets`,
225 base: `https://example.com/base (x86)/Spec.json`,
226 expOutput: `https://example.com/base%20%28x86%29/Resources.yaml#/definitions/Pets`,
227 },
228 {
229
230 refPath: `Resources.yaml#/definitions/Pets`,
231 base: `https://example.com/base [x86]/Spec.json`,
232 expOutput: `https://example.com/base%20%5Bx86%5D/Resources.yaml#/definitions/Pets`,
233 },
234 {
235
236 refPath: `Resources.yaml#/definitions (x86)/Pets`,
237 base: `https://example.com/base/Spec.json`,
238 expOutput: `https://example.com/base/Resources.yaml#/definitions%20(x86)/Pets`,
239 },
240 {
241
242 refPath: `Resources [x86].yaml#/definitions/Pets`,
243 base: `https://example.com/base/Spec.json`,
244 expOutput: `https://example.com/base/Resources%20%5Bx86%5D.yaml#/definitions/Pets`,
245 },
246 }
247 }()
248
249 for _, toPin := range testCases {
250 testCase := toPin
251 if testCase.windows && runtime.GOOS != windowsOS {
252 continue
253 }
254 if testCase.nonWindows && runtime.GOOS == windowsOS {
255 continue
256 }
257 t.Run(testCase.refPath, func(t *testing.T) {
258 t.Parallel()
259 out := normalizeURI(testCase.refPath, testCase.base)
260 assert.Equalf(t, testCase.expOutput, out,
261 "unexpected normalized URL with $ref %q and base %q", testCase.refPath, testCase.base)
262 })
263 }
264 }
265
266 func TestNormalizer_NormalizeBase(t *testing.T) {
267 cwd, err := os.Getwd()
268 require.NoError(t, err)
269 if runtime.GOOS == windowsOS {
270 cwd = "/" + strings.ToLower(filepath.ToSlash(cwd))
271 }
272 const fileScheme = "file:///"
273
274 for _, toPin := range []struct {
275 Base, Expected string
276 Windows bool
277 NonWindows bool
278 }{
279 {
280 Base: "",
281 Expected: "file://$cwd",
282 },
283 {
284 Base: "#",
285 Expected: "file://$cwd",
286 },
287 {
288 Base: "\x7f\x9a",
289 Expected: "file://$cwd",
290 },
291 {
292 Base: ".",
293 Expected: "file://$cwd",
294 },
295 {
296 Base: "https://user:password@www.example.com:123/base/sub/file.json",
297 Expected: "https://user:password@www.example.com:123/base/sub/file.json",
298 },
299 {
300
301 Base: "http://www.anotherexample.com/another/base/path/swagger.json#/definitions/Pet",
302 Expected: "http://www.anotherexample.com/another/base/path/swagger.json",
303 },
304 {
305 Base: "base/sub/file.json",
306 Expected: "file://$cwd/base/sub/file.json",
307 },
308 {
309 Base: "./base/sub/file.json",
310 Expected: "file://$cwd/base/sub/file.json",
311 },
312 {
313 Base: "file:/base/sub/file.json",
314 Expected: "file:///base/sub/file.json",
315 },
316 {
317
318 Base: "smb://host",
319 Expected: "smb://host",
320 },
321 {
322
323 Base: "gs://bucket/folder/file.json",
324 Expected: "gs://bucket/folder/file.json",
325 },
326 {
327
328 Base: "file://folder/file.json",
329 Expected: "file://folder/file.json",
330 },
331 {
332
333 Base: "file:///folder//subfolder///file.json/",
334 Expected: "file:///folder/subfolder/file.json",
335 },
336 {
337
338 Base: "///folder//subfolder///file.json/",
339 Expected: "file:///folder/subfolder/file.json",
340 NonWindows: true,
341 },
342 {
343
344 Base: "///folder//subfolder///file.json/",
345 Expected: fileScheme + currentDriveLetter + ":/folder/subfolder/file.json",
346 Windows: true,
347 },
348 {
349
350 Base: "https:///host/base/sub/file.json?query=param",
351 Expected: "https:///host/base/sub/file.json?query=param",
352 },
353 {
354
355 Base: `file:/base/sub/file.json`,
356 Expected: "file:///base/sub/file.json",
357 },
358 {
359
360 Base: `file:///.`,
361 Expected: "file:///",
362 },
363 {
364
365 Base: "/..",
366 Expected: "file:///",
367 NonWindows: true,
368 },
369 {
370
371 Base: "/..",
372 Expected: fileScheme + currentDriveLetter + ":",
373 Windows: true,
374 },
375 {
376
377 Base: `file:/.`,
378 Expected: fileScheme,
379 },
380 {
381
382 Base: `file:/..`,
383 Expected: fileScheme,
384 },
385 {
386
387 Base: `..`,
388 Expected: "file://$dir",
389 },
390
391 {
392 Base: "/base/sub/file.json",
393 Expected: "file:///base/sub/file.json",
394 NonWindows: true,
395 },
396 {
397
398 Base: "/base/sub/file.json?query=param",
399 Expected: "file:///base/sub/file.json",
400 NonWindows: true,
401 },
402
403 {
404 Base: "/base/sub/file.json",
405 Expected: fileScheme + currentDriveLetter + ":/base/sub/file.json",
406 Windows: true,
407 },
408 {
409
410 Base: `C:\Base\sub\File.json`,
411 Expected: "file:///c:/base/sub/file.json",
412 Windows: true,
413 },
414 {
415
416 Base: `file:///\Base\sub\File.json`,
417 Expected: "file:///base/sub/file.json",
418 Windows: true,
419 },
420 {
421
422 Base: `file:/\Base\sub\File.json`,
423 Expected: "file:///base/sub/file.json",
424 Windows: true,
425 },
426 {
427
428 Base: `\\host\share@1234\Folder\File.json`,
429 Expected: "file://host/share@1234/folder/file.json",
430 Windows: true,
431 },
432 {
433
434 Base: `file:///.\Base\sub\File.json`,
435 Expected: "file://$cwd/base/sub/file.json",
436 Windows: true,
437 },
438 {
439 Base: `file:/E:\Base\sub\File.json`,
440 Expected: "file:///e:/base/sub/file.json",
441 Windows: true,
442 },
443 {
444
445
446 Base: `file://E:\Base\sub\File.json`,
447 Expected: "file:///e:/base/sub/file.json",
448 Windows: true,
449 },
450 {
451
452 Base: `file:///c:/base (x86)/spec.json`,
453 Expected: `file:///c:/base%20%28x86%29/spec.json`,
454 },
455 } {
456 testCase := toPin
457 if testCase.Windows && runtime.GOOS != windowsOS {
458 continue
459 }
460 if testCase.NonWindows && runtime.GOOS == windowsOS {
461 continue
462 }
463 t.Run(testCase.Base, func(t *testing.T) {
464 t.Parallel()
465 expected := strings.ReplaceAll(strings.ReplaceAll(testCase.Expected, "$cwd", cwd), "$dir", path.Dir(cwd))
466 require.Equalf(t, expected, normalizeBase(testCase.Base), "for base %q", testCase.Base)
467
468
469 require.Equalf(t, expected, normalizeBase(normalizeBase(testCase.Base)),
470 "expected idempotent behavior on base %q", testCase.Base)
471 })
472 }
473 }
474
475 func TestNormalizer_Denormalize(t *testing.T) {
476 cwd, err := os.Getwd()
477 require.NoError(t, err)
478
479 for _, toPin := range []struct {
480 OriginalBase, Ref, Expected, ID string
481 Windows bool
482 NonWindows bool
483 }{
484 {
485 OriginalBase: "file:///a/b/c/file.json",
486 Ref: "#/definitions/X",
487 Expected: "#/definitions/X",
488 },
489 {
490 OriginalBase: "file:///a/b/c/file.json#ignoredFragment",
491 Ref: "#/definitions/X",
492 Expected: "#/definitions/X",
493 },
494 {
495 OriginalBase: "https://user:password@example.com/a/b/c/file.json",
496 Ref: "https://user:password@example.com/a/b/c/other.json#/definitions/X",
497 Expected: "other.json#/definitions/X",
498 },
499 {
500 OriginalBase: "file:///a/b/c/file.json",
501 Ref: "file:///a/b/c/items.json#/definitions/X",
502 Expected: "items.json#/definitions/X",
503 },
504 {
505 OriginalBase: "file:///a/b/c/file.json",
506 Ref: "https:///x/y/z/items.json#/definitions/X",
507 Expected: "https:///x/y/z/items.json#/definitions/X",
508 },
509 {
510 OriginalBase: "file:///a/b/c/file.json",
511 Ref: "file:///a/b/c/file.json#/definitions/X",
512 Expected: "#/definitions/X",
513 },
514 {
515 OriginalBase: "file:///a/b/c/file.json",
516 Ref: "file:///a/b/c/d/other.json#/definitions/X",
517 Expected: "d/other.json#/definitions/X",
518 },
519 {
520 OriginalBase: "file:///a/b/c/file.json",
521 Ref: "file:///a/b/c/file.json#",
522 Expected: "",
523 },
524 {
525 OriginalBase: "file:///a/b/c/file.json",
526 Ref: "file:///a/b/c/d/file.json",
527 Expected: "d/file.json",
528 },
529 {
530 OriginalBase: "file:///a/b/c/file.json",
531 Ref: "file:///a/b/c/file.json",
532 Expected: "",
533 },
534 {
535 OriginalBase: "file:///a/b/c/file.json",
536 Ref: "file:///a/b/c/../other.json#/definitions/X",
537 Expected: "../other.json#/definitions/X",
538 },
539 {
540 OriginalBase: "file:///a/b/c/file.json",
541 Ref: "file:///a/b/c/../../other.json#/definitions/X",
542 Expected: "../../other.json#/definitions/X",
543 },
544 {
545
546 OriginalBase: "file:///a1/b/c/file.json",
547 Ref: "file:///a2/b/c/file.json#/definitions/X",
548 Expected: "file:///a2/b/c/file.json#/definitions/X",
549 },
550 {
551 OriginalBase: "file:///file.json",
552 Ref: "file:///file.json#/definitions/X",
553 Expected: "#/definitions/X",
554 },
555 {
556 OriginalBase: "file://host/file.json",
557 Ref: "file://host/file.json#/definitions/X",
558 Expected: "#/definitions/X",
559 },
560 {
561 OriginalBase: "file://host1/file.json",
562 Ref: "file://host2/file.json#/definitions/X",
563 Expected: "file://host2/file.json#/definitions/X",
564 },
565 {
566 OriginalBase: "file:///a/b/c/file.json",
567 ID: "https://myschema/",
568 Ref: "file:///a/b/c/file.json#/definitions/X",
569 Expected: "#/definitions/X",
570 },
571 {
572 OriginalBase: "file:///a/b/c/file.json",
573 ID: "https://myschema/",
574 Ref: "https://myschema#/definitions/X",
575 Expected: "#/definitions/X",
576 },
577 {
578 OriginalBase: "file:///a/b/c/file.json",
579 ID: "https://myschema/",
580 Ref: "https://myschema#/definitions/X",
581 Expected: "#/definitions/X",
582 },
583 {
584 OriginalBase: "file:///folder/file.json",
585 ID: "https://example.com/schema",
586 Ref: "https://example.com/schema#/definitions/X",
587 Expected: "#/definitions/X",
588 },
589 {
590 OriginalBase: "file:///folder/file.json",
591 ID: "https://example.com/schema",
592 Ref: "https://example.com/schema/other-file.json#/definitions/X",
593 Expected: "https://example.com/other-file.json#/definitions/X",
594 },
595 } {
596 testCase := toPin
597 if testCase.Windows && runtime.GOOS != windowsOS {
598 continue
599 }
600 if testCase.NonWindows && runtime.GOOS == windowsOS {
601 continue
602 }
603 t.Run(testCase.Ref, func(t *testing.T) {
604 t.Parallel()
605 expected := strings.ReplaceAll(testCase.Expected, "$cwd", cwd)
606 ref := MustCreateRef(testCase.Ref)
607 newRef := denormalizeRef(&ref, testCase.OriginalBase, testCase.ID)
608 require.NotNil(t, newRef)
609 require.Equalf(t, expected, newRef.String(),
610 "expected %s, but got %s", testCase.Expected, newRef.String())
611 })
612 }
613 }
614
View as plain text