diff --git a/errors/parameter_errors.go b/errors/parameter_errors.go index 48ee25a..aa870c9 100644 --- a/errors/parameter_errors.go +++ b/errors/parameter_errors.go @@ -470,6 +470,31 @@ func IncorrectCookieParamArrayNumber( } } +func QueryParameterCannotBeDecoded(param *v3.Parameter, val string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") + specLine, specCol := paramSchemaTypeLineCol(param) + + return &ValidationError{ + ValidationType: helpers.ParameterValidation, + ValidationSubType: helpers.ParameterValidationQuery, + Message: fmt.Sprintf("Query parameter '%s' cannot be decoded", param.Name), + Reason: fmt.Sprintf("The query parameter '%s' is defined as an object, "+ + "however the value '%s' cannot be decoded as an object", param.Name, val), + SpecLine: specLine, + SpecCol: specCol, + ParameterName: param.Name, + Context: sch, + HowToFix: HowToFixInvalidEncoding, + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Query value '%s' cannot be decoded as object", val), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, + } +} + func IncorrectParamEncodingJSON(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") diff --git a/errors/parameter_errors_test.go b/errors/parameter_errors_test.go index e9a5e51..e31e551 100644 --- a/errors/parameter_errors_test.go +++ b/errors/parameter_errors_test.go @@ -54,6 +54,25 @@ func createMockParameterWithSchema() *v3.Parameter { return v3.NewParameter(param) } +func TestQueryParameterCannotBeDecoded(t *testing.T) { + param := createMockParameterWithSchema() + sch := base.NewSchema(param.GoLow().Schema.Value.Schema()) + val := "not_an_object" + + err := QueryParameterCannotBeDecoded(param, val, sch, "/test-path", "get", "{}") + + require.NotNil(t, err) + require.Equal(t, helpers.ParameterValidation, err.ValidationType) + require.Equal(t, helpers.ParameterValidationQuery, err.ValidationSubType) + require.Equal(t, "testParam", err.ParameterName) + require.Contains(t, err.Message, "Query parameter 'testParam' cannot be decoded") + require.Contains(t, err.Reason, "'not_an_object' cannot be decoded as an object") + require.Equal(t, HowToFixInvalidEncoding, err.HowToFix) + require.NotNil(t, err.Context) + require.Len(t, err.SchemaValidationErrors, 1) + require.Contains(t, err.SchemaValidationErrors[0].Reason, "'not_an_object' cannot be decoded as object") +} + func TestIncorrectFormEncoding(t *testing.T) { param := createMockParameterWithSchema() qp := &helpers.QueryParam{ diff --git a/parameters/query_parameters.go b/parameters/query_parameters.go index 5b24d6d..b775ffb 100644 --- a/parameters/query_parameters.go +++ b/parameters/query_parameters.go @@ -197,9 +197,27 @@ doneLooking: } } + if encodedObj == nil { + validationErrors = append(validationErrors, + errors.QueryParameterCannotBeDecoded(params[p], ef, sch, pathValue, operation, renderedSchema)) + break skipValues + } + objVal, objExists := encodedObj[params[p].Name] + if !objExists || objVal == nil { + validationErrors = append(validationErrors, + errors.QueryParameterCannotBeDecoded(params[p], ef, sch, pathValue, operation, renderedSchema)) + break skipValues + } + objMap, mapOk := objVal.(map[string]interface{}) + if !mapOk { + validationErrors = append(validationErrors, + errors.QueryParameterCannotBeDecoded(params[p], ef, sch, pathValue, operation, renderedSchema)) + break skipValues + } + numErrors := len(validationErrors) validationErrors = append(validationErrors, - ValidateParameterSchema(sch, encodedObj[params[p].Name].(map[string]interface{}), + ValidateParameterSchema(sch, objMap, ef, "Query parameter", "The query parameter", diff --git a/parameters/query_parameters_test.go b/parameters/query_parameters_test.go index 1bc9ca7..6de08a3 100644 --- a/parameters/query_parameters_test.go +++ b/parameters/query_parameters_test.go @@ -15,6 +15,7 @@ import ( "github.com/stretchr/testify/require" "github.com/pb33f/libopenapi-validator/config" + liberrors "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/paths" ) @@ -4140,3 +4141,243 @@ paths: assert.True(t, valid, "issue #91: item_count=10 with type: string should not fail with 'expected string, but got number'") assert.Empty(t, errors) } + +func TestQueryParamObjectMissingKey_NoPanic(t *testing.T) { + // A content-wrapped object parameter with a non-JSON content type cannot + // be decoded into a map. The validator must not panic AND must report a + // validation error (the parameter is present but unsatisfiable). + spec := `openapi: 3.1.0 +paths: + /test: + get: + parameters: + - name: filter + in: query + required: false + content: + text/plain: + schema: + type: object + properties: + color: + type: string + operationId: testFilter +` + + doc, err := libopenapi.NewDocument([]byte(spec)) + require.NoError(t, err) + + m, errs := doc.BuildV3Model() + require.NoError(t, errs) + + v := NewParameterValidator(&m.Model) + + request, _ := http.NewRequest(http.MethodGet, "https://example.com/test?filter=color,blue", nil) + + var valid bool + var valErrors []*liberrors.ValidationError + assert.NotPanics(t, func() { + valid, valErrors = v.ValidateQueryParams(request) + }) + + assert.False(t, valid, "parameter present but not decodable as object should be invalid") + require.Len(t, valErrors, 1) + assert.Equal(t, "Query parameter 'filter' cannot be decoded", valErrors[0].Message) +} + +func TestQueryParamObjectContentJSON_ValidObject(t *testing.T) { + // A content-wrapped JSON parameter with a valid JSON object should validate + // successfully against the schema. + spec := `openapi: 3.1.0 +paths: + /test: + get: + parameters: + - name: filter + in: query + required: false + content: + application/json: + schema: + type: object + properties: + color: + type: string + operationId: testFilter +` + + doc, err := libopenapi.NewDocument([]byte(spec)) + require.NoError(t, err) + + m, errs := doc.BuildV3Model() + require.NoError(t, errs) + + v := NewParameterValidator(&m.Model) + + request, _ := http.NewRequest(http.MethodGet, `https://example.com/test?filter={"color":"blue"}`, nil) + + valid, valErrors := v.ValidateQueryParams(request) + + assert.True(t, valid, "valid JSON object should pass validation") + assert.Empty(t, valErrors) +} + +func TestQueryParamObjectContentJSON_InvalidObject(t *testing.T) { + // A content-wrapped JSON parameter with an invalid JSON object (wrong type + // for a property) should fail schema validation via ValidateParameterSchema, + // exercising the full happy path through guards 1-3 and into the validator. + spec := `openapi: 3.1.0 +paths: + /test: + get: + parameters: + - name: filter + in: query + required: false + content: + application/json: + schema: + type: object + properties: + count: + type: integer + operationId: testFilter +` + + doc, err := libopenapi.NewDocument([]byte(spec)) + require.NoError(t, err) + + m, errs := doc.BuildV3Model() + require.NoError(t, errs) + + v := NewParameterValidator(&m.Model) + + request, _ := http.NewRequest(http.MethodGet, `https://example.com/test?filter={"count":"notAnInt"}`, nil) + + valid, valErrors := v.ValidateQueryParams(request) + + assert.False(t, valid, "wrong property type should fail schema validation") + require.NotEmpty(t, valErrors) +} + +func TestQueryParamObjectFormEncoded_ValidObject(t *testing.T) { + // A form-encoded (default style, not content-wrapped) object parameter with + // valid comma-separated key-value pairs should pass validation. This + // exercises the ConstructParamMapFromFormEncodingArrayWithSchema path through + // all three guards and into ValidateParameterSchema. + spec := `openapi: 3.1.0 +paths: + /test: + get: + parameters: + - name: color + in: query + schema: + type: object + properties: + R: + type: integer + G: + type: integer + B: + type: integer + operationId: testColor +` + + doc, err := libopenapi.NewDocument([]byte(spec)) + require.NoError(t, err) + + m, errs := doc.BuildV3Model() + require.NoError(t, errs) + + v := NewParameterValidator(&m.Model) + + request, _ := http.NewRequest(http.MethodGet, "https://example.com/test?color=R,100,G,200,B,150", nil) + + valid, valErrors := v.ValidateQueryParams(request) + + assert.True(t, valid, "valid form-encoded object should pass validation") + assert.Empty(t, valErrors) +} + +func TestQueryParamObjectFormEncoded_InvalidProperty(t *testing.T) { + // A form-encoded object with an invalid property type should fail + // schema validation, exercising the error path after the guards. + spec := `openapi: 3.1.0 +paths: + /test: + get: + parameters: + - name: color + in: query + schema: + type: object + properties: + R: + type: integer + G: + type: integer + B: + type: integer + operationId: testColor +` + + doc, err := libopenapi.NewDocument([]byte(spec)) + require.NoError(t, err) + + m, errs := doc.BuildV3Model() + require.NoError(t, errs) + + v := NewParameterValidator(&m.Model) + + request, _ := http.NewRequest(http.MethodGet, "https://example.com/test?color=R,notAnInt,G,200,B,150", nil) + + valid, valErrors := v.ValidateQueryParams(request) + + assert.False(t, valid, "invalid property type should fail schema validation") + require.NotEmpty(t, valErrors) +} + +func TestQueryParamObjectContentXML_NoPanic(t *testing.T) { + // A content-wrapped parameter with an unsupported content type (e.g. + // application/xml) hits the encodedObj==nil guard. This is a different + // content type from text/plain in TestQueryParamObjectMissingKey_NoPanic, + // confirming the guard fires for any non-JSON content type. + spec := `openapi: 3.1.0 +paths: + /test: + get: + parameters: + - name: data + in: query + required: false + content: + application/xml: + schema: + type: object + properties: + key: + type: string + operationId: testData +` + + doc, err := libopenapi.NewDocument([]byte(spec)) + require.NoError(t, err) + + m, errs := doc.BuildV3Model() + require.NoError(t, errs) + + v := NewParameterValidator(&m.Model) + + request, _ := http.NewRequest(http.MethodGet, "https://example.com/test?data=val", nil) + + var valid bool + var valErrors []*liberrors.ValidationError + assert.NotPanics(t, func() { + valid, valErrors = v.ValidateQueryParams(request) + }) + + assert.False(t, valid, "unsupported content type should be invalid") + require.Len(t, valErrors, 1) + assert.Equal(t, "Query parameter 'data' cannot be decoded", valErrors[0].Message) +}