Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions errors/parameter_errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,32 @@ func IncorrectCookieParamArrayNumber(
}
}

// QueryParameterCannotBeDecoded reports that an object query parameter value could not be decoded into a map.
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")
Expand Down
19 changes: 19 additions & 0 deletions errors/parameter_errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
11 changes: 10 additions & 1 deletion parameters/query_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,18 @@ doneLooking:
}
}

// A nil map read is safe. A missing key or a non-map value
// makes this assertion false.
objMap, ok := encodedObj[params[p].Name].(map[string]interface{})
if !ok {
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",
Expand Down
236 changes: 236 additions & 0 deletions parameters/query_parameters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/pb33f/testify/require"

"github.com/pb33f/libopenapi-validator/config"
liberrors "github.com/pb33f/libopenapi-validator/errors"
"github.com/pb33f/libopenapi-validator/paths"
)

Expand Down Expand Up @@ -4238,3 +4239,238 @@ 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 a property of the wrong type should
// fail schema validation.
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 object parameter with valid comma-separated key-value pairs
// should pass validation.
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) {
// application/xml is not decoded as an object. This is a different content
// type from text/plain in TestQueryParamObjectMissingKey_NoPanic.
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=<key>val</key>", 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)
}
Loading