From e73b5cb794065b4cd7064d5a3904fadb255e0e7a Mon Sep 17 00:00:00 2001 From: Pascal van Leeuwen <47662958+commoddity@users.noreply.github.com> Date: Wed, 1 May 2024 10:40:01 -0700 Subject: [PATCH] fix: add ValidateHttpRequestSync method for synschronous validation (#1) --- validator.go | 66 ++++++++++- validator_examples_test.go | 38 +++++++ validator_test.go | 220 +++++++++++++++++++++++++++++++++++++ 3 files changed, 321 insertions(+), 3 deletions(-) diff --git a/validator.go b/validator.go index da9495d..2119cfc 100644 --- a/validator.go +++ b/validator.go @@ -4,6 +4,9 @@ package validator import ( + "net/http" + "sync" + "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/parameters" @@ -11,9 +14,7 @@ import ( "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" + v3 "github.com/pb33f/libopenapi/datamodel/high/v3" ) // Validator provides a coarse grained interface for validating an OpenAPI 3+ documents. @@ -27,6 +28,9 @@ type Validator interface { // ValidateHttpRequest will validate an *http.Request object against an OpenAPI 3+ document. // The path, query, cookie and header parameters and request body are validated. ValidateHttpRequest(request *http.Request) (bool, []*errors.ValidationError) + // ValidateHttpRequestSync 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. + ValidateHttpRequestSync(request *http.Request) (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. @@ -277,6 +281,62 @@ func (v *validator) ValidateHttpRequest(request *http.Request) (bool, []*errors. return true, nil } +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 + } + + // 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, + } { + valid, pErrs := validateFunc(request) + if !valid { + paramValidationErrors = append(paramValidationErrors, pErrs...) + } + } + + valid, pErrs := reqBodyValidator.ValidateRequestBody(request) + if !valid { + paramValidationErrors = append(paramValidationErrors, pErrs...) + } + + validationErrors = append(validationErrors, paramValidationErrors...) + + if len(validationErrors) > 0 { + return false, validationErrors + } + + return true, nil +} + type validator struct { v3Model *v3.Document document libopenapi.Document diff --git a/validator_examples_test.go b/validator_examples_test.go index 5a5b4b1..597ed52 100644 --- a/validator_examples_test.go +++ b/validator_examples_test.go @@ -88,6 +88,44 @@ func ExampleNewValidator_validateHttpRequest() { // Type: parameter, Failure: Path parameter 'petId' is not a valid number } +func ExampleNewValidator_validateHttpRequestSync() { + // 1. Load the OpenAPI 3+ spec into a byte array + petstore, err := os.ReadFile("test_specs/petstorev3.json") + + if err != nil { + panic(err) + } + + // 2. Create a new OpenAPI document using libopenapi + document, docErrs := libopenapi.NewDocument(petstore) + + if docErrs != nil { + panic(docErrs) + } + + // 3. Create a new validator + docValidator, validatorErrs := NewValidator(document) + + if validatorErrs != nil { + panic(validatorErrs) + } + + // 4. Create a new *http.Request (normally, this would be where the host application will pass in the request) + request, _ := http.NewRequest(http.MethodGet, "/pet/NotAValidPetId", nil) + + // 5. Validate! + valid, validationErrs := docValidator.ValidateHttpRequestSync(request) + + if !valid { + for _, e := range validationErrs { + // 5. Handle the error + fmt.Printf("Type: %s, Failure: %s\n", e.ValidationType, e.Message) + } + } + // Type: parameter, Failure: Path parameter 'petId' is not a valid number + // Output: Type: security, Failure: API Key api_key not found in header +} + func ExampleNewValidator_validateHttpRequestResponse() { // 1. Load the OpenAPI 3+ spec into a byte array petstore, err := os.ReadFile("test_specs/petstorev3.json") diff --git a/validator_test.go b/validator_test.go index a3f7d7b..3c1d503 100644 --- a/validator_test.go +++ b/validator_test.go @@ -108,6 +108,49 @@ paths: } +func TestNewValidator_ValidateHttpRequestSync_BadPath(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/I am a potato man", + bytes.NewBuffer(bodyBytes)) + request.Header.Set("Content-Type", "application/json") + + valid, errors := v.ValidateHttpRequestSync(request) + + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Equal(t, "POST Path '/I am a potato man' not found", errors[0].Message) + +} + func TestNewValidator_ValidateHttpRequest_ValidPostSimpleSchema(t *testing.T) { spec := `openapi: 3.1.0 @@ -150,6 +193,48 @@ paths: } +func TestNewValidator_ValidateHttpRequestSync_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.ValidateHttpRequestSync(request) + + assert.True(t, valid) + assert.Len(t, errors, 0) + +} + func TestNewValidator_slash_server_url(t *testing.T) { spec := `openapi: 3.1.0 @@ -222,6 +307,48 @@ paths: } +func TestNewValidator_ValidateHttpRequestSync_SetPath_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.ValidateHttpRequestSync(request) + + assert.True(t, valid) + assert.Len(t, errors, 0) + +} + func TestNewValidator_ValidateHttpRequest_InvalidPostSchema(t *testing.T) { spec := `openapi: 3.1.0 @@ -266,6 +393,50 @@ paths: } +func TestNewValidator_ValidateHttpRequestSync_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.ValidateHttpRequestSync(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 @@ -315,6 +486,55 @@ paths: } +func TestNewValidator_ValidateHttpRequestSync_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.ValidateHttpRequestSync(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() {