From 2ce0aac8b46eee1eff5cc46c0b24b6f502a83993 Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Fri, 21 Apr 2023 07:38:27 -0400 Subject: [PATCH] Starting the hardening process coverage will drop during this time. Signed-off-by: Dave Shanley --- parameters/header_parameters.go | 17 +- parameters/header_parameters_test.go | 28 -- parameters/path_parameters_test.go | 2 +- paths/paths.go | 403 +++++++++++++----------- paths/paths_test.go | 211 +++++++------ requests/validate_body.go | 31 +- validation_functions.go | 218 ------------- validation_functions_test.go | 148 --------- validator.go | 250 +++++++++++++++ validator_test.go | 445 +++++++++++++++++++++++++++ 10 files changed, 1057 insertions(+), 696 deletions(-) delete mode 100644 validation_functions.go delete mode 100644 validation_functions_test.go create mode 100644 validator.go create mode 100644 validator_test.go diff --git a/parameters/header_parameters.go b/parameters/header_parameters.go index c220cd2..663c383 100644 --- a/parameters/header_parameters.go +++ b/parameters/header_parameters.go @@ -145,15 +145,16 @@ func (v *paramValidator) ValidateHeaderParams(request *http.Request) (bool, []*e } } + // TODO: this needs to go to the grave. this will trigger everything // check for any headers that are not defined in the spec - for k := range request.Header { - if _, ok := seenHeaders[strings.ToLower(k)]; !ok { - ps := pathItem.GetOperations()[strings.ToLower(request.Method)].GoLow().Parameters - if ps.KeyNode != nil { - validationErrors = append(validationErrors, errors.HeaderParameterNotDefined(k, ps.KeyNode)) - } - } - } + //for k := range request.Header { + // if _, ok := seenHeaders[strings.ToLower(k)]; !ok { + // ps := pathItem.GetOperations()[strings.ToLower(request.Method)].GoLow().Parameters + // if ps.KeyNode != nil { + // validationErrors = append(validationErrors, errors.HeaderParameterNotDefined(k, ps.KeyNode)) + // } + // } + //} if len(validationErrors) > 0 { return false, validationErrors diff --git a/parameters/header_parameters_test.go b/parameters/header_parameters_test.go index c184476..d7e5c90 100644 --- a/parameters/header_parameters_test.go +++ b/parameters/header_parameters_test.go @@ -65,34 +65,6 @@ paths: assert.Equal(t, "Path '/I/do/not/exist' not found", errors[0].Message) } -func TestNewValidator_HeaderParamUndefined(t *testing.T) { - - spec := `openapi: 3.1.0 -paths: - /vending/drinks: - get: - parameters: - - name: fishy - in: header - schema: - type: string -` - - doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() - - v := NewParameterValidator(&m.Model) - - request, _ := http.NewRequest(http.MethodGet, "https://things.com/vending/drinks", nil) - request.Header.Set("Mushypeas", "yes please") //https://github.com/golang/go/issues/5022 - - valid, errors := v.ValidateHeaderParams(request) - - assert.False(t, valid) - assert.Equal(t, 1, len(errors)) - assert.Equal(t, "Header parameter 'Mushypeas' is not defined", errors[0].Message) -} - func TestNewValidator_HeaderParamDefaultEncoding_InvalidParamTypeNumber(t *testing.T) { spec := `openapi: 3.1.0 diff --git a/parameters/path_parameters_test.go b/parameters/path_parameters_test.go index e8bd0ff..cba1906 100644 --- a/parameters/path_parameters_test.go +++ b/parameters/path_parameters_test.go @@ -280,7 +280,7 @@ paths: assert.False(t, valid) assert.Len(t, errors, 1) - assert.Equal(t, "Match for path '/burgers/hello/locate', but the parameter 'hello' is not a number", errors[0].Message) + assert.Equal(t, "Path '/burgers/hello/locate' not found", errors[0].Message) } func TestNewValidator_SimpleEncodedPath_InvalidBoolean(t *testing.T) { diff --git a/paths/paths.go b/paths/paths.go index e8be5c2..6244ef6 100644 --- a/paths/paths.go +++ b/paths/paths.go @@ -4,14 +4,14 @@ 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" + "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,176 +21,235 @@ 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 - for path, pathItem := range document.Paths.PathItems { - segs := strings.Split(path, "/") - if segs[0] == "" { - segs = segs[1:] - } + reqPathSegments := strings.Split(request.URL.Path, "/") + if reqPathSegments[0] == "" { + reqPathSegments = reqPathSegments[1:] + } + 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:] + } - // collect path level params - params := pathItem.Parameters + // collect path level params + params := pathItem.Parameters - switch request.Method { - case http.MethodGet: - if pathItem.Get != nil { - p := append(params, pathItem.Get.Parameters...) - if ok, errs := comparePaths(segs, reqPathSegments, p, request.URL.Path); ok { - pItem = pathItem - foundPath = path - validationErrors = errs - break - } - } - case http.MethodPost: - if pathItem.Post != nil { - p := append(params, pathItem.Post.Parameters...) - if ok, _ := comparePaths(segs, reqPathSegments, p, request.URL.Path); ok { - pItem = pathItem - foundPath = path - break - } - } - case http.MethodPut: - if pathItem.Put != nil { - p := append(params, pathItem.Put.Parameters...) - if ok, errs := comparePaths(segs, reqPathSegments, p, request.URL.Path); ok { - pItem = pathItem - foundPath = path - validationErrors = errs - break - } - } - case http.MethodDelete: - if pathItem.Delete != nil { - p := append(params, pathItem.Delete.Parameters...) - if ok, errs := comparePaths(segs, reqPathSegments, p, request.URL.Path); ok { - pItem = pathItem - foundPath = path - validationErrors = errs - break - } - } - case http.MethodOptions: - if pathItem.Options != nil { - p := append(params, pathItem.Options.Parameters...) - if ok, errs := comparePaths(segs, reqPathSegments, p, request.URL.Path); ok { - pItem = pathItem - foundPath = path - validationErrors = errs - break - } - } - case http.MethodHead: - if pathItem.Head != nil { - p := append(params, pathItem.Head.Parameters...) - if ok, errs := comparePaths(segs, reqPathSegments, p, request.URL.Path); ok { - pItem = pathItem - foundPath = path - validationErrors = errs - break - } - } - case http.MethodPatch: - if pathItem.Patch != nil { - p := append(params, pathItem.Patch.Parameters...) - if ok, errs := comparePaths(segs, reqPathSegments, p, request.URL.Path); ok { - pItem = pathItem - foundPath = path - validationErrors = errs - break - } - } - case http.MethodTrace: - if pathItem.Trace != nil { - p := append(params, pathItem.Trace.Parameters...) - if ok, errs := comparePaths(segs, reqPathSegments, p, request.URL.Path); ok { - pItem = pathItem - foundPath = path - validationErrors = errs - break - } - } - } - } - if pItem == nil { - validationErrors = append(validationErrors, &errors.ValidationError{ - ValidationType: helpers.ParameterValidationPath, - ValidationSubType: "missing", - Message: fmt.Sprintf("Path '%s' not found", request.URL.Path), - Reason: fmt.Sprintf("The requests 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 - } + 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 + } + } + 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, _ := comparePaths(segs, reqPathSegments, p, request.URL.Path); ok { + pItem = pathItem + foundPath = path + break pathFound + } + } + 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 + break pathFound + } + if ok, errs := comparePaths(segs, reqPathSegments, p, request.URL.Path); ok { + pItem = pathItem + foundPath = path + validationErrors = errs + break pathFound + } + } + 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 + } + } + 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 + } + } + 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 + } + } + 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 + } + } + 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 + } + } + } + } + if pItem == nil { + 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 comparePaths(mapped, requested []string, - params []*v3.Parameter, path string) (bool, []*errors.ValidationError) { + params []*v3.Parameter, path 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 - // check for braces - if strings.Contains(seg, "{") { - s = requested[i] - } - // 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 schema.Type[t] == helpers.Number || schema.Type[t] == helpers.Integer { - notaNumber := false - // will return no error on floats or int - if _, err := strconv.ParseFloat(s, 64); err != nil { - notaNumber = true - } else { - continue - } - if notaNumber { - pathErrors = append(pathErrors, &errors.ValidationError{ - ValidationType: helpers.ParameterValidationPath, - ValidationSubType: "number", - Message: fmt.Sprintf("Match for path '%s', but the parameter "+ - "'%s' is not a number", path, s), - Reason: fmt.Sprintf("The parameter '%s' is defined as a number, "+ - "but the value '%s' is not a number", h, s), - SpecLine: params[p].GoLow().Schema.Value.Schema().Type.KeyNode.Line, - SpecCol: params[p].GoLow().Schema.Value.Schema().Type.KeyNode.Column, - Context: schema, - }) - } - } - } - } - } - } - imploded = append(imploded, s) - } - l := filepath.Join(imploded...) - r := filepath.Join(requested...) - if l == r { - return true, pathErrors - } - return false, pathErrors + if len(mapped) != len(requested) { + return false, nil // short circuit out + } + var imploded []string + for i, seg := range mapped { + s := seg + // check for braces + if strings.Contains(seg, "{") { + s = requested[i] + } + // 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: + // 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&&" + } + } + + //if schema.Type[t] == helpers.Number || schema.Type[t] == helpers.Integer { + //notaNumber := false + // will return no error on floats or int + + //if notaNumber { + // pathErrors = append(pathErrors, &errors.ValidationError{ + // ValidationType: helpers.ParameterValidationPath, + // ValidationSubType: "number", + // Message: fmt.Sprintf("Match for path '%s', but the parameter "+ + // "'%s' is not a number", path, s), + // Reason: fmt.Sprintf("The parameter '%s' is defined as a number, "+ + // "but the value '%s' is not a number", h, s), + // SpecLine: params[p].GoLow().Schema.Value.Schema().Type.KeyNode.Line, + // SpecCol: params[p].GoLow().Schema.Value.Schema().Type.KeyNode.Column, + // Context: schema, + // }) + //} + //} + } + } + } + } + imploded = append(imploded, s) + } + l := filepath.Join(imploded...) + r := filepath.Join(requested...) + if l == r { + return true, pathErrors + } + return false, pathErrors } diff --git a/paths/paths_test.go b/paths/paths_test.go index fdd10d8..ca39826 100644 --- a/paths/paths_test.go +++ b/paths/paths_test.go @@ -4,261 +4,260 @@ 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, "Match for path '/pet/doggy', but the parameter 'doggy' is not a number", - errs[0].Message) - assert.Equal(t, "The parameter 'petId' is defined as a number, but the value 'doggy' is not a number", - errs[0].Reason) - assert.Equal(t, 306, errs[0].SpecLine) + 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_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) } diff --git a/requests/validate_body.go b/requests/validate_body.go index bde4775..d9e5394 100644 --- a/requests/validate_body.go +++ b/requests/validate_body.go @@ -40,27 +40,28 @@ func (v *requestBodyValidator) ValidateRequestBody(request *http.Request) (bool, // extract the media type from the content type header. ct, _, _ := helpers.ExtractContentType(contentType) - if mediaType, ok := operation.RequestBody.Content[ct]; ok { + if operation.RequestBody != nil { + if mediaType, ok := operation.RequestBody.Content[ct]; ok { - // we currently only support JSON validation for request bodies - // this will capture *everything* that contains some form of 'json' in the content type - if strings.Contains(strings.ToLower(contentType), helpers.JSONType) { + // we currently only support JSON validation for request bodies + // this will capture *everything* that contains some form of 'json' in the content type + if strings.Contains(strings.ToLower(contentType), helpers.JSONType) { - // extract schema from media type - if mediaType.Schema != nil { - schema := mediaType.Schema.Schema() + // extract schema from media type + if mediaType.Schema != nil { + schema := mediaType.Schema.Schema() - // render the schema, to be used for validation - valid, vErrs := ValidateRequestSchema(request, schema) - if !valid { - validationErrors = append(validationErrors, vErrs...) + // render the schema, to be used for validation + valid, vErrs := ValidateRequestSchema(request, schema) + if !valid { + validationErrors = append(validationErrors, vErrs...) + } } } + } else { + // content type not found in the contract + validationErrors = append(validationErrors, errors.RequestContentTypeNotFound(operation, request)) } - } else { - - // content type not found in the contract - validationErrors = append(validationErrors, errors.RequestContentTypeNotFound(operation, request)) } } if len(validationErrors) > 0 { diff --git a/validation_functions.go b/validation_functions.go deleted file mode 100644 index 3f2810e..0000000 --- a/validation_functions.go +++ /dev/null @@ -1,218 +0,0 @@ -// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley -// SPDX-License-Identifier: MIT - -package validator - -import ( - "github.com/pb33f/libopenapi" - "github.com/pb33f/libopenapi-validator/errors" - "github.com/pb33f/libopenapi-validator/parameters" - "github.com/pb33f/libopenapi-validator/paths" - "github.com/pb33f/libopenapi-validator/requests" - "github.com/pb33f/libopenapi-validator/responses" - "github.com/pb33f/libopenapi-validator/schema_validation" - "github.com/pb33f/libopenapi/datamodel/high/v3" - "net/http" - "sync" -) - -type Validator interface { - ValidateHttpRequest(request *http.Request) (bool, []*errors.ValidationError) - ValidateHttpResponse(request *http.Request, response *http.Response) (bool, []*errors.ValidationError) - ValidateDocument() (bool, []*errors.ValidationError) - GetParameterValidator() parameters.ParameterValidator - GetRequestBodyValidator() requests.RequestBodyValidator - GetResponseBodyValidator() responses.ResponseBodyValidator -} - -// NewValidator will create a new Validator from an OpenAPI 3+ document -func NewValidator(document libopenapi.Document) (Validator, []error) { - m, errs := document.BuildV3Model() - if errs != nil { - return nil, errs - } - - // create a new parameter validator - paramValidator := parameters.NewParameterValidator(&m.Model) - - // create a new request body validator - reqBodyValidator := requests.NewRequestBodyValidator(&m.Model) - - // create a response body validator - respBodyValidator := responses.NewResponseBodyValidator(&m.Model) - - return &validator{ - v3Model: &m.Model, - document: document, - requestValidator: reqBodyValidator, - responseValidator: respBodyValidator, - paramValidator: paramValidator, - }, nil -} - -func (v *validator) GetParameterValidator() parameters.ParameterValidator { - return v.paramValidator -} -func (v *validator) GetRequestBodyValidator() requests.RequestBodyValidator { - return v.requestValidator -} -func (v *validator) GetResponseBodyValidator() responses.ResponseBodyValidator { - return v.responseValidator -} - -func (v *validator) ValidateDocument() (bool, []*errors.ValidationError) { - return schema_validation.ValidateOpenAPIDocument(v.document) -} - -func (v *validator) ValidateHttpResponse(request *http.Request, response *http.Response) (bool, []*errors.ValidationError) { - // find path - pathItem, errs, pathValue := paths.FindPath(request, v.v3Model) - if pathItem == nil || errs != nil { - v.errors = errs - return false, errs - } - - // create a new parameter validator - responseBodyValidator := v.responseValidator - responseBodyValidator.SetPathItem(pathItem, pathValue) - return responseBodyValidator.ValidateResponseBody(request, response) -} - -func (v *validator) ValidateHttpRequest(request *http.Request) (bool, []*errors.ValidationError) { - - // find path - pathItem, errs, pathValue := paths.FindPath(request, v.v3Model) - if pathItem == nil || errs != nil { - v.errors = errs - return false, errs - } - - // create a new parameter validator - paramValidator := v.paramValidator - paramValidator.SetPathItem(pathItem, pathValue) - - // create a new request body validator - reqBodyValidator := v.requestValidator - reqBodyValidator.SetPathItem(pathItem, pathValue) - - // create some channels to handle async validation - doneChan := make(chan bool) - errChan := make(chan []*errors.ValidationError) - controlChan := make(chan bool) - - parameterValidationFunc := func(control chan bool, errorChan chan []*errors.ValidationError) { - paramErrs := make(chan []*errors.ValidationError) - paramControlChan := make(chan bool) - paramFunctionControlChan := make(chan bool) - var paramValidationErrors []*errors.ValidationError - - validations := []validationFunction{ - paramValidator.ValidatePathParams, - paramValidator.ValidateCookieParams, - paramValidator.ValidateHeaderParams, - paramValidator.ValidateQueryParams, - } - - paramListener := func(control chan bool, errorChan chan []*errors.ValidationError) { - completedValidations := 0 - for { - select { - case vErrs := <-errorChan: - paramValidationErrors = append(paramValidationErrors, vErrs...) - case <-control: - completedValidations++ - if completedValidations == len(validations) { - paramFunctionControlChan <- true - return - } - } - } - } - - validateParamFunction := func( - control chan bool, - errorChan chan []*errors.ValidationError, - validatorFunc validationFunction) { - valid, pErrs := validatorFunc(request) - if !valid { - errorChan <- pErrs - } - control <- true - } - go paramListener(paramControlChan, paramErrs) - for i := range validations { - go validateParamFunction(paramControlChan, paramErrs, validations[i]) - } - // wait for all the validations to complete - - <-paramFunctionControlChan - if len(paramValidationErrors) > 0 { - errorChan <- paramValidationErrors - } - // let runValidation know we are done with this part. - controlChan <- true - } - - requestBodyValidationFunc := func(control chan bool, errorChan chan []*errors.ValidationError) { - valid, pErrs := reqBodyValidator.ValidateRequestBody(request) - if !valid { - errorChan <- pErrs - } - control <- true - } - - // build async functions - asyncFunctions := []validationFunctionAsync{ - parameterValidationFunc, - requestBodyValidationFunc, - } - - var validationErrors []*errors.ValidationError - var validationLock sync.Mutex - - runValidation := func(control chan bool, errorChan chan []*errors.ValidationError) { - completedValidations := 0 - for { - select { - case vErrs := <-errorChan: - validationLock.Lock() - validationErrors = append(validationErrors, vErrs...) - validationLock.Unlock() - case <-control: - completedValidations++ - if completedValidations == len(asyncFunctions) { - doneChan <- true - return - } - } - } - } - - // sit and wait for everything to report back. - go runValidation(controlChan, errChan) - - // run async functions - for i := range asyncFunctions { - go asyncFunctions[i](controlChan, errChan) - } - - // wait for all the validations to complete - <-doneChan - - if len(validationErrors) > 0 { - return false, validationErrors - } - return true, nil -} - -type validator struct { - v3Model *v3.Document - document libopenapi.Document - paramValidator parameters.ParameterValidator - requestValidator requests.RequestBodyValidator - responseValidator responses.ResponseBodyValidator - errors []*errors.ValidationError -} - -type validationFunction func(request *http.Request) (bool, []*errors.ValidationError) -type validationFunctionAsync func(control chan bool, errorChan chan []*errors.ValidationError) diff --git a/validation_functions_test.go b/validation_functions_test.go deleted file mode 100644 index e80aedc..0000000 --- a/validation_functions_test.go +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley -// SPDX-License-Identifier: MIT - -package validator - -import ( - "bytes" - "encoding/json" - "github.com/pb33f/libopenapi" - "github.com/stretchr/testify/assert" - "net/http" - "testing" -) - -func TestNewValidator_ValidateHttpRequest_ValidPostSimpleSchema(t *testing.T) { - - spec := `openapi: 3.1.0 -paths: - /burgers/createBurger: - post: - requestBody: - content: - application/json: - schema: - type: object - properties: - name: - type: string - patties: - type: integer - vegetarian: - type: boolean` - - doc, _ := libopenapi.NewDocument([]byte(spec)) - - v, _ := NewValidator(doc) - - body := map[string]interface{}{ - "name": "Big Mac", - "patties": 2, - "vegetarian": true, - } - - bodyBytes, _ := json.Marshal(body) - - request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", - bytes.NewBuffer(bodyBytes)) - request.Header.Set("Content-Type", "application/json") - - valid, errors := v.ValidateHttpRequest(request) - - assert.True(t, valid) - assert.Len(t, errors, 0) - -} - -func TestNewValidator_ValidateHttpRequest_InvalidPostSchema(t *testing.T) { - - spec := `openapi: 3.1.0 -paths: - /burgers/createBurger: - post: - requestBody: - content: - application/json: - schema: - type: object - properties: - name: - type: string - patties: - type: integer - vegetarian: - type: boolean` - - doc, _ := libopenapi.NewDocument([]byte(spec)) - - v, _ := NewValidator(doc) - - // mix up the primitives to fire two schema violations. - body := map[string]interface{}{ - "name": "Big Mac", - "patties": false, // wrong. - "vegetarian": false, - } - - bodyBytes, _ := json.Marshal(body) - - request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", - bytes.NewBuffer(bodyBytes)) - request.Header.Set("Content-Type", "application/json") - - valid, errors := v.ValidateHttpRequest(request) - - assert.False(t, valid) - assert.Len(t, errors, 1) - assert.Equal(t, "expected integer, but got boolean", errors[0].SchemaValidationErrors[0].Reason) - -} - -func TestNewValidator_ValidateHttpRequest_InvalidQuery(t *testing.T) { - - spec := `openapi: 3.1.0 -paths: - /burgers/createBurger: - parameters: - - in: query - name: cheese - required: true - schema: - type: string - post: - requestBody: - content: - application/json: - schema: - type: object - properties: - name: - type: string - patties: - type: integer - vegetarian: - type: boolean` - - doc, _ := libopenapi.NewDocument([]byte(spec)) - - v, _ := NewValidator(doc) - - body := map[string]interface{}{ - "name": "Big Mac", - "patties": 2, // wrong. - "vegetarian": false, - } - - bodyBytes, _ := json.Marshal(body) - - request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", - bytes.NewBuffer(bodyBytes)) - request.Header.Set("Content-Type", "application/json") - - valid, errors := v.ValidateHttpRequest(request) - - assert.False(t, valid) - assert.Len(t, errors, 1) - assert.Equal(t, "Query parameter 'cheese' is missing", errors[0].Message) - -} diff --git a/validator.go b/validator.go new file mode 100644 index 0000000..89e7db3 --- /dev/null +++ b/validator.go @@ -0,0 +1,250 @@ +// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package validator + +import ( + "github.com/pb33f/libopenapi" + "github.com/pb33f/libopenapi-validator/errors" + "github.com/pb33f/libopenapi-validator/parameters" + "github.com/pb33f/libopenapi-validator/paths" + "github.com/pb33f/libopenapi-validator/requests" + "github.com/pb33f/libopenapi-validator/responses" + "github.com/pb33f/libopenapi-validator/schema_validation" + "github.com/pb33f/libopenapi/datamodel/high/v3" + "net/http" + "sync" +) + +type Validator interface { + ValidateHttpRequest(request *http.Request) (bool, []*errors.ValidationError) + ValidateHttpRequestResponse(request *http.Request, response *http.Response) (bool, []*errors.ValidationError) + ValidateDocument() (bool, []*errors.ValidationError) + GetParameterValidator() parameters.ParameterValidator + GetRequestBodyValidator() requests.RequestBodyValidator + GetResponseBodyValidator() responses.ResponseBodyValidator +} + +// NewValidator will create a new Validator from an OpenAPI 3+ document +func NewValidator(document libopenapi.Document) (Validator, []error) { + m, errs := document.BuildV3Model() + if errs != nil { + return nil, errs + } + + // create a new parameter validator + paramValidator := parameters.NewParameterValidator(&m.Model) + + // create a new request body validator + reqBodyValidator := requests.NewRequestBodyValidator(&m.Model) + + // create a response body validator + respBodyValidator := responses.NewResponseBodyValidator(&m.Model) + + return &validator{ + v3Model: &m.Model, + document: document, + requestValidator: reqBodyValidator, + responseValidator: respBodyValidator, + paramValidator: paramValidator, + }, nil +} + +func (v *validator) GetParameterValidator() parameters.ParameterValidator { + return v.paramValidator +} +func (v *validator) GetRequestBodyValidator() requests.RequestBodyValidator { + return v.requestValidator +} +func (v *validator) GetResponseBodyValidator() responses.ResponseBodyValidator { + return v.responseValidator +} + +func (v *validator) ValidateDocument() (bool, []*errors.ValidationError) { + return schema_validation.ValidateOpenAPIDocument(v.document) +} + +func (v *validator) ValidateHttpRequestResponse( + request *http.Request, + response *http.Response) (bool, []*errors.ValidationError) { + + // find path + var pathItem *v3.PathItem + var pathValue string + var errs []*errors.ValidationError + if v.foundPath == nil { + pathItem, errs, pathValue = paths.FindPath(request, v.v3Model) + if pathItem == nil || errs != nil { + v.errors = errs + return false, errs + } + v.foundPath = pathItem + v.foundPathValue = pathValue + } else { + pathItem = v.foundPath + pathValue = v.foundPathValue + } + + responseBodyValidator := v.responseValidator + responseBodyValidator.SetPathItem(pathItem, pathValue) + + // validate request and response + _, requestErrors := v.ValidateHttpRequest(request) + _, responseErrors := responseBodyValidator.ValidateResponseBody(request, response) + + if len(requestErrors) > 0 || len(responseErrors) > 0 { + return false, append(requestErrors, responseErrors...) + } + return true, nil +} + +func (v *validator) ValidateHttpRequest(request *http.Request) (bool, []*errors.ValidationError) { + + // find path + var pathItem *v3.PathItem + var pathValue string + var errs []*errors.ValidationError + if v.foundPath == nil { + pathItem, errs, pathValue = paths.FindPath(request, v.v3Model) + if pathItem == nil || errs != nil { + v.errors = errs + return false, errs + } + v.foundPath = pathItem + v.foundPathValue = pathValue + } else { + pathItem = v.foundPath + pathValue = v.foundPathValue + } + + // create a new parameter validator + paramValidator := v.paramValidator + paramValidator.SetPathItem(pathItem, pathValue) + + // create a new request body validator + reqBodyValidator := v.requestValidator + reqBodyValidator.SetPathItem(pathItem, pathValue) + + // create some channels to handle async validation + doneChan := make(chan bool) + errChan := make(chan []*errors.ValidationError) + controlChan := make(chan bool) + + parameterValidationFunc := func(control chan bool, errorChan chan []*errors.ValidationError) { + paramErrs := make(chan []*errors.ValidationError) + paramControlChan := make(chan bool) + paramFunctionControlChan := make(chan bool) + var paramValidationErrors []*errors.ValidationError + + validations := []validationFunction{ + paramValidator.ValidatePathParams, + paramValidator.ValidateCookieParams, + paramValidator.ValidateHeaderParams, + paramValidator.ValidateQueryParams, + } + + paramListener := func(control chan bool, errorChan chan []*errors.ValidationError) { + completedValidations := 0 + for { + select { + case vErrs := <-errorChan: + paramValidationErrors = append(paramValidationErrors, vErrs...) + case <-control: + completedValidations++ + if completedValidations == len(validations) { + paramFunctionControlChan <- true + return + } + } + } + } + + validateParamFunction := func( + control chan bool, + errorChan chan []*errors.ValidationError, + validatorFunc validationFunction) { + valid, pErrs := validatorFunc(request) + if !valid { + errorChan <- pErrs + } + control <- true + } + go paramListener(paramControlChan, paramErrs) + for i := range validations { + go validateParamFunction(paramControlChan, paramErrs, validations[i]) + } + // wait for all the validations to complete + + <-paramFunctionControlChan + if len(paramValidationErrors) > 0 { + errorChan <- paramValidationErrors + } + // let runValidation know we are done with this part. + controlChan <- true + } + + requestBodyValidationFunc := func(control chan bool, errorChan chan []*errors.ValidationError) { + valid, pErrs := reqBodyValidator.ValidateRequestBody(request) + if !valid { + errorChan <- pErrs + } + control <- true + } + + // build async functions + asyncFunctions := []validationFunctionAsync{ + parameterValidationFunc, + requestBodyValidationFunc, + } + + var validationErrors []*errors.ValidationError + var validationLock sync.Mutex + + runValidation := func(control chan bool, errorChan chan []*errors.ValidationError) { + completedValidations := 0 + for { + select { + case vErrs := <-errorChan: + validationLock.Lock() + validationErrors = append(validationErrors, vErrs...) + validationLock.Unlock() + case <-control: + completedValidations++ + if completedValidations == len(asyncFunctions) { + doneChan <- true + return + } + } + } + } + + // sit and wait for everything to report back. + go runValidation(controlChan, errChan) + + // run async functions + for i := range asyncFunctions { + go asyncFunctions[i](controlChan, errChan) + } + + // wait for all the validations to complete + <-doneChan + + if len(validationErrors) > 0 { + return false, validationErrors + } + return true, nil +} + +type validator struct { + v3Model *v3.Document + document libopenapi.Document + foundPath *v3.PathItem + foundPathValue string + paramValidator parameters.ParameterValidator + requestValidator requests.RequestBodyValidator + responseValidator responses.ResponseBodyValidator + errors []*errors.ValidationError +} + +type validationFunction func(request *http.Request) (bool, []*errors.ValidationError) +type validationFunctionAsync func(control chan bool, errorChan chan []*errors.ValidationError) diff --git a/validator_test.go b/validator_test.go new file mode 100644 index 0000000..9865df0 --- /dev/null +++ b/validator_test.go @@ -0,0 +1,445 @@ +// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package validator + +import ( + "bytes" + "encoding/json" + "github.com/pb33f/libopenapi" + "github.com/pb33f/libopenapi-validator/helpers" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "os" + "testing" +) + +func TestNewValidator_ValidateHttpRequest_ValidPostSimpleSchema(t *testing.T) { + + spec := `openapi: 3.1.0 +paths: + /burgers/createBurger: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + patties: + type: integer + vegetarian: + type: boolean` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + + v, _ := NewValidator(doc) + + body := map[string]interface{}{ + "name": "Big Mac", + "patties": 2, + "vegetarian": true, + } + + bodyBytes, _ := json.Marshal(body) + + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", + bytes.NewBuffer(bodyBytes)) + request.Header.Set("Content-Type", "application/json") + + valid, errors := v.ValidateHttpRequest(request) + + assert.True(t, valid) + assert.Len(t, errors, 0) + +} + +func TestNewValidator_ValidateHttpRequest_InvalidPostSchema(t *testing.T) { + + spec := `openapi: 3.1.0 +paths: + /burgers/createBurger: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + patties: + type: integer + vegetarian: + type: boolean` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + + v, _ := NewValidator(doc) + + // mix up the primitives to fire two schema violations. + body := map[string]interface{}{ + "name": "Big Mac", + "patties": false, // wrong. + "vegetarian": false, + } + + bodyBytes, _ := json.Marshal(body) + + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", + bytes.NewBuffer(bodyBytes)) + request.Header.Set("Content-Type", "application/json") + + valid, errors := v.ValidateHttpRequest(request) + + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Equal(t, "expected integer, but got boolean", errors[0].SchemaValidationErrors[0].Reason) + +} + +func TestNewValidator_ValidateHttpRequest_InvalidQuery(t *testing.T) { + + spec := `openapi: 3.1.0 +paths: + /burgers/createBurger: + parameters: + - in: query + name: cheese + required: true + schema: + type: string + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + patties: + type: integer + vegetarian: + type: boolean` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + + v, _ := NewValidator(doc) + + body := map[string]interface{}{ + "name": "Big Mac", + "patties": 2, // wrong. + "vegetarian": false, + } + + bodyBytes, _ := json.Marshal(body) + + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", + bytes.NewBuffer(bodyBytes)) + request.Header.Set("Content-Type", "application/json") + + valid, errors := v.ValidateHttpRequest(request) + + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Equal(t, "Query parameter 'cheese' is missing", errors[0].Message) + +} + +var petstoreBytes []byte + +func init() { + petstoreBytes, _ = os.ReadFile("test_specs/petstorev3.json") +} + +func TestNewValidator_PetStore_PetPost200_Valid(t *testing.T) { + + // create a new document from the petstore spec + doc, _ := libopenapi.NewDocument(petstoreBytes) + + // create a doc + v, _ := NewValidator(doc) + + // create a pet + body := map[string]interface{}{ + "id": 123, + "name": "cotton", + "category": map[string]interface{}{ + "id": 123, + "name": "dogs", + }, + "photoUrls": []string{"https://example.com"}, + } + + // marshal the body into bytes. + bodyBytes, _ := json.Marshal(body) + + // create a new put request + request, _ := http.NewRequest(http.MethodPut, "https://hyperspace-superherbs.com/pet", + bytes.NewBuffer(bodyBytes)) + request.Header.Set("Content-Type", "application/json") + + // simulate a request/response, in this case the contract returns a 200 with the pet we just created. + res := httptest.NewRecorder() + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) + w.WriteHeader(http.StatusOK) + _, _ = w.Write(bodyBytes) + } + + // fire the request + handler(res, request) + + // validate the response (should be clean) + valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) + + // should all be perfectly valid. + assert.True(t, valid) + assert.Len(t, errors, 0) +} + +func TestNewValidator_PetStore_PetPost200_Invalid(t *testing.T) { + + // create a new document from the petstore spec + doc, _ := libopenapi.NewDocument(petstoreBytes) + + // create a doc + v, _ := NewValidator(doc) + + // create a pet, but is missing the photoUrls field + body := map[string]interface{}{ + "id": 123, + "name": "cotton", + "category": map[string]interface{}{ + "id": 123, + "name": "dogs", + }, + } + + // marshal the body into bytes. + bodyBytes, _ := json.Marshal(body) + + // create a new put request + request, _ := http.NewRequest(http.MethodPost, "https://hyperspace-superherbs.com/pet", + bytes.NewBuffer(bodyBytes)) + request.Header.Set("Content-Type", "application/json") + + // simulate a request/response, in this case the contract returns a 200 with the pet we just created. + res := httptest.NewRecorder() + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) + w.WriteHeader(http.StatusProxyAuthRequired) // this is not defined by the contract, so it should fail. + _, _ = w.Write(bodyBytes) + } + + // fire the request + handler(res, request) + + valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) + + // we have a schema violation, and a response code violation, our validator should have picked them + // both up. + assert.False(t, valid) + assert.Len(t, errors, 2) + + // check errors + for i := range errors { + if errors[i].SchemaValidationErrors != nil { + assert.Equal(t, "missing properties: 'photoUrls'", errors[i].SchemaValidationErrors[0].Reason) + } else { + assert.Equal(t, "POST operation request response code '407' does not exist", errors[i].Message) + } + } +} + +func TestNewValidator_PetStore_PetFindByStatusGet200_Valid(t *testing.T) { + + // create a new document from the petstore spec + doc, _ := libopenapi.NewDocument(petstoreBytes) + + // create a doc + v, _ := NewValidator(doc) + + // create a pet + body := map[string]interface{}{ + "id": 123, + "name": "cotton", + "category": map[string]interface{}{ + "id": 123, + "name": "dogs", + }, + "photoUrls": []string{"https://example.com"}, + } + + // marshal the body into bytes. + bodyBytes, _ := json.Marshal([]interface{}{body}) // operation returns an array of pets + + // create a new put request + request, _ := http.NewRequest(http.MethodGet, + "https://hyperspace-superherbs.com/pet/findByStatus?status=sold", nil) + request.Header.Set("Content-Type", "application/json") + + // simulate a request/response, in this case the contract returns a 200 with the pet we just created. + res := httptest.NewRecorder() + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) + w.WriteHeader(http.StatusOK) + _, _ = w.Write(bodyBytes) + } + + // fire the request + handler(res, request) + + // validate the response (should be clean) + valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) + + // should all be perfectly valid. + assert.True(t, valid) + assert.Len(t, errors, 0) +} + +func TestNewValidator_PetStore_PetFindByStatusGet200_BadEnum(t *testing.T) { + + // create a new document from the petstore spec + doc, _ := libopenapi.NewDocument(petstoreBytes) + + // create a doc + v, _ := NewValidator(doc) + + // create a pet + body := map[string]interface{}{ + "id": 123, + "name": "cotton", + "category": map[string]interface{}{ + "id": 123, + "name": "dogs", + }, + "photoUrls": []string{"https://example.com"}, + } + + // marshal the body into bytes. + bodyBytes, _ := json.Marshal([]interface{}{body}) // operation returns an array of pets + + // create a new put request + request, _ := http.NewRequest(http.MethodGet, + "https://hyperspace-superherbs.com/pet/findByStatus?status=invalidEnum", nil) // enum is invalid + request.Header.Set("Content-Type", "application/json") + + // simulate a request/response, in this case the contract returns a 200 with a pet + res := httptest.NewRecorder() + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) + w.Header().Set("Herbs-And-Spice", helpers.JSONContentType) + w.WriteHeader(http.StatusOK) + _, _ = w.Write(bodyBytes) + } + + // fire the request + handler(res, request) + + // validate the response (should be clean) + valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) + + // should all be perfectly valid. + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Equal(t, "Query parameter 'status' does not match allowed values", errors[0].Message) + assert.Equal(t, "Instead of 'invalidEnum', use one of the allowed values: 'available, pending, sold'", errors[0].HowToFix) + +} + +func TestNewValidator_PetStore_PetFindByTagsGet200_Valid(t *testing.T) { + + // create a new document from the petstore spec + doc, _ := libopenapi.NewDocument(petstoreBytes) + + // create a doc + v, _ := NewValidator(doc) + + // create a pet + body := map[string]interface{}{ + "id": 123, + "name": "cotton", + "category": map[string]interface{}{ + "id": 123, + "name": "dogs", + }, + "photoUrls": []string{"https://example.com"}, + } + + // marshal the body into bytes. + bodyBytes, _ := json.Marshal([]interface{}{body}) // operation returns an array of pets + + // create a new put request + request, _ := http.NewRequest(http.MethodGet, + "https://hyperspace-superherbs.com/pet/findByTags?tags=fuzzy&tags=wuzzy", nil) + request.Header.Set("Content-Type", "application/json") + + // simulate a request/response, in this case the contract returns a 200 with the pet we just created. + res := httptest.NewRecorder() + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) + w.WriteHeader(http.StatusOK) + _, _ = w.Write(bodyBytes) + } + + // fire the request + handler(res, request) + + // validate the response (should be clean) + valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) + + // should all be perfectly valid. + assert.True(t, valid) + assert.Len(t, errors, 0) +} + +func TestNewValidator_PetStore_PetFindByTagsGet200_InvalidExplode(t *testing.T) { + + // create a new document from the petstore spec + doc, _ := libopenapi.NewDocument(petstoreBytes) + + // create a doc + v, _ := NewValidator(doc) + + // create a pet + body := map[string]interface{}{ + "id": 123, + "name": "cotton", + "category": map[string]interface{}{ + "id": 123, + "name": "dogs", + }, + "photoUrls": []string{"https://example.com"}, + } + + // marshal the body into bytes. + bodyBytes, _ := json.Marshal([]interface{}{body}) // operation returns an array of pets + + // create a new put request + request, _ := http.NewRequest(http.MethodGet, + "https://hyperspace-superherbs.com/pet/findByTags?tags=fuzzy,wuzzy", nil) + request.Header.Set("Content-Type", "application/json") + + // simulate a request/response, in this case the contract returns a 200 with the pet we just created. + res := httptest.NewRecorder() + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) + w.WriteHeader(http.StatusOK) + _, _ = w.Write(bodyBytes) + } + + // fire the request + handler(res, request) + + // validate the response will fail because the explode is wrong. + valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) + + // will fail. + assert.False(t, valid) + assert.Len(t, errors, 2) // will fire allow reserved error, and explode error. +}