From 83e719611e771b0c7c4983be17bd558d45e76f9a Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Mon, 24 Apr 2023 09:51:36 -0400 Subject: [PATCH 1/8] Working through some performance issues. Signed-off-by: Dave Shanley --- requests/request_body.go | 18 +++-- requests/validate_body.go | 143 ++++++++++++++++++++------------- requests/validate_request.go | 13 +-- responses/response_body.go | 18 +++-- responses/validate_body.go | 32 +++++++- responses/validate_response.go | 13 +-- validator.go | 7 +- 7 files changed, 163 insertions(+), 81 deletions(-) diff --git a/requests/request_body.go b/requests/request_body.go index 765277d..2b131f0 100644 --- a/requests/request_body.go +++ b/requests/request_body.go @@ -5,6 +5,7 @@ package requests import ( "github.com/pb33f/libopenapi-validator/errors" + "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/datamodel/high/v3" "net/http" ) @@ -28,7 +29,7 @@ type RequestBodyValidator interface { // NewRequestBodyValidator will create a new RequestBodyValidator from an OpenAPI 3+ document func NewRequestBodyValidator(document *v3.Document) RequestBodyValidator { - return &requestBodyValidator{document: document} + return &requestBodyValidator{document: document, schemaCache: make(map[[32]byte]*schemaCache)} } func (v *requestBodyValidator) SetPathItem(path *v3.PathItem, pathValue string) { @@ -36,9 +37,16 @@ func (v *requestBodyValidator) SetPathItem(path *v3.PathItem, pathValue string) v.pathValue = pathValue } +type schemaCache struct { + schema *base.Schema + renderedInline []byte + renderedJSON []byte +} + type requestBodyValidator struct { - document *v3.Document - pathItem *v3.PathItem - pathValue string - errors []*errors.ValidationError + document *v3.Document + pathItem *v3.PathItem + pathValue string + errors []*errors.ValidationError + schemaCache map[[32]byte]*schemaCache } diff --git a/requests/validate_body.go b/requests/validate_body.go index c94b773..da4942f 100644 --- a/requests/validate_body.go +++ b/requests/validate_body.go @@ -4,65 +4,94 @@ package requests 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" - "net/http" - "strings" + "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/utils" + "net/http" + "strings" ) func (v *requestBodyValidator) ValidateRequestBody(request *http.Request) (bool, []*errors.ValidationError) { - // find path - var pathItem *v3.PathItem - var errs []*errors.ValidationError - if v.pathItem == nil { - pathItem, errs, _ = paths.FindPath(request, v.document) - if pathItem == nil || errs != nil { - v.errors = errs - return false, errs - } - } else { - pathItem = v.pathItem - } - - var validationErrors []*errors.ValidationError - operation := helpers.ExtractOperation(request, pathItem) - - var contentType string - // extract the content type from the request - - if contentType = request.Header.Get(helpers.ContentTypeHeader); contentType != "" { - - // extract the media type from the content type header. - ct, _, _ := helpers.ExtractContentType(contentType) - 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) { - - // 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...) - } - } - } - } else { - // content type not found in the contract - validationErrors = append(validationErrors, errors.RequestContentTypeNotFound(operation, request)) - } - } - } - if len(validationErrors) > 0 { - return false, validationErrors - } - return true, nil + // find path + var pathItem *v3.PathItem + var errs []*errors.ValidationError + if v.pathItem == nil { + pathItem, errs, _ = paths.FindPath(request, v.document) + if pathItem == nil || errs != nil { + v.errors = errs + return false, errs + } + } else { + pathItem = v.pathItem + } + + var validationErrors []*errors.ValidationError + operation := helpers.ExtractOperation(request, pathItem) + + var contentType string + // extract the content type from the request + + if contentType = request.Header.Get(helpers.ContentTypeHeader); contentType != "" { + + // extract the media type from the content type header. + ct, _, _ := helpers.ExtractContentType(contentType) + 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) { + + // extract schema from media type + if mediaType.Schema != nil { + + var schema *base.Schema + var renderedInline, renderedJSON []byte + + // have we seen this schema before? let's hash it and check the cache. + hash := mediaType.GoLow().Schema.Value.Hash() + + // perform work only once and cache the result in the validator. + if cacheHit, ch := v.schemaCache[hash]; ch { + + // got a hit, use cached values + schema = cacheHit.schema + renderedInline = cacheHit.renderedInline + renderedJSON = cacheHit.renderedJSON + + } else { + + // render the schema inline and perform the intensive work of rendering and converting + // this is only performed once per schema and cached in the validator. + schema = mediaType.Schema.Schema() + renderedInline, _ = schema.RenderInline() + renderedJSON, _ = utils.ConvertYAMLtoJSON(renderedInline) + v.schemaCache[hash] = &schemaCache{ + schema: schema, + renderedInline: renderedInline, + renderedJSON: renderedJSON, + } + } + + //render the schema, to be used for validation + valid, vErrs := ValidateRequestSchema(request, schema, renderedInline, renderedJSON) + if !valid { + validationErrors = append(validationErrors, vErrs...) + } + } + } + } else { + // content type not found in the contract + validationErrors = append(validationErrors, errors.RequestContentTypeNotFound(operation, request)) + } + } + } + if len(validationErrors) > 0 { + return false, validationErrors + } + return true, nil } diff --git a/requests/validate_request.go b/requests/validate_request.go index 77f5870..efeef7d 100644 --- a/requests/validate_request.go +++ b/requests/validate_request.go @@ -11,7 +11,6 @@ import ( "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/schema_validation" "github.com/pb33f/libopenapi/datamodel/high/base" - "github.com/pb33f/libopenapi/utils" "github.com/santhosh-tekuri/jsonschema/v5" "gopkg.in/yaml.v3" "io" @@ -23,13 +22,12 @@ import ( // If validation fails, it will return a list of validation errors as the second return value. func ValidateRequestSchema( request *http.Request, - schema *base.Schema) (bool, []*errors.ValidationError) { + schema *base.Schema, + renderedSchema, + jsonSchema []byte) (bool, []*errors.ValidationError) { var validationErrors []*errors.ValidationError - // render the schema, to be used for validation - renderedSchema, _ := schema.RenderInline() - jsonSchema, _ := utils.ConvertYAMLtoJSON(renderedSchema) requestBody, _ := io.ReadAll(request.Body) // close the request body, so it can be re-read later by another player in the chain @@ -39,6 +37,11 @@ func ValidateRequestSchema( var decodedObj interface{} _ = json.Unmarshal(requestBody, &decodedObj) + // no request body? failed to decode anything? nothing to do here. + if requestBody == nil || decodedObj == nil { + return true, nil + } + compiler := jsonschema.NewCompiler() _ = compiler.AddResource("requestBody.json", strings.NewReader(string(jsonSchema))) jsch, _ := compiler.Compile("requestBody.json") diff --git a/responses/response_body.go b/responses/response_body.go index f561fa7..c54ac08 100644 --- a/responses/response_body.go +++ b/responses/response_body.go @@ -5,6 +5,7 @@ package responses import ( "github.com/pb33f/libopenapi-validator/errors" + "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/datamodel/high/v3" "net/http" ) @@ -33,12 +34,19 @@ func (v *responseBodyValidator) SetPathItem(path *v3.PathItem, pathValue string) // NewResponseBodyValidator will create a new ResponseBodyValidator from an OpenAPI 3+ document func NewResponseBodyValidator(document *v3.Document) ResponseBodyValidator { - return &responseBodyValidator{document: document} + return &responseBodyValidator{document: document, schemaCache: make(map[[32]byte]*schemaCache)} +} + +type schemaCache struct { + schema *base.Schema + renderedInline []byte + renderedJSON []byte } type responseBodyValidator struct { - document *v3.Document - pathItem *v3.PathItem - pathValue string - errors []*errors.ValidationError + document *v3.Document + pathItem *v3.PathItem + pathValue string + errors []*errors.ValidationError + schemaCache map[[32]byte]*schemaCache } diff --git a/responses/validate_body.go b/responses/validate_body.go index 5377771..8420831 100644 --- a/responses/validate_body.go +++ b/responses/validate_body.go @@ -7,7 +7,9 @@ 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/datamodel/high/v3" + "github.com/pb33f/libopenapi/utils" "net/http" "strconv" "strings" @@ -113,10 +115,36 @@ func (v *responseBodyValidator) checkResponseSchema( // extract schema from media type if mediaType.Schema != nil { - schema := mediaType.Schema.Schema() + + var schema *base.Schema + var renderedInline, renderedJSON []byte + + // have we seen this schema before? let's hash it and check the cache. + hash := mediaType.GoLow().Schema.Value.Hash() + + if cacheHit, ch := v.schemaCache[hash]; ch { + + // got a hit, use cached values + schema = cacheHit.schema + renderedInline = cacheHit.renderedInline + renderedJSON = cacheHit.renderedJSON + + } else { + + // render the schema inline and perform the intensive work of rendering and converting + // this is only performed once per schema and cached in the validator. + schema = mediaType.Schema.Schema() + renderedSchema, _ := schema.RenderInline() + jsonSchema, _ := utils.ConvertYAMLtoJSON(renderedSchema) + v.schemaCache[hash] = &schemaCache{ + schema: schema, + renderedInline: renderedInline, + renderedJSON: jsonSchema, + } + } // render the schema, to be used for validation - valid, vErrs := ValidateResponseSchema(request, response, schema) + valid, vErrs := ValidateResponseSchema(request, response, schema, renderedInline, renderedJSON) if !valid { validationErrors = append(validationErrors, vErrs...) } diff --git a/responses/validate_response.go b/responses/validate_response.go index 5760f05..f372eb7 100644 --- a/responses/validate_response.go +++ b/responses/validate_response.go @@ -11,7 +11,6 @@ import ( "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/schema_validation" "github.com/pb33f/libopenapi/datamodel/high/base" - "github.com/pb33f/libopenapi/utils" "github.com/santhosh-tekuri/jsonschema/v5" "gopkg.in/yaml.v3" "io" @@ -27,13 +26,12 @@ import ( func ValidateResponseSchema( request *http.Request, response *http.Response, - schema *base.Schema) (bool, []*errors.ValidationError) { + schema *base.Schema, + renderedSchema, + jsonSchema []byte) (bool, []*errors.ValidationError) { var validationErrors []*errors.ValidationError - // render the schema, to be used for validation - renderedSchema, _ := schema.RenderInline() - jsonSchema, _ := utils.ConvertYAMLtoJSON(renderedSchema) responseBody, _ := io.ReadAll(response.Body) // close the request body, so it can be re-read later by another player in the chain @@ -43,6 +41,11 @@ func ValidateResponseSchema( var decodedObj interface{} _ = json.Unmarshal(responseBody, &decodedObj) + // no response body? failed to decode anything? nothing to do here. + if responseBody == nil || decodedObj == nil { + return true, nil + } + // create a new jsonschema compiler and add in the rendered JSON schema. compiler := jsonschema.NewCompiler() fName := fmt.Sprintf("%s.json", helpers.ResponseBodyValidation) diff --git a/validator.go b/validator.go index 31a1b0a..dc01c92 100644 --- a/validator.go +++ b/validator.go @@ -88,10 +88,10 @@ 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 + pathItem, errs, pathValue = paths.FindPath(request, v.v3Model) if pathItem == nil || errs != nil { v.errors = errs @@ -110,6 +110,8 @@ func (v *validator) ValidateHttpRequestResponse( if len(requestErrors) > 0 || len(responseErrors) > 0 { return false, append(requestErrors, responseErrors...) } + v.foundPath = nil + v.foundPathValue = "" return true, nil } @@ -227,7 +229,8 @@ 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 } From bc067652e6770910d276d78ed9ee238f3e896bc9 Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Mon, 24 Apr 2023 10:31:53 -0400 Subject: [PATCH 2/8] adding some test cases Signed-off-by: Dave Shanley --- requests/validate_body_test.go | 47 ++ responses/validate_body.go | 282 +++---- validator_test.go | 1373 +++++++++++++++++--------------- 3 files changed, 901 insertions(+), 801 deletions(-) diff --git a/requests/validate_body_test.go b/requests/validate_body_test.go index ded1da1..cc9a277 100644 --- a/requests/validate_body_test.go +++ b/requests/validate_body_test.go @@ -148,6 +148,53 @@ paths: } +func TestValidateBody_ContentTypeNotFound(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)) + + m, _ := doc.BuildV3Model() + v := NewRequestBodyValidator(&m.Model) + + // mix up the primitives to fire two schema violations. + 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/not-json") + + // preset the path + path, _, pv := paths.FindPath(request, &m.Model) + v.SetPathItem(path, pv) + + valid, errors := v.ValidateRequestBody(request) + + assert.False(t, valid) + assert.Len(t, errors, 1) + +} + func TestValidateBody_InvalidBasicSchema(t *testing.T) { spec := `openapi: 3.1.0 paths: diff --git a/responses/validate_body.go b/responses/validate_body.go index 8420831..b969230 100644 --- a/responses/validate_body.go +++ b/responses/validate_body.go @@ -4,151 +4,151 @@ package responses 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/datamodel/high/v3" - "github.com/pb33f/libopenapi/utils" - "net/http" - "strconv" - "strings" + "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/datamodel/high/v3" + "github.com/pb33f/libopenapi/utils" + "net/http" + "strconv" + "strings" ) func (v *responseBodyValidator) ValidateResponseBody( - request *http.Request, - response *http.Response) (bool, []*errors.ValidationError) { - - // find path - var pathItem *v3.PathItem - var errs []*errors.ValidationError - if v.pathItem == nil { - pathItem, errs, _ = paths.FindPath(request, v.document) - if pathItem == nil || errs != nil { - v.errors = errs - return false, errs - } - } else { - pathItem = v.pathItem - } - - var validationErrors []*errors.ValidationError - operation := helpers.ExtractOperation(request, pathItem) - - // extract the response code from the response - httpCode := response.StatusCode - contentType := response.Header.Get(helpers.ContentTypeHeader) - - // extract the media type from the content type header. - mediaTypeSting, _, _ := helpers.ExtractContentType(contentType) - - // check if the response code is in the contract - foundResponse := operation.Responses.FindResponseByCode(httpCode) - if foundResponse != nil { - - // check content type has been defined in the contract - if mediaType, ok := foundResponse.Content[mediaTypeSting]; ok { - - validationErrors = append(validationErrors, - v.checkResponseSchema(request, response, mediaTypeSting, mediaType)...) - - } else { - - // check that the operation *actually* returns a body. (i.e. a 204 response) - if foundResponse.Content != nil { - - // content type not found in the contract - codeStr := strconv.Itoa(httpCode) - validationErrors = append(validationErrors, - errors.ResponseContentTypeNotFound(operation, request, response, codeStr, false)) - - } - } - } else { - - // no code match, check for default response - if operation.Responses.Default != nil { - - // check content type has been defined in the contract - if mediaType, ok := operation.Responses.Default.Content[mediaTypeSting]; ok { - - validationErrors = append(validationErrors, - v.checkResponseSchema(request, response, contentType, mediaType)...) - - } else { - - // check that the operation *actually* returns a body. (i.e. a 204 response) - if operation.Responses.Default.Content != nil { - - // content type not found in the contract - codeStr := strconv.Itoa(httpCode) - validationErrors = append(validationErrors, - errors.ResponseContentTypeNotFound(operation, request, response, codeStr, true)) - } - } - - } else { - // TODO: add support for '2XX' and '3XX' responses in the contract - // no default, no code match, nothing! - validationErrors = append(validationErrors, - errors.ResponseCodeNotFound(operation, request, httpCode)) - } - } - if len(validationErrors) > 0 { - return false, validationErrors - } - return true, nil + request *http.Request, + response *http.Response) (bool, []*errors.ValidationError) { + + // find path + var pathItem *v3.PathItem + var errs []*errors.ValidationError + if v.pathItem == nil { + pathItem, errs, _ = paths.FindPath(request, v.document) + if pathItem == nil || errs != nil { + v.errors = errs + return false, errs + } + } else { + pathItem = v.pathItem + } + + var validationErrors []*errors.ValidationError + operation := helpers.ExtractOperation(request, pathItem) + + // extract the response code from the response + httpCode := response.StatusCode + contentType := response.Header.Get(helpers.ContentTypeHeader) + + // extract the media type from the content type header. + mediaTypeSting, _, _ := helpers.ExtractContentType(contentType) + + // check if the response code is in the contract + foundResponse := operation.Responses.FindResponseByCode(httpCode) + if foundResponse != nil { + + // check content type has been defined in the contract + if mediaType, ok := foundResponse.Content[mediaTypeSting]; ok { + + validationErrors = append(validationErrors, + v.checkResponseSchema(request, response, mediaTypeSting, mediaType)...) + + } else { + + // check that the operation *actually* returns a body. (i.e. a 204 response) + if foundResponse.Content != nil { + + // content type not found in the contract + codeStr := strconv.Itoa(httpCode) + validationErrors = append(validationErrors, + errors.ResponseContentTypeNotFound(operation, request, response, codeStr, false)) + + } + } + } else { + + // no code match, check for default response + if operation.Responses.Default != nil { + + // check content type has been defined in the contract + if mediaType, ok := operation.Responses.Default.Content[mediaTypeSting]; ok { + + validationErrors = append(validationErrors, + v.checkResponseSchema(request, response, contentType, mediaType)...) + + } else { + + // check that the operation *actually* returns a body. (i.e. a 204 response) + if operation.Responses.Default.Content != nil { + + // content type not found in the contract + codeStr := strconv.Itoa(httpCode) + validationErrors = append(validationErrors, + errors.ResponseContentTypeNotFound(operation, request, response, codeStr, true)) + } + } + + } else { + // TODO: add support for '2XX' and '3XX' responses in the contract + // no default, no code match, nothing! + validationErrors = append(validationErrors, + errors.ResponseCodeNotFound(operation, request, httpCode)) + } + } + if len(validationErrors) > 0 { + return false, validationErrors + } + return true, nil } func (v *responseBodyValidator) checkResponseSchema( - request *http.Request, - response *http.Response, - contentType string, - mediaType *v3.MediaType) []*errors.ValidationError { - - var validationErrors []*errors.ValidationError - - // currently, we can only validate JSON based responses, so check for the presence - // of 'json' in the content type (what ever it may be) so we can perform a schema check on it. - // anything other than JSON, will be ignored. - if strings.Contains(strings.ToLower(contentType), helpers.JSONType) { - - // extract schema from media type - if mediaType.Schema != nil { - - var schema *base.Schema - var renderedInline, renderedJSON []byte - - // have we seen this schema before? let's hash it and check the cache. - hash := mediaType.GoLow().Schema.Value.Hash() - - if cacheHit, ch := v.schemaCache[hash]; ch { - - // got a hit, use cached values - schema = cacheHit.schema - renderedInline = cacheHit.renderedInline - renderedJSON = cacheHit.renderedJSON - - } else { - - // render the schema inline and perform the intensive work of rendering and converting - // this is only performed once per schema and cached in the validator. - schema = mediaType.Schema.Schema() - renderedSchema, _ := schema.RenderInline() - jsonSchema, _ := utils.ConvertYAMLtoJSON(renderedSchema) - v.schemaCache[hash] = &schemaCache{ - schema: schema, - renderedInline: renderedInline, - renderedJSON: jsonSchema, - } - } - - // render the schema, to be used for validation - valid, vErrs := ValidateResponseSchema(request, response, schema, renderedInline, renderedJSON) - if !valid { - validationErrors = append(validationErrors, vErrs...) - } - } - } - return validationErrors + request *http.Request, + response *http.Response, + contentType string, + mediaType *v3.MediaType) []*errors.ValidationError { + + var validationErrors []*errors.ValidationError + + // currently, we can only validate JSON based responses, so check for the presence + // of 'json' in the content type (what ever it may be) so we can perform a schema check on it. + // anything other than JSON, will be ignored. + if strings.Contains(strings.ToLower(contentType), helpers.JSONType) { + + // extract schema from media type + if mediaType.Schema != nil { + + var schema *base.Schema + var renderedInline, renderedJSON []byte + + // have we seen this schema before? let's hash it and check the cache. + hash := mediaType.GoLow().Schema.Value.Hash() + + if cacheHit, ch := v.schemaCache[hash]; ch { + + // got a hit, use cached values + schema = cacheHit.schema + renderedInline = cacheHit.renderedInline + renderedJSON = cacheHit.renderedJSON + + } else { + + // render the schema inline and perform the intensive work of rendering and converting + // this is only performed once per schema and cached in the validator. + schema = mediaType.Schema.Schema() + renderedInline, _ = schema.RenderInline() + renderedJSON, _ = utils.ConvertYAMLtoJSON(renderedInline) + v.schemaCache[hash] = &schemaCache{ + schema: schema, + renderedInline: renderedInline, + renderedJSON: renderedJSON, + } + } + + // render the schema, to be used for validation + valid, vErrs := ValidateResponseSchema(request, response, schema, renderedInline, renderedJSON) + if !valid { + validationErrors = append(validationErrors, vErrs...) + } + } + } + return validationErrors } diff --git a/validator_test.go b/validator_test.go index 92d4893..5fb2c42 100644 --- a/validator_test.go +++ b/validator_test.go @@ -4,20 +4,20 @@ 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" + "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(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -34,38 +34,38 @@ paths: vegetarian: type: boolean` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - v, _ := NewValidator(doc) - assert.NotNil(t, v.GetParameterValidator()) - assert.NotNil(t, v.GetResponseBodyValidator()) - assert.NotNil(t, v.GetRequestBodyValidator()) + v, _ := NewValidator(doc) + assert.NotNil(t, v.GetParameterValidator()) + assert.NotNil(t, v.GetResponseBodyValidator()) + assert.NotNil(t, v.GetRequestBodyValidator()) } func TestNewValidator_ValidateDocument(t *testing.T) { - doc, _ := libopenapi.NewDocument(petstoreBytes) - v, _ := NewValidator(doc) - valid, errs := v.ValidateDocument() - assert.True(t, valid) - assert.Len(t, errs, 0) + doc, _ := libopenapi.NewDocument(petstoreBytes) + v, _ := NewValidator(doc) + valid, errs := v.ValidateDocument() + assert.True(t, valid) + assert.Len(t, errs, 0) } func TestNewValidator_BadDoc(t *testing.T) { - spec := `swagger: 2.0` + spec := `swagger: 2.0` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - _, errs := NewValidator(doc) + _, errs := NewValidator(doc) - assert.Len(t, errs, 1) + assert.Len(t, errs, 1) } func TestNewValidator_ValidateHttpRequest_BadPath(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -82,33 +82,33 @@ paths: vegetarian: type: boolean` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - v, _ := NewValidator(doc) + v, _ := NewValidator(doc) - body := map[string]interface{}{ - "name": "Big Mac", - "patties": 2, - "vegetarian": true, - } + body := map[string]interface{}{ + "name": "Big Mac", + "patties": 2, + "vegetarian": true, + } - bodyBytes, _ := json.Marshal(body) + 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") + 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.ValidateHttpRequest(request) + valid, errors := v.ValidateHttpRequest(request) - assert.False(t, valid) - assert.Len(t, errors, 1) - assert.Equal(t, "Path '/I am a potato man' not found", errors[0].Message) + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Equal(t, "Path '/I am a potato man' not found", errors[0].Message) } func TestNewValidator_ValidateHttpRequest_ValidPostSimpleSchema(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -125,32 +125,32 @@ paths: vegetarian: type: boolean` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - v, _ := NewValidator(doc) + v, _ := NewValidator(doc) - body := map[string]interface{}{ - "name": "Big Mac", - "patties": 2, - "vegetarian": true, - } + body := map[string]interface{}{ + "name": "Big Mac", + "patties": 2, + "vegetarian": true, + } - bodyBytes, _ := json.Marshal(body) + bodyBytes, _ := json.Marshal(body) - request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", - bytes.NewBuffer(bodyBytes)) - request.Header.Set("Content-Type", "application/json") + 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) + valid, errors := v.ValidateHttpRequest(request) - assert.True(t, valid) - assert.Len(t, errors, 0) + assert.True(t, valid) + assert.Len(t, errors, 0) } func TestNewValidator_ValidateHttpRequest_SetPath_ValidPostSimpleSchema(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -167,32 +167,32 @@ paths: vegetarian: type: boolean` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - v, _ := NewValidator(doc) + v, _ := NewValidator(doc) - body := map[string]interface{}{ - "name": "Big Mac", - "patties": 2, - "vegetarian": true, - } + body := map[string]interface{}{ + "name": "Big Mac", + "patties": 2, + "vegetarian": true, + } - bodyBytes, _ := json.Marshal(body) + bodyBytes, _ := json.Marshal(body) - request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", - bytes.NewBuffer(bodyBytes)) - request.Header.Set("Content-Type", "application/json") + 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) + valid, errors := v.ValidateHttpRequest(request) - assert.True(t, valid) - assert.Len(t, errors, 0) + assert.True(t, valid) + assert.Len(t, errors, 0) } func TestNewValidator_ValidateHttpRequest_InvalidPostSchema(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -209,34 +209,34 @@ paths: vegetarian: type: boolean` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - v, _ := NewValidator(doc) + v, _ := NewValidator(doc) - // mix up the primitives to fire two schema violations. - body := map[string]interface{}{ - "name": "Big Mac", - "patties": false, // wrong. - "vegetarian": false, - } + // 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) + bodyBytes, _ := json.Marshal(body) - request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", - bytes.NewBuffer(bodyBytes)) - request.Header.Set("Content-Type", "application/json") + 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) + 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) + 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 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: parameters: @@ -259,700 +259,753 @@ paths: vegetarian: type: boolean` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - v, _ := NewValidator(doc) + v, _ := NewValidator(doc) - body := map[string]interface{}{ - "name": "Big Mac", - "patties": 2, // wrong. - "vegetarian": false, - } + body := map[string]interface{}{ + "name": "Big Mac", + "patties": 2, // wrong. + "vegetarian": false, + } - bodyBytes, _ := json.Marshal(body) + bodyBytes, _ := json.Marshal(body) - request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", - bytes.NewBuffer(bodyBytes)) - request.Header.Set("Content-Type", "application/json") + 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) + 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) + 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") + petstoreBytes, _ = os.ReadFile("test_specs/petstorev3.json") +} + +func TestNewValidator_PetStore_MissingContentType(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, "application/not-json") + 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, "PUT / 200 operation response content type 'application/not-json' does not exist", + errors[0].Message) + + assert.Equal(t, "The content type 'application/not-json' of the PUT response received "+ + "has not been defined, it's an unknown type", + errors[0].Reason) + } func TestNewValidator_PetStore_PetPost200_Valid_PathPreset(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) + // 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_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) + // 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) - } - } + // 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) + // 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) + // 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) + // 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 - 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 - 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. + // 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 + 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 + 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. } func TestNewValidator_PetStore_PetGet200_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) // operation returns pet - - // create a new put request - request, _ := http.NewRequest(http.MethodGet, - "https://hyperspace-superherbs.com/pet/12345", 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 - valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) - - assert.True(t, valid) - assert.Len(t, errors, 0) + // 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) // operation returns pet + + // create a new put request + request, _ := http.NewRequest(http.MethodGet, + "https://hyperspace-superherbs.com/pet/12345", 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 + valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) + + assert.True(t, valid) + assert.Len(t, errors, 0) } func TestNewValidator_PetStore_PetGet200_PathNotFound(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) // operation returns pet - - // create a new put request - request, _ := http.NewRequest(http.MethodGet, - "https://hyperspace-superherbs.com/pet/IamNotANumber", 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 - valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) - - assert.False(t, valid) - assert.Len(t, errors, 1) - assert.Equal(t, "Path '/pet/IamNotANumber' not found", errors[0].Message) + // 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) // operation returns pet + + // create a new put request + request, _ := http.NewRequest(http.MethodGet, + "https://hyperspace-superherbs.com/pet/IamNotANumber", 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 + valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) + + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Equal(t, "Path '/pet/IamNotANumber' not found", errors[0].Message) } func TestNewValidator_PetStore_PetGet200(t *testing.T) { - // create a new document from the petstore spec - doc, _ := libopenapi.NewDocument(petstoreBytes) + // create a new document from the petstore spec + doc, _ := libopenapi.NewDocument(petstoreBytes) - // create a doc - v, _ := NewValidator(doc) + // create a doc + v, _ := NewValidator(doc) - // create a new put request - request, _ := http.NewRequest(http.MethodGet, - "https://hyperspace-superherbs.com/pet/112233", nil) - request.Header.Set("Content-Type", "application/json") + // create a new put request + request, _ := http.NewRequest(http.MethodGet, + "https://hyperspace-superherbs.com/pet/112233", 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) + // 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) - // create a pet - body := map[string]interface{}{ - "id": 123, - "name": "cotton", - "category": map[string]interface{}{ - "id": 123, - "name": "dogs", - }, - "photoUrls": []string{"https://example.com"}, - } + // 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) // operation returns pet + // marshal the body into bytes. + bodyBytes, _ := json.Marshal(body) // operation returns pet - _, _ = w.Write(bodyBytes) - } + _, _ = w.Write(bodyBytes) + } - // fire the request - handler(res, request) + // fire the request + handler(res, request) - // validate the response - valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) + // validate the response + valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) - assert.True(t, valid) - assert.Len(t, errors, 0) + assert.True(t, valid) + assert.Len(t, errors, 0) } func TestNewValidator_PetStore_PetGet200_ServerBadMediaType(t *testing.T) { - // create a new document from the petstore spec - doc, _ := libopenapi.NewDocument(petstoreBytes) + // create a new document from the petstore spec + doc, _ := libopenapi.NewDocument(petstoreBytes) - // create a doc - v, _ := NewValidator(doc) + // create a doc + v, _ := NewValidator(doc) - // create a new put request - request, _ := http.NewRequest(http.MethodGet, - "https://hyperspace-superherbs.com/pet/112233", nil) - request.Header.Set("Content-Type", "application/json") + // create a new put request + request, _ := http.NewRequest(http.MethodGet, + "https://hyperspace-superherbs.com/pet/112233", 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, "hot-diggity/coffee; charset=cakes") // wut? - w.WriteHeader(http.StatusOK) + // 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, "hot-diggity/coffee; charset=cakes") // wut? + w.WriteHeader(http.StatusOK) - // create a pet - body := map[string]interface{}{ - "id": 123, - "name": "cotton", + // create a pet + body := map[string]interface{}{ + "id": 123, + "name": "cotton", - "category": map[string]interface{}{ - "id": 123, - "name": "dogs", - }, - "photoUrls": []string{"https://example.com"}, - } + "category": map[string]interface{}{ + "id": 123, + "name": "dogs", + }, + "photoUrls": []string{"https://example.com"}, + } - // marshal the body into bytes. - bodyBytes, _ := json.Marshal(body) // operation returns pet + // marshal the body into bytes. + bodyBytes, _ := json.Marshal(body) // operation returns pet - _, _ = w.Write(bodyBytes) - } + _, _ = w.Write(bodyBytes) + } - // fire the request - handler(res, request) + // fire the request + handler(res, request) - // validate the response - valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) + // validate the response + valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) - assert.False(t, valid) - assert.Len(t, errors, 1) - assert.Equal(t, "GET / 200 operation response content type 'hot-diggity/coffee' does not exist", errors[0].Message) + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Equal(t, "GET / 200 operation response content type 'hot-diggity/coffee' does not exist", errors[0].Message) } func TestNewValidator_PetStore_PetWithIdPost200_Missing200(t *testing.T) { - // create a new document from the petstore spec - doc, _ := libopenapi.NewDocument(petstoreBytes) + // create a new document from the petstore spec + doc, _ := libopenapi.NewDocument(petstoreBytes) - // create a doc - v, _ := NewValidator(doc) + // create a doc + v, _ := NewValidator(doc) - // create a new put request - request, _ := http.NewRequest(http.MethodPost, - "https://hyperspace-superherbs.com/pet/112233?name=peter&query=thing", nil) - request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) + // create a new put request + request, _ := http.NewRequest(http.MethodPost, + "https://hyperspace-superherbs.com/pet/112233?name=peter&query=thing", nil) + request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) - // 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) - } + // 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) + } - // fire the request - handler(res, request) + // fire the request + handler(res, request) - // validate the response - valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) + // validate the response + valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) - assert.False(t, valid) - assert.Len(t, errors, 1) - assert.Equal(t, "POST operation request response code '200' does not exist", errors[0].Message) + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Equal(t, "POST operation request response code '200' does not exist", errors[0].Message) } func TestNewValidator_PetStore_UploadImage200_InvalidRequestBodyType(t *testing.T) { - // create a new document from the petstore spec - doc, _ := libopenapi.NewDocument(petstoreBytes) + // create a new document from the petstore spec + doc, _ := libopenapi.NewDocument(petstoreBytes) - // create a doc - v, _ := NewValidator(doc) + // create a doc + v, _ := NewValidator(doc) - // create a new put request - request, _ := http.NewRequest(http.MethodPost, - "https://hyperspace-superherbs.com/pet/112233/uploadImage?additionalMetadata=blem", nil) - request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) + // create a new put request + request, _ := http.NewRequest(http.MethodPost, + "https://hyperspace-superherbs.com/pet/112233/uploadImage?additionalMetadata=blem", nil) + request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) - // 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) - // forget to write an API response - } + // 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) + // forget to write an API response + } - // fire the request - handler(res, request) + // fire the request + handler(res, request) - // validate the response - valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) + // validate the response + valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) - assert.False(t, valid) - assert.Len(t, errors, 2) // missing response code and failed response body schema validation. + assert.False(t, valid) + assert.Len(t, errors, 2) // missing response code and failed response body schema validation. } func TestNewValidator_PetStore_UploadImage200_Valid(t *testing.T) { - // create a new document from the petstore spec - doc, _ := libopenapi.NewDocument(petstoreBytes) + // create a new document from the petstore spec + doc, _ := libopenapi.NewDocument(petstoreBytes) - // create a doc - v, _ := NewValidator(doc) + // create a doc + v, _ := NewValidator(doc) - // create a new put request - request, _ := http.NewRequest(http.MethodPost, - "https://hyperspace-superherbs.com/pet/112233/uploadImage?additionalMetadata=blem", nil) - request.Header.Set(helpers.ContentTypeHeader, "application/octet-stream") + // create a new put request + request, _ := http.NewRequest(http.MethodPost, + "https://hyperspace-superherbs.com/pet/112233/uploadImage?additionalMetadata=blem", nil) + request.Header.Set(helpers.ContentTypeHeader, "application/octet-stream") - // 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) + // 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) - // create an API response - body := map[string]interface{}{ - "code": 200, - "type": "herbs", - "message": "smoke them every day.", - } + // create an API response + body := map[string]interface{}{ + "code": 200, + "type": "herbs", + "message": "smoke them every day.", + } - // marshal the body into bytes. - bodyBytes, _ := json.Marshal(body) // operation returns APIResponse + // marshal the body into bytes. + bodyBytes, _ := json.Marshal(body) // operation returns APIResponse - _, _ = w.Write(bodyBytes) - } + _, _ = w.Write(bodyBytes) + } - // fire the request - handler(res, request) + // fire the request + handler(res, request) - // validate the response - valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) + // validate the response + valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) - assert.True(t, valid) - assert.Len(t, errors, 0) + assert.True(t, valid) + assert.Len(t, errors, 0) } func TestNewValidator_PetStore_UploadImage200_InvalidAPIResponse(t *testing.T) { - // create a new document from the petstore spec - doc, _ := libopenapi.NewDocument(petstoreBytes) + // create a new document from the petstore spec + doc, _ := libopenapi.NewDocument(petstoreBytes) - // create a doc - v, _ := NewValidator(doc) + // create a doc + v, _ := NewValidator(doc) - // create a new put request - request, _ := http.NewRequest(http.MethodPost, - "https://hyperspace-superherbs.com/pet/112233/uploadImage?additionalMetadata=blem", nil) - request.Header.Set(helpers.ContentTypeHeader, "application/octet-stream") + // create a new put request + request, _ := http.NewRequest(http.MethodPost, + "https://hyperspace-superherbs.com/pet/112233/uploadImage?additionalMetadata=blem", nil) + request.Header.Set(helpers.ContentTypeHeader, "application/octet-stream") - // 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) + // 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) - // create an API response - body := map[string]interface{}{ - "code": 200, - "type": false, - "message": "smoke them every day.", - } + // create an API response + body := map[string]interface{}{ + "code": 200, + "type": false, + "message": "smoke them every day.", + } - // marshal the body into bytes. - bodyBytes, _ := json.Marshal(body) // operation returns APIResponse + // marshal the body into bytes. + bodyBytes, _ := json.Marshal(body) // operation returns APIResponse - _, _ = w.Write(bodyBytes) - } + _, _ = w.Write(bodyBytes) + } - // fire the request - handler(res, request) + // fire the request + handler(res, request) - // validate the response - valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) + // validate the response + valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) - assert.False(t, valid) - assert.Len(t, errors, 1) - assert.Equal(t, "200 response body for '/pet/112233/uploadImage' failed to validate schema", errors[0].Message) + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Equal(t, "200 response body for '/pet/112233/uploadImage' failed to validate schema", errors[0].Message) } From d49738d10e172052c509a3215ceae6fe3081ea0b Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Mon, 24 Apr 2023 10:34:08 -0400 Subject: [PATCH 3/8] fixing borked test Signed-off-by: Dave Shanley --- validator_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validator_test.go b/validator_test.go index 5fb2c42..c59ae25 100644 --- a/validator_test.go +++ b/validator_test.go @@ -921,7 +921,7 @@ func TestNewValidator_PetStore_UploadImage200_InvalidRequestBodyType(t *testing. valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) assert.False(t, valid) - assert.Len(t, errors, 2) // missing response code and failed response body schema validation. + assert.Len(t, errors, 1) } From e9cb2d2c6e81af734114f75f9a0b3b17f0eddf3a Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Mon, 24 Apr 2023 10:47:20 -0400 Subject: [PATCH 4/8] Adding CareRequest example test Signed-off-by: Dave Shanley --- test_specs/care_request.yaml | 63 ++++++++++++++++++++++++++++++++++++ validator_test.go | 48 +++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 test_specs/care_request.yaml diff --git a/test_specs/care_request.yaml b/test_specs/care_request.yaml new file mode 100644 index 0000000..9294ec4 --- /dev/null +++ b/test_specs/care_request.yaml @@ -0,0 +1,63 @@ +# Example from https://deliveroo.engineering/2022/06/27/openapi-design-first.html +# © All-Rights-Reserved +openapi: 3.1.0 +info: + title: Care Request API + version: 0.1.0 +paths: + "/requests/{request-id}": + get: + summary: Get all requests + operationId: getRequest + parameters: + - $ref: '#/components/parameters/RequestId' + - $ref: '#/components/parameters/TracingId' + responses: + '200': + description: 'Completed successfully' + content: + application/json: + schema: + $ref: '#/components/schemas/CareRequest' + '404': + description: 'The resource could not be found' + content: {} + # we'd also add other response options here too +components: + parameters: + RequestId: + name: request-id + in: path + required: true + schema: + $ref: '#/components/schemas/RequestId' + x-go-name: RequestIdParameter + TracingId: + description: A unique tracing ID that can be used for end-to-end tracing + name: tracing-id + in: header + required: false + schema: + type: string + format: uuid + pattern: "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[89abAB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}" + schemas: + CareRequest: + type: object + properties: + id: + $ref: '#/components/schemas/RequestId' + status: + $ref: '#/components/schemas/RequestStatus' + required: + - id + - status + RequestId: + type: string + format: uuid + pattern: "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[89abAB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}" + RequestStatus: + type: string + enum: + - active + - completed diff --git a/validator_test.go b/validator_test.go index c59ae25..5503455 100644 --- a/validator_test.go +++ b/validator_test.go @@ -1009,3 +1009,51 @@ func TestNewValidator_PetStore_UploadImage200_InvalidAPIResponse(t *testing.T) { assert.Len(t, errors, 1) assert.Equal(t, "200 response body for '/pet/112233/uploadImage' failed to validate schema", errors[0].Message) } + +func TestNewValidator_CareRequest_WrongContentType(t *testing.T) { + + careRequestBytes, _ := os.ReadFile("test_specs/care_request.yaml") + doc, _ := libopenapi.NewDocument(careRequestBytes) + + // create a doc + v, _ := NewValidator(doc) + + // create a new put request + request, _ := http.NewRequest(http.MethodGet, + "https://hyperspace-superherbs.com/requests/d4bc1a0c-c4ee-4be5-9281-26b1a041634", 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, "application/not-json") + w.WriteHeader(http.StatusOK) + + // create a CareRequest + body := map[string]interface{}{ + "id": "d4bc1a0c-c4ee-4be5-9281-26b1a041634d", + "status": "active", + } + + // marshal the body into bytes. + bodyBytes, _ := json.Marshal(body) + + _, _ = w.Write(bodyBytes) + } + + // fire the request + handler(res, request) + + // validate the response + valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) + + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Equal(t, "GET / 200 operation response content type 'application/not-json' does not exist", + errors[0].Message) + + assert.Equal(t, "The content type 'application/not-json' "+ + "of the GET response received has not been defined, it's an unknown type", + errors[0].Reason) + +} From 91037452eca67f1833603382c24c770e1d083fc9 Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Mon, 24 Apr 2023 10:49:09 -0400 Subject: [PATCH 5/8] removing some comments. Signed-off-by: Dave Shanley --- validator_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/validator_test.go b/validator_test.go index 5503455..5c0099f 100644 --- a/validator_test.go +++ b/validator_test.go @@ -1023,7 +1023,7 @@ func TestNewValidator_CareRequest_WrongContentType(t *testing.T) { "https://hyperspace-superherbs.com/requests/d4bc1a0c-c4ee-4be5-9281-26b1a041634", 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. + // simulate a request/response, res := httptest.NewRecorder() handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set(helpers.ContentTypeHeader, "application/not-json") @@ -1046,7 +1046,7 @@ func TestNewValidator_CareRequest_WrongContentType(t *testing.T) { // validate the response valid, errors := v.ValidateHttpRequestResponse(request, res.Result()) - + assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "GET / 200 operation response content type 'application/not-json' does not exist", From 2401fdc999d4589cc5ee76d13357df461612c2ae Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Mon, 24 Apr 2023 10:56:08 -0400 Subject: [PATCH 6/8] fix: #3 Signed-off-by: Dave Shanley --- errors/parameter_errors.go | 25 +++++++++++------------ errors/parameters_howtofix.go | 38 ++++++++++++++++++----------------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/errors/parameter_errors.go b/errors/parameter_errors.go index 958aa31..65b77be 100644 --- a/errors/parameter_errors.go +++ b/errors/parameter_errors.go @@ -5,7 +5,6 @@ import ( "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/datamodel/high/v3" - "gopkg.in/yaml.v3" "net/url" "strings" ) @@ -76,40 +75,40 @@ func InvalidDeepObject(param *v3.Parameter, qp *helpers.QueryParam) *ValidationE func QueryParameterMissing(param *v3.Parameter) *ValidationError { return &ValidationError{ - Message: fmt.Sprintf("Query parameter '%s' is missing", param.Name), + ValidationType: helpers.ParameterValidation, + ValidationSubType: helpers.ParameterValidationQuery, + Message: fmt.Sprintf("Query parameter '%s' is missing", param.Name), Reason: fmt.Sprintf("The query parameter '%s' is defined as being required, "+ "however it's missing from the requests", param.Name), SpecLine: param.GoLow().Required.KeyNode.Line, SpecCol: param.GoLow().Required.KeyNode.Column, + HowToFix: HowToFixMissingValue, } } func HeaderParameterMissing(param *v3.Parameter) *ValidationError { return &ValidationError{ - Message: fmt.Sprintf("Header parameter '%s' is missing", param.Name), + ValidationType: helpers.ParameterValidation, + ValidationSubType: helpers.ParameterValidationHeader, + Message: fmt.Sprintf("Header parameter '%s' is missing", param.Name), Reason: fmt.Sprintf("The header parameter '%s' is defined as being required, "+ "however it's missing from the requests", param.Name), SpecLine: param.GoLow().Required.KeyNode.Line, SpecCol: param.GoLow().Required.KeyNode.Column, - } -} - -func HeaderParameterNotDefined(paramName string, kn *yaml.Node) *ValidationError { - return &ValidationError{ - Message: fmt.Sprintf("Header parameter '%s' is not defined", paramName), - Reason: fmt.Sprintf("The header parameter '%s' is not defined as part of the specification for the operation", paramName), - SpecLine: kn.Line, - SpecCol: kn.Column, + HowToFix: HowToFixMissingValue, } } func HeaderParameterCannotBeDecoded(param *v3.Parameter, val string) *ValidationError { return &ValidationError{ - Message: fmt.Sprintf("Header parameter '%s' cannot be decoded", param.Name), + ValidationType: helpers.ParameterValidation, + ValidationSubType: helpers.ParameterValidationHeader, + Message: fmt.Sprintf("Header parameter '%s' cannot be decoded", param.Name), Reason: fmt.Sprintf("The header parameter '%s' cannot be "+ "extracted into an object, '%s' is malformed", param.Name, val), SpecLine: param.GoLow().Schema.Value.Schema().Type.KeyNode.Line, SpecCol: param.GoLow().Schema.Value.Schema().Type.KeyNode.Line, + HowToFix: HowToFixInvalidEncoding, } } diff --git a/errors/parameters_howtofix.go b/errors/parameters_howtofix.go index 7ed317d..f509d2a 100644 --- a/errors/parameters_howtofix.go +++ b/errors/parameters_howtofix.go @@ -4,22 +4,24 @@ package errors const ( - HowToFixReservedValues string = "parameter values need to URL Encoded to ensure reserved " + - "values are correctly encoded, for example: '%s'" - HowToFixParamInvalidNumber string = "Convert the value '%s' into a number" - HowToFixParamInvalidString string = "Convert the value '%s' into a string (cannot start with a number, or be a floating point)" - HowToFixParamInvalidBoolean string = "Convert the value '%s' into a true/false value" - HowToFixParamInvalidEnum string = "Instead of '%s', use one of the allowed values: '%s'" - HowToFixParamInvalidFormEncode string = "Use a form style encoding for parameter values, for example: '%s'" - HowToFixInvalidSchema string = "Ensure that the object being submitted, matches the schema correctly" - HowToFixParamInvalidSpaceDelimitedObjectExplode string = "When using 'explode' with space delimited parameters, " + - "they should be separated by spaces. For example: '%s'" - HowToFixParamInvalidPipeDelimitedObjectExplode string = "When using 'explode' with pipe delimited parameters, " + - "they should be separated by pipes '|'. For example: '%s'" - HowToFixParamInvalidDeepObjectMultipleValues string = "There can only be a single value per property name, " + - "deepObject parameters should contain the property key in square brackets next to the parameter name. For example: '%s'" - HowToFixInvalidJSON string = "The JSON submitted is invalid, please check the syntax" - HowToFixDecodingError = "The object can't be decoded, so make sure it's being encoded correctly according to the spec." - HowToFixInvalidContentType = "The content type is invalid, Use one of the %d supported types for this operation: %s" - HowToFixInvalidResponseCode = "The service is responding with a code that is not defined in the spec, fix the service!" + HowToFixReservedValues string = "parameter values need to URL Encoded to ensure reserved " + + "values are correctly encoded, for example: '%s'" + HowToFixParamInvalidNumber string = "Convert the value '%s' into a number" + HowToFixParamInvalidString string = "Convert the value '%s' into a string (cannot start with a number, or be a floating point)" + HowToFixParamInvalidBoolean string = "Convert the value '%s' into a true/false value" + HowToFixParamInvalidEnum string = "Instead of '%s', use one of the allowed values: '%s'" + HowToFixParamInvalidFormEncode string = "Use a form style encoding for parameter values, for example: '%s'" + HowToFixInvalidSchema string = "Ensure that the object being submitted, matches the schema correctly" + HowToFixParamInvalidSpaceDelimitedObjectExplode string = "When using 'explode' with space delimited parameters, " + + "they should be separated by spaces. For example: '%s'" + HowToFixParamInvalidPipeDelimitedObjectExplode string = "When using 'explode' with pipe delimited parameters, " + + "they should be separated by pipes '|'. For example: '%s'" + HowToFixParamInvalidDeepObjectMultipleValues string = "There can only be a single value per property name, " + + "deepObject parameters should contain the property key in square brackets next to the parameter name. For example: '%s'" + HowToFixInvalidJSON string = "The JSON submitted is invalid, please check the syntax" + HowToFixDecodingError = "The object can't be decoded, so make sure it's being encoded correctly according to the spec." + HowToFixInvalidContentType = "The content type is invalid, Use one of the %d supported types for this operation: %s" + HowToFixInvalidResponseCode = "The service is responding with a code that is not defined in the spec, fix the service!" + HowToFixInvalidEncoding = "Ensure the correct encoding has been used on the object" + HowToFixMissingValue = "Ensure the value has been set" ) From 3399802434c0a016ba12927d1a3003cd3ef6496d Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Mon, 24 Apr 2023 11:14:24 -0400 Subject: [PATCH 7/8] fix: #4 added `IsPathMissingError()` to `ValidationError` Signed-off-by: Dave Shanley --- errors/validation_error.go | 93 ++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 44 deletions(-) diff --git a/errors/validation_error.go b/errors/validation_error.go index a27f786..e1fb017 100644 --- a/errors/validation_error.go +++ b/errors/validation_error.go @@ -4,78 +4,83 @@ package errors import ( - "fmt" - "github.com/santhosh-tekuri/jsonschema/v5" + "fmt" + "github.com/santhosh-tekuri/jsonschema/v5" ) // SchemaValidationFailure is a wrapper around the jsonschema.ValidationError object, to provide a more // user-friendly way to break down what went wrong. type SchemaValidationFailure struct { - // Reason is a human-readable message describing the reason for the error. - Reason string `json:"reason,omitempty" yaml:"reason,omitempty"` + // Reason is a human-readable message describing the reason for the error. + Reason string `json:"reason,omitempty" yaml:"reason,omitempty"` - // Location is the XPath-like location of the validation failure - Location string `json:"location,omitempty" yaml:"location,omitempty"` + // Location is the XPath-like location of the validation failure + Location string `json:"location,omitempty" yaml:"location,omitempty"` - // Line is the line number where the violation occurred. This may a local line number - // if the validation is a schema (only schemas are validated locally, so the line number will be relative to - // the Context object held by the ValidationError object). - Line int `json:"line,omitempty" yaml:"line,omitempty"` + // Line is the line number where the violation occurred. This may a local line number + // if the validation is a schema (only schemas are validated locally, so the line number will be relative to + // the Context object held by the ValidationError object). + Line int `json:"line,omitempty" yaml:"line,omitempty"` - // Column is the column number where the violation occurred. This may a local column number - // if the validation is a schema (only schemas are validated locally, so the column number will be relative to - // the Context object held by the ValidationError object). - Column int `json:"column,omitempty" yaml:"column,omitempty"` + // Column is the column number where the violation occurred. This may a local column number + // if the validation is a schema (only schemas are validated locally, so the column number will be relative to + // the Context object held by the ValidationError object). + Column int `json:"column,omitempty" yaml:"column,omitempty"` - // The original error object, which is a jsonschema.ValidationError object. - OriginalError *jsonschema.ValidationError `json:"-" yaml:"-"` + // The original error object, which is a jsonschema.ValidationError object. + OriginalError *jsonschema.ValidationError `json:"-" yaml:"-"` } // Error returns a string representation of the error func (s *SchemaValidationFailure) Error() string { - return fmt.Sprintf("Reason: %s, Location: %s", s.Reason, s.Location) + return fmt.Sprintf("Reason: %s, Location: %s", s.Reason, s.Location) } // ValidationError is a struct that contains all the information about a validation error. type ValidationError struct { - // Message is a human-readable message describing the error. - Message string `json:"message" yaml:"message"` + // Message is a human-readable message describing the error. + Message string `json:"message" yaml:"message"` - // Reason is a human-readable message describing the reason for the error. - Reason string `json:"reason" yaml:"reason"` + // Reason is a human-readable message describing the reason for the error. + Reason string `json:"reason" yaml:"reason"` - // ValidationType is a string that describes the type of validation that failed. - ValidationType string `json:"validationType" yaml:"validationType"` + // ValidationType is a string that describes the type of validation that failed. + ValidationType string `json:"validationType" yaml:"validationType"` - // ValidationSubType is a string that describes the subtype of validation that failed. - ValidationSubType string `json:"validationSubType" yaml:"validationSubType"` + // ValidationSubType is a string that describes the subtype of validation that failed. + ValidationSubType string `json:"validationSubType" yaml:"validationSubType"` - // SpecLine is the line number in the spec where the error occurred. - SpecLine int `json:"specLine" yaml:"specLine"` + // SpecLine is the line number in the spec where the error occurred. + SpecLine int `json:"specLine" yaml:"specLine"` - // SpecCol is the column number in the spec where the error occurred. - SpecCol int `json:"specColumn" yaml:"specColumn"` + // SpecCol is the column number in the spec where the error occurred. + SpecCol int `json:"specColumn" yaml:"specColumn"` - // HowToFix is a human-readable message describing how to fix the error. - HowToFix string `json:"howToFix" yaml:"howToFix"` + // HowToFix is a human-readable message describing how to fix the error. + HowToFix string `json:"howToFix" yaml:"howToFix"` - // SchemaValidationErrors is a slice of SchemaValidationFailure objects that describe the validation errors - // This is only populated whe the validation type is against a schema. - SchemaValidationErrors []*SchemaValidationFailure `json:"validationErrors,omitempty" yaml:"validationErrors,omitempty"` + // SchemaValidationErrors is a slice of SchemaValidationFailure objects that describe the validation errors + // This is only populated whe the validation type is against a schema. + SchemaValidationErrors []*SchemaValidationFailure `json:"validationErrors,omitempty" yaml:"validationErrors,omitempty"` - // Context is the object that the validation error occurred on. This is usually a pointer to a schema - // or a parameter object. - Context interface{} `json:"-" yaml:"-"` + // Context is the object that the validation error occurred on. This is usually a pointer to a schema + // or a parameter object. + Context interface{} `json:"-" yaml:"-"` } // Error returns a string representation of the error func (v *ValidationError) Error() string { - if v.SchemaValidationErrors != nil { - return fmt.Sprintf("Error: %s, Reason: %s, Validation Errors: %s, Line: %d, Column: %d", - v.Message, v.Reason, v.SchemaValidationErrors, v.SpecLine, v.SpecCol) - } else { - return fmt.Sprintf("Error: %s, Reason: %s, Line: %d, Column: %d", - v.Message, v.Reason, v.SpecLine, v.SpecCol) - } + if v.SchemaValidationErrors != nil { + return fmt.Sprintf("Error: %s, Reason: %s, Validation Errors: %s, Line: %d, Column: %d", + v.Message, v.Reason, v.SchemaValidationErrors, v.SpecLine, v.SpecCol) + } else { + return fmt.Sprintf("Error: %s, Reason: %s, Line: %d, Column: %d", + v.Message, v.Reason, v.SpecLine, v.SpecCol) + } +} + +// IsPathMissingError returns true if the error has a ValidationType of "path" and a ValidationSubType of "missing" +func (v *ValidationError) IsPathMissingError() bool { + return v.ValidationType == "path" && v.ValidationSubType == "missing" } From 804f14322087194085479ce12e7bf9d654477f8b Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Mon, 24 Apr 2023 11:28:11 -0400 Subject: [PATCH 8/8] bumped coverage. Signed-off-by: Dave Shanley --- requests/validate_body_test.go | 47 ++ responses/validate_body_test.go | 839 +++++++++++++++++--------------- 2 files changed, 497 insertions(+), 389 deletions(-) diff --git a/requests/validate_body_test.go b/requests/validate_body_test.go index cc9a277..48c66bd 100644 --- a/requests/validate_body_test.go +++ b/requests/validate_body_test.go @@ -233,6 +233,9 @@ paths: valid, errors := v.ValidateRequestBody(request) + // double-tap to hit the cache + _, _ = v.ValidateRequestBody(request) + assert.False(t, valid) assert.Len(t, errors, 1) assert.Len(t, errors[0].SchemaValidationErrors, 2) @@ -746,3 +749,47 @@ components: assert.Equal(t, "maximum 2 items required, but found 4 items", errors[0].SchemaValidationErrors[0].Reason) assert.Equal(t, 11, errors[0].SchemaValidationErrors[0].Column) } + +func TestValidateBody_MissingBody(t *testing.T) { + spec := `openapi: 3.1.0 +paths: + /burgers/createBurger: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schema_validation/TestBody' +components: + schema_validation: + TestBody: + type: array + maxItems: 2 + items: + type: object + properties: + name: + type: string + patties: + type: integer + maximum: 3 + minimum: 1 + vegetarian: + type: boolean + required: [name, patties, vegetarian] ` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + + m, _ := doc.BuildV3Model() + v := NewRequestBodyValidator(&m.Model) + + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", + http.NoBody) + request.Header.Set("Content-Type", "application/json") + + valid, errors := v.ValidateRequestBody(request) + + assert.True(t, valid) + assert.Len(t, errors, 0) + +} diff --git a/responses/validate_body_test.go b/responses/validate_body_test.go index d7a08d8..2f929c3 100644 --- a/responses/validate_body_test.go +++ b/responses/validate_body_test.go @@ -4,20 +4,20 @@ package responses import ( - "bytes" - "encoding/json" - "fmt" - "github.com/pb33f/libopenapi" - "github.com/pb33f/libopenapi-validator/helpers" - "github.com/pb33f/libopenapi-validator/paths" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - "testing" + "bytes" + "encoding/json" + "fmt" + "github.com/pb33f/libopenapi" + "github.com/pb33f/libopenapi-validator/helpers" + "github.com/pb33f/libopenapi-validator/paths" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "testing" ) func TestValidateBody_MissingContentType(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -35,49 +35,49 @@ paths: vegetarian: type: boolean` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() - v := NewResponseBodyValidator(&m.Model) + m, _ := doc.BuildV3Model() + v := NewResponseBodyValidator(&m.Model) - body := map[string]interface{}{ - "name": "Big Mac", - "patties": false, - "vegetarian": 2, - } + body := map[string]interface{}{ + "name": "Big Mac", + "patties": false, + "vegetarian": 2, + } - bodyBytes, _ := json.Marshal(body) + 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) + // build a request + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader(bodyBytes)) + request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) - // simulate a request/response - res := httptest.NewRecorder() - handler := func(w http.ResponseWriter, r *http.Request) { - w.Header().Set(helpers.ContentTypeHeader, "cheeky/monkey") - w.WriteHeader(http.StatusOK) - _, _ = w.Write(bodyBytes) - } + // simulate a request/response + res := httptest.NewRecorder() + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set(helpers.ContentTypeHeader, "cheeky/monkey") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(bodyBytes) + } - // fire the request - handler(res, request) + // fire the request + handler(res, request) - // record response - response := res.Result() + // record response + response := res.Result() - // validate! - valid, errors := v.ValidateResponseBody(request, response) + // validate! + valid, errors := v.ValidateResponseBody(request, response) - assert.False(t, valid) - assert.Len(t, errors, 1) - assert.Equal(t, "POST / 200 operation response content type 'cheeky/monkey' does not exist", errors[0].Message) - assert.Equal(t, "The content type is invalid, Use one of the 1 "+ - "supported types for this operation: application/json", errors[0].HowToFix) + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Equal(t, "POST / 200 operation response content type 'cheeky/monkey' does not exist", errors[0].Message) + assert.Equal(t, "The content type is invalid, Use one of the 1 "+ + "supported types for this operation: application/json", errors[0].HowToFix) } func TestValidateBody_MissingPath(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -95,47 +95,47 @@ paths: vegetarian: type: boolean` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() - v := NewResponseBodyValidator(&m.Model) + m, _ := doc.BuildV3Model() + v := NewResponseBodyValidator(&m.Model) - body := map[string]interface{}{ - "name": "Big Mac", - "patties": false, - "vegetarian": 2, - } + body := map[string]interface{}{ + "name": "Big Mac", + "patties": false, + "vegetarian": 2, + } - bodyBytes, _ := json.Marshal(body) + bodyBytes, _ := json.Marshal(body) - // build a request - request, _ := http.NewRequest(http.MethodPost, "https://things.com/I do not exist", bytes.NewReader(bodyBytes)) - request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) + // build a request + request, _ := http.NewRequest(http.MethodPost, "https://things.com/I do not exist", bytes.NewReader(bodyBytes)) + request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) - // simulate a request/response - res := httptest.NewRecorder() - handler := func(w http.ResponseWriter, r *http.Request) { - w.Header().Set(helpers.ContentTypeHeader, "cheeky/monkey") // won't even matter! - w.WriteHeader(http.StatusUnprocessableEntity) // does not matter. - _, _ = w.Write(bodyBytes) - } + // simulate a request/response + res := httptest.NewRecorder() + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set(helpers.ContentTypeHeader, "cheeky/monkey") // won't even matter! + w.WriteHeader(http.StatusUnprocessableEntity) // does not matter. + _, _ = w.Write(bodyBytes) + } - // fire the request - handler(res, request) + // fire the request + handler(res, request) - // record response - response := res.Result() + // record response + response := res.Result() - // validate! - valid, errors := v.ValidateResponseBody(request, response) + // validate! + valid, errors := v.ValidateResponseBody(request, response) - assert.False(t, valid) - assert.Len(t, errors, 1) - assert.Equal(t, "Path '/I do not exist' not found", errors[0].Message) + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Equal(t, "Path '/I do not exist' not found", errors[0].Message) } func TestValidateBody_SetPath(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -153,51 +153,51 @@ paths: vegetarian: type: boolean` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() - v := NewResponseBodyValidator(&m.Model) + m, _ := doc.BuildV3Model() + v := NewResponseBodyValidator(&m.Model) - body := map[string]interface{}{ - "name": "Big Mac", - "patties": false, - "vegetarian": 2, - } + body := map[string]interface{}{ + "name": "Big Mac", + "patties": false, + "vegetarian": 2, + } - bodyBytes, _ := json.Marshal(body) + bodyBytes, _ := json.Marshal(body) - // build a request - request, _ := http.NewRequest(http.MethodPost, "https://things.com/I do not exist", bytes.NewReader(bodyBytes)) - request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) + // build a request + request, _ := http.NewRequest(http.MethodPost, "https://things.com/I do not exist", bytes.NewReader(bodyBytes)) + request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) - // preset the path - path, _, pv := paths.FindPath(request, &m.Model) - v.SetPathItem(path, pv) + // preset the path + path, _, pv := paths.FindPath(request, &m.Model) + v.SetPathItem(path, pv) - // simulate a request/response - res := httptest.NewRecorder() - handler := func(w http.ResponseWriter, r *http.Request) { - w.Header().Set(helpers.ContentTypeHeader, "cheeky/monkey") // won't even matter! - w.WriteHeader(http.StatusUnprocessableEntity) // does not matter. - _, _ = w.Write(bodyBytes) - } + // simulate a request/response + res := httptest.NewRecorder() + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set(helpers.ContentTypeHeader, "cheeky/monkey") // won't even matter! + w.WriteHeader(http.StatusUnprocessableEntity) // does not matter. + _, _ = w.Write(bodyBytes) + } - // fire the request - handler(res, request) + // fire the request + handler(res, request) - // record response - response := res.Result() + // record response + response := res.Result() - // validate! - valid, errors := v.ValidateResponseBody(request, response) + // validate! + valid, errors := v.ValidateResponseBody(request, response) - assert.False(t, valid) - assert.Len(t, errors, 1) - assert.Equal(t, "Path '/I do not exist' not found", errors[0].Message) + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Equal(t, "Path '/I do not exist' not found", errors[0].Message) } func TestValidateBody_MissingStatusCode(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -215,48 +215,48 @@ paths: vegetarian: type: boolean` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() - v := NewResponseBodyValidator(&m.Model) + m, _ := doc.BuildV3Model() + v := NewResponseBodyValidator(&m.Model) - body := map[string]interface{}{ - "name": "Big Mac", - "patties": false, - "vegetarian": 2, - } + body := map[string]interface{}{ + "name": "Big Mac", + "patties": false, + "vegetarian": 2, + } - bodyBytes, _ := json.Marshal(body) + 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) + // build a request + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader(bodyBytes)) + request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) - // simulate a request/response - res := httptest.NewRecorder() - handler := func(w http.ResponseWriter, r *http.Request) { - w.Header().Set(helpers.ContentTypeHeader, "cheeky/monkey") // won't even matter! - w.WriteHeader(http.StatusUnprocessableEntity) // undefined in the spec. - _, _ = w.Write(bodyBytes) - } + // simulate a request/response + res := httptest.NewRecorder() + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set(helpers.ContentTypeHeader, "cheeky/monkey") // won't even matter! + w.WriteHeader(http.StatusUnprocessableEntity) // undefined in the spec. + _, _ = w.Write(bodyBytes) + } - // fire the request - handler(res, request) + // fire the request + handler(res, request) - // record response - response := res.Result() + // record response + response := res.Result() - // validate! - valid, errors := v.ValidateResponseBody(request, response) + // validate! + valid, errors := v.ValidateResponseBody(request, response) - assert.False(t, valid) - assert.Len(t, errors, 1) - assert.Equal(t, "POST operation request response code '422' does not exist", errors[0].Message) - assert.Equal(t, "The service is responding with a code that is not defined in the spec, fix the service!", errors[0].HowToFix) + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Equal(t, "POST operation request response code '422' does not exist", errors[0].Message) + assert.Equal(t, "The service is responding with a code that is not defined in the spec, fix the service!", errors[0].HowToFix) } func TestValidateBody_InvalidBasicSchema(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -274,48 +274,109 @@ paths: vegetarian: type: boolean` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() - v := NewResponseBodyValidator(&m.Model) + m, _ := doc.BuildV3Model() + v := NewResponseBodyValidator(&m.Model) - // mix up the primitives to fire two schema violations. - body := map[string]interface{}{ - "name": "Big Mac", - "patties": false, - "vegetarian": 2, - } + // mix up the primitives to fire two schema violations. + body := map[string]interface{}{ + "name": "Big Mac", + "patties": false, + "vegetarian": 2, + } - bodyBytes, _ := json.Marshal(body) + 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) + // build a request + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader(bodyBytes)) + request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) - // simulate a request/response - 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) - } + // simulate a request/response + 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) + // fire the request + handler(res, request) - // record response - response := res.Result() + // record response + response := res.Result() - // validate! - valid, errors := v.ValidateResponseBody(request, response) + // validate! + valid, errors := v.ValidateResponseBody(request, response) + // doubletap to hit cache + _, _ = v.ValidateResponseBody(request, response) - assert.False(t, valid) - assert.Len(t, errors, 1) - assert.Len(t, errors[0].SchemaValidationErrors, 2) + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Len(t, errors[0].SchemaValidationErrors, 2) +} + +func TestValidateBody_NoBody(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) + + // build a request + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", http.NoBody) + request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) + + // simulate a request/response + res := httptest.NewRecorder() + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) + w.WriteHeader(http.StatusOK) + //body := map[string]interface{}{ + // "name": "Big Mac", + // "patties": false, + // "vegetarian": 2, + //} + + // bodyBytes, _ := json.Marshal(body) + _, _ = w.Write(nil) + } + + // fire the request + handler(res, request) + + // record response + response := res.Result() + + // validate! + valid, errors := v.ValidateResponseBody(request, response) + // doubletap to hit cache + _, _ = v.ValidateResponseBody(request, response) + + assert.True(t, valid) + assert.Len(t, errors, 0) + //assert.Len(t, errors[0].SchemaValidationErrors, 2) } func TestValidateBody_InvalidBasicSchema_SetPath(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -333,53 +394,53 @@ paths: vegetarian: type: boolean` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() - v := NewResponseBodyValidator(&m.Model) + m, _ := doc.BuildV3Model() + v := NewResponseBodyValidator(&m.Model) - // mix up the primitives to fire two schema violations. - body := map[string]interface{}{ - "name": "Big Mac", - "patties": false, - "vegetarian": 2, - } + // mix up the primitives to fire two schema violations. + body := map[string]interface{}{ + "name": "Big Mac", + "patties": false, + "vegetarian": 2, + } - bodyBytes, _ := json.Marshal(body) + 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) + // build a request + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader(bodyBytes)) + request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) - // simulate a request/response - 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) - } + // simulate a request/response + 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) + // fire the request + handler(res, request) - // record response - response := res.Result() + // record response + response := res.Result() - // preset the path - path, _, pv := paths.FindPath(request, &m.Model) - v.SetPathItem(path, pv) + // preset the path + path, _, pv := paths.FindPath(request, &m.Model) + v.SetPathItem(path, pv) - // validate! - valid, errors := v.ValidateResponseBody(request, response) + // validate! + valid, errors := v.ValidateResponseBody(request, response) - assert.False(t, valid) - assert.Len(t, errors, 1) - assert.Len(t, errors[0].SchemaValidationErrors, 2) - assert.Equal(t, "200 response body for '/burgers/createBurger' failed to validate schema", errors[0].Message) + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Len(t, errors[0].SchemaValidationErrors, 2) + assert.Equal(t, "200 response body for '/burgers/createBurger' failed to validate schema", errors[0].Message) } func TestValidateBody_ValidComplexSchema(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -438,51 +499,51 @@ components: type: boolean required: [name, patties, vegetarian]` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() - v := NewResponseBodyValidator(&m.Model) + m, _ := doc.BuildV3Model() + v := NewResponseBodyValidator(&m.Model) - body := map[string]interface{}{ - "name": "Big Mac", - "patties": 2, - "vegetarian": true, - "fat": 10.0, - "salt": 0.5, - "meat": "beef", - "usedOil": true, - "usedAnimalFat": false, - } + body := map[string]interface{}{ + "name": "Big Mac", + "patties": 2, + "vegetarian": true, + "fat": 10.0, + "salt": 0.5, + "meat": "beef", + "usedOil": true, + "usedAnimalFat": false, + } - bodyBytes, _ := json.Marshal(body) + 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) + // build a request + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader(bodyBytes)) + request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) - // simulate a request/response - 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) - } + // simulate a request/response + 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) + // fire the request + handler(res, request) - // record response - response := res.Result() + // record response + response := res.Result() - // validate! - valid, errors := v.ValidateResponseBody(request, response) + // validate! + valid, errors := v.ValidateResponseBody(request, response) - assert.True(t, valid) - assert.Len(t, errors, 0) + assert.True(t, valid) + assert.Len(t, errors, 0) } func TestValidateBody_InvalidComplexSchema(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -541,53 +602,53 @@ components: type: boolean required: [name, patties, vegetarian]` - doc, _ := libopenapi.NewDocument([]byte(spec)) - - m, _ := doc.BuildV3Model() - v := NewResponseBodyValidator(&m.Model) - - body := map[string]interface{}{ - "name": "Big Mac", - "patties": 2, - "vegetarian": true, - "fat": 10.0, - "salt": 0.5, - "meat": "beef", - "usedOil": 12345, // invalid, should be bool - "usedAnimalFat": 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) - - // simulate a request/response - 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) - - // record response - response := res.Result() - - // validate! - valid, errors := v.ValidateResponseBody(request, response) - - assert.False(t, valid) - assert.Len(t, errors, 1) - assert.Len(t, errors[0].SchemaValidationErrors, 3) - assert.Equal(t, "expected boolean, but got number", errors[0].SchemaValidationErrors[2].Reason) + doc, _ := libopenapi.NewDocument([]byte(spec)) + + m, _ := doc.BuildV3Model() + v := NewResponseBodyValidator(&m.Model) + + body := map[string]interface{}{ + "name": "Big Mac", + "patties": 2, + "vegetarian": true, + "fat": 10.0, + "salt": 0.5, + "meat": "beef", + "usedOil": 12345, // invalid, should be bool + "usedAnimalFat": 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) + + // simulate a request/response + 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) + + // record response + response := res.Result() + + // validate! + valid, errors := v.ValidateResponseBody(request, response) + + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Len(t, errors[0].SchemaValidationErrors, 3) + assert.Equal(t, "expected boolean, but got number", errors[0].SchemaValidationErrors[2].Reason) } func TestValidateBody_ValidBasicSchema(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -605,46 +666,46 @@ paths: vegetarian: type: boolean` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() - v := NewResponseBodyValidator(&m.Model) + m, _ := doc.BuildV3Model() + v := NewResponseBodyValidator(&m.Model) - body := map[string]interface{}{ - "name": "Big Mac", - "patties": 2, - "vegetarian": false, - } + body := map[string]interface{}{ + "name": "Big Mac", + "patties": 2, + "vegetarian": false, + } - bodyBytes, _ := json.Marshal(body) + 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) + // build a request + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader(bodyBytes)) + request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) - // simulate a request/response - 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) - } + // simulate a request/response + 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) + // fire the request + handler(res, request) - // record response - response := res.Result() + // record response + response := res.Result() - // validate! - valid, errors := v.ValidateResponseBody(request, response) + // validate! + valid, errors := v.ValidateResponseBody(request, response) - assert.True(t, valid) - assert.Len(t, errors, 0) + assert.True(t, valid) + assert.Len(t, errors, 0) } func TestValidateBody_ValidBasicSchema_WithFullContentTypeHeader(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -662,49 +723,49 @@ paths: vegetarian: type: boolean` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() - v := NewResponseBodyValidator(&m.Model) + m, _ := doc.BuildV3Model() + v := NewResponseBodyValidator(&m.Model) - body := map[string]interface{}{ - "name": "Big Mac", - "patties": 2, - "vegetarian": false, - } + body := map[string]interface{}{ + "name": "Big Mac", + "patties": 2, + "vegetarian": false, + } - bodyBytes, _ := json.Marshal(body) + 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) + // build a request + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader(bodyBytes)) + request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) - // simulate a request/response - res := httptest.NewRecorder() - handler := func(w http.ResponseWriter, r *http.Request) { + // simulate a request/response + res := httptest.NewRecorder() + handler := func(w http.ResponseWriter, r *http.Request) { - // inject a full content type header, including charset and boundary - w.Header().Set(helpers.ContentTypeHeader, - fmt.Sprintf("%s; charset=utf-8; boundary=---12223344", helpers.JSONContentType)) - w.WriteHeader(http.StatusOK) - _, _ = w.Write(bodyBytes) - } + // inject a full content type header, including charset and boundary + w.Header().Set(helpers.ContentTypeHeader, + fmt.Sprintf("%s; charset=utf-8; boundary=---12223344", helpers.JSONContentType)) + w.WriteHeader(http.StatusOK) + _, _ = w.Write(bodyBytes) + } - // fire the request - handler(res, request) + // fire the request + handler(res, request) - // record response - response := res.Result() + // record response + response := res.Result() - // validate! - valid, errors := v.ValidateResponseBody(request, response) + // validate! + valid, errors := v.ValidateResponseBody(request, response) - assert.True(t, valid) - assert.Len(t, errors, 0) + assert.True(t, valid) + assert.Len(t, errors, 0) } func TestValidateBody_ValidBasicSchemaUsingDefault(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -722,46 +783,46 @@ paths: vegetarian: type: boolean` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() - v := NewResponseBodyValidator(&m.Model) + m, _ := doc.BuildV3Model() + v := NewResponseBodyValidator(&m.Model) - body := map[string]interface{}{ - "name": "Big Mac", - "patties": 2, - "vegetarian": false, - } + body := map[string]interface{}{ + "name": "Big Mac", + "patties": 2, + "vegetarian": false, + } - bodyBytes, _ := json.Marshal(body) + 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) + // build a request + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader(bodyBytes)) + request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType) - // simulate a request/response - 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) - } + // simulate a request/response + 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) + // fire the request + handler(res, request) - // record response - response := res.Result() + // record response + response := res.Result() - // validate! - valid, errors := v.ValidateResponseBody(request, response) + // validate! + valid, errors := v.ValidateResponseBody(request, response) - assert.True(t, valid) - assert.Len(t, errors, 0) + assert.True(t, valid) + assert.Len(t, errors, 0) } func TestValidateBody_InvalidBasicSchemaUsingDefault_MissingContentType(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -779,42 +840,42 @@ paths: vegetarian: type: boolean` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() - v := NewResponseBodyValidator(&m.Model) + m, _ := doc.BuildV3Model() + v := NewResponseBodyValidator(&m.Model) - // primitives are now correct. - body := map[string]interface{}{ - "name": "Big Mac", - "patties": 2, - "vegetarian": false, - } + // primitives are now correct. + body := map[string]interface{}{ + "name": "Big Mac", + "patties": 2, + "vegetarian": false, + } - bodyBytes, _ := json.Marshal(body) + 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, "chicken/nuggets;chicken=soup") + // build a request + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader(bodyBytes)) + request.Header.Set(helpers.ContentTypeHeader, "chicken/nuggets;chicken=soup") - // simulate a request/response - res := httptest.NewRecorder() - handler := func(w http.ResponseWriter, r *http.Request) { - w.Header().Set(helpers.ContentTypeHeader, r.Header.Get(helpers.ContentTypeHeader)) - w.WriteHeader(http.StatusOK) - _, _ = w.Write(bodyBytes) - } + // simulate a request/response + res := httptest.NewRecorder() + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set(helpers.ContentTypeHeader, r.Header.Get(helpers.ContentTypeHeader)) + w.WriteHeader(http.StatusOK) + _, _ = w.Write(bodyBytes) + } - // fire the request - handler(res, request) + // fire the request + handler(res, request) - // record response - response := res.Result() + // record response + response := res.Result() - // validate! - valid, errors := v.ValidateResponseBody(request, response) + // validate! + valid, errors := v.ValidateResponseBody(request, response) - assert.False(t, valid) - assert.Len(t, errors, 1) - assert.Equal(t, "POST / 200 operation response content type 'chicken/nuggets' does not exist", errors[0].Message) + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Equal(t, "POST / 200 operation response content type 'chicken/nuggets' does not exist", errors[0].Message) }