diff --git a/config/config.go b/config/config.go index f46acce..fa95c65 100644 --- a/config/config.go +++ b/config/config.go @@ -21,16 +21,17 @@ type RegexCache interface { // // Generally fluent With... style functions are used to establish the desired behavior. type ValidationOptions struct { - RegexEngine jsonschema.RegexpEngine - RegexCache RegexCache // Enable compiled regex caching - FormatAssertions bool - ContentAssertions bool - SecurityValidation bool - OpenAPIMode bool // Enable OpenAPI-specific vocabulary validation - AllowScalarCoercion bool // Enable string->boolean/number coercion - Formats map[string]func(v any) error - SchemaCache cache.SchemaCache // Optional cache for compiled schemas - Logger *slog.Logger // Logger for debug/error output (nil = silent) + RegexEngine jsonschema.RegexpEngine + RegexCache RegexCache // Enable compiled regex caching + FormatAssertions bool + ContentAssertions bool + SecurityValidation bool + OpenAPIMode bool // Enable OpenAPI-specific vocabulary validation + AllowScalarCoercion bool // Enable string->boolean/number coercion + Formats map[string]func(v any) error + SchemaCache cache.SchemaCache // Optional cache for compiled schemas + Logger *slog.Logger // Logger for debug/error output (nil = silent) + AllowXMLBodyValidation bool // Allows to convert XML to JSON when validating a request/response body. // strict mode options - detect undeclared properties even when additionalProperties: true StrictMode bool // Enable strict property validation @@ -75,6 +76,7 @@ func WithExistingOpts(options *ValidationOptions) Option { o.Formats = options.Formats o.SchemaCache = options.SchemaCache o.Logger = options.Logger + o.AllowXMLBodyValidation = options.AllowXMLBodyValidation o.StrictMode = options.StrictMode o.StrictIgnorePaths = options.StrictIgnorePaths o.StrictIgnoredHeaders = options.StrictIgnoredHeaders @@ -161,6 +163,14 @@ func WithScalarCoercion() Option { } } +// WithXmlBodyValidation enables converting an XML body to a JSON when validating the schema from a request and response body +// The default option is set to false +func WithXmlBodyValidation() Option { + return func(o *ValidationOptions) { + o.AllowXMLBodyValidation = true + } +} + // WithSchemaCache sets a custom cache implementation or disables caching if nil. // Pass nil to disable schema caching and skip cache warming during validator initialization. // The default cache is a thread-safe sync.Map wrapper. diff --git a/config/config_test.go b/config/config_test.go index dddd739..40ea92d 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -19,8 +19,9 @@ func TestNewValidationOptions_Defaults(t *testing.T) { assert.False(t, opts.FormatAssertions) assert.False(t, opts.ContentAssertions) assert.True(t, opts.SecurityValidation) - assert.True(t, opts.OpenAPIMode) // Default is true - assert.False(t, opts.AllowScalarCoercion) // Default is false + assert.True(t, opts.OpenAPIMode) // Default is true + assert.False(t, opts.AllowScalarCoercion) // Default is false + assert.False(t, opts.AllowXMLBodyValidation) // Default is false assert.Nil(t, opts.RegexEngine) assert.Nil(t, opts.RegexCache) } @@ -32,8 +33,9 @@ func TestNewValidationOptions_WithNilOption(t *testing.T) { assert.False(t, opts.FormatAssertions) assert.False(t, opts.ContentAssertions) assert.True(t, opts.SecurityValidation) - assert.True(t, opts.OpenAPIMode) // Default is true - assert.False(t, opts.AllowScalarCoercion) // Default is false + assert.True(t, opts.OpenAPIMode) // Default is true + assert.False(t, opts.AllowScalarCoercion) // Default is false + assert.False(t, opts.AllowXMLBodyValidation) // Default is false assert.Nil(t, opts.RegexEngine) assert.Nil(t, opts.RegexCache) } @@ -44,8 +46,9 @@ func TestWithFormatAssertions(t *testing.T) { assert.True(t, opts.FormatAssertions) assert.False(t, opts.ContentAssertions) assert.True(t, opts.SecurityValidation) - assert.True(t, opts.OpenAPIMode) // Default is true - assert.False(t, opts.AllowScalarCoercion) // Default is false + assert.True(t, opts.OpenAPIMode) // Default is true + assert.False(t, opts.AllowScalarCoercion) // Default is false + assert.False(t, opts.AllowXMLBodyValidation) // Default is false assert.Nil(t, opts.RegexEngine) assert.Nil(t, opts.RegexCache) } @@ -56,8 +59,9 @@ func TestWithContentAssertions(t *testing.T) { assert.False(t, opts.FormatAssertions) assert.True(t, opts.ContentAssertions) assert.True(t, opts.SecurityValidation) - assert.True(t, opts.OpenAPIMode) // Default is true - assert.False(t, opts.AllowScalarCoercion) // Default is false + assert.True(t, opts.OpenAPIMode) // Default is true + assert.False(t, opts.AllowScalarCoercion) // Default is false + assert.False(t, opts.AllowXMLBodyValidation) // Default is false assert.Nil(t, opts.RegexEngine) assert.Nil(t, opts.RegexCache) } @@ -93,11 +97,12 @@ func TestWithExistingOpts(t *testing.T) { // Create original options with all settings enabled var testEngine jsonschema.RegexpEngine = nil original := &ValidationOptions{ - RegexEngine: testEngine, - RegexCache: &sync.Map{}, - FormatAssertions: true, - ContentAssertions: true, - SecurityValidation: false, + RegexEngine: testEngine, + RegexCache: &sync.Map{}, + FormatAssertions: true, + AllowXMLBodyValidation: true, + ContentAssertions: true, + SecurityValidation: false, } // Create new options using existing options @@ -105,6 +110,7 @@ func TestWithExistingOpts(t *testing.T) { assert.Nil(t, opts.RegexEngine) // Both should be nil assert.NotNil(t, opts.RegexCache) + assert.Equal(t, original.AllowXMLBodyValidation, opts.AllowXMLBodyValidation) assert.Equal(t, original.FormatAssertions, opts.FormatAssertions) assert.Equal(t, original.ContentAssertions, opts.ContentAssertions) assert.Equal(t, original.SecurityValidation, opts.SecurityValidation) @@ -119,8 +125,9 @@ func TestWithExistingOpts_NilSource(t *testing.T) { assert.False(t, opts.FormatAssertions) assert.False(t, opts.ContentAssertions) assert.True(t, opts.SecurityValidation) - assert.True(t, opts.OpenAPIMode) // Default is true - assert.False(t, opts.AllowScalarCoercion) // Default is false + assert.True(t, opts.OpenAPIMode) // Default is true + assert.False(t, opts.AllowScalarCoercion) // Default is false + assert.False(t, opts.AllowXMLBodyValidation) // Default is false assert.Nil(t, opts.RegexEngine) assert.Nil(t, opts.RegexCache) } @@ -129,11 +136,13 @@ func TestMultipleOptions(t *testing.T) { opts := NewValidationOptions( WithFormatAssertions(), WithContentAssertions(), + WithXmlBodyValidation(), ) assert.True(t, opts.FormatAssertions) assert.True(t, opts.ContentAssertions) assert.True(t, opts.SecurityValidation) + assert.True(t, opts.AllowXMLBodyValidation) assert.True(t, opts.OpenAPIMode) // Default is true assert.False(t, opts.AllowScalarCoercion) // Default is false assert.Nil(t, opts.RegexEngine) diff --git a/requests/validate_body.go b/requests/validate_body.go index 6e9c13a..b5f004a 100644 --- a/requests/validate_body.go +++ b/requests/validate_body.go @@ -4,7 +4,10 @@ package requests import ( + "bytes" + "encoding/json" "fmt" + "io" "net/http" "strings" @@ -14,6 +17,7 @@ import ( "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/paths" + "github.com/pb33f/libopenapi-validator/schema_validation" ) func (v *requestBodyValidator) ValidateRequestBody(request *http.Request) (bool, []*errors.ValidationError) { @@ -24,6 +28,22 @@ func (v *requestBodyValidator) ValidateRequestBody(request *http.Request) (bool, return v.ValidateRequestBodyWithPathItem(request, pathItem, foundPath) } +func generateXmlValidationError(err error, referenceObject string) []*errors.ValidationError { + return []*errors.ValidationError{{ + ValidationType: helpers.RequestBodyValidation, + ValidationSubType: helpers.Schema, + Message: "xml example is malformed", + Reason: fmt.Sprintf("failed to parse xml: %s", err.Error()), + SchemaValidationErrors: []*errors.SchemaValidationFailure{{ + Reason: err.Error(), + Location: "xml parsing", + ReferenceSchema: "", + ReferenceObject: referenceObject, + }}, + HowToFix: "ensure xml is well-formed and matches schema structure", + }} +} + func (v *requestBodyValidator) ValidateRequestBodyWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) { if pathItem == nil { return false, []*errors.ValidationError{{ @@ -66,12 +86,6 @@ func (v *requestBodyValidator) ValidateRequestBodyWithPathItem(request *http.Req return false, []*errors.ValidationError{errors.RequestContentTypeNotFound(operation, request, pathValue)} } - // 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) { - return true, nil - } - // Nothing to validate if mediaType.Schema == nil { return true, nil @@ -80,6 +94,32 @@ func (v *requestBodyValidator) ValidateRequestBodyWithPathItem(request *http.Req // extract schema from media type schema := mediaType.Schema.Schema() + if !strings.Contains(strings.ToLower(contentType), helpers.JSONType) { + // we currently only support JSON and XML validation for request bodies + // this will capture *everything* that contains some form of 'json' in the content type + if !v.options.AllowXMLBodyValidation || !schema_validation.IsXMLContentType(contentType) { + return true, nil + } + + if request != nil && request.Body != nil { + requestBody, _ := io.ReadAll(request.Body) + _ = request.Body.Close() + + stringedBody := string(requestBody) + jsonBody, err := schema_validation.TransformXMLToSchemaJSON(stringedBody, schema) + if err != nil { + return false, generateXmlValidationError(err, stringedBody) + } + + transformedBytes, err := json.Marshal(jsonBody) + if err != nil { + return false, generateXmlValidationError(err, stringedBody) + } + + request.Body = io.NopCloser(bytes.NewBuffer(transformedBytes)) + } + } + validationSucceeded, validationErrors := ValidateRequestSchema(&ValidateRequestSchemaInput{ Request: request, Schema: schema, diff --git a/requests/validate_body_test.go b/requests/validate_body_test.go index bc96085..22088db 100644 --- a/requests/validate_body_test.go +++ b/requests/validate_body_test.go @@ -16,6 +16,7 @@ import ( "github.com/stretchr/testify/require" "github.com/pb33f/libopenapi-validator/config" + "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/paths" ) @@ -1576,3 +1577,121 @@ paths: assert.True(t, valid) assert.Len(t, errors, 0) } + +func TestValidateBody_XmlRequest(t *testing.T) { + spec := `openapi: 3.1.0 +paths: + /burgers/createBurger: + post: + requestBody: + content: + application/xml: + schema: + type: object + required: + - name + properties: + name: + type: string + patties: + type: integer + xml: + name: cost` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + + m, _ := doc.BuildV3Model() + v := NewRequestBodyValidator(&m.Model, config.WithXmlBodyValidation()) + + body := "cheeseburger23" + + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", + bytes.NewBuffer([]byte(body))) + request.Header.Set("Content-Type", "application/xml") + + valid, errors := v.ValidateRequestBody(request) + + assert.True(t, valid) + assert.Len(t, errors, 0) +} + +func TestValidateBody_XmlMalformedRequest(t *testing.T) { + spec := `openapi: 3.1.0 +paths: + /burgers/createBurger: + post: + requestBody: + content: + application/xml: + schema: + type: object + required: + - name + properties: + name: + type: string + patties: + type: integer + xml: + name: cost` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + + m, _ := doc.BuildV3Model() + v := NewRequestBodyValidator(&m.Model, config.WithXmlBodyValidation()) + + body := "" + + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", + bytes.NewBuffer([]byte(body))) + request.Header.Set("Content-Type", "application/xml") + + valid, errors := v.ValidateRequestBody(request) + + assert.False(t, valid) + assert.Len(t, errors, 1) + + err := errors[0] + assert.Equal(t, helpers.RequestBodyValidation, err.ValidationType) + assert.Contains(t, err.Reason, "failed to parse xml") +} + +func TestValidateBody_XmlRequestTransformations(t *testing.T) { + spec := `openapi: 3.1.0 +paths: + /burgers/createBurger: + post: + requestBody: + content: + application/xml: + schema: + type: object + xml: + name: Burger + required: + - name + - patties + properties: + name: + type: string + patties: + type: integer + xml: + name: cost` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + + m, _ := doc.BuildV3Model() + v := NewRequestBodyValidator(&m.Model, config.WithXmlBodyValidation()) + + body := "cheeseburger23" + + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", + bytes.NewBuffer([]byte(body))) + request.Header.Set("Content-Type", "application/xml") + + valid, errors := v.ValidateRequestBody(request) + + assert.True(t, valid) + assert.Len(t, errors, 0) +} diff --git a/responses/validate_body.go b/responses/validate_body.go index 4d532d8..edeb649 100644 --- a/responses/validate_body.go +++ b/responses/validate_body.go @@ -4,7 +4,10 @@ package responses import ( + "bytes" + "encoding/json" "fmt" + "io" "net/http" "strconv" "strings" @@ -17,6 +20,7 @@ import ( "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" "github.com/pb33f/libopenapi-validator/paths" + "github.com/pb33f/libopenapi-validator/schema_validation" ) func (v *responseBodyValidator) ValidateResponseBody( @@ -123,6 +127,22 @@ func (v *responseBodyValidator) ValidateResponseBodyWithPathItem(request *http.R return true, nil } +func generateXmlValidationError(err error, referenceObject string) []*errors.ValidationError { + return []*errors.ValidationError{{ + ValidationType: helpers.RequestBodyValidation, + ValidationSubType: helpers.Schema, + Message: "xml response is malformed", + Reason: fmt.Sprintf("failed to parse xml: %s", err.Error()), + SchemaValidationErrors: []*errors.SchemaValidationFailure{{ + Reason: err.Error(), + Location: "xml parsing", + ReferenceSchema: "", + ReferenceObject: referenceObject, + }}, + HowToFix: "ensure xml is well-formed and matches schema structure", + }} +} + func (v *responseBodyValidator) checkResponseSchema( request *http.Request, response *http.Response, @@ -131,26 +151,54 @@ func (v *responseBodyValidator) checkResponseSchema( ) []*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 { - schema := mediaType.Schema.Schema() - - // Validate response schema - valid, vErrs := ValidateResponseSchema(&ValidateResponseSchemaInput{ - Request: request, - Response: response, - Schema: schema, - Version: helpers.VersionToFloat(v.document.Version), - Options: []config.Option{config.WithExistingOpts(v.options)}, - }) - if !valid { - validationErrors = append(validationErrors, vErrs...) + if mediaType.Schema == nil { + return validationErrors + } + + // currently, we can only validate JSON and XML based responses, so check for the presence + // of 'json' (what ever it may be) and for XML content type so we can perform a schema check on it. + // anything other than JSON or XML, will be ignored. + + isXml := schema_validation.IsXMLContentType(contentType) + + if !strings.Contains(strings.ToLower(contentType), helpers.JSONType) && (!v.options.AllowXMLBodyValidation || !isXml) { + return validationErrors + } + + schema := mediaType.Schema.Schema() + + if isXml { + if response != nil && response.Body != http.NoBody { + responseBody, _ := io.ReadAll(response.Body) + _ = response.Body.Close() + + stringedBody := string(responseBody) + jsonBody, err := schema_validation.TransformXMLToSchemaJSON(stringedBody, schema) + if err != nil { + return generateXmlValidationError(err, stringedBody) + } + + transformedBytes, err := json.Marshal(jsonBody) + if err != nil { + return generateXmlValidationError(err, stringedBody) } + + response.Body = io.NopCloser(bytes.NewBuffer(transformedBytes)) } } + + // Validate response schema + valid, vErrs := ValidateResponseSchema(&ValidateResponseSchemaInput{ + Request: request, + Response: response, + Schema: schema, + Version: helpers.VersionToFloat(v.document.Version), + Options: []config.Option{config.WithExistingOpts(v.options)}, + }) + + if !valid { + validationErrors = append(validationErrors, vErrs...) + } + return validationErrors } diff --git a/responses/validate_body_test.go b/responses/validate_body_test.go index a40bdd1..7815638 100644 --- a/responses/validate_body_test.go +++ b/responses/validate_body_test.go @@ -1593,6 +1593,208 @@ paths: assert.Len(t, errs, 0) } +func TestValidateBody_ValidXmlDecode(t *testing.T) { + spec := `openapi: 3.1.0 +paths: + /burgers/createBurger: + post: + responses: + default: + content: + application/xml: + schema: + type: object + properties: + name: + type: string + patties: + type: integer` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + + m, _ := doc.BuildV3Model() + v := NewResponseBodyValidator(&m.Model, config.WithXmlBodyValidation()) + + body := "test2" + + // build a request + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader([]byte(body))) + 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, "application/xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(body)) + } + + // fire the request + handler(res, request) + + // record response + response := res.Result() + + // validate! + valid, errors := v.ValidateResponseBody(request, response) + + assert.True(t, valid) + assert.Len(t, errors, 0) +} + +func TestValidateBody_ValidXmlFailedValidation(t *testing.T) { + spec := `openapi: 3.1.0 +paths: + /burgers/createBurger: + post: + responses: + default: + content: + application/xml: + schema: + type: object + properties: + name: + type: string + patties: + type: integer` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + + m, _ := doc.BuildV3Model() + v := NewResponseBodyValidator(&m.Model, config.WithXmlBodyValidation()) + + body := "20text" + + // build a request + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader([]byte(body))) + 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, "application/xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(body)) + } + + // 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, 2) +} + +func TestValidateBody_IgnoreXmlValidation(t *testing.T) { + spec := `openapi: 3.1.0 +paths: + /burgers/createBurger: + post: + responses: + default: + content: + application/xml: + schema: + type: object + properties: + name: + type: string + patties: + type: integer + vegetarian: + type: boolean` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + + m, _ := doc.BuildV3Model() + v := NewResponseBodyValidator(&m.Model) + + body := "invalidbodycausenoxml" + + // build a request + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader([]byte(body))) + 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, "application/xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(body)) + } + + // fire the request + handler(res, request) + + // record response + response := res.Result() + + // validate! + valid, errors := v.ValidateResponseBody(request, response) + + assert.True(t, valid) + assert.Len(t, errors, 0) +} + +func TestValidateBody_InvalidXmlParse(t *testing.T) { + spec := `openapi: 3.1.0 +paths: + /burgers/createBurger: + post: + responses: + default: + content: + application/xml: + 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, config.WithXmlBodyValidation()) + + body := "" + + // build a request + request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader([]byte(body))) + 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, "application/xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(body)) + } + + // 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, errors[0].Message, "xml response is malformed") +} + type errorReader struct{} func (er *errorReader) Read(p []byte) (n int, err error) { diff --git a/schema_validation/validate_xml.go b/schema_validation/validate_xml.go index ad30bd8..518bcd4 100644 --- a/schema_validation/validate_xml.go +++ b/schema_validation/validate_xml.go @@ -26,7 +26,7 @@ func (x *xmlValidator) validateXMLWithVersion(schema *base.Schema, xmlString str } // parse xml and transform to json structure matching schema - transformedJSON, err := transformXMLToSchemaJSON(xmlString, schema) + transformedJSON, err := TransformXMLToSchemaJSON(xmlString, schema) if err != nil { violation := &liberrors.SchemaValidationFailure{ Reason: err.Error(), @@ -49,9 +49,9 @@ func (x *xmlValidator) validateXMLWithVersion(schema *base.Schema, xmlString str return x.schemaValidator.validateSchemaWithVersion(schema, nil, transformedJSON, log, version) } -// transformXMLToSchemaJSON converts xml to json structure matching openapi schema. +// TransformXMLToSchemaJSON converts xml to json structure matching openapi schema. // applies xml object transformations: name, attribute, wrapped. -func transformXMLToSchemaJSON(xmlString string, schema *base.Schema) (interface{}, error) { +func TransformXMLToSchemaJSON(xmlString string, schema *base.Schema) (interface{}, error) { if xmlString == "" { return nil, fmt.Errorf("empty xml content") } diff --git a/schema_validation/validate_xml_test.go b/schema_validation/validate_xml_test.go index 1d3faad..692e32b 100644 --- a/schema_validation/validate_xml_test.go +++ b/schema_validation/validate_xml_test.go @@ -960,7 +960,7 @@ func TestValidateXML_UnwrapArrayElementMissingItem(t *testing.T) { func TestTransformXMLToSchemaJSON_EmptyString(t *testing.T) { // test empty string error path (line 68) schema := &base.Schema{} - _, err := transformXMLToSchemaJSON("", schema) + _, err := TransformXMLToSchemaJSON("", schema) assert.Error(t, err) assert.Contains(t, err.Error(), "empty xml") }