From 09407cab40a6b80a8fa2728c0f2222b50d68947f Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Wed, 26 Apr 2023 10:08:56 -0400 Subject: [PATCH 1/2] Added support for base paths defined by server definitions. Signed-off-by: Dave Shanley --- helpers/constants.go | 81 +++--- paths/paths.go | 484 +++++++++++++++++--------------- paths/paths_test.go | 394 ++++++++++++++------------ responses/validate_body_test.go | 7 - 4 files changed, 507 insertions(+), 459 deletions(-) diff --git a/helpers/constants.go b/helpers/constants.go index 459bdff..a134a7a 100644 --- a/helpers/constants.go +++ b/helpers/constants.go @@ -4,44 +4,45 @@ package helpers const ( - ParameterValidation = "parameter" - ParameterValidationPath = "path" - ParameterValidationQuery = "query" - ParameterValidationHeader = "header" - ParameterValidationCookie = "cookie" - RequestBodyValidation = "requestBody" - Schema = "schema" - ResponseBodyValidation = "response" - RequestBodyContentType = "contentType" - ResponseBodyResponseCode = "statusCode" - SpaceDelimited = "spaceDelimited" - PipeDelimited = "pipeDelimited" - DefaultDelimited = "default" - MatrixStyle = "matrix" - LabelStyle = "label" - Pipe = "|" - Comma = "," - Space = " " - SemiColon = ";" - Asterisk = "*" - Period = "." - Equals = "=" - Integer = "integer" - Number = "number" - Slash = "/" - Object = "object" - String = "string" - Array = "array" - Boolean = "boolean" - DeepObject = "deepObject" - Header = "header" - Cookie = "cookie" - Path = "path" - Form = "form" - Query = "query" - JSONContentType = "application/json" - JSONType = "json" - ContentTypeHeader = "Content-Type" - Charset = "charset" - Boundary = "boundary" + ParameterValidation = "parameter" + ParameterValidationPath = "path" + ParameterValidationQuery = "query" + ParameterValidationHeader = "header" + ParameterValidationCookie = "cookie" + RequestBodyValidation = "requestBody" + Schema = "schema" + ResponseBodyValidation = "response" + RequestBodyContentType = "contentType" + ResponseBodyResponseCode = "statusCode" + SpaceDelimited = "spaceDelimited" + PipeDelimited = "pipeDelimited" + DefaultDelimited = "default" + MatrixStyle = "matrix" + LabelStyle = "label" + Pipe = "|" + Comma = "," + Space = " " + SemiColon = ";" + Asterisk = "*" + Period = "." + Equals = "=" + Integer = "integer" + Number = "number" + Slash = "/" + Object = "object" + String = "string" + Array = "array" + Boolean = "boolean" + DeepObject = "deepObject" + Header = "header" + Cookie = "cookie" + Path = "path" + Form = "form" + Query = "query" + JSONContentType = "application/json" + JSONType = "json" + ContentTypeHeader = "Content-Type" + Charset = "charset" + Boundary = "boundary" + FailSegment = "**&&FAIL&&**" ) diff --git a/paths/paths.go b/paths/paths.go index 1370a53..ea88c87 100644 --- a/paths/paths.go +++ b/paths/paths.go @@ -4,14 +4,15 @@ package paths import ( - "fmt" - "github.com/pb33f/libopenapi-validator/errors" - "github.com/pb33f/libopenapi-validator/helpers" - "github.com/pb33f/libopenapi/datamodel/high/v3" - "net/http" - "path/filepath" - "strconv" - "strings" + "fmt" + "github.com/pb33f/libopenapi-validator/errors" + "github.com/pb33f/libopenapi-validator/helpers" + "github.com/pb33f/libopenapi/datamodel/high/v3" + "net/http" + "net/url" + "path/filepath" + "strconv" + "strings" ) // FindPath will find the path in the document that matches the request path. If a successful match was found, then @@ -21,238 +22,255 @@ import ( // parameters will not have been replaced with their values from the request - allowing model lookups. func FindPath(request *http.Request, document *v3.Document) (*v3.PathItem, []*errors.ValidationError, string) { - var validationErrors []*errors.ValidationError + var validationErrors []*errors.ValidationError - reqPathSegments := strings.Split(request.URL.Path, "/") - if reqPathSegments[0] == "" { - reqPathSegments = reqPathSegments[1:] - } - var pItem *v3.PathItem - var foundPath string + reqPathSegments := strings.Split(request.URL.Path, "/") + if reqPathSegments[0] == "" { + reqPathSegments = reqPathSegments[1:] + } + + // extract base path from document to check against paths. + basePaths := make([]string, len(document.Servers)) + for i, s := range document.Servers { + u, _ := url.Parse(s.URL) + basePaths[i] = u.Path + } + + var pItem *v3.PathItem + var foundPath string pathFound: - for path, pathItem := range document.Paths.PathItems { - segs := strings.Split(path, "/") - if segs[0] == "" { - segs = segs[1:] - } + for path, pathItem := range document.Paths.PathItems { + segs := strings.Split(path, "/") + if segs[0] == "" { + segs = segs[1:] + } + + // collect path level params + params := pathItem.Parameters + var errs []*errors.ValidationError + var ok bool + switch request.Method { + case http.MethodGet: + if pathItem.Get != nil { + p := append(params, pathItem.Get.Parameters...) + if checkPathAgainstBase(request.URL.Path, path, basePaths) { + pItem = pathItem + foundPath = path + break pathFound + } + if ok, errs = comparePaths(segs, reqPathSegments, p, basePaths); ok { + pItem = pathItem + foundPath = path + validationErrors = errs + break pathFound + } else { + validationErrors = errs + } + } + case http.MethodPost: + if pathItem.Post != nil { + p := append(params, pathItem.Post.Parameters...) + if checkPathAgainstBase(request.URL.Path, path, basePaths) { + pItem = pathItem + foundPath = path + break pathFound + } + if ok, errs = comparePaths(segs, reqPathSegments, p, basePaths); ok { + pItem = pathItem + foundPath = path + validationErrors = errs + break pathFound + } else { + validationErrors = errs + } + } + case http.MethodPut: + if pathItem.Put != nil { + p := append(params, pathItem.Put.Parameters...) + // check for a literal match + if checkPathAgainstBase(request.URL.Path, path, basePaths) { + pItem = pathItem + foundPath = path + validationErrors = errs + break pathFound + } + if ok, errs = comparePaths(segs, reqPathSegments, p, basePaths); ok { + pItem = pathItem + foundPath = path + validationErrors = errs + break pathFound + } else { + validationErrors = errs + } + } + case http.MethodDelete: + if pathItem.Delete != nil { + p := append(params, pathItem.Delete.Parameters...) + // check for a literal match + if checkPathAgainstBase(request.URL.Path, path, basePaths) { + pItem = pathItem + foundPath = path + break pathFound + } + if ok, errs = comparePaths(segs, reqPathSegments, p, basePaths); ok { + pItem = pathItem + foundPath = path + validationErrors = errs + break pathFound + } else { + validationErrors = errs + } + } + case http.MethodOptions: + if pathItem.Options != nil { + p := append(params, pathItem.Options.Parameters...) + // check for a literal match + if checkPathAgainstBase(request.URL.Path, path, basePaths) { + pItem = pathItem + foundPath = path + break pathFound + } + if ok, errs = comparePaths(segs, reqPathSegments, p, basePaths); ok { + pItem = pathItem + foundPath = path + validationErrors = errs + break pathFound + } else { + validationErrors = errs + } + } + case http.MethodHead: + if pathItem.Head != nil { + p := append(params, pathItem.Head.Parameters...) + if checkPathAgainstBase(request.URL.Path, path, basePaths) { + pItem = pathItem + foundPath = path + break pathFound + } + if ok, errs = comparePaths(segs, reqPathSegments, p, basePaths); ok { + pItem = pathItem + foundPath = path + validationErrors = errs + break pathFound + } else { + validationErrors = errs + } + } + case http.MethodPatch: + if pathItem.Patch != nil { + p := append(params, pathItem.Patch.Parameters...) + // check for a literal match + if checkPathAgainstBase(request.URL.Path, path, basePaths) { + pItem = pathItem + foundPath = path + break pathFound + } + if ok, errs = comparePaths(segs, reqPathSegments, p, basePaths); ok { + pItem = pathItem + foundPath = path + validationErrors = errs + break pathFound + } else { + validationErrors = errs + } + } + case http.MethodTrace: + if pathItem.Trace != nil { + p := append(params, pathItem.Trace.Parameters...) + if checkPathAgainstBase(request.URL.Path, path, basePaths) { + pItem = pathItem + foundPath = path + break pathFound + } + if ok, errs = comparePaths(segs, reqPathSegments, p, basePaths); ok { + pItem = pathItem + foundPath = path + validationErrors = errs + break pathFound + } else { + validationErrors = errs + } + } + } + } + if pItem == nil && len(validationErrors) == 0 { + validationErrors = append(validationErrors, &errors.ValidationError{ + ValidationType: helpers.ParameterValidationPath, + ValidationSubType: "missing", + Message: fmt.Sprintf("Path '%s' not found", request.URL.Path), + Reason: fmt.Sprintf("The request contains a path of '%s' "+ + "however that path does not exist in the specification", request.URL.Path), + SpecLine: -1, + SpecCol: -1, + }) + return pItem, validationErrors, foundPath + } else { + return pItem, validationErrors, foundPath + } +} - // collect path level params - params := pathItem.Parameters - var errs []*errors.ValidationError - var ok bool - switch request.Method { - case http.MethodGet: - if pathItem.Get != nil { - p := append(params, pathItem.Get.Parameters...) - // check for a literal match - if request.URL.Path == path { - pItem = pathItem - foundPath = path - break pathFound - } - if ok, errs = comparePaths(segs, reqPathSegments, p, request.URL.Path); ok { - pItem = pathItem - foundPath = path - validationErrors = errs - break pathFound - } else { - validationErrors = errs - } - } - case http.MethodPost: - if pathItem.Post != nil { - p := append(params, pathItem.Post.Parameters...) - // check for a literal match - if request.URL.Path == path { - pItem = pathItem - foundPath = path - break pathFound - } - if ok, errs = comparePaths(segs, reqPathSegments, p, request.URL.Path); ok { - pItem = pathItem - foundPath = path - validationErrors = errs - break pathFound - } else { - validationErrors = errs - } - } - case http.MethodPut: - if pathItem.Put != nil { - p := append(params, pathItem.Put.Parameters...) - // check for a literal match - if request.URL.Path == path { - pItem = pathItem - foundPath = path - validationErrors = errs - break pathFound - } - if ok, errs = comparePaths(segs, reqPathSegments, p, request.URL.Path); ok { - pItem = pathItem - foundPath = path - validationErrors = errs - break pathFound - } else { - validationErrors = errs - } - } - case http.MethodDelete: - if pathItem.Delete != nil { - p := append(params, pathItem.Delete.Parameters...) - // check for a literal match - if request.URL.Path == path { - pItem = pathItem - foundPath = path - break pathFound - } - if ok, errs = comparePaths(segs, reqPathSegments, p, request.URL.Path); ok { - pItem = pathItem - foundPath = path - validationErrors = errs - break pathFound - } else { - validationErrors = errs - } - } - case http.MethodOptions: - if pathItem.Options != nil { - p := append(params, pathItem.Options.Parameters...) - // check for a literal match - if request.URL.Path == path { - pItem = pathItem - foundPath = path - break pathFound - } - if ok, errs = comparePaths(segs, reqPathSegments, p, request.URL.Path); ok { - pItem = pathItem - foundPath = path - validationErrors = errs - break pathFound - } else { - validationErrors = errs - } - } - case http.MethodHead: - if pathItem.Head != nil { - p := append(params, pathItem.Head.Parameters...) - // check for a literal match - if request.URL.Path == path { - pItem = pathItem - foundPath = path - break pathFound - } - if ok, errs = comparePaths(segs, reqPathSegments, p, request.URL.Path); ok { - pItem = pathItem - foundPath = path - validationErrors = errs - break pathFound - } else { - validationErrors = errs - } - } - case http.MethodPatch: - if pathItem.Patch != nil { - p := append(params, pathItem.Patch.Parameters...) - // check for a literal match - if request.URL.Path == path { - pItem = pathItem - foundPath = path - break pathFound - } - if ok, errs = comparePaths(segs, reqPathSegments, p, request.URL.Path); ok { - pItem = pathItem - foundPath = path - validationErrors = errs - break pathFound - } else { - validationErrors = errs - } - } - case http.MethodTrace: - if pathItem.Trace != nil { - p := append(params, pathItem.Trace.Parameters...) - // check for a literal match - if request.URL.Path == path { - pItem = pathItem - foundPath = path - break pathFound - } - if ok, errs = comparePaths(segs, reqPathSegments, p, request.URL.Path); ok { - pItem = pathItem - foundPath = path - validationErrors = errs - break pathFound - } else { - validationErrors = errs - } - } - } - } - if pItem == nil && len(validationErrors) == 0 { - validationErrors = append(validationErrors, &errors.ValidationError{ - ValidationType: helpers.ParameterValidationPath, - ValidationSubType: "missing", - Message: fmt.Sprintf("Path '%s' not found", request.URL.Path), - Reason: fmt.Sprintf("The request contains a path of '%s' "+ - "however that path does not exist in the specification", request.URL.Path), - SpecLine: -1, - SpecCol: -1, - }) - return pItem, validationErrors, foundPath - } else { - return pItem, validationErrors, foundPath - } +func checkPathAgainstBase(docPath, urlPath string, basePaths []string) bool { + if docPath == urlPath { + return true + } + for i := range basePaths { + if basePaths[i][len(basePaths[i])-1] == '/' { + basePaths[i] = basePaths[i][:len(basePaths[i])-1] + } + merged := fmt.Sprintf("%s%s", basePaths[i], urlPath) + if docPath == merged { + return true + } + } + return false } func comparePaths(mapped, requested []string, - params []*v3.Parameter, path string) (bool, []*errors.ValidationError) { + params []*v3.Parameter, basePaths []string) (bool, []*errors.ValidationError) { - // check lengths first - var pathErrors []*errors.ValidationError + // check lengths first + var pathErrors []*errors.ValidationError - if len(mapped) != len(requested) { - return false, nil // short circuit out - } - var imploded []string - for i, seg := range mapped { - s := seg - //sOrig := seg - // check for braces - if strings.Contains(seg, "{") { - s = requested[i] - //sOrig = s - } - // check param against type, check if it's a number or not, and if it validates. - for p := range params { - if params[p].In == helpers.Path { - h := seg[1 : len(seg)-1] - if params[p].Name == h { - schema := params[p].Schema.Schema() - for t := range schema.Type { + if len(mapped) != len(requested) { + return false, nil // short circuit out + } + var imploded []string + for i, seg := range mapped { + s := seg + //sOrig := seg + // check for braces + if strings.Contains(seg, "{") { + s = requested[i] + //sOrig = s + } + // check param against type, check if it's a number or not, and if it validates. + for p := range params { + if params[p].In == helpers.Path { + h := seg[1 : len(seg)-1] + if params[p].Name == h { + schema := params[p].Schema.Schema() + for t := range schema.Type { - switch schema.Type[t] { - case helpers.String, helpers.Object, helpers.Array: - // should not be a number. - if _, err := strconv.ParseFloat(s, 64); err == nil { - s = "**&&FAIL&&**" - } - case helpers.Number, helpers.Integer: - // should not be a string. - if _, err := strconv.ParseFloat(s, 64); err != nil { - s = "**&&FAIL&&**" - } - // TODO: check for encoded objects and arrays (yikes) - } - } - } - } - } - imploded = append(imploded, s) - } - l := filepath.Join(imploded...) - r := filepath.Join(requested...) - if l == r { - return true, pathErrors - } - return false, pathErrors + switch schema.Type[t] { + case helpers.String, helpers.Object, helpers.Array: + // should not be a number. + if _, err := strconv.ParseFloat(s, 64); err == nil { + s = helpers.FailSegment + } + case helpers.Number, helpers.Integer: + // should not be a string. + if _, err := strconv.ParseFloat(s, 64); err != nil { + s = helpers.FailSegment + } + // TODO: check for encoded objects and arrays (yikes) + } + } + } + } + } + imploded = append(imploded, s) + } + l := filepath.Join(imploded...) + r := filepath.Join(requested...) + return checkPathAgainstBase(l, r, basePaths), pathErrors } diff --git a/paths/paths_test.go b/paths/paths_test.go index 4322235..7ce431d 100644 --- a/paths/paths_test.go +++ b/paths/paths_test.go @@ -4,315 +4,351 @@ package paths import ( - "github.com/pb33f/libopenapi" - "github.com/stretchr/testify/assert" - "net/http" - "os" - "testing" + "github.com/pb33f/libopenapi" + "github.com/stretchr/testify/assert" + "net/http" + "os" + "testing" ) func TestNewValidator_BadParam(t *testing.T) { - request, _ := http.NewRequest(http.MethodGet, "https://things.com/pet/doggy", nil) + request, _ := http.NewRequest(http.MethodGet, "https://things.com/pet/doggy", nil) - // load a doc - b, _ := os.ReadFile("../test_specs/petstorev3.json") - doc, _ := libopenapi.NewDocument(b) + // load a doc + b, _ := os.ReadFile("../test_specs/petstorev3.json") + doc, _ := libopenapi.NewDocument(b) - m, _ := doc.BuildV3Model() + m, _ := doc.BuildV3Model() - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model) - assert.Equal(t, "Path '/pet/doggy' not found", - errs[0].Message) - assert.Equal(t, "The request contains a path of '/pet/doggy' however that path does not exist in the specification", - errs[0].Reason) + assert.Equal(t, "Path '/pet/doggy' not found", + errs[0].Message) + assert.Equal(t, "The request contains a path of '/pet/doggy' however that path does not exist in the specification", + errs[0].Reason) } func TestNewValidator_GoodParamFloat(t *testing.T) { - request, _ := http.NewRequest(http.MethodGet, "https://things.com/pet/232.233", nil) + request, _ := http.NewRequest(http.MethodGet, "https://things.com/pet/232.233", nil) - b, _ := os.ReadFile("../test_specs/petstorev3.json") - doc, _ := libopenapi.NewDocument(b) - m, _ := doc.BuildV3Model() + b, _ := os.ReadFile("../test_specs/petstorev3.json") + doc, _ := libopenapi.NewDocument(b) + m, _ := doc.BuildV3Model() - pathItem, _, _ := FindPath(request, &m.Model) - assert.NotNil(t, pathItem) + pathItem, _, _ := FindPath(request, &m.Model) + assert.NotNil(t, pathItem) } func TestNewValidator_GoodParamInt(t *testing.T) { - request, _ := http.NewRequest(http.MethodGet, "https://things.com/pet/12334", nil) + request, _ := http.NewRequest(http.MethodGet, "https://things.com/pet/12334", nil) - b, _ := os.ReadFile("../test_specs/petstorev3.json") - doc, _ := libopenapi.NewDocument(b) + b, _ := os.ReadFile("../test_specs/petstorev3.json") + doc, _ := libopenapi.NewDocument(b) - m, _ := doc.BuildV3Model() - pathItem, _, _ := FindPath(request, &m.Model) - assert.NotNil(t, pathItem) + m, _ := doc.BuildV3Model() + pathItem, _, _ := FindPath(request, &m.Model) + assert.NotNil(t, pathItem) } func TestNewValidator_FindSimpleEncodedArrayPath(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/{burgerId*}/locate: patch: operationId: locateBurger ` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + m, _ := doc.BuildV3Model() - request, _ := http.NewRequest(http.MethodPatch, "https://things.com/burgers/1,2,3,4,5/locate", nil) + request, _ := http.NewRequest(http.MethodPatch, "https://things.com/burgers/1,2,3,4,5/locate", nil) - pathItem, _, _ := FindPath(request, &m.Model) - assert.NotNil(t, pathItem) - assert.Equal(t, "locateBurger", pathItem.Patch.OperationId) + pathItem, _, _ := FindPath(request, &m.Model) + assert.NotNil(t, pathItem) + assert.Equal(t, "locateBurger", pathItem.Patch.OperationId) } func TestNewValidator_FindSimpleEncodedObjectPath(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/{burgerId*}/locate: patch: operationId: locateBurger ` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + m, _ := doc.BuildV3Model() - request, _ := http.NewRequest(http.MethodPatch, "https://things.com/burgers/bish=bosh,wish=wash/locate", nil) + request, _ := http.NewRequest(http.MethodPatch, "https://things.com/burgers/bish=bosh,wish=wash/locate", nil) - pathItem, _, _ := FindPath(request, &m.Model) - assert.NotNil(t, pathItem) - assert.Equal(t, "locateBurger", pathItem.Patch.OperationId) + pathItem, _, _ := FindPath(request, &m.Model) + assert.NotNil(t, pathItem) + assert.Equal(t, "locateBurger", pathItem.Patch.OperationId) } func TestNewValidator_FindLabelEncodedArrayPath(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/{.burgerId}/locate: patch: operationId: locateBurger ` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() - request, _ := http.NewRequest(http.MethodPatch, "https://things.com/burgers/.1.2.3.4.5/locate", nil) + m, _ := doc.BuildV3Model() + request, _ := http.NewRequest(http.MethodPatch, "https://things.com/burgers/.1.2.3.4.5/locate", nil) - pathItem, _, _ := FindPath(request, &m.Model) - assert.NotNil(t, pathItem) - assert.Equal(t, "locateBurger", pathItem.Patch.OperationId) + pathItem, _, _ := FindPath(request, &m.Model) + assert.NotNil(t, pathItem) + assert.Equal(t, "locateBurger", pathItem.Patch.OperationId) } func TestNewValidator_FindPathPost(t *testing.T) { - // load a doc - b, _ := os.ReadFile("../test_specs/petstorev3.json") - doc, _ := libopenapi.NewDocument(b) + // load a doc + b, _ := os.ReadFile("../test_specs/petstorev3.json") + doc, _ := libopenapi.NewDocument(b) - m, _ := doc.BuildV3Model() + m, _ := doc.BuildV3Model() - request, _ := http.NewRequest(http.MethodPost, "https://things.com/pet/12334", nil) + request, _ := http.NewRequest(http.MethodPost, "https://things.com/pet/12334", nil) - pathItem, _, _ := FindPath(request, &m.Model) - assert.NotNil(t, pathItem) + pathItem, _, _ := FindPath(request, &m.Model) + assert.NotNil(t, pathItem) } func TestNewValidator_FindPathDelete(t *testing.T) { - // load a doc - b, _ := os.ReadFile("../test_specs/petstorev3.json") - doc, _ := libopenapi.NewDocument(b) + // load a doc + b, _ := os.ReadFile("../test_specs/petstorev3.json") + doc, _ := libopenapi.NewDocument(b) - m, _ := doc.BuildV3Model() - request, _ := http.NewRequest(http.MethodDelete, "https://things.com/pet/12334", nil) + m, _ := doc.BuildV3Model() + request, _ := http.NewRequest(http.MethodDelete, "https://things.com/pet/12334", nil) - pathItem, _, _ := FindPath(request, &m.Model) - assert.NotNil(t, pathItem) + pathItem, _, _ := FindPath(request, &m.Model) + assert.NotNil(t, pathItem) } func TestNewValidator_FindPathPatch(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/{burgerId}: patch: operationId: locateBurger ` - doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() - request, _ := http.NewRequest(http.MethodPatch, "https://things.com/burgers/12345", nil) + request, _ := http.NewRequest(http.MethodPatch, "https://things.com/burgers/12345", nil) - pathItem, _, _ := FindPath(request, &m.Model) - assert.NotNil(t, pathItem) - assert.Equal(t, "locateBurger", pathItem.Patch.OperationId) + pathItem, _, _ := FindPath(request, &m.Model) + assert.NotNil(t, pathItem) + assert.Equal(t, "locateBurger", pathItem.Patch.OperationId) } func TestNewValidator_FindPathOptions(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/{burgerId}: options: operationId: locateBurger ` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() - request, _ := http.NewRequest(http.MethodOptions, "https://things.com/burgers/12345", nil) + m, _ := doc.BuildV3Model() + request, _ := http.NewRequest(http.MethodOptions, "https://things.com/burgers/12345", nil) - pathItem, _, _ := FindPath(request, &m.Model) - assert.NotNil(t, pathItem) - assert.Equal(t, "locateBurger", pathItem.Options.OperationId) + pathItem, _, _ := FindPath(request, &m.Model) + assert.NotNil(t, pathItem) + assert.Equal(t, "locateBurger", pathItem.Options.OperationId) } func TestNewValidator_FindPathTrace(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/{burgerId}: trace: operationId: locateBurger ` - doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() - request, _ := http.NewRequest(http.MethodTrace, "https://things.com/burgers/12345", nil) + request, _ := http.NewRequest(http.MethodTrace, "https://things.com/burgers/12345", nil) - pathItem, _, _ := FindPath(request, &m.Model) - assert.NotNil(t, pathItem) - assert.Equal(t, "locateBurger", pathItem.Trace.OperationId) + pathItem, _, _ := FindPath(request, &m.Model) + assert.NotNil(t, pathItem) + assert.Equal(t, "locateBurger", pathItem.Trace.OperationId) } func TestNewValidator_FindPathPut(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/{burgerId}: put: operationId: locateBurger ` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + m, _ := doc.BuildV3Model() - request, _ := http.NewRequest(http.MethodPut, "https://things.com/burgers/12345", nil) + request, _ := http.NewRequest(http.MethodPut, "https://things.com/burgers/12345", nil) - pathItem, _, _ := FindPath(request, &m.Model) - assert.NotNil(t, pathItem) - assert.Equal(t, "locateBurger", pathItem.Put.OperationId) + pathItem, _, _ := FindPath(request, &m.Model) + assert.NotNil(t, pathItem) + assert.Equal(t, "locateBurger", pathItem.Put.OperationId) } func TestNewValidator_FindPathHead(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/{burgerId}: head: operationId: locateBurger ` - doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() - request, _ := http.NewRequest(http.MethodHead, "https://things.com/burgers/12345", nil) + request, _ := http.NewRequest(http.MethodHead, "https://things.com/burgers/12345", nil) - pathItem, _, _ := FindPath(request, &m.Model) - assert.NotNil(t, pathItem) - assert.Equal(t, "locateBurger", pathItem.Head.OperationId) + pathItem, _, _ := FindPath(request, &m.Model) + assert.NotNil(t, pathItem) + assert.Equal(t, "locateBurger", pathItem.Head.OperationId) + +} + +func TestNewValidator_FindPathWithBaseURLInServer(t *testing.T) { + + spec := `openapi: 3.1.0 +servers: + - url: https://things.com/base1 + - url: https://things.com/base2 + - url: https://things.com/base3/base4/base5/base6/ +paths: + /user: + post: + operationId: addUser +` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() + + // check against base1 + request, _ := http.NewRequest(http.MethodPost, "https://things.com/base1/user", nil) + pathItem, _, _ := FindPath(request, &m.Model) + assert.NotNil(t, pathItem) + assert.Equal(t, "addUser", pathItem.Post.OperationId) + + // check against base2 + request, _ = http.NewRequest(http.MethodPost, "https://things.com/base2/user", nil) + pathItem, _, _ = FindPath(request, &m.Model) + assert.NotNil(t, pathItem) + assert.Equal(t, "addUser", pathItem.Post.OperationId) + + // check against a deeper base + request, _ = http.NewRequest(http.MethodPost, "https://things.com/base3/base4/base5/base6/user", nil) + pathItem, _, _ = FindPath(request, &m.Model) + assert.NotNil(t, pathItem) + assert.Equal(t, "addUser", pathItem.Post.OperationId) } func TestNewValidator_FindPathMissing(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /a/fishy/on/a/dishy: head: operationId: locateFishy ` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + m, _ := doc.BuildV3Model() - request, _ := http.NewRequest(http.MethodHead, "https://things.com/not/here", nil) + request, _ := http.NewRequest(http.MethodHead, "https://things.com/not/here", nil) - pathItem, errs, _ := FindPath(request, &m.Model) - assert.Nil(t, pathItem) - assert.NotNil(t, errs) - assert.Equal(t, "Path '/not/here' not found", errs[0].Message) + pathItem, errs, _ := FindPath(request, &m.Model) + assert.Nil(t, pathItem) + assert.NotNil(t, errs) + assert.Equal(t, "Path '/not/here' not found", errs[0].Message) } func TestNewValidator_GetLiteralMatch(t *testing.T) { - request, _ := http.NewRequest(http.MethodGet, "https://things.com/store/inventory", nil) + request, _ := http.NewRequest(http.MethodGet, "https://things.com/store/inventory", nil) - // load a doc - b, _ := os.ReadFile("../test_specs/petstorev3.json") - doc, _ := libopenapi.NewDocument(b) + // load a doc + b, _ := os.ReadFile("../test_specs/petstorev3.json") + doc, _ := libopenapi.NewDocument(b) - m, _ := doc.BuildV3Model() + m, _ := doc.BuildV3Model() - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model) - assert.Len(t, errs, 0) + assert.Len(t, errs, 0) } func TestNewValidator_PostLiteralMatch(t *testing.T) { - request, _ := http.NewRequest(http.MethodPost, "https://things.com/user", nil) + request, _ := http.NewRequest(http.MethodPost, "https://things.com/user", nil) - // load a doc - b, _ := os.ReadFile("../test_specs/petstorev3.json") - doc, _ := libopenapi.NewDocument(b) + // load a doc + b, _ := os.ReadFile("../test_specs/petstorev3.json") + doc, _ := libopenapi.NewDocument(b) - m, _ := doc.BuildV3Model() + m, _ := doc.BuildV3Model() - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model) - assert.Len(t, errs, 0) + assert.Len(t, errs, 0) } func TestNewValidator_PutLiteralMatch(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /pizza/burger: put: operationId: locateBurger` - doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() - request, _ := http.NewRequest(http.MethodPut, "https://things.com/pizza/burger", nil) + request, _ := http.NewRequest(http.MethodPut, "https://things.com/pizza/burger", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model) - assert.Len(t, errs, 0) + assert.Len(t, errs, 0) } func TestNewValidator_PutMatch_Error(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /pizza/{cakes}: put: @@ -324,19 +360,19 @@ paths: schema: type: string` - doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() - request, _ := http.NewRequest(http.MethodPut, "https://things.com/pizza/1234", nil) + request, _ := http.NewRequest(http.MethodPut, "https://things.com/pizza/1234", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model) - assert.Len(t, errs, 1) + assert.Len(t, errs, 1) } func TestNewValidator_OptionsMatch_Error(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /pizza/{cakes}: options: @@ -348,37 +384,37 @@ paths: schema: type: string` - doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() - request, _ := http.NewRequest(http.MethodOptions, "https://things.com/pizza/1234", nil) + request, _ := http.NewRequest(http.MethodOptions, "https://things.com/pizza/1234", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model) - assert.Len(t, errs, 1) + assert.Len(t, errs, 1) } func TestNewValidator_PatchLiteralMatch(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /pizza/burger: patch: operationId: locateBurger` - doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() - request, _ := http.NewRequest(http.MethodPatch, "https://things.com/pizza/burger", nil) + request, _ := http.NewRequest(http.MethodPatch, "https://things.com/pizza/burger", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model) - assert.Len(t, errs, 0) + assert.Len(t, errs, 0) } func TestNewValidator_PatchMatch_Error(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /pizza/{cakes}: patch: @@ -390,91 +426,91 @@ paths: schema: type: string` - doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() - request, _ := http.NewRequest(http.MethodPatch, "https://things.com/pizza/1234", nil) + request, _ := http.NewRequest(http.MethodPatch, "https://things.com/pizza/1234", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model) - assert.Len(t, errs, 1) + assert.Len(t, errs, 1) } func TestNewValidator_DeleteLiteralMatch(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /pizza/burger: delete: operationId: locateBurger` - doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() - request, _ := http.NewRequest(http.MethodDelete, "https://things.com/pizza/burger", nil) + request, _ := http.NewRequest(http.MethodDelete, "https://things.com/pizza/burger", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model) - assert.Len(t, errs, 0) + assert.Len(t, errs, 0) } func TestNewValidator_OptionsLiteralMatch(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /pizza/burger: options: operationId: locateBurger` - doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() - request, _ := http.NewRequest(http.MethodOptions, "https://things.com/pizza/burger", nil) + request, _ := http.NewRequest(http.MethodOptions, "https://things.com/pizza/burger", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model) - assert.Len(t, errs, 0) + assert.Len(t, errs, 0) } func TestNewValidator_HeadLiteralMatch(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /pizza/burger: head: operationId: locateBurger` - doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() - request, _ := http.NewRequest(http.MethodHead, "https://things.com/pizza/burger", nil) + request, _ := http.NewRequest(http.MethodHead, "https://things.com/pizza/burger", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model) - assert.Len(t, errs, 0) + assert.Len(t, errs, 0) } func TestNewValidator_TraceLiteralMatch(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /pizza/burger: trace: operationId: locateBurger` - doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() - request, _ := http.NewRequest(http.MethodTrace, "https://things.com/pizza/burger", nil) + request, _ := http.NewRequest(http.MethodTrace, "https://things.com/pizza/burger", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model) - assert.Len(t, errs, 0) + assert.Len(t, errs, 0) } func TestNewValidator_TraceMatch_Error(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /pizza/{cakes}: trace: @@ -486,12 +522,12 @@ paths: schema: type: string` - doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() - request, _ := http.NewRequest(http.MethodTrace, "https://things.com/pizza/1234", nil) + request, _ := http.NewRequest(http.MethodTrace, "https://things.com/pizza/1234", nil) - _, errs, _ := FindPath(request, &m.Model) + _, errs, _ := FindPath(request, &m.Model) - assert.Len(t, errs, 1) + assert.Len(t, errs, 1) } diff --git a/responses/validate_body_test.go b/responses/validate_body_test.go index 2f929c3..d4a4d55 100644 --- a/responses/validate_body_test.go +++ b/responses/validate_body_test.go @@ -349,13 +349,6 @@ paths: handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) w.WriteHeader(http.StatusOK) - //body := map[string]interface{}{ - // "name": "Big Mac", - // "patties": false, - // "vegetarian": 2, - //} - - // bodyBytes, _ := json.Marshal(body) _, _ = w.Write(nil) } From 423f0ecb97994fca310ec0091cdc4fb9463b88ca Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Wed, 26 Apr 2023 10:22:21 -0400 Subject: [PATCH 2/2] Added more coverage for deep base paths multible path args with deep base paths now also supported. Signed-off-by: Dave Shanley --- paths/paths.go | 22 +++++++++++++++++----- paths/paths_test.go | 22 ++++++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/paths/paths.go b/paths/paths.go index ea88c87..895461e 100644 --- a/paths/paths.go +++ b/paths/paths.go @@ -24,11 +24,6 @@ func FindPath(request *http.Request, document *v3.Document) (*v3.PathItem, []*er var validationErrors []*errors.ValidationError - reqPathSegments := strings.Split(request.URL.Path, "/") - if reqPathSegments[0] == "" { - reqPathSegments = reqPathSegments[1:] - } - // extract base path from document to check against paths. basePaths := make([]string, len(document.Servers)) for i, s := range document.Servers { @@ -36,6 +31,14 @@ func FindPath(request *http.Request, document *v3.Document) (*v3.PathItem, []*er basePaths[i] = u.Path } + // strip any base path + stripped := stripBaseFromPath(request.URL.Path, basePaths) + + reqPathSegments := strings.Split(stripped, "/") + if reqPathSegments[0] == "" { + reqPathSegments = reqPathSegments[1:] + } + var pItem *v3.PathItem var foundPath string pathFound: @@ -225,6 +228,15 @@ func checkPathAgainstBase(docPath, urlPath string, basePaths []string) bool { return false } +func stripBaseFromPath(path string, basePaths []string) string { + for i := range basePaths { + if strings.HasPrefix(path, basePaths[i]) { + return path[len(basePaths[i]):] + } + } + return path +} + func comparePaths(mapped, requested []string, params []*v3.Parameter, basePaths []string) (bool, []*errors.ValidationError) { diff --git a/paths/paths_test.go b/paths/paths_test.go index 7ce431d..69bf315 100644 --- a/paths/paths_test.go +++ b/paths/paths_test.go @@ -276,6 +276,28 @@ paths: } +func TestNewValidator_FindPathWithBaseURLInServer_Args(t *testing.T) { + + spec := `openapi: 3.1.0 +servers: + - url: https://things.com/base3/base4/base5/base6/ +paths: + /user/{userId}/thing/{thingId}: + post: + operationId: addUser +` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() + + // check against a deeper base + request, _ := http.NewRequest(http.MethodPost, "https://things.com/base3/base4/base5/base6/user/1234/thing/abcd", nil) + pathItem, _, _ := FindPath(request, &m.Model) + assert.NotNil(t, pathItem) + assert.Equal(t, "addUser", pathItem.Post.OperationId) + +} + func TestNewValidator_FindPathMissing(t *testing.T) { spec := `openapi: 3.1.0