github.com/hashicorp/vault/sdk@v0.13.0/framework/openapi_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package framework
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/json"
     9  	"io/ioutil"
    10  	"path/filepath"
    11  	"reflect"
    12  	"regexp"
    13  	"sort"
    14  	"strings"
    15  	"testing"
    16  
    17  	"github.com/go-test/deep"
    18  	"github.com/hashicorp/vault/sdk/helper/jsonutil"
    19  	"github.com/hashicorp/vault/sdk/helper/wrapping"
    20  	"github.com/hashicorp/vault/sdk/logical"
    21  )
    22  
    23  func TestOpenAPI_Regex(t *testing.T) {
    24  	t.Run("Path fields", func(t *testing.T) {
    25  		input := `/foo/bar/{inner}/baz/{outer}`
    26  
    27  		matches := pathFieldsRe.FindAllStringSubmatch(input, -1)
    28  
    29  		exp1 := "inner"
    30  		exp2 := "outer"
    31  		if matches[0][1] != exp1 || matches[1][1] != exp2 {
    32  			t.Fatalf("Capture error. Expected %s and %s, got %v", exp1, exp2, matches)
    33  		}
    34  
    35  		input = `/foo/bar/inner/baz/outer`
    36  		matches = pathFieldsRe.FindAllStringSubmatch(input, -1)
    37  
    38  		if matches != nil {
    39  			t.Fatalf("Expected nil match (%s), got %+v", input, matches)
    40  		}
    41  	})
    42  	t.Run("Filtering", func(t *testing.T) {
    43  		tests := []struct {
    44  			input  string
    45  			regex  *regexp.Regexp
    46  			output string
    47  		}{
    48  			{
    49  				input:  `abcde`,
    50  				regex:  wsRe,
    51  				output: "abcde",
    52  			},
    53  			{
    54  				input:  `  a         b    cd   e   `,
    55  				regex:  wsRe,
    56  				output: "abcde",
    57  			},
    58  		}
    59  
    60  		for _, test := range tests {
    61  			result := test.regex.ReplaceAllString(test.input, "")
    62  			if result != test.output {
    63  				t.Fatalf("Clean Regex error (%s). Expected %s, got %s", test.input, test.output, result)
    64  			}
    65  		}
    66  	})
    67  }
    68  
    69  func TestOpenAPI_ExpandPattern(t *testing.T) {
    70  	tests := []struct {
    71  		inPattern   string
    72  		outPathlets []string
    73  	}{
    74  		// A simple string without regexp metacharacters passes through as is
    75  		{"rekey/backup", []string{"rekey/backup"}},
    76  		// A trailing regexp anchor metacharacter is removed
    77  		{"rekey/backup$", []string{"rekey/backup"}},
    78  		// As is a leading one
    79  		{"^rekey/backup", []string{"rekey/backup"}},
    80  		// Named capture groups become OpenAPI parameters
    81  		{"auth/(?P<path>.+?)/tune$", []string{"auth/{path}/tune"}},
    82  		{"auth/(?P<path>.+?)/tune/(?P<more>.*?)$", []string{"auth/{path}/tune/{more}"}},
    83  		// Even if the capture group contains very complex regexp structure inside it
    84  		{"something/(?P<something>(a|b(c|d))|e+|f{1,3}[ghi-k]?.*)", []string{"something/{something}"}},
    85  		// A question-mark results in a result without and with the optional path part
    86  		{"tools/hash(/(?P<urlalgorithm>.+))?", []string{
    87  			"tools/hash",
    88  			"tools/hash/{urlalgorithm}",
    89  		}},
    90  		// Multiple question-marks evaluate each possible combination
    91  		{"(leases/)?renew(/(?P<url_lease_id>.+))?", []string{
    92  			"leases/renew",
    93  			"leases/renew/{url_lease_id}",
    94  			"renew",
    95  			"renew/{url_lease_id}",
    96  		}},
    97  		// GenericNameRegex is one particular way of writing a named capture group, so behaves the same
    98  		{`config/ui/headers/` + GenericNameRegex("header"), []string{"config/ui/headers/{header}"}},
    99  		// The question-mark behaviour is still works when the question-mark is directly applied to a named capture group
   100  		{`leases/lookup/(?P<prefix>.+?)?`, []string{
   101  			"leases/lookup/",
   102  			"leases/lookup/{prefix}",
   103  		}},
   104  		// Optional trailing slashes at the end of the path get stripped - even if appearing deep inside an alternation
   105  		{`(raw/?$|raw/(?P<path>.+))`, []string{
   106  			"raw",
   107  			"raw/{path}",
   108  		}},
   109  		// OptionalParamRegex is also another way of writing a named capture group, that is optional
   110  		{"lookup" + OptionalParamRegex("urltoken"), []string{
   111  			"lookup",
   112  			"lookup/{urltoken}",
   113  		}},
   114  		// Optional trailign slashes at the end of the path get stripped in simpler cases too
   115  		{"roles/?$", []string{
   116  			"roles",
   117  		}},
   118  		{"roles/?", []string{
   119  			"roles",
   120  		}},
   121  		// Non-optional trailing slashes remain... although don't do this, it breaks HelpOperation!
   122  		// (Existing real examples of this pattern being fixed via https://github.com/hashicorp/vault/pull/18571)
   123  		{"accessors/$", []string{
   124  			"accessors/",
   125  		}},
   126  		// GenericNameRegex and OptionalParamRegex still work when concatenated
   127  		{"verify/" + GenericNameRegex("name") + OptionalParamRegex("urlalgorithm"), []string{
   128  			"verify/{name}",
   129  			"verify/{name}/{urlalgorithm}",
   130  		}},
   131  		// Named capture groups that specify enum-like parameters work as expected
   132  		{"^plugins/catalog/(?P<type>auth|database|secret)/(?P<name>.+)$", []string{
   133  			"plugins/catalog/{type}/{name}",
   134  		}},
   135  		{"^plugins/catalog/(?P<type>auth|database|secret)/?$", []string{
   136  			"plugins/catalog/{type}",
   137  		}},
   138  		// Alternations between various literal path segments work
   139  		{"(pathOne|pathTwo)/", []string{"pathOne/", "pathTwo/"}},
   140  		{"(pathOne|pathTwo)/" + GenericNameRegex("name"), []string{"pathOne/{name}", "pathTwo/{name}"}},
   141  		{
   142  			"(pathOne|path-2|Path_3)/" + GenericNameRegex("name"),
   143  			[]string{"Path_3/{name}", "path-2/{name}", "pathOne/{name}"},
   144  		},
   145  		// They still work when combined with GenericNameWithAtRegex
   146  		{"(creds|sts)/" + GenericNameWithAtRegex("name"), []string{
   147  			"creds/{name}",
   148  			"sts/{name}",
   149  		}},
   150  		// And when they're somewhere other than the start of the pattern
   151  		{"keys/generate/(internal|exported|kms)", []string{
   152  			"keys/generate/exported",
   153  			"keys/generate/internal",
   154  			"keys/generate/kms",
   155  		}},
   156  		// If a plugin author makes their list operation support both singular and plural forms, the OpenAPI notices
   157  		{"rolesets?/?", []string{"roleset", "rolesets"}},
   158  		// Complex nested alternation and question-marks are correctly interpreted
   159  		{"crl(/pem|/delta(/pem)?)?", []string{"crl", "crl/delta", "crl/delta/pem", "crl/pem"}},
   160  	}
   161  
   162  	for i, test := range tests {
   163  		paths, _, err := expandPattern(test.inPattern)
   164  		if err != nil {
   165  			t.Fatal(err)
   166  		}
   167  		sort.Strings(paths)
   168  		if !reflect.DeepEqual(paths, test.outPathlets) {
   169  			t.Fatalf("Test %d: Expected %v got %v", i, test.outPathlets, paths)
   170  		}
   171  	}
   172  }
   173  
   174  func TestOpenAPI_ExpandPattern_ReturnsError(t *testing.T) {
   175  	tests := []struct {
   176  		inPattern string
   177  		outError  error
   178  	}{
   179  		// None of these regexp constructs are allowed outside of named capture groups
   180  		{"[a-z]", errUnsupportableRegexpOperationForOpenAPI},
   181  		{".", errUnsupportableRegexpOperationForOpenAPI},
   182  		{"a+", errUnsupportableRegexpOperationForOpenAPI},
   183  		{"a*", errUnsupportableRegexpOperationForOpenAPI},
   184  		// So this pattern, which is a combination of two of the above isn't either - this pattern occurs in the KV
   185  		// secrets engine for its catch-all error handler, which provides a helpful hint to people treating a KV v2 as
   186  		// a KV v1.
   187  		{".*", errUnsupportableRegexpOperationForOpenAPI},
   188  	}
   189  
   190  	for i, test := range tests {
   191  		_, _, err := expandPattern(test.inPattern)
   192  		if err != test.outError {
   193  			t.Fatalf("Test %d: Expected %q got %q", i, test.outError, err)
   194  		}
   195  	}
   196  }
   197  
   198  func TestOpenAPI_SplitFields(t *testing.T) {
   199  	paths, captures, err := expandPattern("some/" + GenericNameRegex("a") + "/path" + OptionalParamRegex("e"))
   200  	if err != nil {
   201  		t.Fatal(err)
   202  	}
   203  
   204  	fields := map[string]*FieldSchema{
   205  		"a": {Description: "path"},
   206  		"b": {Description: "body"},
   207  		"c": {Description: "body"},
   208  		"d": {Description: "body"},
   209  		"e": {Description: "path"},
   210  		"f": {Description: "query", Query: true},
   211  	}
   212  
   213  	for index, path := range paths {
   214  		pathFields, queryFields, bodyFields := splitFields(fields, path, captures)
   215  
   216  		numPath := len(pathFields)
   217  		numQuery := len(queryFields)
   218  		numBody := len(bodyFields)
   219  		numExpectedDiscarded := 0
   220  		// The first path generated is expected to be the one omitting the optional parameter field "e"
   221  		if index == 0 {
   222  			numExpectedDiscarded = 1
   223  		}
   224  		l := len(fields)
   225  		if numPath+numQuery+numBody+numExpectedDiscarded != l {
   226  			t.Fatalf("split length error: %d + %d + %d + %d != %d", numPath, numQuery, numBody, numExpectedDiscarded, l)
   227  		}
   228  
   229  		for name, field := range pathFields {
   230  			if field.Description != "path" {
   231  				t.Fatalf("expected field %s to be in 'path', found in %s", name, field.Description)
   232  			}
   233  		}
   234  		for name, field := range queryFields {
   235  			if field.Description != "query" {
   236  				t.Fatalf("expected field %s to be in 'query', found in %s", name, field.Description)
   237  			}
   238  		}
   239  		for name, field := range bodyFields {
   240  			if field.Description != "body" {
   241  				t.Fatalf("expected field %s to be in 'body', found in %s", name, field.Description)
   242  			}
   243  		}
   244  	}
   245  }
   246  
   247  func TestOpenAPI_SpecialPaths(t *testing.T) {
   248  	tests := map[string]struct {
   249  		pattern                 string
   250  		rootPaths               []string
   251  		rootExpected            bool
   252  		unauthenticatedPaths    []string
   253  		unauthenticatedExpected bool
   254  	}{
   255  		"empty": {
   256  			pattern:                 "foo",
   257  			rootPaths:               []string{},
   258  			rootExpected:            false,
   259  			unauthenticatedPaths:    []string{},
   260  			unauthenticatedExpected: false,
   261  		},
   262  		"exact-match-unauthenticated": {
   263  			pattern:                 "foo",
   264  			rootPaths:               []string{},
   265  			rootExpected:            false,
   266  			unauthenticatedPaths:    []string{"foo"},
   267  			unauthenticatedExpected: true,
   268  		},
   269  		"exact-match-root": {
   270  			pattern:                 "foo",
   271  			rootPaths:               []string{"foo"},
   272  			rootExpected:            true,
   273  			unauthenticatedPaths:    []string{"bar"},
   274  			unauthenticatedExpected: false,
   275  		},
   276  		"asterisk-match-unauthenticated": {
   277  			pattern:                 "foo/bar",
   278  			rootPaths:               []string{"foo"},
   279  			rootExpected:            false,
   280  			unauthenticatedPaths:    []string{"foo/*"},
   281  			unauthenticatedExpected: true,
   282  		},
   283  		"asterisk-match-root": {
   284  			pattern:                 "foo/bar",
   285  			rootPaths:               []string{"foo/*"},
   286  			rootExpected:            true,
   287  			unauthenticatedPaths:    []string{"foo"},
   288  			unauthenticatedExpected: false,
   289  		},
   290  		"path-ends-with-slash": {
   291  			pattern:                 "foo/",
   292  			rootPaths:               []string{"foo/*"},
   293  			rootExpected:            true,
   294  			unauthenticatedPaths:    []string{"a", "b", "foo*"},
   295  			unauthenticatedExpected: true,
   296  		},
   297  		"asterisk-match-no-slash": {
   298  			pattern:                 "foo",
   299  			rootPaths:               []string{"foo*"},
   300  			rootExpected:            true,
   301  			unauthenticatedPaths:    []string{"a", "fo*"},
   302  			unauthenticatedExpected: true,
   303  		},
   304  		"multiple-root-paths": {
   305  			pattern:                 "foo/bar",
   306  			rootPaths:               []string{"a", "b", "foo/*"},
   307  			rootExpected:            true,
   308  			unauthenticatedPaths:    []string{"foo/baz/*"},
   309  			unauthenticatedExpected: false,
   310  		},
   311  		"plus-match-unauthenticated": {
   312  			pattern:                 "foo/bar/baz",
   313  			rootPaths:               []string{"foo/bar"},
   314  			rootExpected:            false,
   315  			unauthenticatedPaths:    []string{"foo/+/baz"},
   316  			unauthenticatedExpected: true,
   317  		},
   318  		"plus-match-root": {
   319  			pattern:                 "foo/bar/baz",
   320  			rootPaths:               []string{"foo/+/baz"},
   321  			rootExpected:            true,
   322  			unauthenticatedPaths:    []string{"foo/bar"},
   323  			unauthenticatedExpected: false,
   324  		},
   325  		"plus-and-asterisk": {
   326  			pattern:                 "foo/bar/baz/something",
   327  			rootPaths:               []string{"foo/+/baz/*"},
   328  			rootExpected:            true,
   329  			unauthenticatedPaths:    []string{"foo/+/baz*"},
   330  			unauthenticatedExpected: true,
   331  		},
   332  		"double-plus-good": {
   333  			pattern:                 "foo/bar/baz",
   334  			rootPaths:               []string{"foo/+/+"},
   335  			rootExpected:            true,
   336  			unauthenticatedPaths:    []string{"foo/bar"},
   337  			unauthenticatedExpected: false,
   338  		},
   339  	}
   340  	for name, test := range tests {
   341  		t.Run(name, func(t *testing.T) {
   342  			doc := NewOASDocument("version")
   343  			path := Path{
   344  				Pattern: test.pattern,
   345  			}
   346  			backend := &Backend{
   347  				PathsSpecial: &logical.Paths{
   348  					Root:            test.rootPaths,
   349  					Unauthenticated: test.unauthenticatedPaths,
   350  				},
   351  				BackendType: logical.TypeLogical,
   352  			}
   353  
   354  			if err := documentPath(&path, backend, "kv", doc); err != nil {
   355  				t.Fatal(err)
   356  			}
   357  
   358  			actual := doc.Paths["/"+test.pattern].Sudo
   359  			if actual != test.rootExpected {
   360  				t.Fatalf("Test (root): expected: %v; got: %v", test.rootExpected, actual)
   361  			}
   362  
   363  			actual = doc.Paths["/"+test.pattern].Unauthenticated
   364  			if actual != test.unauthenticatedExpected {
   365  				t.Fatalf("Test (unauth): expected: %v; got: %v", test.unauthenticatedExpected, actual)
   366  			}
   367  		})
   368  	}
   369  }
   370  
   371  func TestOpenAPI_Paths(t *testing.T) {
   372  	origDepth := deep.MaxDepth
   373  	defer func() { deep.MaxDepth = origDepth }()
   374  	deep.MaxDepth = 20
   375  
   376  	t.Run("Legacy callbacks", func(t *testing.T) {
   377  		p := &Path{
   378  			Pattern: "lookup/" + GenericNameRegex("id"),
   379  
   380  			Fields: map[string]*FieldSchema{
   381  				"id": {
   382  					Type:        TypeString,
   383  					Description: "My id parameter",
   384  				},
   385  				"token": {
   386  					Type:        TypeString,
   387  					Description: "My token",
   388  				},
   389  			},
   390  
   391  			Callbacks: map[logical.Operation]OperationFunc{
   392  				logical.ReadOperation:   nil,
   393  				logical.UpdateOperation: nil,
   394  			},
   395  
   396  			HelpSynopsis:    "Synopsis",
   397  			HelpDescription: "Description",
   398  		}
   399  
   400  		sp := &logical.Paths{
   401  			Root:            []string{},
   402  			Unauthenticated: []string{},
   403  		}
   404  		testPath(t, p, sp, expected("legacy"))
   405  	})
   406  
   407  	t.Run("Operations - All Operations", func(t *testing.T) {
   408  		p := &Path{
   409  			Pattern: "foo/" + GenericNameRegex("id"),
   410  			Fields: map[string]*FieldSchema{
   411  				"id": {
   412  					Type:        TypeString,
   413  					Description: "id path parameter",
   414  				},
   415  				"flavors": {
   416  					Type:        TypeCommaStringSlice,
   417  					Description: "the flavors",
   418  				},
   419  				"name": {
   420  					Type:        TypeNameString,
   421  					Default:     "Larry",
   422  					Description: "the name",
   423  				},
   424  				"age": {
   425  					Type:          TypeInt,
   426  					Description:   "the age",
   427  					AllowedValues: []interface{}{1, 2, 3},
   428  					Required:      true,
   429  					DisplayAttrs: &DisplayAttributes{
   430  						Name:      "Age",
   431  						Sensitive: true,
   432  						Group:     "Some Group",
   433  						Value:     7,
   434  					},
   435  				},
   436  				"x-abc-token": {
   437  					Type:          TypeHeader,
   438  					Description:   "a header value",
   439  					AllowedValues: []interface{}{"a", "b", "c"},
   440  				},
   441  				"maximum": {
   442  					Type:        TypeInt64,
   443  					Description: "a maximum value",
   444  				},
   445  				"format": {
   446  					Type:        TypeString,
   447  					Description: "a query param",
   448  					Query:       true,
   449  				},
   450  			},
   451  			HelpSynopsis:    "Synopsis",
   452  			HelpDescription: "Description",
   453  			Operations: map[logical.Operation]OperationHandler{
   454  				logical.ReadOperation: &PathOperation{
   455  					Summary:     "My Summary",
   456  					Description: "My Description",
   457  				},
   458  				logical.UpdateOperation: &PathOperation{
   459  					Summary:     "Update Summary",
   460  					Description: "Update Description",
   461  				},
   462  				logical.CreateOperation: &PathOperation{
   463  					Summary:     "Create Summary",
   464  					Description: "Create Description",
   465  				},
   466  				logical.ListOperation: &PathOperation{
   467  					Summary:     "List Summary",
   468  					Description: "List Description",
   469  				},
   470  				logical.DeleteOperation: &PathOperation{
   471  					Summary:     "This shouldn't show up",
   472  					Unpublished: true,
   473  				},
   474  			},
   475  			DisplayAttrs: &DisplayAttributes{
   476  				Navigation: true,
   477  			},
   478  		}
   479  
   480  		sp := &logical.Paths{
   481  			Root: []string{"foo*"},
   482  		}
   483  		testPath(t, p, sp, expected("operations"))
   484  	})
   485  
   486  	t.Run("Operations - List Only", func(t *testing.T) {
   487  		p := &Path{
   488  			Pattern: "foo/" + GenericNameRegex("id"),
   489  			Fields: map[string]*FieldSchema{
   490  				"id": {
   491  					Type:        TypeString,
   492  					Description: "id path parameter",
   493  				},
   494  				"flavors": {
   495  					Type:        TypeCommaStringSlice,
   496  					Description: "the flavors",
   497  				},
   498  				"name": {
   499  					Type:        TypeNameString,
   500  					Default:     "Larry",
   501  					Description: "the name",
   502  				},
   503  				"age": {
   504  					Type:          TypeInt,
   505  					Description:   "the age",
   506  					AllowedValues: []interface{}{1, 2, 3},
   507  					Required:      true,
   508  					DisplayAttrs: &DisplayAttributes{
   509  						Name:      "Age",
   510  						Sensitive: true,
   511  						Group:     "Some Group",
   512  						Value:     7,
   513  					},
   514  				},
   515  				"x-abc-token": {
   516  					Type:          TypeHeader,
   517  					Description:   "a header value",
   518  					AllowedValues: []interface{}{"a", "b", "c"},
   519  				},
   520  				"format": {
   521  					Type:        TypeString,
   522  					Description: "a query param",
   523  					Query:       true,
   524  				},
   525  			},
   526  			HelpSynopsis:    "Synopsis",
   527  			HelpDescription: "Description",
   528  			Operations: map[logical.Operation]OperationHandler{
   529  				logical.ListOperation: &PathOperation{
   530  					Summary:     "List Summary",
   531  					Description: "List Description",
   532  				},
   533  			},
   534  			DisplayAttrs: &DisplayAttributes{
   535  				Navigation: true,
   536  			},
   537  		}
   538  
   539  		sp := &logical.Paths{
   540  			Root: []string{"foo*"},
   541  		}
   542  		testPath(t, p, sp, expected("operations_list"))
   543  	})
   544  
   545  	t.Run("Responses", func(t *testing.T) {
   546  		p := &Path{
   547  			Pattern:         "foo",
   548  			HelpSynopsis:    "Synopsis",
   549  			HelpDescription: "Description",
   550  			Operations: map[logical.Operation]OperationHandler{
   551  				logical.ReadOperation: &PathOperation{
   552  					Summary:     "My Summary",
   553  					Description: "My Description",
   554  					Responses: map[int][]Response{
   555  						202: {{
   556  							Description: "Amazing",
   557  							Example: &logical.Response{
   558  								Data: map[string]interface{}{
   559  									"amount": 42,
   560  								},
   561  							},
   562  							Fields: map[string]*FieldSchema{
   563  								"field_a": {
   564  									Type:        TypeString,
   565  									Description: "field_a description",
   566  								},
   567  								"field_b": {
   568  									Type:        TypeBool,
   569  									Description: "field_b description",
   570  								},
   571  							},
   572  						}},
   573  					},
   574  				},
   575  				logical.DeleteOperation: &PathOperation{
   576  					Summary: "Delete stuff",
   577  				},
   578  			},
   579  		}
   580  
   581  		sp := &logical.Paths{
   582  			Unauthenticated: []string{"x", "y", "foo"},
   583  		}
   584  
   585  		testPath(t, p, sp, expected("responses"))
   586  	})
   587  }
   588  
   589  func TestOpenAPI_CustomDecoder(t *testing.T) {
   590  	p := &Path{
   591  		Pattern:      "foo",
   592  		HelpSynopsis: "Synopsis",
   593  		Operations: map[logical.Operation]OperationHandler{
   594  			logical.ReadOperation: &PathOperation{
   595  				Summary: "My Summary",
   596  				Responses: map[int][]Response{
   597  					100: {{
   598  						Description: "OK",
   599  						Example: &logical.Response{
   600  							Data: map[string]interface{}{
   601  								"foo": 42,
   602  							},
   603  						},
   604  					}},
   605  					200: {{
   606  						Description: "Good",
   607  						Example:     (*logical.Response)(nil),
   608  					}},
   609  					599: {{
   610  						Description: "Bad",
   611  					}},
   612  				},
   613  			},
   614  		},
   615  	}
   616  
   617  	docOrig := NewOASDocument("version")
   618  	err := documentPath(p, &Backend{BackendType: logical.TypeLogical}, "kv", docOrig)
   619  	if err != nil {
   620  		t.Fatal(err)
   621  	}
   622  
   623  	docJSON := mustJSONMarshal(t, docOrig)
   624  
   625  	var intermediate map[string]interface{}
   626  	if err := jsonutil.DecodeJSON(docJSON, &intermediate); err != nil {
   627  		t.Fatal(err)
   628  	}
   629  
   630  	docNew, err := NewOASDocumentFromMap(intermediate)
   631  	if err != nil {
   632  		t.Fatal(err)
   633  	}
   634  
   635  	docNewJSON := mustJSONMarshal(t, docNew)
   636  
   637  	if diff := deep.Equal(docJSON, docNewJSON); diff != nil {
   638  		t.Fatal(diff)
   639  	}
   640  }
   641  
   642  func TestOpenAPI_CleanResponse(t *testing.T) {
   643  	// Verify that an all-null input results in empty JSON
   644  	orig := &logical.Response{}
   645  
   646  	cr := cleanResponse(orig)
   647  
   648  	newJSON := mustJSONMarshal(t, cr)
   649  
   650  	if !bytes.Equal(newJSON, []byte("{}")) {
   651  		t.Fatalf("expected {}, got: %q", newJSON)
   652  	}
   653  
   654  	// Verify that all non-null inputs results in JSON that matches the marshalling of
   655  	// logical.Response. This will fail if logical.Response changes without a corresponding
   656  	// change to cleanResponse()
   657  	orig = &logical.Response{
   658  		Secret:    new(logical.Secret),
   659  		Auth:      new(logical.Auth),
   660  		Data:      map[string]interface{}{"foo": 42},
   661  		Redirect:  "foo",
   662  		Warnings:  []string{"foo"},
   663  		WrapInfo:  &wrapping.ResponseWrapInfo{Token: "foo"},
   664  		Headers:   map[string][]string{"foo": {"bar"}},
   665  		MountType: "mount",
   666  	}
   667  	origJSON := mustJSONMarshal(t, orig)
   668  
   669  	cr = cleanResponse(orig)
   670  
   671  	cleanJSON := mustJSONMarshal(t, cr)
   672  
   673  	if diff := deep.Equal(origJSON, cleanJSON); diff != nil {
   674  		t.Fatal(diff)
   675  	}
   676  }
   677  
   678  func TestOpenAPI_constructOperationID(t *testing.T) {
   679  	tests := map[string]struct {
   680  		path                string
   681  		pathIndex           int
   682  		pathAttributes      *DisplayAttributes
   683  		operation           logical.Operation
   684  		operationAttributes *DisplayAttributes
   685  		defaultPrefix       string
   686  		expected            string
   687  	}{
   688  		"empty": {
   689  			path:                "",
   690  			pathIndex:           0,
   691  			pathAttributes:      nil,
   692  			operation:           logical.Operation(""),
   693  			operationAttributes: nil,
   694  			defaultPrefix:       "",
   695  			expected:            "",
   696  		},
   697  		"simple-read": {
   698  			path:                "path/to/thing",
   699  			pathIndex:           0,
   700  			pathAttributes:      nil,
   701  			operation:           logical.ReadOperation,
   702  			operationAttributes: nil,
   703  			defaultPrefix:       "test",
   704  			expected:            "test-read-path-to-thing",
   705  		},
   706  		"simple-write": {
   707  			path:                "path/to/thing",
   708  			pathIndex:           0,
   709  			pathAttributes:      nil,
   710  			operation:           logical.UpdateOperation,
   711  			operationAttributes: nil,
   712  			defaultPrefix:       "test",
   713  			expected:            "test-write-path-to-thing",
   714  		},
   715  		"operation-verb": {
   716  			path:                "path/to/thing",
   717  			pathIndex:           0,
   718  			pathAttributes:      &DisplayAttributes{OperationVerb: "do-something"},
   719  			operation:           logical.UpdateOperation,
   720  			operationAttributes: nil,
   721  			defaultPrefix:       "test",
   722  			expected:            "do-something",
   723  		},
   724  		"operation-verb-override": {
   725  			path:                "path/to/thing",
   726  			pathIndex:           0,
   727  			pathAttributes:      &DisplayAttributes{OperationVerb: "do-something"},
   728  			operation:           logical.UpdateOperation,
   729  			operationAttributes: &DisplayAttributes{OperationVerb: "do-something-else"},
   730  			defaultPrefix:       "test",
   731  			expected:            "do-something-else",
   732  		},
   733  		"operation-prefix": {
   734  			path:                "path/to/thing",
   735  			pathIndex:           0,
   736  			pathAttributes:      &DisplayAttributes{OperationPrefix: "my-prefix"},
   737  			operation:           logical.UpdateOperation,
   738  			operationAttributes: nil,
   739  			defaultPrefix:       "test",
   740  			expected:            "my-prefix-write-path-to-thing",
   741  		},
   742  		"operation-prefix-override": {
   743  			path:                "path/to/thing",
   744  			pathIndex:           0,
   745  			pathAttributes:      &DisplayAttributes{OperationPrefix: "my-prefix"},
   746  			operation:           logical.UpdateOperation,
   747  			operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix"},
   748  			defaultPrefix:       "test",
   749  			expected:            "better-prefix-write-path-to-thing",
   750  		},
   751  		"operation-prefix-and-suffix": {
   752  			path:                "path/to/thing",
   753  			pathIndex:           0,
   754  			pathAttributes:      &DisplayAttributes{OperationPrefix: "my-prefix", OperationSuffix: "my-suffix"},
   755  			operation:           logical.UpdateOperation,
   756  			operationAttributes: nil,
   757  			defaultPrefix:       "test",
   758  			expected:            "my-prefix-write-my-suffix",
   759  		},
   760  		"operation-prefix-and-suffix-override": {
   761  			path:                "path/to/thing",
   762  			pathIndex:           0,
   763  			pathAttributes:      &DisplayAttributes{OperationPrefix: "my-prefix", OperationSuffix: "my-suffix"},
   764  			operation:           logical.UpdateOperation,
   765  			operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "better-suffix"},
   766  			defaultPrefix:       "test",
   767  			expected:            "better-prefix-write-better-suffix",
   768  		},
   769  		"operation-prefix-verb-suffix": {
   770  			path:                "path/to/thing",
   771  			pathIndex:           0,
   772  			pathAttributes:      &DisplayAttributes{OperationPrefix: "my-prefix", OperationSuffix: "my-suffix", OperationVerb: "Create"},
   773  			operation:           logical.UpdateOperation,
   774  			operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "better-suffix"},
   775  			defaultPrefix:       "test",
   776  			expected:            "better-prefix-create-better-suffix",
   777  		},
   778  		"operation-prefix-verb-suffix-override": {
   779  			path:                "path/to/thing",
   780  			pathIndex:           0,
   781  			pathAttributes:      &DisplayAttributes{OperationPrefix: "my-prefix", OperationSuffix: "my-suffix", OperationVerb: "Create"},
   782  			operation:           logical.UpdateOperation,
   783  			operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "better-suffix", OperationVerb: "Login"},
   784  			defaultPrefix:       "test",
   785  			expected:            "better-prefix-login-better-suffix",
   786  		},
   787  		"operation-prefix-verb": {
   788  			path:                "path/to/thing",
   789  			pathIndex:           0,
   790  			pathAttributes:      nil,
   791  			operation:           logical.UpdateOperation,
   792  			operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationVerb: "Login"},
   793  			defaultPrefix:       "test",
   794  			expected:            "better-prefix-login",
   795  		},
   796  		"operation-verb-suffix": {
   797  			path:                "path/to/thing",
   798  			pathIndex:           0,
   799  			pathAttributes:      nil,
   800  			operation:           logical.UpdateOperation,
   801  			operationAttributes: &DisplayAttributes{OperationVerb: "Login", OperationSuffix: "better-suffix"},
   802  			defaultPrefix:       "test",
   803  			expected:            "login-better-suffix",
   804  		},
   805  		"pipe-delimited-suffix-0": {
   806  			path:                "path/to/thing",
   807  			pathIndex:           0,
   808  			pathAttributes:      nil,
   809  			operation:           logical.UpdateOperation,
   810  			operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "suffix0|suffix1"},
   811  			defaultPrefix:       "test",
   812  			expected:            "better-prefix-write-suffix0",
   813  		},
   814  		"pipe-delimited-suffix-1": {
   815  			path:                "path/to/thing",
   816  			pathIndex:           1,
   817  			pathAttributes:      nil,
   818  			operation:           logical.UpdateOperation,
   819  			operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "suffix0|suffix1"},
   820  			defaultPrefix:       "test",
   821  			expected:            "better-prefix-write-suffix1",
   822  		},
   823  		"pipe-delimited-suffix-2-fallback": {
   824  			path:                "path/to/thing",
   825  			pathIndex:           2,
   826  			pathAttributes:      nil,
   827  			operation:           logical.UpdateOperation,
   828  			operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "suffix0|suffix1"},
   829  			defaultPrefix:       "test",
   830  			expected:            "better-prefix-write-path-to-thing",
   831  		},
   832  	}
   833  
   834  	for name, test := range tests {
   835  		name, test := name, test
   836  		t.Run(name, func(t *testing.T) {
   837  			t.Parallel()
   838  			actual := constructOperationID(
   839  				test.path,
   840  				test.pathIndex,
   841  				test.pathAttributes,
   842  				test.operation,
   843  				test.operationAttributes,
   844  				test.defaultPrefix,
   845  			)
   846  			if actual != test.expected {
   847  				t.Fatalf("expected: %s; got: %s", test.expected, actual)
   848  			}
   849  		})
   850  	}
   851  }
   852  
   853  func TestOpenAPI_hyphenatedToTitleCase(t *testing.T) {
   854  	tests := map[string]struct {
   855  		in       string
   856  		expected string
   857  	}{
   858  		"simple": {
   859  			in:       "test",
   860  			expected: "Test",
   861  		},
   862  		"two-words": {
   863  			in:       "two-words",
   864  			expected: "TwoWords",
   865  		},
   866  		"three-words": {
   867  			in:       "one-two-three",
   868  			expected: "OneTwoThree",
   869  		},
   870  		"not-hyphenated": {
   871  			in:       "something_like_this",
   872  			expected: "Something_like_this",
   873  		},
   874  	}
   875  
   876  	for name, test := range tests {
   877  		name, test := name, test
   878  		t.Run(name, func(t *testing.T) {
   879  			t.Parallel()
   880  			actual := hyphenatedToTitleCase(test.in)
   881  			if actual != test.expected {
   882  				t.Fatalf("expected: %s; got: %s", test.expected, actual)
   883  			}
   884  		})
   885  	}
   886  }
   887  
   888  func testPath(t *testing.T, path *Path, sp *logical.Paths, expectedJSON string) {
   889  	t.Helper()
   890  
   891  	doc := NewOASDocument("dummyversion")
   892  	if err := documentPath(path, &Backend{
   893  		PathsSpecial: sp,
   894  		BackendType:  logical.TypeLogical,
   895  	}, "kv", doc); err != nil {
   896  		t.Fatal(err)
   897  	}
   898  	doc.CreateOperationIDs("")
   899  
   900  	docJSON, err := json.MarshalIndent(doc, "", "  ")
   901  	if err != nil {
   902  		t.Fatal(err)
   903  	}
   904  	// Compare json by first decoding, then comparing with a deep equality check.
   905  	var expected, actual interface{}
   906  	if err := jsonutil.DecodeJSON(docJSON, &actual); err != nil {
   907  		t.Fatal(err)
   908  	}
   909  
   910  	if err := jsonutil.DecodeJSON([]byte(expectedJSON), &expected); err != nil {
   911  		t.Fatal(err)
   912  	}
   913  
   914  	if diff := deep.Equal(actual, expected); diff != nil {
   915  		// fmt.Println(string(docJSON)) // uncomment to debug generated JSON (very helpful when fixing tests)
   916  		t.Fatal(diff)
   917  	}
   918  }
   919  
   920  func getPathOp(pi *OASPathItem, op string) *OASOperation {
   921  	switch op {
   922  	case "get":
   923  		return pi.Get
   924  	case "post":
   925  		return pi.Post
   926  	case "delete":
   927  		return pi.Delete
   928  	default:
   929  		panic("unexpected operation: " + op)
   930  	}
   931  }
   932  
   933  func expected(name string) string {
   934  	data, err := ioutil.ReadFile(filepath.Join("testdata", name+".json"))
   935  	if err != nil {
   936  		panic(err)
   937  	}
   938  
   939  	content := strings.Replace(string(data), "<vault_version>", "dummyversion", 1)
   940  
   941  	return content
   942  }
   943  
   944  func mustJSONMarshal(t *testing.T, data interface{}) []byte {
   945  	j, err := json.MarshalIndent(data, "", "  ")
   946  	if err != nil {
   947  		t.Fatal(err)
   948  	}
   949  	return j
   950  }