diff --git a/parameters/cookie_parameters.go b/parameters/cookie_parameters.go index 888d29e..5e85e08 100644 --- a/parameters/cookie_parameters.go +++ b/parameters/cookie_parameters.go @@ -7,32 +7,36 @@ import ( "fmt" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" - "github.com/pb33f/libopenapi-validator/paths" "github.com/pb33f/libopenapi/datamodel/high/base" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "net/http" "strconv" "strings" + "github.com/pb33f/libopenapi-validator/paths" ) func (v *paramValidator) ValidateCookieParams(request *http.Request) (bool, []*errors.ValidationError) { - - // find path - var pathItem *v3.PathItem - var foundPath string - var errs []*errors.ValidationError - - if v.pathItem == nil { - pathItem, errs, foundPath = paths.FindPath(request, v.document) - if pathItem == nil || errs != nil { - v.errors = errs - return false, errs - } - } else { - pathItem = v.pathItem - foundPath = v.pathValue + pathItem, errs, foundPath := paths.FindPath(request, v.document) + if len(errs) > 0 { + return false, errs } + return v.ValidateCookieParamsWithPathItem(request, pathItem, foundPath) +} +func (v *paramValidator) ValidateCookieParamsWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) { + if pathItem == nil { + return false, []*errors.ValidationError{{ + ValidationType: helpers.ParameterValidationPath, + ValidationSubType: "missing", + Message: fmt.Sprintf("%s Path '%s' not found", request.Method, request.URL.Path), + Reason: fmt.Sprintf("The %s request contains a path of '%s' "+ + "however that path, or the %s method for that path does not exist in the specification", + request.Method, request.URL.Path, request.Method), + SpecLine: -1, + SpecCol: -1, + HowToFix: errors.HowToFixPath, + }} + } // extract params for the operation var params = helpers.ExtractParamsForOperation(request, pathItem) var validationErrors []*errors.ValidationError @@ -125,7 +129,7 @@ func (v *paramValidator) ValidateCookieParams(request *http.Request) (bool, []*e } } - errors.PopulateValidationErrors(validationErrors, request, foundPath) + errors.PopulateValidationErrors(validationErrors, request, pathValue) if len(validationErrors) > 0 { return false, validationErrors diff --git a/parameters/cookie_parameters_test.go b/parameters/cookie_parameters_test.go index 34bf329..fc3ab3d 100644 --- a/parameters/cookie_parameters_test.go +++ b/parameters/cookie_parameters_test.go @@ -550,11 +550,41 @@ paths: // preset the path path, _, pv := paths.FindPath(request, &m.Model) - v.SetPathItem(path, pv) - valid, errors := v.ValidateCookieParams(request) + valid, errors := v.ValidateCookieParamsWithPathItem(request, path, pv) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Instead of '2500', use one of the allowed values: '1, 2, 99'", errors[0].HowToFix) } + +func TestNewValidator_PresetPath_notfound(t *testing.T) { + + spec := `openapi: 3.1.0 +paths: + /burgers/beef: + get: + parameters: + - name: PattyPreference + in: cookie + required: true + schema: + type: integer + enum: [1, 2, 99]` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() + v := NewParameterValidator(&m.Model) + + request, _ := http.NewRequest(http.MethodGet, "https://things.com/pizza/beef", nil) + request.AddCookie(&http.Cookie{Name: "PattyPreference", Value: "2500"}) // too many dude. + + // preset the path + path, _, pv := paths.FindPath(request, &m.Model) + + valid, errors := v.ValidateCookieParamsWithPathItem(request, path, pv) + + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Equal(t, "GET Path '/pizza/beef' not found", errors[0].Message) +} diff --git a/parameters/header_parameters.go b/parameters/header_parameters.go index 2eb02b5..3d872ef 100644 --- a/parameters/header_parameters.go +++ b/parameters/header_parameters.go @@ -11,27 +11,33 @@ import ( "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" - "github.com/pb33f/libopenapi-validator/paths" "github.com/pb33f/libopenapi/datamodel/high/base" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" + "github.com/pb33f/libopenapi-validator/paths" ) func (v *paramValidator) ValidateHeaderParams(request *http.Request) (bool, []*errors.ValidationError) { - // find path - var pathItem *v3.PathItem - var specPath string - var errs []*errors.ValidationError - if v.pathItem == nil { - pathItem, errs, specPath = paths.FindPath(request, v.document) - if pathItem == nil || errs != nil { - v.errors = errs - return false, errs - } - } else { - pathItem = v.pathItem - specPath = v.pathValue + pathItem, errs, foundPath := paths.FindPath(request, v.document) + if len(errs) > 0 { + return false, errs } + return v.ValidateHeaderParamsWithPathItem(request, pathItem, foundPath) +} +func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) { + if pathItem == nil { + return false, []*errors.ValidationError{{ + ValidationType: helpers.ParameterValidationPath, + ValidationSubType: "missing", + Message: fmt.Sprintf("%s Path '%s' not found", request.Method, request.URL.Path), + Reason: fmt.Sprintf("The %s request contains a path of '%s' "+ + "however that path, or the %s method for that path does not exist in the specification", + request.Method, request.URL.Path, request.Method), + SpecLine: -1, + SpecCol: -1, + HowToFix: errors.HowToFixPath, + }} + } // extract params for the operation params := helpers.ExtractParamsForOperation(request, pathItem) @@ -145,7 +151,7 @@ func (v *paramValidator) ValidateHeaderParams(request *http.Request) (bool, []*e } } - errors.PopulateValidationErrors(validationErrors, request, specPath) + errors.PopulateValidationErrors(validationErrors, request, pathValue) if len(validationErrors) > 0 { return false, validationErrors diff --git a/parameters/header_parameters_test.go b/parameters/header_parameters_test.go index 69f3bc2..d5e4bad 100644 --- a/parameters/header_parameters_test.go +++ b/parameters/header_parameters_test.go @@ -624,12 +624,42 @@ paths: // preset the path path, _, pv := paths.FindPath(request, &m.Model) - v.SetPathItem(path, pv) - valid, errors := v.ValidateHeaderParams(request) + valid, errors := v.ValidateHeaderParamsWithPathItem(request, path, pv) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "Instead of '1200', "+ "use one of the allowed values: '1, 2, 99'", errors[0].HowToFix) } + +func TestNewValidator_HeaderParamSetPath_notfound(t *testing.T) { + + spec := `openapi: 3.1.0 +paths: + /vending/drinks: + get: + parameters: + - name: coffeeCups + in: header + required: true + schema: + type: integer + enum: [1,2,99]` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() + v := NewParameterValidator(&m.Model) + + request, _ := http.NewRequest(http.MethodGet, "https://things.com/buying/drinks", nil) + request.Header.Set("coffeecups", "1200") // that's a lot of cups dude, we only have one dishwasher. + + // preset the path + path, _, pv := paths.FindPath(request, &m.Model) + + valid, errors := v.ValidateHeaderParamsWithPathItem(request, path, pv) + + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Equal(t, "GET Path '/buying/drinks' not found", errors[0].Message) +} diff --git a/parameters/parameters.go b/parameters/parameters.go index 2f49f82..399ffef 100644 --- a/parameters/parameters.go +++ b/parameters/parameters.go @@ -14,42 +14,53 @@ import ( // // ValidateQueryParams will validate the query parameters for the request // ValidateHeaderParams will validate the header parameters for the request -// ValidateCookieParams will validate the cookie parameters for the request +// ValidateCookieParamsWithPathItem will validate the cookie parameters for the request // ValidatePathParams will validate the path parameters for the request // // Each method accepts an *http.Request and returns true if validation passed, // false if validation failed and a slice of ValidationError pointers. type ParameterValidator interface { - - // SetPathItem will set the pathItem for the ParameterValidator, all validations will be performed against this pathItem - // otherwise if not set, each validation will perform a lookup for the pathItem based on the *http.Request - SetPathItem(path *v3.PathItem, pathValue string) - // ValidateQueryParams accepts an *http.Request and validates the query parameters against the OpenAPI specification. // The method will locate the correct path, and operation, based on the verb. The parameters for the operation // will be matched and validated against what has been supplied in the http.Request query string. ValidateQueryParams(request *http.Request) (bool, []*errors.ValidationError) + // ValidateQueryParamsWithPathItem accepts an *http.Request and validates the query parameters against the OpenAPI specification. + // The method will locate the correct path, and operation, based on the verb. The parameters for the operation + // will be matched and validated against what has been supplied in the http.Request query string. + ValidateQueryParamsWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) + // ValidateHeaderParams validates the header parameters contained within *http.Request. It returns a boolean // stating true if validation passed (false for failed), and a slice of errors if validation failed. ValidateHeaderParams(request *http.Request) (bool, []*errors.ValidationError) + // ValidateHeaderParamsWithPathItem validates the header parameters contained within *http.Request. It returns a boolean + // stating true if validation passed (false for failed), and a slice of errors if validation failed. + ValidateHeaderParamsWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) + // ValidateCookieParams validates the cookie parameters contained within *http.Request. // It returns a boolean stating true if validation passed (false for failed), and a slice of errors if validation failed. ValidateCookieParams(request *http.Request) (bool, []*errors.ValidationError) + // ValidateCookieParamsWithPathItem validates the cookie parameters contained within *http.Request. + // It returns a boolean stating true if validation passed (false for failed), and a slice of errors if validation failed. + ValidateCookieParamsWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) + // ValidatePathParams validates the path parameters contained within *http.Request. It returns a boolean stating true // if validation passed (false for failed), and a slice of errors if validation failed. ValidatePathParams(request *http.Request) (bool, []*errors.ValidationError) + // ValidatePathParamsWithPathItem validates the path parameters contained within *http.Request. It returns a boolean stating true + // if validation passed (false for failed), and a slice of errors if validation failed. + ValidatePathParamsWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) + // ValidateSecurity validates the security requirements for the operation. It returns a boolean stating true // if validation passed (false for failed), and a slice of errors if validation failed. ValidateSecurity(request *http.Request) (bool, []*errors.ValidationError) -} -func (v *paramValidator) SetPathItem(path *v3.PathItem, pathValue string) { - v.pathItem = path - v.pathValue = pathValue + // ValidateSecurityWithPathItem validates the security requirements for the operation. It returns a boolean stating true + // if validation passed (false for failed), and a slice of errors if validation failed. + ValidateSecurityWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) } // NewParameterValidator will create a new ParameterValidator from an OpenAPI 3+ document @@ -58,8 +69,5 @@ func NewParameterValidator(document *v3.Document) ParameterValidator { } type paramValidator struct { - document *v3.Document - pathItem *v3.PathItem - pathValue string - errors []*errors.ValidationError + document *v3.Document } diff --git a/parameters/path_parameters.go b/parameters/path_parameters.go index 00726ca..2a6dce7 100644 --- a/parameters/path_parameters.go +++ b/parameters/path_parameters.go @@ -17,25 +17,30 @@ import ( ) func (v *paramValidator) ValidatePathParams(request *http.Request) (bool, []*errors.ValidationError) { - - // find path - var pathItem *v3.PathItem - var errs []*errors.ValidationError - var foundPath string - if v.pathItem == nil && v.pathValue == "" { - pathItem, errs, foundPath = paths.FindPath(request, v.document) - if pathItem == nil || errs != nil { - v.errors = errs - return false, errs - } - } else { - pathItem = v.pathItem - foundPath = v.pathValue + pathItem, errs, foundPath := paths.FindPath(request, v.document) + if len(errs) > 0 { + return false, errs } + return v.ValidatePathParamsWithPathItem(request, pathItem, foundPath) +} +func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) { + if pathItem == nil { + return false, []*errors.ValidationError{{ + ValidationType: helpers.ParameterValidationPath, + ValidationSubType: "missing", + Message: fmt.Sprintf("%s Path '%s' not found", request.Method, request.URL.Path), + Reason: fmt.Sprintf("The %s request contains a path of '%s' "+ + "however that path, or the %s method for that path does not exist in the specification", + request.Method, request.URL.Path, request.Method), + SpecLine: -1, + SpecCol: -1, + HowToFix: errors.HowToFixPath, + }} + } // split the path into segments submittedSegments := strings.Split(paths.StripRequestPath(request, v.document), helpers.Slash) - pathSegments := strings.Split(foundPath, helpers.Slash) + pathSegments := strings.Split(pathValue, helpers.Slash) // extract params for the operation var params = helpers.ExtractParamsForOperation(request, pathItem) @@ -283,7 +288,7 @@ func (v *paramValidator) ValidatePathParams(request *http.Request) (bool, []*err } } - errors.PopulateValidationErrors(validationErrors, request, foundPath) + errors.PopulateValidationErrors(validationErrors, request, pathValue) if len(validationErrors) > 0 { return false, validationErrors diff --git a/parameters/path_parameters_test.go b/parameters/path_parameters_test.go index 3ff659d..c3c22d0 100644 --- a/parameters/path_parameters_test.go +++ b/parameters/path_parameters_test.go @@ -1397,9 +1397,8 @@ paths: // preset the path path, _, pv := paths.FindPath(request, &m.Model) - v.SetPathItem(path, pv) - valid, errors := v.ValidatePathParams(request) + valid, errors := v.ValidatePathParamsWithPathItem(request, path, pv) assert.False(t, valid) assert.Len(t, errors, 1) @@ -1407,6 +1406,39 @@ paths: assert.Equal(t, "Instead of '22334', use one of the allowed values: '1, 2, 99, 100'", errors[0].HowToFix) } +func TestNewValidator_SetPathForPathParam_notfound(t *testing.T) { + + spec := `openapi: 3.1.0 +paths: + /burgers/{;burgerId}/locate: + parameters: + - name: burgerId + in: path + style: matrix + schema: + type: number + enum: [1,2,99,100] + get: + operationId: locateBurgers` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + + m, _ := doc.BuildV3Model() + + v := NewParameterValidator(&m.Model) + + request, _ := http.NewRequest(http.MethodGet, "https://things.com/pizza/;burgerId=22334/locate", nil) + + // preset the path + path, _, pv := paths.FindPath(request, &m.Model) + + valid, errors := v.ValidatePathParamsWithPathItem(request, path, pv) + + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Equal(t, "GET Path '/pizza/;burgerId=22334/locate' not found", errors[0].Message) +} + func TestNewValidator_ServerPathPrefixInRequestPath(t *testing.T) { spec := `openapi: 3.1.0 diff --git a/parameters/query_parameters.go b/parameters/query_parameters.go index b611763..5945372 100644 --- a/parameters/query_parameters.go +++ b/parameters/query_parameters.go @@ -13,28 +13,34 @@ import ( "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" - "github.com/pb33f/libopenapi-validator/paths" "github.com/pb33f/libopenapi/datamodel/high/base" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi/orderedmap" + "github.com/pb33f/libopenapi-validator/paths" ) func (v *paramValidator) ValidateQueryParams(request *http.Request) (bool, []*errors.ValidationError) { - // find path - var pathItem *v3.PathItem - var foundPath string - var errs []*errors.ValidationError - if v.pathItem == nil { - pathItem, errs, foundPath = paths.FindPath(request, v.document) - if pathItem == nil || errs != nil { - v.errors = errs - return false, errs - } - } else { - pathItem = v.pathItem - foundPath = v.pathValue + pathItem, errs, foundPath := paths.FindPath(request, v.document) + if len(errs) > 0 { + return false, errs } + return v.ValidateQueryParamsWithPathItem(request, pathItem, foundPath) +} +func (v *paramValidator) ValidateQueryParamsWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) { + if pathItem == nil { + return false, []*errors.ValidationError{{ + ValidationType: helpers.ParameterValidationPath, + ValidationSubType: "missing", + Message: fmt.Sprintf("%s Path '%s' not found", request.Method, request.URL.Path), + Reason: fmt.Sprintf("The %s request contains a path of '%s' "+ + "however that path, or the %s method for that path does not exist in the specification", + request.Method, request.URL.Path, request.Method), + SpecLine: -1, + SpecCol: -1, + HowToFix: errors.HowToFixPath, + }} + } // extract params for the operation params := helpers.ExtractParamsForOperation(request, pathItem) queryParams := make(map[string][]*helpers.QueryParam) @@ -213,9 +219,8 @@ doneLooking: } } - errors.PopulateValidationErrors(validationErrors, request, foundPath) + errors.PopulateValidationErrors(validationErrors, request, pathValue) - v.errors = validationErrors if len(validationErrors) > 0 { return false, validationErrors } diff --git a/parameters/query_parameters_test.go b/parameters/query_parameters_test.go index 648ca2b..81ebaa9 100644 --- a/parameters/query_parameters_test.go +++ b/parameters/query_parameters_test.go @@ -2347,15 +2347,53 @@ paths: // preset the path path, _, pv := paths.FindPath(request, &m.Model) - v.SetPathItem(path, pv) - valid, errors := v.ValidateQueryParams(request) + valid, errors := v.ValidateQueryParamsWithPathItem(request, path, pv) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "expected boolean, but got number", errors[0].SchemaValidationErrors[0].Reason) } +func TestNewValidator_QueryParamSetPath_notfound(t *testing.T) { + spec := `openapi: 3.1.0 +paths: + /a/fishy/on/a/dishy: + get: + parameters: + - name: fishy + in: query + required: true + style: deepObject + schema: + type: object + properties: + ocean: + type: string + salt: + type: boolean + required: [ocean, salt] + operationId: locateFishy` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + + m, _ := doc.BuildV3Model() + + v := NewParameterValidator(&m.Model) + + request, _ := http.NewRequest(http.MethodGet, + "https://things.com/a/beef/on/a/dishy?fishy[ocean]=atlantic&fishy[salt]=12", nil) + + // preset the path + path, _, pv := paths.FindPath(request, &m.Model) + + valid, errors := v.ValidateQueryParamsWithPathItem(request, path, pv) + assert.False(t, valid) + + assert.Len(t, errors, 1) + assert.Equal(t, "GET Path '/a/beef/on/a/dishy' not found", errors[0].Message) +} + func TestNewValidator_QueryParamValidateStyle_DeepObjectMultiValuesFailedMultipleSchemas(t *testing.T) { spec := `--- openapi: 3.1.0 diff --git a/parameters/validate_security.go b/parameters/validate_security.go index e5e7ee5..d2e6e9b 100644 --- a/parameters/validate_security.go +++ b/parameters/validate_security.go @@ -10,27 +10,33 @@ import ( "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" - "github.com/pb33f/libopenapi-validator/paths" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi/orderedmap" + "github.com/pb33f/libopenapi-validator/paths" ) func (v *paramValidator) ValidateSecurity(request *http.Request) (bool, []*errors.ValidationError) { - // find path - var pathItem *v3.PathItem - var pathFound string - var errs []*errors.ValidationError - if v.pathItem == nil { - pathItem, errs, pathFound = paths.FindPath(request, v.document) - if pathItem == nil || errs != nil { - v.errors = errs - return false, errs - } - } else { - pathItem = v.pathItem - pathFound = v.pathValue + pathItem, errs, foundPath := paths.FindPath(request, v.document) + if len(errs) > 0 { + return false, errs } + return v.ValidateSecurityWithPathItem(request, pathItem, foundPath) +} +func (v *paramValidator) ValidateSecurityWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) { + if pathItem == nil { + return false, []*errors.ValidationError{{ + ValidationType: helpers.ParameterValidationPath, + ValidationSubType: "missing", + Message: fmt.Sprintf("%s Path '%s' not found", request.Method, request.URL.Path), + Reason: fmt.Sprintf("The %s request contains a path of '%s' "+ + "however that path, or the %s method for that path does not exist in the specification", + request.Method, request.URL.Path, request.Method), + SpecLine: -1, + SpecCol: -1, + HowToFix: errors.HowToFixPath, + }} + } // extract security for the operation security := helpers.ExtractSecurityForOperation(request, pathItem) @@ -55,7 +61,7 @@ func (v *paramValidator) ValidateSecurity(request *http.Request) (bool, []*error HowToFix: "Add the missing security scheme to the components", }, } - errors.PopulateValidationErrors(validationErrors, request, pathFound) + errors.PopulateValidationErrors(validationErrors, request, pathValue) return false, validationErrors } @@ -78,7 +84,7 @@ func (v *paramValidator) ValidateSecurity(request *http.Request) (bool, []*error }, } - errors.PopulateValidationErrors(validationErrors, request, pathFound) + errors.PopulateValidationErrors(validationErrors, request, pathValue) return false, validationErrors } @@ -100,7 +106,7 @@ func (v *paramValidator) ValidateSecurity(request *http.Request) (bool, []*error }, } - errors.PopulateValidationErrors(validationErrors, request, pathFound) + errors.PopulateValidationErrors(validationErrors, request, pathValue) return false, validationErrors } @@ -126,7 +132,7 @@ func (v *paramValidator) ValidateSecurity(request *http.Request) (bool, []*error }, } - errors.PopulateValidationErrors(validationErrors, request, pathFound) + errors.PopulateValidationErrors(validationErrors, request, pathValue) return false, validationErrors } @@ -153,7 +159,7 @@ func (v *paramValidator) ValidateSecurity(request *http.Request) (bool, []*error }, } - errors.PopulateValidationErrors(validationErrors, request, pathFound) + errors.PopulateValidationErrors(validationErrors, request, pathValue) return false, validationErrors } diff --git a/parameters/validate_security_test.go b/parameters/validate_security_test.go index 823b20d..2d68164 100644 --- a/parameters/validate_security_test.go +++ b/parameters/validate_security_test.go @@ -368,11 +368,33 @@ paths: v := NewParameterValidator(&m.Model) request, _ := http.NewRequest(http.MethodPost, "https://things.com/products", nil) - pathItem, errs, _ := paths.FindPath(request, &m.Model) + pathItem, errs, pv := paths.FindPath(request, &m.Model) assert.Nil(t, errs) - v.(*paramValidator).pathItem = pathItem - valid, errors := v.ValidateSecurity(request) + valid, errors := v.ValidateSecurityWithPathItem(request, pathItem, pv) assert.True(t, valid) assert.Equal(t, 0, len(errors)) } + +func TestParamValidator_ValidateSecurity_PresetPath_notfound(t *testing.T) { + + spec := `openapi: 3.1.0 +paths: + /products: + post: +` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + + m, _ := doc.BuildV3Model() + + v := NewParameterValidator(&m.Model) + + request, _ := http.NewRequest(http.MethodPost, "https://things.com/beef", nil) + pathItem, _, pv := paths.FindPath(request, &m.Model) + + valid, errors := v.ValidateSecurityWithPathItem(request, pathItem, pv) + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Equal(t, "POST Path '/beef' not found", errors[0].Message) +} diff --git a/requests/request_body.go b/requests/request_body.go index cf95ef9..0475def 100644 --- a/requests/request_body.go +++ b/requests/request_body.go @@ -13,19 +13,18 @@ import ( // RequestBodyValidator is an interface that defines the methods for validating request bodies for Operations. // -// ValidateRequestBody method accepts an *http.Request and returns true if validation passed, +// ValidateRequestBodyWithPathItem method accepts an *http.Request and returns true if validation passed, // false if validation failed and a slice of ValidationError pointers. type RequestBodyValidator interface { - // ValidateRequestBody will validate the request body for an operation. The first return value will be true if the // request body is valid, false if it is not. The second return value will be a slice of ValidationError pointers if // the body is not valid. ValidateRequestBody(request *http.Request) (bool, []*errors.ValidationError) - // SetPathItem will set the pathItem for the RequestBodyValidator, all validations will be performed - // against this pathItem otherwise if not set, each validation will perform a lookup for the pathItem - // based on the *http.Request - SetPathItem(path *v3.PathItem, pathValue string) + // ValidateRequestBodyWithPathItem will validate the request body for an operation. The first return value will be true if the + // request body is valid, false if it is not. The second return value will be a slice of ValidationError pointers if + // the body is not valid. + ValidateRequestBodyWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) } // NewRequestBodyValidator will create a new RequestBodyValidator from an OpenAPI 3+ document @@ -33,10 +32,6 @@ func NewRequestBodyValidator(document *v3.Document) RequestBodyValidator { return &requestBodyValidator{document: document, schemaCache: &sync.Map{}} } -func (v *requestBodyValidator) SetPathItem(path *v3.PathItem, pathValue string) { - v.pathItem = path - v.pathValue = pathValue -} type schemaCache struct { schema *base.Schema @@ -46,8 +41,6 @@ type schemaCache struct { type requestBodyValidator struct { document *v3.Document - pathItem *v3.PathItem - pathValue string errors []*errors.ValidationError schemaCache *sync.Map } diff --git a/requests/validate_body.go b/requests/validate_body.go index c13294d..99added 100644 --- a/requests/validate_body.go +++ b/requests/validate_body.go @@ -9,29 +9,38 @@ import ( "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" - "github.com/pb33f/libopenapi-validator/paths" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/utils" + v3 "github.com/pb33f/libopenapi/datamodel/high/v3" + "github.com/pb33f/libopenapi-validator/paths" + "fmt" ) func (v *requestBodyValidator) ValidateRequestBody(request *http.Request) (bool, []*errors.ValidationError) { - // find path - var pathItem = v.pathItem - var foundPath string - if v.pathItem == nil { - var validationErrors []*errors.ValidationError - pathItem, validationErrors, foundPath = paths.FindPath(request, v.document) - if pathItem == nil || validationErrors != nil { - v.errors = validationErrors - return false, validationErrors - } - } else { - foundPath = v.pathValue + pathItem, errs, foundPath := paths.FindPath(request, v.document) + if len(errs) > 0 { + return false, errs } + return v.ValidateRequestBodyWithPathItem(request, pathItem, foundPath) +} +func (v *requestBodyValidator) ValidateRequestBodyWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) { + if pathItem == nil { + return false, []*errors.ValidationError{{ + ValidationType: helpers.ParameterValidationPath, + ValidationSubType: "missing", + Message: fmt.Sprintf("%s Path '%s' not found", request.Method, request.URL.Path), + Reason: fmt.Sprintf("The %s request contains a path of '%s' "+ + "however that path, or the %s method for that path does not exist in the specification", + request.Method, request.URL.Path, request.Method), + SpecLine: -1, + SpecCol: -1, + HowToFix: errors.HowToFixPath, + }} + } operation := helpers.ExtractOperation(request, pathItem) if operation == nil { - return false, []*errors.ValidationError{errors.OperationNotFound(pathItem, request, request.Method, foundPath)} + return false, []*errors.ValidationError{errors.OperationNotFound(pathItem, request, request.Method, pathValue)} } if operation.RequestBody == nil { return true, nil @@ -48,14 +57,14 @@ func (v *requestBodyValidator) ValidateRequestBody(request *http.Request) (bool, // request body is not required, the validation stop there. return true, nil } - return false, []*errors.ValidationError{errors.RequestContentTypeNotFound(operation, request, foundPath)} + return false, []*errors.ValidationError{errors.RequestContentTypeNotFound(operation, request, pathValue)} } // extract the media type from the content type header. ct, _, _ := helpers.ExtractContentType(contentType) mediaType, ok := operation.RequestBody.Content.Get(ct) if !ok { - return false, []*errors.ValidationError{errors.RequestContentTypeNotFound(operation, request, foundPath)} + return false, []*errors.ValidationError{errors.RequestContentTypeNotFound(operation, request, pathValue)} } // we currently only support JSON validation for request bodies @@ -100,7 +109,7 @@ func (v *requestBodyValidator) ValidateRequestBody(request *http.Request) (bool, // render the schema, to be used for validation validationSucceeded, validationErrors := ValidateRequestSchema(request, schema, renderedInline, renderedJSON) - errors.PopulateValidationErrors(validationErrors, request, foundPath) + errors.PopulateValidationErrors(validationErrors, request, pathValue) return validationSucceeded, validationErrors } diff --git a/requests/validate_body_test.go b/requests/validate_body_test.go index 4bc8717..6be1a62 100644 --- a/requests/validate_body_test.go +++ b/requests/validate_body_test.go @@ -209,26 +209,28 @@ paths: // mix up the primitives to fire two schema violations. body := map[string]interface{}{ "name": "Big Mac", - "patties": false, - "vegetarian": 2, + "patties": 2, + "vegetarian": true, } bodyBytes, _ := json.Marshal(body) - request, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/createBurger", + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") - pathItem := m.Model.Paths.PathItems.First().Value() - pathValue := m.Model.Paths.PathItems.First().Key() - v.SetPathItem(pathItem, pathValue) + pathItem, validationErrors, pathValue := paths.FindPath(request, &m.Model) + assert.Len(t, validationErrors, 0) - valid, errors := v.ValidateRequestBody(request) + request2, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/createBurger", + bytes.NewBuffer(bodyBytes)) + request2.Header.Set("Content-Type", "application/json") + valid, errors := v.ValidateRequestBodyWithPathItem(request2, pathItem, pathValue) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "GET operation request content type 'GET' does not exist", errors[0].Message) - assert.Equal(t, request.Method, errors[0].RequestMethod) + assert.Equal(t, request2.Method, errors[0].RequestMethod) assert.Equal(t, request.URL.Path, errors[0].RequestPath) assert.Equal(t, "/burgers/createBurger", errors[0].SpecPath) } @@ -269,14 +271,11 @@ paths: bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") - // preset the path - path, _, pv := paths.FindPath(request, &m.Model) - v.SetPathItem(path, pv) - - valid, errors := v.ValidateRequestBody(request) + valid, errors := v.ValidateRequestBodyWithPathItem(request, nil, "") - assert.True(t, valid) - assert.Len(t, errors, 0) + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Equal(t, "POST Path '/burgers/createBurger' not found", errors[0].Message) } @@ -317,11 +316,9 @@ paths: bytes.NewBuffer(bodyBytes)) request.Header.Set("content-type", "application/not-json") - // preset the path - path, _, pv := paths.FindPath(request, &m.Model) - v.SetPathItem(path, pv) - - valid, errors := v.ValidateRequestBody(request) + pathItem, validationErrors, pathValue := paths.FindPath(request, &m.Model) + assert.Len(t, validationErrors, 0) + valid, errors := v.ValidateRequestBodyWithPathItem(request, pathItem, pathValue) assert.False(t, valid) assert.Len(t, errors, 1) @@ -363,11 +360,9 @@ paths: request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewBuffer(bodyBytes)) - // preset the path - path, _, pv := paths.FindPath(request, &m.Model) - v.SetPathItem(path, pv) - - valid, errors := v.ValidateRequestBody(request) + pathItem, validationErrors, pathValue := paths.FindPath(request, &m.Model) + assert.Len(t, validationErrors, 0) + valid, errors := v.ValidateRequestBodyWithPathItem(request, pathItem, pathValue) assert.False(t, valid) assert.Len(t, errors, 1) diff --git a/responses/response_body.go b/responses/response_body.go index 923fac5..90fe637 100644 --- a/responses/response_body.go +++ b/responses/response_body.go @@ -23,15 +23,10 @@ type ResponseBodyValidator interface { // schema of the response body are valid. ValidateResponseBody(request *http.Request, response *http.Response) (bool, []*errors.ValidationError) - // SetPathItem will set the pathItem for the ResponseBodyValidator, all validations will be performed - // against this pathItem otherwise if not set, each validation will perform a lookup for the - // pathItem based on the *http.Request - SetPathItem(path *v3.PathItem, pathValue string) -} - -func (v *responseBodyValidator) SetPathItem(path *v3.PathItem, pathValue string) { - v.pathItem = path - v.pathValue = pathValue + // ValidateResponseBodyWithPathItem will validate the response body for a http.Response pointer. The request is used to + // locate the operation in the specification, the response is used to ensure the response code, media type and the + // schema of the response body are valid. + ValidateResponseBodyWithPathItem(request *http.Request, response *http.Response, pathItem *v3.PathItem, pathFound string) (bool, []*errors.ValidationError) } // NewResponseBodyValidator will create a new ResponseBodyValidator from an OpenAPI 3+ document @@ -47,8 +42,5 @@ type schemaCache struct { type responseBodyValidator struct { document *v3.Document - pathItem *v3.PathItem - pathValue string - errors []*errors.ValidationError schemaCache *sync.Map } diff --git a/responses/validate_body.go b/responses/validate_body.go index 33f53a7..ceede57 100644 --- a/responses/validate_body.go +++ b/responses/validate_body.go @@ -22,24 +22,32 @@ func (v *responseBodyValidator) ValidateResponseBody( request *http.Request, response *http.Response, ) (bool, []*errors.ValidationError) { - // find path - var pathItem *v3.PathItem - var pathFound string - var errs []*errors.ValidationError - if v.pathItem == nil { - pathItem, errs, pathFound = paths.FindPath(request, v.document) - if pathItem == nil || errs != nil { - v.errors = errs - return false, errs - } - } else { - pathItem = v.pathItem - pathFound = v.pathValue + pathItem, errs, foundPath := paths.FindPath(request, v.document) + if len(errs) > 0 { + return false, errs } + return v.ValidateResponseBodyWithPathItem(request, response, pathItem, foundPath) +} +func (v *responseBodyValidator) ValidateResponseBodyWithPathItem(request *http.Request, response *http.Response, pathItem *v3.PathItem, pathFound string) (bool, []*errors.ValidationError) { + if pathItem == nil { + return false, []*errors.ValidationError{{ + ValidationType: helpers.ParameterValidationPath, + ValidationSubType: "missing", + Message: fmt.Sprintf("%s Path '%s' not found", request.Method, request.URL.Path), + Reason: fmt.Sprintf("The %s request contains a path of '%s' "+ + "however that path, or the %s method for that path does not exist in the specification", + request.Method, request.URL.Path, request.Method), + SpecLine: -1, + SpecCol: -1, + HowToFix: errors.HowToFixPath, + }} + } var validationErrors []*errors.ValidationError operation := helpers.ExtractOperation(request, pathItem) - + if operation == nil { + return false, []*errors.ValidationError{errors.OperationNotFound(pathItem, request, request.Method, pathFound)} + } // extract the response code from the response httpCode := response.StatusCode contentType := response.Header.Get(helpers.ContentTypeHeader) diff --git a/responses/validate_body_test.go b/responses/validate_body_test.go index 94e47c2..62e279f 100644 --- a/responses/validate_body_test.go +++ b/responses/validate_body_test.go @@ -239,7 +239,6 @@ paths: // preset the path path, _, pv := paths.FindPath(request, &m.Model) - v.SetPathItem(path, pv) // simulate a request/response res := httptest.NewRecorder() @@ -256,13 +255,77 @@ paths: response := res.Result() // validate! - valid, errors := v.ValidateResponseBody(request, response) + valid, errors := v.ValidateResponseBodyWithPathItem(request, response, path, pv) assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "POST Path '/I do not exist' not found", errors[0].Message) } +func TestValidateBody_SetPath_missing_operation(t *testing.T) { + spec := `openapi: 3.1.0 +paths: + /burgers/createBurger: + post: + responses: + '200': + content: + application/json: + schema: + type: object + properties: + name: + type: string + patties: + type: integer + vegetarian: + type: boolean` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + + m, _ := doc.BuildV3Model() + v := NewResponseBodyValidator(&m.Model) + + body := map[string]interface{}{ + "name": "Big Mac", + "patties": 2, + "vegetarian": false, + } + + bodyBytes, _ := json.Marshal(body) + + // build a request + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader(bodyBytes)) + request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) + + // preset the path + path, _, pv := paths.FindPath(request, &m.Model) + + // simulate a request/response + res := httptest.NewRecorder() + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) // won't even matter! + w.WriteHeader(http.StatusOK) + _, _ = w.Write(bodyBytes) + } + + // fire the request + handler(res, request) + + // record response + response := res.Result() + + request2, _ := http.NewRequest(http.MethodGet, "https://things.com/burgers/createBurger", bytes.NewReader(bodyBytes)) + request2.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) + + // validate! + valid, errors := v.ValidateResponseBodyWithPathItem(request2, response, path, pv) + + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Equal(t, "GET operation request content type 'GET' does not exist", errors[0].Message) +} + func TestValidateBody_MissingStatusCode(t *testing.T) { spec := `openapi: 3.1.0 paths: @@ -432,7 +495,7 @@ paths: assert.True(t, valid) assert.Len(t, errors, 0) - //assert.Len(t, errors[0].SchemaValidationErrors, 2) + // assert.Len(t, errors[0].SchemaValidationErrors, 2) } func TestValidateBody_InvalidResponseBodyNil(t *testing.T) { @@ -578,10 +641,9 @@ paths: // preset the path path, _, pv := paths.FindPath(request, &m.Model) - v.SetPathItem(path, pv) // validate! - valid, errors := v.ValidateResponseBody(request, response) + valid, errors := v.ValidateResponseBodyWithPathItem(request, response, path, pv) assert.False(t, valid) assert.Len(t, errors, 1) diff --git a/validator.go b/validator.go index 2119cfc..3a95114 100644 --- a/validator.go +++ b/validator.go @@ -32,6 +32,14 @@ type Validator interface { // The path, query, cookie and header parameters and request body are validated. ValidateHttpRequestSync(request *http.Request) (bool, []*errors.ValidationError) + // ValidateHttpRequestWithPathItem will validate an *http.Request object against an OpenAPI 3+ document. + // The path, query, cookie and header parameters and request body are validated. + ValidateHttpRequestWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) + + // ValidateHttpRequestSyncWithPathItem will validate an *http.Request object against an OpenAPI 3+ document syncronously and without spawning any goroutines. + // The path, query, cookie and header parameters and request body are validated. + ValidateHttpRequestSyncWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) + // ValidateHttpResponse will an *http.Response object against an OpenAPI 3+ document. // The response body is validated. The request is only used to extract the correct reponse from the spec. ValidateHttpResponse(request *http.Request, response *http.Response) (bool, []*errors.ValidationError) @@ -107,23 +115,17 @@ func (v *validator) ValidateHttpResponse( 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 responseBodyValidator := v.responseValidator - responseBodyValidator.SetPathItem(pathItem, pathValue) // validate response - _, responseErrors := responseBodyValidator.ValidateResponseBody(request, response) + _, responseErrors := responseBodyValidator.ValidateResponseBodyWithPathItem(request, response, pathItem, pathValue) if len(responseErrors) > 0 { return false, responseErrors } - v.foundPath = nil - v.foundPathValue = "" return true, nil } @@ -137,53 +139,35 @@ func (v *validator) ValidateHttpRequestResponse( 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 responseBodyValidator := v.responseValidator - responseBodyValidator.SetPathItem(pathItem, pathValue) // validate request and response - _, requestErrors := v.ValidateHttpRequest(request) - _, responseErrors := responseBodyValidator.ValidateResponseBody(request, response) + _, requestErrors := v.ValidateHttpRequestWithPathItem(request, pathItem, pathValue) + _, responseErrors := responseBodyValidator.ValidateResponseBodyWithPathItem(request, response, pathItem, pathValue) if len(requestErrors) > 0 || len(responseErrors) > 0 { return false, append(requestErrors, responseErrors...) } - v.foundPath = nil - v.foundPathValue = "" 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 + pathItem, errs, foundPath := paths.FindPath(request, v.v3Model) + if len(errs) > 0 { + return false, errs } + return v.ValidateHttpRequestWithPathItem(request, pathItem, foundPath) +} +func (v *validator) ValidateHttpRequestWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) { // 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) @@ -198,11 +182,11 @@ func (v *validator) ValidateHttpRequest(request *http.Request) (bool, []*errors. var paramValidationErrors []*errors.ValidationError validations := []validationFunction{ - paramValidator.ValidatePathParams, - paramValidator.ValidateCookieParams, - paramValidator.ValidateHeaderParams, - paramValidator.ValidateQueryParams, - paramValidator.ValidateSecurity, + paramValidator.ValidatePathParamsWithPathItem, + paramValidator.ValidateCookieParamsWithPathItem, + paramValidator.ValidateHeaderParamsWithPathItem, + paramValidator.ValidateQueryParamsWithPathItem, + paramValidator.ValidateSecurityWithPathItem, } // listen for validation errors on parameters. everything will run async. @@ -226,7 +210,7 @@ func (v *validator) ValidateHttpRequest(request *http.Request) (bool, []*errors. control chan bool, errorChan chan []*errors.ValidationError, validatorFunc validationFunction) { - valid, pErrs := validatorFunc(request) + valid, pErrs := validatorFunc(request, pathItem, pathValue) if !valid { errorChan <- pErrs } @@ -248,7 +232,7 @@ func (v *validator) ValidateHttpRequest(request *http.Request) (bool, []*errors. } requestBodyValidationFunc := func(control chan bool, errorChan chan []*errors.ValidationError) { - valid, pErrs := reqBodyValidator.ValidateRequestBody(request) + valid, pErrs := reqBodyValidator.ValidateRequestBodyWithPathItem(request, pathItem, pathValue) if !valid { errorChan <- pErrs } @@ -273,8 +257,6 @@ func (v *validator) ValidateHttpRequest(request *http.Request) (bool, []*errors. // wait for all the validations to complete <-doneChan - v.foundPathValue = "" - v.foundPath = nil if len(validationErrors) > 0 { return false, validationErrors } @@ -282,48 +264,37 @@ func (v *validator) ValidateHttpRequest(request *http.Request) (bool, []*errors. } func (v *validator) ValidateHttpRequestSync(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 + pathItem, errs, foundPath := paths.FindPath(request, v.v3Model) + if len(errs) > 0 { + return false, errs } + return v.ValidateHttpRequestSyncWithPathItem(request, pathItem, foundPath) +} +func (v *validator) ValidateHttpRequestSyncWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) { // 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) validationErrors := make([]*errors.ValidationError, 0) paramValidationErrors := make([]*errors.ValidationError, 0) for _, validateFunc := range []validationFunction{ - paramValidator.ValidatePathParams, - paramValidator.ValidateCookieParams, - paramValidator.ValidateHeaderParams, - paramValidator.ValidateQueryParams, - paramValidator.ValidateSecurity, + paramValidator.ValidatePathParamsWithPathItem, + paramValidator.ValidateCookieParamsWithPathItem, + paramValidator.ValidateHeaderParamsWithPathItem, + paramValidator.ValidateQueryParamsWithPathItem, + paramValidator.ValidateSecurityWithPathItem, } { - valid, pErrs := validateFunc(request) + valid, pErrs := validateFunc(request, pathItem, pathValue) if !valid { paramValidationErrors = append(paramValidationErrors, pErrs...) } } - valid, pErrs := reqBodyValidator.ValidateRequestBody(request) + valid, pErrs := reqBodyValidator.ValidateRequestBodyWithPathItem(request, pathItem, pathValue) if !valid { paramValidationErrors = append(paramValidationErrors, pErrs...) } @@ -340,12 +311,9 @@ func (v *validator) ValidateHttpRequestSync(request *http.Request) (bool, []*err 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 } var validationLock sync.Mutex @@ -371,5 +339,5 @@ func runValidation(control, doneChan chan bool, } } -type validationFunction func(request *http.Request) (bool, []*errors.ValidationError) +type validationFunction func(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) type validationFunctionAsync func(control chan bool, errorChan chan []*errors.ValidationError) diff --git a/validator_test.go b/validator_test.go index d543131..e803a18 100644 --- a/validator_test.go +++ b/validator_test.go @@ -16,6 +16,7 @@ import ( v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "sync" ) func TestNewValidator(t *testing.T) { @@ -45,6 +46,88 @@ paths: assert.NotNil(t, v.GetRequestBodyValidator()) } +func TestNewValidator_concurrent(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 + /burgers/createBurger/{burgerId}: + post: + parameters: + - in: path + name: burgerId + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + patties: + type: integer + vegetarian: + type: boolean` + + doc, err := libopenapi.NewDocument([]byte(spec)) + + assert.Empty(t, err) + + v, _ := NewValidator(doc) + + body := map[string]interface{}{ + "name": "Big Mac", + "patties": 2, + "vegetarian": true, + } + + bodyBytes, _ := json.Marshal(body) + + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + 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) + }() + + go func() { + defer wg.Done() + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger/toto", + bytes.NewBuffer(bodyBytes)) + request.Header.Set("Content-Type", "application/json") + + valid, errors := v.ValidateHttpRequest(request) + + assert.True(t, valid) + assert.Len(t, errors, 0) + }() + + wg.Wait() +} + func TestNewValidator_ValidateDocument(t *testing.T) { doc, _ := libopenapi.NewDocument(petstoreBytes) @@ -258,10 +341,6 @@ paths: doc, _ := libopenapi.NewDocument([]byte(spec)) v, _ := NewValidator(doc) - v.(*validator).foundPath = &v3.PathItem{ - Post: &v3.Operation{}, - } - v.(*validator).foundPathValue = "/burgers/createBurger" body := map[string]interface{}{ "name": "Big Mac", @@ -275,7 +354,9 @@ paths: bytes.NewBuffer(bodyBytes)) request.Header.Set("Content-Type", "application/json") - valid, errors := v.ValidateHttpRequestSync(request) + valid, errors := v.ValidateHttpRequestSyncWithPathItem(request, &v3.PathItem{ + Post: &v3.Operation{}, + }, "/burgers/createBurger") assert.True(t, valid) assert.Len(t, errors, 0)