Skip to content
Open
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
25 changes: 25 additions & 0 deletions errors/parameter_errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
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
20 changes: 19 additions & 1 deletion parameters/query_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,27 @@ doneLooking:
}
}

if encodedObj == nil {
Comment thread
SebTardif marked this conversation as resolved.
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",
Expand Down
241 changes: 241 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/stretchr/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 @@ -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=<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