926 lines
27 KiB
Go
926 lines
27 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package framework
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io/ioutil"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/go-test/deep"
|
|
"github.com/hashicorp/vault/sdk/helper/jsonutil"
|
|
"github.com/hashicorp/vault/sdk/helper/wrapping"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
func TestOpenAPI_Regex(t *testing.T) {
|
|
t.Run("Path fields", func(t *testing.T) {
|
|
input := `/foo/bar/{inner}/baz/{outer}`
|
|
|
|
matches := pathFieldsRe.FindAllStringSubmatch(input, -1)
|
|
|
|
exp1 := "inner"
|
|
exp2 := "outer"
|
|
if matches[0][1] != exp1 || matches[1][1] != exp2 {
|
|
t.Fatalf("Capture error. Expected %s and %s, got %v", exp1, exp2, matches)
|
|
}
|
|
|
|
input = `/foo/bar/inner/baz/outer`
|
|
matches = pathFieldsRe.FindAllStringSubmatch(input, -1)
|
|
|
|
if matches != nil {
|
|
t.Fatalf("Expected nil match (%s), got %+v", input, matches)
|
|
}
|
|
})
|
|
t.Run("Filtering", func(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
regex *regexp.Regexp
|
|
output string
|
|
}{
|
|
{
|
|
input: `abcde`,
|
|
regex: wsRe,
|
|
output: "abcde",
|
|
},
|
|
{
|
|
input: ` a b cd e `,
|
|
regex: wsRe,
|
|
output: "abcde",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
result := test.regex.ReplaceAllString(test.input, "")
|
|
if result != test.output {
|
|
t.Fatalf("Clean Regex error (%s). Expected %s, got %s", test.input, test.output, result)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestOpenAPI_ExpandPattern(t *testing.T) {
|
|
tests := []struct {
|
|
inPattern string
|
|
outPathlets []string
|
|
}{
|
|
// A simple string without regexp metacharacters passes through as is
|
|
{"rekey/backup", []string{"rekey/backup"}},
|
|
// A trailing regexp anchor metacharacter is removed
|
|
{"rekey/backup$", []string{"rekey/backup"}},
|
|
// As is a leading one
|
|
{"^rekey/backup", []string{"rekey/backup"}},
|
|
// Named capture groups become OpenAPI parameters
|
|
{"auth/(?P<path>.+?)/tune$", []string{"auth/{path}/tune"}},
|
|
{"auth/(?P<path>.+?)/tune/(?P<more>.*?)$", []string{"auth/{path}/tune/{more}"}},
|
|
// Even if the capture group contains very complex regexp structure inside it
|
|
{"something/(?P<something>(a|b(c|d))|e+|f{1,3}[ghi-k]?.*)", []string{"something/{something}"}},
|
|
// A question-mark results in a result without and with the optional path part
|
|
{"tools/hash(/(?P<urlalgorithm>.+))?", []string{
|
|
"tools/hash",
|
|
"tools/hash/{urlalgorithm}",
|
|
}},
|
|
// Multiple question-marks evaluate each possible combination
|
|
{"(leases/)?renew(/(?P<url_lease_id>.+))?", []string{
|
|
"leases/renew",
|
|
"leases/renew/{url_lease_id}",
|
|
"renew",
|
|
"renew/{url_lease_id}",
|
|
}},
|
|
// GenericNameRegex is one particular way of writing a named capture group, so behaves the same
|
|
{`config/ui/headers/` + GenericNameRegex("header"), []string{"config/ui/headers/{header}"}},
|
|
// The question-mark behaviour is still works when the question-mark is directly applied to a named capture group
|
|
{`leases/lookup/(?P<prefix>.+?)?`, []string{
|
|
"leases/lookup/",
|
|
"leases/lookup/{prefix}",
|
|
}},
|
|
// Optional trailing slashes at the end of the path get stripped - even if appearing deep inside an alternation
|
|
{`(raw/?$|raw/(?P<path>.+))`, []string{
|
|
"raw",
|
|
"raw/{path}",
|
|
}},
|
|
// OptionalParamRegex is also another way of writing a named capture group, that is optional
|
|
{"lookup" + OptionalParamRegex("urltoken"), []string{
|
|
"lookup",
|
|
"lookup/{urltoken}",
|
|
}},
|
|
// Optional trailign slashes at the end of the path get stripped in simpler cases too
|
|
{"roles/?$", []string{
|
|
"roles",
|
|
}},
|
|
{"roles/?", []string{
|
|
"roles",
|
|
}},
|
|
// Non-optional trailing slashes remain... although don't do this, it breaks HelpOperation!
|
|
// (Existing real examples of this pattern being fixed via https://github.com/hashicorp/vault/pull/18571)
|
|
{"accessors/$", []string{
|
|
"accessors/",
|
|
}},
|
|
// GenericNameRegex and OptionalParamRegex still work when concatenated
|
|
{"verify/" + GenericNameRegex("name") + OptionalParamRegex("urlalgorithm"), []string{
|
|
"verify/{name}",
|
|
"verify/{name}/{urlalgorithm}",
|
|
}},
|
|
// Named capture groups that specify enum-like parameters work as expected
|
|
{"^plugins/catalog/(?P<type>auth|database|secret)/(?P<name>.+)$", []string{
|
|
"plugins/catalog/{type}/{name}",
|
|
}},
|
|
{"^plugins/catalog/(?P<type>auth|database|secret)/?$", []string{
|
|
"plugins/catalog/{type}",
|
|
}},
|
|
// Alternations between various literal path segments work
|
|
{"(pathOne|pathTwo)/", []string{"pathOne/", "pathTwo/"}},
|
|
{"(pathOne|pathTwo)/" + GenericNameRegex("name"), []string{"pathOne/{name}", "pathTwo/{name}"}},
|
|
{
|
|
"(pathOne|path-2|Path_3)/" + GenericNameRegex("name"),
|
|
[]string{"Path_3/{name}", "path-2/{name}", "pathOne/{name}"},
|
|
},
|
|
// They still work when combined with GenericNameWithAtRegex
|
|
{"(creds|sts)/" + GenericNameWithAtRegex("name"), []string{
|
|
"creds/{name}",
|
|
"sts/{name}",
|
|
}},
|
|
// And when they're somewhere other than the start of the pattern
|
|
{"keys/generate/(internal|exported|kms)", []string{
|
|
"keys/generate/exported",
|
|
"keys/generate/internal",
|
|
"keys/generate/kms",
|
|
}},
|
|
// If a plugin author makes their list operation support both singular and plural forms, the OpenAPI notices
|
|
{"rolesets?/?", []string{"roleset", "rolesets"}},
|
|
// Complex nested alternation and question-marks are correctly interpreted
|
|
{"crl(/pem|/delta(/pem)?)?", []string{"crl", "crl/delta", "crl/delta/pem", "crl/pem"}},
|
|
}
|
|
|
|
for i, test := range tests {
|
|
out, err := expandPattern(test.inPattern)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sort.Strings(out)
|
|
if !reflect.DeepEqual(out, test.outPathlets) {
|
|
t.Fatalf("Test %d: Expected %v got %v", i, test.outPathlets, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOpenAPI_ExpandPattern_ReturnsError(t *testing.T) {
|
|
tests := []struct {
|
|
inPattern string
|
|
outError error
|
|
}{
|
|
// None of these regexp constructs are allowed outside of named capture groups
|
|
{"[a-z]", errUnsupportableRegexpOperationForOpenAPI},
|
|
{".", errUnsupportableRegexpOperationForOpenAPI},
|
|
{"a+", errUnsupportableRegexpOperationForOpenAPI},
|
|
{"a*", errUnsupportableRegexpOperationForOpenAPI},
|
|
// So this pattern, which is a combination of two of the above isn't either - this pattern occurs in the KV
|
|
// secrets engine for its catch-all error handler, which provides a helpful hint to people treating a KV v2 as
|
|
// a KV v1.
|
|
{".*", errUnsupportableRegexpOperationForOpenAPI},
|
|
}
|
|
|
|
for i, test := range tests {
|
|
_, err := expandPattern(test.inPattern)
|
|
if err != test.outError {
|
|
t.Fatalf("Test %d: Expected %q got %q", i, test.outError, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOpenAPI_SplitFields(t *testing.T) {
|
|
fields := map[string]*FieldSchema{
|
|
"a": {Description: "path"},
|
|
"b": {Description: "body"},
|
|
"c": {Description: "body"},
|
|
"d": {Description: "body"},
|
|
"e": {Description: "path"},
|
|
}
|
|
|
|
pathFields, bodyFields := splitFields(fields, "some/{a}/path/{e}")
|
|
|
|
lp := len(pathFields)
|
|
lb := len(bodyFields)
|
|
l := len(fields)
|
|
if lp+lb != l {
|
|
t.Fatalf("split length error: %d + %d != %d", lp, lb, l)
|
|
}
|
|
|
|
for name, field := range pathFields {
|
|
if field.Description != "path" {
|
|
t.Fatalf("expected field %s to be in 'path', found in %s", name, field.Description)
|
|
}
|
|
}
|
|
for name, field := range bodyFields {
|
|
if field.Description != "body" {
|
|
t.Fatalf("expected field %s to be in 'body', found in %s", name, field.Description)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOpenAPI_SpecialPaths(t *testing.T) {
|
|
tests := map[string]struct {
|
|
pattern string
|
|
rootPaths []string
|
|
rootExpected bool
|
|
unauthenticatedPaths []string
|
|
unauthenticatedExpected bool
|
|
}{
|
|
"empty": {
|
|
pattern: "foo",
|
|
rootPaths: []string{},
|
|
rootExpected: false,
|
|
unauthenticatedPaths: []string{},
|
|
unauthenticatedExpected: false,
|
|
},
|
|
"exact-match-unauthenticated": {
|
|
pattern: "foo",
|
|
rootPaths: []string{},
|
|
rootExpected: false,
|
|
unauthenticatedPaths: []string{"foo"},
|
|
unauthenticatedExpected: true,
|
|
},
|
|
"exact-match-root": {
|
|
pattern: "foo",
|
|
rootPaths: []string{"foo"},
|
|
rootExpected: true,
|
|
unauthenticatedPaths: []string{"bar"},
|
|
unauthenticatedExpected: false,
|
|
},
|
|
"asterisk-match-unauthenticated": {
|
|
pattern: "foo/bar",
|
|
rootPaths: []string{"foo"},
|
|
rootExpected: false,
|
|
unauthenticatedPaths: []string{"foo/*"},
|
|
unauthenticatedExpected: true,
|
|
},
|
|
"asterisk-match-root": {
|
|
pattern: "foo/bar",
|
|
rootPaths: []string{"foo/*"},
|
|
rootExpected: true,
|
|
unauthenticatedPaths: []string{"foo"},
|
|
unauthenticatedExpected: false,
|
|
},
|
|
"path-ends-with-slash": {
|
|
pattern: "foo/",
|
|
rootPaths: []string{"foo/*"},
|
|
rootExpected: true,
|
|
unauthenticatedPaths: []string{"a", "b", "foo*"},
|
|
unauthenticatedExpected: true,
|
|
},
|
|
"asterisk-match-no-slash": {
|
|
pattern: "foo",
|
|
rootPaths: []string{"foo*"},
|
|
rootExpected: true,
|
|
unauthenticatedPaths: []string{"a", "fo*"},
|
|
unauthenticatedExpected: true,
|
|
},
|
|
"multiple-root-paths": {
|
|
pattern: "foo/bar",
|
|
rootPaths: []string{"a", "b", "foo/*"},
|
|
rootExpected: true,
|
|
unauthenticatedPaths: []string{"foo/baz/*"},
|
|
unauthenticatedExpected: false,
|
|
},
|
|
"plus-match-unauthenticated": {
|
|
pattern: "foo/bar/baz",
|
|
rootPaths: []string{"foo/bar"},
|
|
rootExpected: false,
|
|
unauthenticatedPaths: []string{"foo/+/baz"},
|
|
unauthenticatedExpected: true,
|
|
},
|
|
"plus-match-root": {
|
|
pattern: "foo/bar/baz",
|
|
rootPaths: []string{"foo/+/baz"},
|
|
rootExpected: true,
|
|
unauthenticatedPaths: []string{"foo/bar"},
|
|
unauthenticatedExpected: false,
|
|
},
|
|
"plus-and-asterisk": {
|
|
pattern: "foo/bar/baz/something",
|
|
rootPaths: []string{"foo/+/baz/*"},
|
|
rootExpected: true,
|
|
unauthenticatedPaths: []string{"foo/+/baz*"},
|
|
unauthenticatedExpected: true,
|
|
},
|
|
"double-plus-good": {
|
|
pattern: "foo/bar/baz",
|
|
rootPaths: []string{"foo/+/+"},
|
|
rootExpected: true,
|
|
unauthenticatedPaths: []string{"foo/bar"},
|
|
unauthenticatedExpected: false,
|
|
},
|
|
}
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
doc := NewOASDocument("version")
|
|
path := Path{
|
|
Pattern: test.pattern,
|
|
}
|
|
specialPaths := &logical.Paths{
|
|
Root: test.rootPaths,
|
|
Unauthenticated: test.unauthenticatedPaths,
|
|
}
|
|
|
|
if err := documentPath(&path, specialPaths, "kv", logical.TypeLogical, doc); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
actual := doc.Paths["/"+test.pattern].Sudo
|
|
if actual != test.rootExpected {
|
|
t.Fatalf("Test (root): expected: %v; got: %v", test.rootExpected, actual)
|
|
}
|
|
|
|
actual = doc.Paths["/"+test.pattern].Unauthenticated
|
|
if actual != test.unauthenticatedExpected {
|
|
t.Fatalf("Test (unauth): expected: %v; got: %v", test.unauthenticatedExpected, actual)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOpenAPI_Paths(t *testing.T) {
|
|
origDepth := deep.MaxDepth
|
|
defer func() { deep.MaxDepth = origDepth }()
|
|
deep.MaxDepth = 20
|
|
|
|
t.Run("Legacy callbacks", func(t *testing.T) {
|
|
p := &Path{
|
|
Pattern: "lookup/" + GenericNameRegex("id"),
|
|
|
|
Fields: map[string]*FieldSchema{
|
|
"id": {
|
|
Type: TypeString,
|
|
Description: "My id parameter",
|
|
},
|
|
"token": {
|
|
Type: TypeString,
|
|
Description: "My token",
|
|
},
|
|
},
|
|
|
|
Callbacks: map[logical.Operation]OperationFunc{
|
|
logical.ReadOperation: nil,
|
|
logical.UpdateOperation: nil,
|
|
},
|
|
|
|
HelpSynopsis: "Synopsis",
|
|
HelpDescription: "Description",
|
|
}
|
|
|
|
sp := &logical.Paths{
|
|
Root: []string{},
|
|
Unauthenticated: []string{},
|
|
}
|
|
testPath(t, p, sp, expected("legacy"))
|
|
})
|
|
|
|
t.Run("Operations - All Operations", func(t *testing.T) {
|
|
p := &Path{
|
|
Pattern: "foo/" + GenericNameRegex("id"),
|
|
Fields: map[string]*FieldSchema{
|
|
"id": {
|
|
Type: TypeString,
|
|
Description: "id path parameter",
|
|
},
|
|
"flavors": {
|
|
Type: TypeCommaStringSlice,
|
|
Description: "the flavors",
|
|
},
|
|
"name": {
|
|
Type: TypeNameString,
|
|
Default: "Larry",
|
|
Description: "the name",
|
|
},
|
|
"age": {
|
|
Type: TypeInt,
|
|
Description: "the age",
|
|
AllowedValues: []interface{}{1, 2, 3},
|
|
Required: true,
|
|
DisplayAttrs: &DisplayAttributes{
|
|
Name: "Age",
|
|
Sensitive: true,
|
|
Group: "Some Group",
|
|
Value: 7,
|
|
},
|
|
},
|
|
"x-abc-token": {
|
|
Type: TypeHeader,
|
|
Description: "a header value",
|
|
AllowedValues: []interface{}{"a", "b", "c"},
|
|
},
|
|
"maximum": {
|
|
Type: TypeInt64,
|
|
Description: "a maximum value",
|
|
},
|
|
"format": {
|
|
Type: TypeString,
|
|
Description: "a query param",
|
|
Query: true,
|
|
},
|
|
},
|
|
HelpSynopsis: "Synopsis",
|
|
HelpDescription: "Description",
|
|
Operations: map[logical.Operation]OperationHandler{
|
|
logical.ReadOperation: &PathOperation{
|
|
Summary: "My Summary",
|
|
Description: "My Description",
|
|
},
|
|
logical.UpdateOperation: &PathOperation{
|
|
Summary: "Update Summary",
|
|
Description: "Update Description",
|
|
},
|
|
logical.CreateOperation: &PathOperation{
|
|
Summary: "Create Summary",
|
|
Description: "Create Description",
|
|
},
|
|
logical.ListOperation: &PathOperation{
|
|
Summary: "List Summary",
|
|
Description: "List Description",
|
|
},
|
|
logical.DeleteOperation: &PathOperation{
|
|
Summary: "This shouldn't show up",
|
|
Unpublished: true,
|
|
},
|
|
},
|
|
DisplayAttrs: &DisplayAttributes{
|
|
Navigation: true,
|
|
},
|
|
}
|
|
|
|
sp := &logical.Paths{
|
|
Root: []string{"foo*"},
|
|
}
|
|
testPath(t, p, sp, expected("operations"))
|
|
})
|
|
|
|
t.Run("Operations - List Only", func(t *testing.T) {
|
|
p := &Path{
|
|
Pattern: "foo/" + GenericNameRegex("id"),
|
|
Fields: map[string]*FieldSchema{
|
|
"id": {
|
|
Type: TypeString,
|
|
Description: "id path parameter",
|
|
},
|
|
"flavors": {
|
|
Type: TypeCommaStringSlice,
|
|
Description: "the flavors",
|
|
},
|
|
"name": {
|
|
Type: TypeNameString,
|
|
Default: "Larry",
|
|
Description: "the name",
|
|
},
|
|
"age": {
|
|
Type: TypeInt,
|
|
Description: "the age",
|
|
AllowedValues: []interface{}{1, 2, 3},
|
|
Required: true,
|
|
DisplayAttrs: &DisplayAttributes{
|
|
Name: "Age",
|
|
Sensitive: true,
|
|
Group: "Some Group",
|
|
Value: 7,
|
|
},
|
|
},
|
|
"x-abc-token": {
|
|
Type: TypeHeader,
|
|
Description: "a header value",
|
|
AllowedValues: []interface{}{"a", "b", "c"},
|
|
},
|
|
"format": {
|
|
Type: TypeString,
|
|
Description: "a query param",
|
|
Query: true,
|
|
},
|
|
},
|
|
HelpSynopsis: "Synopsis",
|
|
HelpDescription: "Description",
|
|
Operations: map[logical.Operation]OperationHandler{
|
|
logical.ListOperation: &PathOperation{
|
|
Summary: "List Summary",
|
|
Description: "List Description",
|
|
},
|
|
},
|
|
DisplayAttrs: &DisplayAttributes{
|
|
Navigation: true,
|
|
},
|
|
}
|
|
|
|
sp := &logical.Paths{
|
|
Root: []string{"foo*"},
|
|
}
|
|
testPath(t, p, sp, expected("operations_list"))
|
|
})
|
|
|
|
t.Run("Responses", func(t *testing.T) {
|
|
p := &Path{
|
|
Pattern: "foo",
|
|
HelpSynopsis: "Synopsis",
|
|
HelpDescription: "Description",
|
|
Operations: map[logical.Operation]OperationHandler{
|
|
logical.ReadOperation: &PathOperation{
|
|
Summary: "My Summary",
|
|
Description: "My Description",
|
|
Responses: map[int][]Response{
|
|
202: {{
|
|
Description: "Amazing",
|
|
Example: &logical.Response{
|
|
Data: map[string]interface{}{
|
|
"amount": 42,
|
|
},
|
|
},
|
|
Fields: map[string]*FieldSchema{
|
|
"field_a": {
|
|
Type: TypeString,
|
|
Description: "field_a description",
|
|
},
|
|
"field_b": {
|
|
Type: TypeBool,
|
|
Description: "field_b description",
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
logical.DeleteOperation: &PathOperation{
|
|
Summary: "Delete stuff",
|
|
},
|
|
},
|
|
}
|
|
|
|
sp := &logical.Paths{
|
|
Unauthenticated: []string{"x", "y", "foo"},
|
|
}
|
|
|
|
testPath(t, p, sp, expected("responses"))
|
|
})
|
|
}
|
|
|
|
func TestOpenAPI_CustomDecoder(t *testing.T) {
|
|
p := &Path{
|
|
Pattern: "foo",
|
|
HelpSynopsis: "Synopsis",
|
|
Operations: map[logical.Operation]OperationHandler{
|
|
logical.ReadOperation: &PathOperation{
|
|
Summary: "My Summary",
|
|
Responses: map[int][]Response{
|
|
100: {{
|
|
Description: "OK",
|
|
Example: &logical.Response{
|
|
Data: map[string]interface{}{
|
|
"foo": 42,
|
|
},
|
|
},
|
|
}},
|
|
200: {{
|
|
Description: "Good",
|
|
Example: (*logical.Response)(nil),
|
|
}},
|
|
599: {{
|
|
Description: "Bad",
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
docOrig := NewOASDocument("version")
|
|
err := documentPath(p, nil, "kv", logical.TypeLogical, docOrig)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
docJSON := mustJSONMarshal(t, docOrig)
|
|
|
|
var intermediate map[string]interface{}
|
|
if err := jsonutil.DecodeJSON(docJSON, &intermediate); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
docNew, err := NewOASDocumentFromMap(intermediate)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
docNewJSON := mustJSONMarshal(t, docNew)
|
|
|
|
if diff := deep.Equal(docJSON, docNewJSON); diff != nil {
|
|
t.Fatal(diff)
|
|
}
|
|
}
|
|
|
|
func TestOpenAPI_CleanResponse(t *testing.T) {
|
|
// Verify that an all-null input results in empty JSON
|
|
orig := &logical.Response{}
|
|
|
|
cr := cleanResponse(orig)
|
|
|
|
newJSON := mustJSONMarshal(t, cr)
|
|
|
|
if !bytes.Equal(newJSON, []byte("{}")) {
|
|
t.Fatalf("expected {}, got: %q", newJSON)
|
|
}
|
|
|
|
// Verify that all non-null inputs results in JSON that matches the marshalling of
|
|
// logical.Response. This will fail if logical.Response changes without a corresponding
|
|
// change to cleanResponse()
|
|
orig = &logical.Response{
|
|
Secret: new(logical.Secret),
|
|
Auth: new(logical.Auth),
|
|
Data: map[string]interface{}{"foo": 42},
|
|
Redirect: "foo",
|
|
Warnings: []string{"foo"},
|
|
WrapInfo: &wrapping.ResponseWrapInfo{Token: "foo"},
|
|
Headers: map[string][]string{"foo": {"bar"}},
|
|
}
|
|
origJSON := mustJSONMarshal(t, orig)
|
|
|
|
cr = cleanResponse(orig)
|
|
|
|
cleanJSON := mustJSONMarshal(t, cr)
|
|
|
|
if diff := deep.Equal(origJSON, cleanJSON); diff != nil {
|
|
t.Fatal(diff)
|
|
}
|
|
}
|
|
|
|
func TestOpenAPI_constructOperationID(t *testing.T) {
|
|
tests := map[string]struct {
|
|
path string
|
|
pathIndex int
|
|
pathAttributes *DisplayAttributes
|
|
operation logical.Operation
|
|
operationAttributes *DisplayAttributes
|
|
defaultPrefix string
|
|
expected string
|
|
}{
|
|
"empty": {
|
|
path: "",
|
|
pathIndex: 0,
|
|
pathAttributes: nil,
|
|
operation: logical.Operation(""),
|
|
operationAttributes: nil,
|
|
defaultPrefix: "",
|
|
expected: "",
|
|
},
|
|
"simple-read": {
|
|
path: "path/to/thing",
|
|
pathIndex: 0,
|
|
pathAttributes: nil,
|
|
operation: logical.ReadOperation,
|
|
operationAttributes: nil,
|
|
defaultPrefix: "test",
|
|
expected: "test-read-path-to-thing",
|
|
},
|
|
"simple-write": {
|
|
path: "path/to/thing",
|
|
pathIndex: 0,
|
|
pathAttributes: nil,
|
|
operation: logical.UpdateOperation,
|
|
operationAttributes: nil,
|
|
defaultPrefix: "test",
|
|
expected: "test-write-path-to-thing",
|
|
},
|
|
"operation-verb": {
|
|
path: "path/to/thing",
|
|
pathIndex: 0,
|
|
pathAttributes: &DisplayAttributes{OperationVerb: "do-something"},
|
|
operation: logical.UpdateOperation,
|
|
operationAttributes: nil,
|
|
defaultPrefix: "test",
|
|
expected: "do-something",
|
|
},
|
|
"operation-verb-override": {
|
|
path: "path/to/thing",
|
|
pathIndex: 0,
|
|
pathAttributes: &DisplayAttributes{OperationVerb: "do-something"},
|
|
operation: logical.UpdateOperation,
|
|
operationAttributes: &DisplayAttributes{OperationVerb: "do-something-else"},
|
|
defaultPrefix: "test",
|
|
expected: "do-something-else",
|
|
},
|
|
"operation-prefix": {
|
|
path: "path/to/thing",
|
|
pathIndex: 0,
|
|
pathAttributes: &DisplayAttributes{OperationPrefix: "my-prefix"},
|
|
operation: logical.UpdateOperation,
|
|
operationAttributes: nil,
|
|
defaultPrefix: "test",
|
|
expected: "my-prefix-write-path-to-thing",
|
|
},
|
|
"operation-prefix-override": {
|
|
path: "path/to/thing",
|
|
pathIndex: 0,
|
|
pathAttributes: &DisplayAttributes{OperationPrefix: "my-prefix"},
|
|
operation: logical.UpdateOperation,
|
|
operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix"},
|
|
defaultPrefix: "test",
|
|
expected: "better-prefix-write-path-to-thing",
|
|
},
|
|
"operation-prefix-and-suffix": {
|
|
path: "path/to/thing",
|
|
pathIndex: 0,
|
|
pathAttributes: &DisplayAttributes{OperationPrefix: "my-prefix", OperationSuffix: "my-suffix"},
|
|
operation: logical.UpdateOperation,
|
|
operationAttributes: nil,
|
|
defaultPrefix: "test",
|
|
expected: "my-prefix-write-my-suffix",
|
|
},
|
|
"operation-prefix-and-suffix-override": {
|
|
path: "path/to/thing",
|
|
pathIndex: 0,
|
|
pathAttributes: &DisplayAttributes{OperationPrefix: "my-prefix", OperationSuffix: "my-suffix"},
|
|
operation: logical.UpdateOperation,
|
|
operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "better-suffix"},
|
|
defaultPrefix: "test",
|
|
expected: "better-prefix-write-better-suffix",
|
|
},
|
|
"operation-prefix-verb-suffix": {
|
|
path: "path/to/thing",
|
|
pathIndex: 0,
|
|
pathAttributes: &DisplayAttributes{OperationPrefix: "my-prefix", OperationSuffix: "my-suffix", OperationVerb: "Create"},
|
|
operation: logical.UpdateOperation,
|
|
operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "better-suffix"},
|
|
defaultPrefix: "test",
|
|
expected: "better-prefix-create-better-suffix",
|
|
},
|
|
"operation-prefix-verb-suffix-override": {
|
|
path: "path/to/thing",
|
|
pathIndex: 0,
|
|
pathAttributes: &DisplayAttributes{OperationPrefix: "my-prefix", OperationSuffix: "my-suffix", OperationVerb: "Create"},
|
|
operation: logical.UpdateOperation,
|
|
operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "better-suffix", OperationVerb: "Login"},
|
|
defaultPrefix: "test",
|
|
expected: "better-prefix-login-better-suffix",
|
|
},
|
|
"operation-prefix-verb": {
|
|
path: "path/to/thing",
|
|
pathIndex: 0,
|
|
pathAttributes: nil,
|
|
operation: logical.UpdateOperation,
|
|
operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationVerb: "Login"},
|
|
defaultPrefix: "test",
|
|
expected: "better-prefix-login",
|
|
},
|
|
"operation-verb-suffix": {
|
|
path: "path/to/thing",
|
|
pathIndex: 0,
|
|
pathAttributes: nil,
|
|
operation: logical.UpdateOperation,
|
|
operationAttributes: &DisplayAttributes{OperationVerb: "Login", OperationSuffix: "better-suffix"},
|
|
defaultPrefix: "test",
|
|
expected: "login-better-suffix",
|
|
},
|
|
"pipe-delimited-suffix-0": {
|
|
path: "path/to/thing",
|
|
pathIndex: 0,
|
|
pathAttributes: nil,
|
|
operation: logical.UpdateOperation,
|
|
operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "suffix0|suffix1"},
|
|
defaultPrefix: "test",
|
|
expected: "better-prefix-write-suffix0",
|
|
},
|
|
"pipe-delimited-suffix-1": {
|
|
path: "path/to/thing",
|
|
pathIndex: 1,
|
|
pathAttributes: nil,
|
|
operation: logical.UpdateOperation,
|
|
operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "suffix0|suffix1"},
|
|
defaultPrefix: "test",
|
|
expected: "better-prefix-write-suffix1",
|
|
},
|
|
"pipe-delimited-suffix-2-fallback": {
|
|
path: "path/to/thing",
|
|
pathIndex: 2,
|
|
pathAttributes: nil,
|
|
operation: logical.UpdateOperation,
|
|
operationAttributes: &DisplayAttributes{OperationPrefix: "better-prefix", OperationSuffix: "suffix0|suffix1"},
|
|
defaultPrefix: "test",
|
|
expected: "better-prefix-write-path-to-thing",
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
name, test := name, test
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
actual := constructOperationID(
|
|
test.path,
|
|
test.pathIndex,
|
|
test.pathAttributes,
|
|
test.operation,
|
|
test.operationAttributes,
|
|
test.defaultPrefix,
|
|
)
|
|
if actual != test.expected {
|
|
t.Fatalf("expected: %s; got: %s", test.expected, actual)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOpenAPI_hyphenatedToTitleCase(t *testing.T) {
|
|
tests := map[string]struct {
|
|
in string
|
|
expected string
|
|
}{
|
|
"simple": {
|
|
in: "test",
|
|
expected: "Test",
|
|
},
|
|
"two-words": {
|
|
in: "two-words",
|
|
expected: "TwoWords",
|
|
},
|
|
"three-words": {
|
|
in: "one-two-three",
|
|
expected: "OneTwoThree",
|
|
},
|
|
"not-hyphenated": {
|
|
in: "something_like_this",
|
|
expected: "Something_like_this",
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
name, test := name, test
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
actual := hyphenatedToTitleCase(test.in)
|
|
if actual != test.expected {
|
|
t.Fatalf("expected: %s; got: %s", test.expected, actual)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func testPath(t *testing.T, path *Path, sp *logical.Paths, expectedJSON string) {
|
|
t.Helper()
|
|
|
|
doc := NewOASDocument("dummyversion")
|
|
if err := documentPath(path, sp, "kv", logical.TypeLogical, doc); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
doc.CreateOperationIDs("")
|
|
|
|
docJSON, err := json.MarshalIndent(doc, "", " ")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Compare json by first decoding, then comparing with a deep equality check.
|
|
var expected, actual interface{}
|
|
if err := jsonutil.DecodeJSON(docJSON, &actual); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := jsonutil.DecodeJSON([]byte(expectedJSON), &expected); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if diff := deep.Equal(actual, expected); diff != nil {
|
|
// fmt.Println(string(docJSON)) // uncomment to debug generated JSON (very helpful when fixing tests)
|
|
t.Fatal(diff)
|
|
}
|
|
}
|
|
|
|
func getPathOp(pi *OASPathItem, op string) *OASOperation {
|
|
switch op {
|
|
case "get":
|
|
return pi.Get
|
|
case "post":
|
|
return pi.Post
|
|
case "delete":
|
|
return pi.Delete
|
|
default:
|
|
panic("unexpected operation: " + op)
|
|
}
|
|
}
|
|
|
|
func expected(name string) string {
|
|
data, err := ioutil.ReadFile(filepath.Join("testdata", name+".json"))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
content := strings.Replace(string(data), "<vault_version>", "dummyversion", 1)
|
|
|
|
return content
|
|
}
|
|
|
|
func mustJSONMarshal(t *testing.T, data interface{}) []byte {
|
|
j, err := json.MarshalIndent(data, "", " ")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return j
|
|
}
|