...

Source file src/github.com/go-openapi/spec/normalizer_test.go

Documentation: github.com/go-openapi/spec

     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  // only used for windows
    18  var currentDriveLetter = getCurrentDrive()
    19  
    20  // get the current drive letter in lowercase on windows that the test is running
    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  				// http basePath, absolute refPath
    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  				// http basePath, absolute refPath
    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  				// http basePath, absolute refPath
    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  				// http basePath, absolute refPath
    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  				// http basePath, relative refPath
    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  				// file basePath, absolute refPath, no fragment
    78  				refPath:   "/another/base/path.json",
    79  				base:      "/base/path.json",
    80  				expOutput: "/another/base/path.json",
    81  			},
    82  			{
    83  				// path clean
    84  				refPath:   "/another///base//path.json/",
    85  				base:      "/base/path.json",
    86  				expOutput: "/another/base/path.json",
    87  			},
    88  			{
    89  				// path clean edge case
    90  				refPath:   "",
    91  				base:      "/base/path.json",
    92  				expOutput: "/base/path.json",
    93  			},
    94  			{
    95  				// file basePath, absolute refPath
    96  				refPath:   "/another/base/path.json#/definitions/Pet",
    97  				base:      "/base/path.json",
    98  				expOutput: "/another/base/path.json#/definitions/Pet",
    99  			},
   100  			{
   101  				// file basePath, relative refPath
   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  				// file basePath, relative refPath
   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  				// params in base
   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  				// params in ref
   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  				// invalid URI
   156  				refPath:   "\x7f\x9a",
   157  				base:      "file:///base/path.json",
   158  				expOutput: "file:///base/path.json",
   159  			},
   160  			{
   161  				// file basePath, absolute refPath, no fragment
   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  				// file basePath, absolute refPath
   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  				// file basePath, relative refPath
   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  				// file basePath, relative refPath
   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  				// repair URI
   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  				// case sensitivity on local paths only (1/4)
   203  				// see note:
   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  				// case sensitivity on local paths only (2/4)
   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  				// case sensitivity on local paths only (3/4)
   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  				// escaped characters in base (1)
   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  				// escaped characters in base (2)
   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  				// escaped characters in joined fragment
   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  				// escaped characters in joined path
   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", // edge case: this won't work because a document is a file
   282  		},
   283  		{
   284  			Base:     "#",
   285  			Expected: "file://$cwd", // edge case: this won't work because a document is a file
   286  		},
   287  		{
   288  			Base:     "\x7f\x9a",
   289  			Expected: "file://$cwd", // edge case: invalid URI
   290  		},
   291  		{
   292  			Base:     ".",
   293  			Expected: "file://$cwd", // edge case: this won't work because a document is a file
   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  			// irrelevant fragment: cleaned
   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  			// funny scheme, no path
   318  			Base:     "smb://host",
   319  			Expected: "smb://host",
   320  		},
   321  		{
   322  			// explicit scheme, with host and path
   323  			Base:     "gs://bucket/folder/file.json",
   324  			Expected: "gs://bucket/folder/file.json",
   325  		},
   326  		{
   327  			// explicit file scheme, with host and path
   328  			Base:     "file://folder/file.json",
   329  			Expected: "file://folder/file.json",
   330  		},
   331  		{
   332  			// path clean
   333  			Base:     "file:///folder//subfolder///file.json/",
   334  			Expected: "file:///folder/subfolder/file.json",
   335  		},
   336  		{
   337  			// path clean
   338  			Base:       "///folder//subfolder///file.json/",
   339  			Expected:   "file:///folder/subfolder/file.json",
   340  			NonWindows: true,
   341  		},
   342  		{
   343  			// path clean
   344  			Base:     "///folder//subfolder///file.json/",
   345  			Expected: fileScheme + currentDriveLetter + ":/folder/subfolder/file.json",
   346  			Windows:  true,
   347  		},
   348  		{
   349  			// relevant query param: kept
   350  			Base:     "https:///host/base/sub/file.json?query=param",
   351  			Expected: "https:///host/base/sub/file.json?query=param",
   352  		},
   353  		{
   354  			// no host component, absolute path
   355  			Base:     `file:/base/sub/file.json`,
   356  			Expected: "file:///base/sub/file.json",
   357  		},
   358  		{
   359  			// handling dots (1/6): dodgy specification - resolved to /
   360  			Base:     `file:///.`,
   361  			Expected: "file:///",
   362  		},
   363  		{
   364  			// handling dots (2/6): valid, cleaned to /
   365  			Base:       "/..",
   366  			Expected:   "file:///",
   367  			NonWindows: true,
   368  		},
   369  		{
   370  			// handling dots (3/6): valid, cleaned to /c:/ on windows
   371  			Base:     "/..",
   372  			Expected: fileScheme + currentDriveLetter + ":",
   373  			Windows:  true,
   374  		},
   375  		{
   376  			// handling dots (4/6): dodgy specification - resolved to /
   377  			Base:     `file:/.`,
   378  			Expected: fileScheme,
   379  		},
   380  		{
   381  			// handling dots (5/6): dodgy specification - resolved to /
   382  			Base:     `file:/..`,
   383  			Expected: fileScheme,
   384  		},
   385  		{
   386  			// handling dots (6/6)
   387  			Base:     `..`,
   388  			Expected: "file://$dir",
   389  		},
   390  		// non-windows case
   391  		{
   392  			Base:       "/base/sub/file.json",
   393  			Expected:   "file:///base/sub/file.json",
   394  			NonWindows: true,
   395  		},
   396  		{
   397  			// irrelevant query param (local file resolver): cleaned
   398  			Base:       "/base/sub/file.json?query=param",
   399  			Expected:   "file:///base/sub/file.json",
   400  			NonWindows: true,
   401  		},
   402  		// windows-only cases
   403  		{
   404  			Base:     "/base/sub/file.json",
   405  			Expected: fileScheme + currentDriveLetter + ":/base/sub/file.json", // on windows, filepath.Abs("/a/b") prepends the "c:" drive
   406  			Windows:  true,
   407  		},
   408  		{
   409  			// case sensitivity
   410  			Base:     `C:\Base\sub\File.json`,
   411  			Expected: "file:///c:/base/sub/file.json",
   412  			Windows:  true,
   413  		},
   414  		{
   415  			// This one is parsed correctly: notice the third slash
   416  			Base:     `file:///\Base\sub\File.json`,
   417  			Expected: "file:///base/sub/file.json",
   418  			Windows:  true,
   419  		},
   420  		{
   421  			// absolute path
   422  			Base:     `file:/\Base\sub\File.json`,
   423  			Expected: "file:///base/sub/file.json",
   424  			Windows:  true,
   425  		},
   426  		{
   427  			// windows UNC path, no drive
   428  			Base:     `\\host\share@1234\Folder\File.json`,
   429  			Expected: "file://host/share@1234/folder/file.json",
   430  			Windows:  true,
   431  		},
   432  		{
   433  			// repair invalid use of leading "." on windows
   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  			// repair URI (windows)
   445  			// This one exhibits an example of invalid URI (missing a 3rd "/")
   446  			Base:     `file://E:\Base\sub\File.json`,
   447  			Expected: "file:///e:/base/sub/file.json",
   448  			Windows:  true,
   449  		},
   450  		{
   451  			// escaped characters in base (1)
   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  			// check for idempotence
   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  			// we may end up in this situation following ../.. in paths
   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 { // windows only
   598  			continue
   599  		}
   600  		if testCase.NonWindows && runtime.GOOS == windowsOS { // non-windows only
   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