From 43e0d564687e2be5bdd82ffd2f679715b0ddce72 Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Mon, 7 Aug 2023 11:25:44 -0400 Subject: [PATCH] Added extra validation for payloads when decoding schemas #17 Signed-off-by: Dave Shanley --- requests/validate_body_test.go | 43 +++ requests/validate_request.go | 288 ++++++++------- responses/validate_body_test.go | 54 +++ responses/validate_response.go | 28 +- schema_validation/validate_schema.go | 29 +- schema_validation/validate_schema_test.go | 426 ++++++++++++---------- 6 files changed, 539 insertions(+), 329 deletions(-) diff --git a/requests/validate_body_test.go b/requests/validate_body_test.go index 0282519..3ffef3a 100644 --- a/requests/validate_body_test.go +++ b/requests/validate_body_test.go @@ -854,3 +854,46 @@ paths: assert.Equal(t, "POST request body for '/burgers/createBurger' failed to validate schema", errors[0].Message) } + +func TestValidateBody_InvalidSchema_BadDecode(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: 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", + bytes.NewBuffer([]byte("{\"bad\": \"json\",}"))) + request.Header.Set("Content-Type", "application/json") + + valid, errors := v.ValidateRequestBody(request) + + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Len(t, errors[0].SchemaValidationErrors, 1) + assert.Equal(t, "invalid character '}' looking for beginning of object key string", errors[0].SchemaValidationErrors[0].Reason) + +} diff --git a/requests/validate_request.go b/requests/validate_request.go index 81b149a..d00e88f 100644 --- a/requests/validate_request.go +++ b/requests/validate_request.go @@ -4,21 +4,21 @@ package requests import ( - "bytes" - "encoding/json" - "fmt" - "github.com/pb33f/libopenapi-validator/errors" - "github.com/pb33f/libopenapi-validator/helpers" - "github.com/pb33f/libopenapi-validator/schema_validation" - "github.com/pb33f/libopenapi/datamodel/high/base" - "github.com/santhosh-tekuri/jsonschema/v5" - "gopkg.in/yaml.v3" - "io" - "net/http" - "reflect" - "regexp" - "strconv" - "strings" + "bytes" + "encoding/json" + "fmt" + "github.com/pb33f/libopenapi-validator/errors" + "github.com/pb33f/libopenapi-validator/helpers" + "github.com/pb33f/libopenapi-validator/schema_validation" + "github.com/pb33f/libopenapi/datamodel/high/base" + "github.com/santhosh-tekuri/jsonschema/v5" + "gopkg.in/yaml.v3" + "io" + "net/http" + "reflect" + "regexp" + "strconv" + "strings" ) var instanceLocationRegex = regexp.MustCompile(`^/(\d+)`) @@ -26,120 +26,146 @@ var instanceLocationRegex = regexp.MustCompile(`^/(\d+)`) // ValidateRequestSchema will validate an http.Request pointer against a schema. // If validation fails, it will return a list of validation errors as the second return value. func ValidateRequestSchema( - request *http.Request, - schema *base.Schema, - renderedSchema, - jsonSchema []byte) (bool, []*errors.ValidationError) { - - var validationErrors []*errors.ValidationError - - requestBody, _ := io.ReadAll(request.Body) - - // close the request body, so it can be re-read later by another player in the chain - _ = request.Body.Close() - request.Body = io.NopCloser(bytes.NewBuffer(requestBody)) - - 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") - - // 4. validate the object against the schema - scErrs := jsch.Validate(decodedObj) - if scErrs != nil { - jk := scErrs.(*jsonschema.ValidationError) - - // flatten the validationErrors - schFlatErrs := jk.BasicOutput().Errors - var schemaValidationErrors []*errors.SchemaValidationFailure - for q := range schFlatErrs { - er := schFlatErrs[q] - if er.KeywordLocation == "" || strings.HasPrefix(er.Error, "doesn't validate with") { - continue // ignore this error, it's useless tbh, utter noise. - } - if er.Error != "" { - - // re-encode the schema. - var renderedNode yaml.Node - _ = yaml.Unmarshal(renderedSchema, &renderedNode) - - // locate the violated property in the schema - located := schema_validation.LocateSchemaPropertyNodeByJSONPath(renderedNode.Content[0], er.KeywordLocation) - - // extract the element specified by the instance - val := instanceLocationRegex.FindStringSubmatch(er.InstanceLocation) - var referenceObject string - - if len(val) > 0 { - referenceIndex, _ := strconv.Atoi(val[1]) - if reflect.ValueOf(decodedObj).Type().Kind() == reflect.Slice { - found := decodedObj.([]any)[referenceIndex] - recoded, _ := json.MarshalIndent(found, "", " ") - referenceObject = string(recoded) - } - } - if referenceObject == "" { - referenceObject = string(requestBody) - } - - violation := &errors.SchemaValidationFailure{ - Reason: er.Error, - Location: er.KeywordLocation, - ReferenceSchema: string(renderedSchema), - ReferenceObject: referenceObject, - OriginalError: jk, - } - // if we have a location within the schema, add it to the error - if located != nil { - - line := located.Line - // if the located node is a map or an array, then the actual human interpretable - // line on which the violation occurred is the line of the key, not the value. - if located.Kind == yaml.MappingNode || located.Kind == yaml.SequenceNode { - if line > 0 { - line-- - } - } - - // location of the violation within the rendered schema. - violation.Line = line - violation.Column = located.Column - } - schemaValidationErrors = append(schemaValidationErrors, violation) - } - } - - line := 1 - col := 0 - if schema.GoLow().Type.KeyNode != nil { - line = schema.GoLow().Type.KeyNode.Line - col = schema.GoLow().Type.KeyNode.Column - } - - // add the error to the list - validationErrors = append(validationErrors, &errors.ValidationError{ - ValidationType: helpers.RequestBodyValidation, - ValidationSubType: helpers.Schema, - Message: fmt.Sprintf("%s request body for '%s' failed to validate schema", - request.Method, request.URL.Path), - Reason: "The request body is defined as an object. " + - "However, it does not meet the schema requirements of the specification", - SpecLine: line, - SpecCol: col, - SchemaValidationErrors: schemaValidationErrors, - HowToFix: errors.HowToFixInvalidSchema, - Context: string(renderedSchema), // attach the rendered schema to the error - }) - } - if len(validationErrors) > 0 { - return false, validationErrors - } - return true, nil + request *http.Request, + schema *base.Schema, + renderedSchema, + jsonSchema []byte) (bool, []*errors.ValidationError) { + + var validationErrors []*errors.ValidationError + + requestBody, _ := io.ReadAll(request.Body) + + // close the request body, so it can be re-read later by another player in the chain + _ = request.Body.Close() + request.Body = io.NopCloser(bytes.NewBuffer(requestBody)) + + var decodedObj interface{} + + if len(requestBody) > 0 { + err := json.Unmarshal(requestBody, &decodedObj) + + if err != nil { + // cannot decode the request body, so it's not valid + violation := &errors.SchemaValidationFailure{ + Reason: err.Error(), + Location: "unavailable", + ReferenceSchema: string(renderedSchema), + ReferenceObject: string(requestBody), + } + validationErrors = append(validationErrors, &errors.ValidationError{ + ValidationType: helpers.RequestBodyValidation, + ValidationSubType: helpers.Schema, + Message: fmt.Sprintf("%s request body for '%s' failed to validate schema", + request.Method, request.URL.Path), + Reason: fmt.Sprintf("The request body cannot be decoded: %s", err.Error()), + SpecLine: 1, + SpecCol: 0, + SchemaValidationErrors: []*errors.SchemaValidationFailure{violation}, + HowToFix: errors.HowToFixInvalidSchema, + Context: string(renderedSchema), // attach the rendered schema to the error + }) + return false, validationErrors + } + } + + // 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") + + // 4. validate the object against the schema + scErrs := jsch.Validate(decodedObj) + if scErrs != nil { + jk := scErrs.(*jsonschema.ValidationError) + + // flatten the validationErrors + schFlatErrs := jk.BasicOutput().Errors + var schemaValidationErrors []*errors.SchemaValidationFailure + for q := range schFlatErrs { + er := schFlatErrs[q] + if er.KeywordLocation == "" || strings.HasPrefix(er.Error, "doesn't validate with") { + continue // ignore this error, it's useless tbh, utter noise. + } + if er.Error != "" { + + // re-encode the schema. + var renderedNode yaml.Node + _ = yaml.Unmarshal(renderedSchema, &renderedNode) + + // locate the violated property in the schema + located := schema_validation.LocateSchemaPropertyNodeByJSONPath(renderedNode.Content[0], er.KeywordLocation) + + // extract the element specified by the instance + val := instanceLocationRegex.FindStringSubmatch(er.InstanceLocation) + var referenceObject string + + if len(val) > 0 { + referenceIndex, _ := strconv.Atoi(val[1]) + if reflect.ValueOf(decodedObj).Type().Kind() == reflect.Slice { + found := decodedObj.([]any)[referenceIndex] + recoded, _ := json.MarshalIndent(found, "", " ") + referenceObject = string(recoded) + } + } + if referenceObject == "" { + referenceObject = string(requestBody) + } + + violation := &errors.SchemaValidationFailure{ + Reason: er.Error, + Location: er.KeywordLocation, + ReferenceSchema: string(renderedSchema), + ReferenceObject: referenceObject, + OriginalError: jk, + } + // if we have a location within the schema, add it to the error + if located != nil { + + line := located.Line + // if the located node is a map or an array, then the actual human interpretable + // line on which the violation occurred is the line of the key, not the value. + if located.Kind == yaml.MappingNode || located.Kind == yaml.SequenceNode { + if line > 0 { + line-- + } + } + + // location of the violation within the rendered schema. + violation.Line = line + violation.Column = located.Column + } + schemaValidationErrors = append(schemaValidationErrors, violation) + } + } + + line := 1 + col := 0 + if schema.GoLow().Type.KeyNode != nil { + line = schema.GoLow().Type.KeyNode.Line + col = schema.GoLow().Type.KeyNode.Column + } + + // add the error to the list + validationErrors = append(validationErrors, &errors.ValidationError{ + ValidationType: helpers.RequestBodyValidation, + ValidationSubType: helpers.Schema, + Message: fmt.Sprintf("%s request body for '%s' failed to validate schema", + request.Method, request.URL.Path), + Reason: "The request body is defined as an object. " + + "However, it does not meet the schema requirements of the specification", + SpecLine: line, + SpecCol: col, + SchemaValidationErrors: schemaValidationErrors, + HowToFix: errors.HowToFixInvalidSchema, + Context: string(renderedSchema), // attach the rendered schema to the error + }) + } + if len(validationErrors) > 0 { + return false, validationErrors + } + return true, nil } diff --git a/responses/validate_body_test.go b/responses/validate_body_test.go index 2414079..aaa6667 100644 --- a/responses/validate_body_test.go +++ b/responses/validate_body_test.go @@ -988,3 +988,57 @@ paths: assert.Len(t, errors, 0) } + +func TestValidateBody_InvalidBodyJSON(t *testing.T) { + spec := `openapi: 3.1.0 +paths: + /burgers/createBurger: + post: + responses: + default: + 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) + + badJson := []byte("{\"bad\": \"json\",}") + + // build a request + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader(badJson)) + request.Header.Set(helpers.ContentTypeHeader, "application/json") + + // 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(badJson) + } + + // 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.Equal(t, "POST response body for '/burgers/createBurger' failed to validate schema", errors[0].Message) + assert.Equal(t, "invalid character '}' looking for beginning of object key string", errors[0].SchemaValidationErrors[0].Reason) + +} diff --git a/responses/validate_response.go b/responses/validate_response.go index 124c12e..8aadaed 100644 --- a/responses/validate_response.go +++ b/responses/validate_response.go @@ -44,7 +44,33 @@ func ValidateResponseSchema( response.Body = io.NopCloser(bytes.NewBuffer(responseBody)) var decodedObj interface{} - _ = json.Unmarshal(responseBody, &decodedObj) + + if len(responseBody) > 0 { + err := json.Unmarshal(responseBody, &decodedObj) + + if err != nil { + // cannot decode the response body, so it's not valid + violation := &errors.SchemaValidationFailure{ + Reason: err.Error(), + Location: "unavailable", + ReferenceSchema: string(renderedSchema), + ReferenceObject: string(responseBody), + } + validationErrors = append(validationErrors, &errors.ValidationError{ + ValidationType: helpers.ResponseBodyValidation, + ValidationSubType: helpers.Schema, + Message: fmt.Sprintf("%s response body for '%s' failed to validate schema", + request.Method, request.URL.Path), + Reason: fmt.Sprintf("The response body cannot be decoded: %s", err.Error()), + SpecLine: 1, + SpecCol: 0, + SchemaValidationErrors: []*errors.SchemaValidationFailure{violation}, + HowToFix: errors.HowToFixInvalidSchema, + Context: string(renderedSchema), // attach the rendered schema to the error + }) + return false, validationErrors + } + } // no response body? failed to decode anything? nothing to do here. if responseBody == nil || decodedObj == nil { diff --git a/schema_validation/validate_schema.go b/schema_validation/validate_schema.go index 038e39e..03a2b6d 100644 --- a/schema_validation/validate_schema.go +++ b/schema_validation/validate_schema.go @@ -5,6 +5,7 @@ package schema_validation import ( "encoding/json" + "fmt" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi/datamodel/high/base" @@ -76,8 +77,31 @@ func validateSchema(schema *base.Schema, payload []byte, decodedObject interface renderedSchema, _ := schema.RenderInline() jsonSchema, _ := utils.ConvertYAMLtoJSON(renderedSchema) - if decodedObject == nil { - _ = json.Unmarshal(payload, &decodedObject) + if decodedObject == nil && len(payload) > 0 { + err := json.Unmarshal(payload, &decodedObject) + + if err != nil { + // cannot decode the request body, so it's not valid + violation := &errors.SchemaValidationFailure{ + Reason: err.Error(), + Location: "unavailable", + ReferenceSchema: string(renderedSchema), + ReferenceObject: string(payload), + } + validationErrors = append(validationErrors, &errors.ValidationError{ + ValidationType: helpers.RequestBodyValidation, + ValidationSubType: helpers.Schema, + Message: "schema does not pass validation", + Reason: fmt.Sprintf("The schema cannot be decoded: %s", err.Error()), + SpecLine: 1, + SpecCol: 0, + SchemaValidationErrors: []*errors.SchemaValidationFailure{violation}, + HowToFix: errors.HowToFixInvalidSchema, + Context: string(renderedSchema), // attach the rendered schema to the error + }) + return false, validationErrors + } + } compiler := jsonschema.NewCompiler() _ = compiler.AddResource("schema.json", strings.NewReader(string(jsonSchema))) @@ -168,7 +192,6 @@ func validateSchema(schema *base.Schema, payload []byte, decodedObject interface col = schema.GoLow().Type.KeyNode.Column } - // add the error to the list validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: helpers.Schema, diff --git a/schema_validation/validate_schema_test.go b/schema_validation/validate_schema_test.go index ec22a2a..59a0d2b 100644 --- a/schema_validation/validate_schema_test.go +++ b/schema_validation/validate_schema_test.go @@ -4,16 +4,16 @@ package schema_validation import ( - "encoding/json" - "github.com/pb33f/libopenapi" - "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" - "testing" + "encoding/json" + "github.com/pb33f/libopenapi" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" + "testing" ) func TestLocateSchemaPropertyNodeByJSONPath(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -30,23 +30,23 @@ paths: vegetarian: type: boolean` - var node yaml.Node - _ = yaml.Unmarshal([]byte(spec), &node) + var node yaml.Node + _ = yaml.Unmarshal([]byte(spec), &node) - foundNode := LocateSchemaPropertyNodeByJSONPath(node.Content[0], - "/paths/~1burgers~1createBurger/post/requestBody/content/application~1json/schema/properties/vegetarian") + foundNode := LocateSchemaPropertyNodeByJSONPath(node.Content[0], + "/paths/~1burgers~1createBurger/post/requestBody/content/application~1json/schema/properties/vegetarian") - assert.Equal(t, "boolean", foundNode.Content[1].Value) + assert.Equal(t, "boolean", foundNode.Content[1].Value) - foundNode = LocateSchemaPropertyNodeByJSONPath(node.Content[0], - "/i/do/not/exist") + foundNode = LocateSchemaPropertyNodeByJSONPath(node.Content[0], + "/i/do/not/exist") - assert.Nil(t, foundNode) + assert.Nil(t, foundNode) } func TestValidateSchema_SimpleValid_String(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -63,32 +63,32 @@ paths: vegetarian: type: boolean` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + m, _ := doc.BuildV3Model() - 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) - sch := m.Model.Paths.PathItems["/burgers/createBurger"].Post.RequestBody.Content["application/json"].Schema + bodyBytes, _ := json.Marshal(body) + sch := m.Model.Paths.PathItems["/burgers/createBurger"].Post.RequestBody.Content["application/json"].Schema - // create a schema validator - v := NewSchemaValidator() + // create a schema validator + v := NewSchemaValidator() - // validate! - valid, errors := v.ValidateSchemaString(sch.Schema(), string(bodyBytes)) + // validate! + valid, errors := v.ValidateSchemaString(sch.Schema(), string(bodyBytes)) - assert.True(t, valid) - assert.Len(t, errors, 0) + assert.True(t, valid) + assert.Len(t, errors, 0) } func TestValidateSchema_SimpleValid(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -105,32 +105,32 @@ paths: vegetarian: type: boolean` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + m, _ := doc.BuildV3Model() - body := map[string]interface{}{ - "name": "Big Mac", - "patties": 2, - "vegetarian": true, - } + body := map[string]interface{}{ + "name": "Big Mac", + "patties": 2, + "vegetarian": true, + } - // create a schema validator - v := NewSchemaValidator() + // create a schema validator + v := NewSchemaValidator() - bodyBytes, _ := json.Marshal(body) - sch := m.Model.Paths.PathItems["/burgers/createBurger"].Post.RequestBody.Content["application/json"].Schema + bodyBytes, _ := json.Marshal(body) + sch := m.Model.Paths.PathItems["/burgers/createBurger"].Post.RequestBody.Content["application/json"].Schema - // validate! - valid, errors := v.ValidateSchemaBytes(sch.Schema(), bodyBytes) + // validate! + valid, errors := v.ValidateSchemaBytes(sch.Schema(), bodyBytes) - assert.True(t, valid) - assert.Len(t, errors, 0) + assert.True(t, valid) + assert.Len(t, errors, 0) } func TestValidateSchema_SimpleInValid(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -147,32 +147,32 @@ paths: vegetarian: type: boolean` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + m, _ := doc.BuildV3Model() - body := map[string]interface{}{ - "name": "Big Mac", - "patties": "I am not a number", // will fail - "vegetarian": 23, // will fail - } + body := map[string]interface{}{ + "name": "Big Mac", + "patties": "I am not a number", // will fail + "vegetarian": 23, // will fail + } - // create a schema validator - v := NewSchemaValidator() + // create a schema validator + v := NewSchemaValidator() - bodyBytes, _ := json.Marshal(body) - sch := m.Model.Paths.PathItems["/burgers/createBurger"].Post.RequestBody.Content["application/json"].Schema + bodyBytes, _ := json.Marshal(body) + sch := m.Model.Paths.PathItems["/burgers/createBurger"].Post.RequestBody.Content["application/json"].Schema - // validate! - valid, errors := v.ValidateSchemaBytes(sch.Schema(), bodyBytes) + // validate! + valid, errors := v.ValidateSchemaBytes(sch.Schema(), bodyBytes) - 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 TestValidateSchema_InvalidJSONType(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -189,29 +189,29 @@ paths: vegetarian: type: boolean` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + m, _ := doc.BuildV3Model() - body := struct{ name string }{"hello world"} + body := struct{ name string }{"hello world"} - // create a schema validator - v := NewSchemaValidator() + // create a schema validator + v := NewSchemaValidator() - //bodyBytes, _ := json.Marshal(body) - sch := m.Model.Paths.PathItems["/burgers/createBurger"].Post.RequestBody.Content["application/json"].Schema + //bodyBytes, _ := json.Marshal(body) + sch := m.Model.Paths.PathItems["/burgers/createBurger"].Post.RequestBody.Content["application/json"].Schema - // validate! - valid, errors := v.ValidateSchemaObject(sch.Schema(), body) + // validate! + valid, errors := v.ValidateSchemaObject(sch.Schema(), body) - assert.False(t, valid) - assert.Len(t, errors, 1) - assert.Len(t, errors[0].SchemaValidationErrors, 1) - assert.Equal(t, "jsonschema: invalid jsonType: struct { name string }", errors[0].SchemaValidationErrors[0].Reason) + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Len(t, errors[0].SchemaValidationErrors, 1) + assert.Equal(t, "jsonschema: invalid jsonType: struct { name string }", errors[0].SchemaValidationErrors[0].Reason) } func TestValidateSchema_ReffyComplex_Valid(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 components: schemas: Death: @@ -263,57 +263,57 @@ paths: schema: $ref: '#/components/schemas/One'` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + m, _ := doc.BuildV3Model() - cakePlease := map[string]interface{}{ - "two": map[string]interface{}{ - "three": map[string]interface{}{ - "four": map[string]interface{}{ - "cakeOrDeath": "cake please", - }, - }, - }, - } + cakePlease := map[string]interface{}{ + "two": map[string]interface{}{ + "three": map[string]interface{}{ + "four": map[string]interface{}{ + "cakeOrDeath": "cake please", + }, + }, + }, + } - death := map[string]interface{}{ - "two": map[string]interface{}{ - "three": map[string]interface{}{ - "four": map[string]interface{}{ - "cakeOrDeath": "death", - }, - }, - }, - } + death := map[string]interface{}{ + "two": map[string]interface{}{ + "three": map[string]interface{}{ + "four": map[string]interface{}{ + "cakeOrDeath": "death", + }, + }, + }, + } - // cake? (https://www.youtube.com/watch?v=PVH0gZO5lq0) - bodyBytes, _ := json.Marshal(cakePlease) - sch := m.Model.Paths.PathItems["/burgers/createBurger"].Post.RequestBody.Content["application/json"].Schema + // cake? (https://www.youtube.com/watch?v=PVH0gZO5lq0) + bodyBytes, _ := json.Marshal(cakePlease) + sch := m.Model.Paths.PathItems["/burgers/createBurger"].Post.RequestBody.Content["application/json"].Schema - // create a schema validator - v := NewSchemaValidator() + // create a schema validator + v := NewSchemaValidator() - // validate! - valid, errors := v.ValidateSchemaBytes(sch.Schema(), bodyBytes) + // validate! + valid, errors := v.ValidateSchemaBytes(sch.Schema(), bodyBytes) - assert.True(t, valid) - assert.Len(t, errors, 0) + assert.True(t, valid) + assert.Len(t, errors, 0) - // or death! - bodyBytes, _ = json.Marshal(death) - sch = m.Model.Paths.PathItems["/burgers/createBurger"].Post.RequestBody.Content["application/json"].Schema + // or death! + bodyBytes, _ = json.Marshal(death) + sch = m.Model.Paths.PathItems["/burgers/createBurger"].Post.RequestBody.Content["application/json"].Schema - // validate! - valid, errors = v.ValidateSchemaBytes(sch.Schema(), bodyBytes) + // validate! + valid, errors = v.ValidateSchemaBytes(sch.Schema(), bodyBytes) - assert.True(t, valid) - assert.Len(t, errors, 0) + assert.True(t, valid) + assert.Len(t, errors, 0) } func TestValidateSchema_ReffyComplex_Invalid(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 components: schemas: Death: @@ -365,83 +365,83 @@ paths: schema: $ref: '#/components/schemas/One'` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + m, _ := doc.BuildV3Model() - cakePlease := map[string]interface{}{ - "two": map[string]interface{}{ - "three": map[string]interface{}{ - "four": map[string]interface{}{ - "cakeOrDeath": "no more cake? so the choice is 'or death?'", - }, - }, - }, - } + cakePlease := map[string]interface{}{ + "two": map[string]interface{}{ + "three": map[string]interface{}{ + "four": map[string]interface{}{ + "cakeOrDeath": "no more cake? so the choice is 'or death?'", + }, + }, + }, + } - death := map[string]interface{}{ - "two": map[string]interface{}{ - "three": map[string]interface{}{ - "four": map[string]interface{}{ - "cakeOrDeath": "i'll have the chicken", - }, - }, - }, - } + death := map[string]interface{}{ + "two": map[string]interface{}{ + "three": map[string]interface{}{ + "four": map[string]interface{}{ + "cakeOrDeath": "i'll have the chicken", + }, + }, + }, + } - // cake? (https://www.youtube.com/watch?v=PVH0gZO5lq0) - bodyBytes, _ := json.Marshal(cakePlease) - sch := m.Model.Paths.PathItems["/burgers/createBurger"].Post.RequestBody.Content["application/json"].Schema + // cake? (https://www.youtube.com/watch?v=PVH0gZO5lq0) + bodyBytes, _ := json.Marshal(cakePlease) + sch := m.Model.Paths.PathItems["/burgers/createBurger"].Post.RequestBody.Content["application/json"].Schema - // create a schema validator - v := NewSchemaValidator() + // create a schema validator + v := NewSchemaValidator() - // validate! - valid, errors := v.ValidateSchemaBytes(sch.Schema(), bodyBytes) + // validate! + valid, errors := v.ValidateSchemaBytes(sch.Schema(), bodyBytes) - assert.False(t, valid) - assert.Len(t, errors, 1) - assert.Len(t, errors[0].SchemaValidationErrors, 3) + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Len(t, errors[0].SchemaValidationErrors, 3) - valid, errors = v.ValidateSchemaObject(sch.Schema(), cakePlease) + valid, errors = v.ValidateSchemaObject(sch.Schema(), cakePlease) - assert.False(t, valid) - assert.Len(t, errors, 1) - assert.Len(t, errors[0].SchemaValidationErrors, 3) + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Len(t, errors[0].SchemaValidationErrors, 3) - // or death! - bodyBytes, _ = json.Marshal(death) - sch = m.Model.Paths.PathItems["/burgers/createBurger"].Post.RequestBody.Content["application/json"].Schema + // or death! + bodyBytes, _ = json.Marshal(death) + sch = m.Model.Paths.PathItems["/burgers/createBurger"].Post.RequestBody.Content["application/json"].Schema - // validate! - valid, errors = v.ValidateSchemaBytes(sch.Schema(), bodyBytes) + // validate! + valid, errors = v.ValidateSchemaBytes(sch.Schema(), bodyBytes) - assert.False(t, valid) - assert.Len(t, errors, 1) - assert.Len(t, errors[0].SchemaValidationErrors, 3) + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Len(t, errors[0].SchemaValidationErrors, 3) - valid, errors = v.ValidateSchemaObject(sch.Schema(), death) + valid, errors = v.ValidateSchemaObject(sch.Schema(), death) - assert.False(t, valid) - assert.Len(t, errors, 1) - assert.Len(t, errors[0].SchemaValidationErrors, 3) + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Len(t, errors[0].SchemaValidationErrors, 3) } func TestValidateSchema_EmptySchema(t *testing.T) { - // create a schema validator - v := NewSchemaValidator() + // create a schema validator + v := NewSchemaValidator() - // validate! - valid, errors := v.ValidateSchemaObject(nil, nil) + // validate! + valid, errors := v.ValidateSchemaObject(nil, nil) - assert.False(t, valid) - assert.Len(t, errors, 0) + assert.False(t, valid) + assert.Len(t, errors, 0) } func TestValidateSchema_SimpleInvalid_Multiple(t *testing.T) { - spec := `openapi: 3.1.0 + spec := `openapi: 3.1.0 paths: /burgers/createBurger: post: @@ -462,37 +462,75 @@ paths: vegetarian: type: boolean` - doc, _ := libopenapi.NewDocument([]byte(spec)) + doc, _ := libopenapi.NewDocument([]byte(spec)) - m, _ := doc.BuildV3Model() + m, _ := doc.BuildV3Model() - var items []map[string]interface{} - items = append(items, map[string]interface{}{ - "patties": 1, - "vegetarian": true, - }) - items = append(items, map[string]interface{}{ - "name": "Quarter Pounder", - "patties": true, - "vegetarian": false, - }) - items = append(items, map[string]interface{}{ - "name": "Big Mac", - "patties": 2, - "vegetarian": false, - }) + var items []map[string]interface{} + items = append(items, map[string]interface{}{ + "patties": 1, + "vegetarian": true, + }) + items = append(items, map[string]interface{}{ + "name": "Quarter Pounder", + "patties": true, + "vegetarian": false, + }) + items = append(items, map[string]interface{}{ + "name": "Big Mac", + "patties": 2, + "vegetarian": false, + }) - bodyBytes, _ := json.Marshal(items) + bodyBytes, _ := json.Marshal(items) - // create a schema validator - v := NewSchemaValidator() + // create a schema validator + v := NewSchemaValidator() - sch := m.Model.Paths.PathItems["/burgers/createBurger"].Post.RequestBody.Content["application/json"].Schema + sch := m.Model.Paths.PathItems["/burgers/createBurger"].Post.RequestBody.Content["application/json"].Schema - // validate! - valid, errors := v.ValidateSchemaBytes(sch.Schema(), bodyBytes) + // validate! + valid, errors := v.ValidateSchemaBytes(sch.Schema(), bodyBytes) + + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Len(t, errors[0].SchemaValidationErrors, 2) +} + +func TestValidateSchema_BadJSON(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() + + bodyBytes := []byte("{\"bad\": \"json\",}") + sch := m.Model.Paths.PathItems["/burgers/createBurger"].Post.RequestBody.Content["application/json"].Schema + + // create a schema validator + v := NewSchemaValidator() + + // validate! + valid, errors := v.ValidateSchemaString(sch.Schema(), string(bodyBytes)) + + assert.False(t, valid) + assert.Len(t, errors, 1) + assert.Equal(t, "schema does not pass validation", errors[0].Message) + assert.Equal(t, "invalid character '}' looking for beginning of object key string", errors[0].SchemaValidationErrors[0].Reason) - assert.False(t, valid) - assert.Len(t, errors, 1) - assert.Len(t, errors[0].SchemaValidationErrors, 2) }