Skip to content

Commit

Permalink
openapi3filter: validate non-string headers (#712)
Browse files Browse the repository at this point in the history
Co-authored-by: Steve Lessard <steve.lessard@teradata.com>
  • Loading branch information
slessard and sl255051 authored Dec 17, 2022
1 parent 25a5fe4 commit 3be535f
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 22 deletions.
4 changes: 4 additions & 0 deletions openapi3/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,10 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
return
}

// The value is not considered in visitJSONNull because according to the spec
// "null is not supported as a type" unless `nullable` is also set to true
// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#data-types
// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schema-object
func (schema *Schema) visitJSONNull(settings *schemaValidationSettings) (err error) {
if schema.Nullable {
return
Expand Down
8 changes: 6 additions & 2 deletions openapi3filter/issue201_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func TestIssue201(t *testing.T) {
loader := openapi3.NewLoader()
ctx := loader.Context
spec := `
openapi: '3'
openapi: '3.0.3'
info:
version: 1.0.0
title: Sample API
Expand All @@ -37,20 +37,24 @@ paths:
description: ''
required: true
schema:
type: string
pattern: '^blip$'
x-blop:
description: ''
schema:
type: string
pattern: '^blop$'
X-Blap:
description: ''
required: true
schema:
type: string
pattern: '^blap$'
X-Blup:
description: ''
required: true
schema:
type: string
pattern: '^blup$'
`[1:]

Expand Down Expand Up @@ -94,7 +98,7 @@ paths:
},

"invalid required header": {
err: `response header "X-Blup" doesn't match the schema: string "bluuuuuup" doesn't match the regular expression "^blup$"`,
err: `response header "X-Blup" doesn't match schema: string "bluuuuuup" doesn't match the regular expression "^blup$"`,
headers: map[string]string{
"X-Blip": "blip",
"x-blop": "blop",
Expand Down
4 changes: 2 additions & 2 deletions openapi3filter/req_resp_decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMetho
}
_, found = vDecoder.values[param]
case *headerParamDecoder:
_, found = vDecoder.header[param]
_, found = vDecoder.header[http.CanonicalHeaderKey(param)]
case *cookieParamDecoder:
_, err := vDecoder.req.Cookie(param)
found = err != http.ErrNoCookie
Expand Down Expand Up @@ -888,7 +888,7 @@ func parseArray(raw []string, schemaRef *openapi3.SchemaRef) ([]interface{}, err

// parsePrimitive returns a value that is created by parsing a source string to a primitive type
// that is specified by a schema. The function returns nil when the source string is empty.
// The function panics when a schema has a non primitive type.
// The function panics when a schema has a non-primitive type.
func parsePrimitive(raw string, schema *openapi3.SchemaRef) (interface{}, error) {
if raw == "" {
return nil, nil
Expand Down
62 changes: 44 additions & 18 deletions openapi3filter/validate_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,24 +78,10 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error
}
}
sort.Strings(headers)
for _, k := range headers {
s := response.Headers[k]
h := input.Header.Get(k)
if h == "" {
if s.Value.Required {
return &ResponseError{
Input: input,
Reason: fmt.Sprintf("response header %q missing", k),
}
}
continue
}
if err := s.Value.Schema.Value.VisitJSON(h, opts...); err != nil {
return &ResponseError{
Input: input,
Reason: fmt.Sprintf("response header %q doesn't match the schema", k),
Err: err,
}
for _, headerName := range headers {
headerRef := response.Headers[headerName]
if err := validateResponseHeader(headerName, headerRef, input, opts); err != nil {
return err
}
}

Expand Down Expand Up @@ -171,6 +157,46 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error
return nil
}

func validateResponseHeader(headerName string, headerRef *openapi3.HeaderRef, input *ResponseValidationInput, opts []openapi3.SchemaValidationOption) error {
var err error
var decodedValue interface{}
var found bool
var sm *openapi3.SerializationMethod
dec := &headerParamDecoder{header: input.Header}

if sm, err = headerRef.Value.SerializationMethod(); err != nil {
return &ResponseError{
Input: input,
Reason: fmt.Sprintf("unable to get header %q serialization method", headerName),
Err: err,
}
}

if decodedValue, found, err = decodeValue(dec, headerName, sm, headerRef.Value.Schema, headerRef.Value.Required); err != nil {
return &ResponseError{
Input: input,
Reason: fmt.Sprintf("unable to decode header %q value", headerName),
Err: err,
}
}

if found {
if err = headerRef.Value.Schema.Value.VisitJSON(decodedValue, opts...); err != nil {
return &ResponseError{
Input: input,
Reason: fmt.Sprintf("response header %q doesn't match schema", headerName),
Err: err,
}
}
} else if headerRef.Value.Required {
return &ResponseError{
Input: input,
Reason: fmt.Sprintf("response header %q missing", headerName),
}
}
return nil
}

// getSchemaIdentifier gets something by which a schema could be identified.
// A schema by itself doesn't have a true identity field. This function makes
// a best effort to get a value that can fill that void.
Expand Down
215 changes: 215 additions & 0 deletions openapi3filter/validate_response_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package openapi3filter

import (
"io"
"net/http"
"strings"
"testing"

"github.com/stretchr/testify/require"

"github.com/getkin/kin-openapi/openapi3"
)

func Test_validateResponseHeader(t *testing.T) {
type args struct {
headerName string
headerRef *openapi3.HeaderRef
}
tests := []struct {
name string
args args
isHeaderPresent bool
headerVals []string
wantErr bool
wantErrMsg string
}{
{
name: "test required string header with single string value",
args: args{
headerName: "X-Blab",
headerRef: newHeaderRef(openapi3.NewStringSchema(), true),
},
isHeaderPresent: true,
headerVals: []string{"blab"},
wantErr: false,
},
{
name: "test required string header with single, empty string value",
args: args{
headerName: "X-Blab",
headerRef: newHeaderRef(openapi3.NewStringSchema(), true),
},
isHeaderPresent: true,
headerVals: []string{""},
wantErr: true,
wantErrMsg: `response header "X-Blab" doesn't match schema: Value is not nullable`,
},
{
name: "test optional string header with single string value",
args: args{
headerName: "X-Blab",
headerRef: newHeaderRef(openapi3.NewStringSchema(), false),
},
isHeaderPresent: false,
headerVals: []string{"blab"},
wantErr: false,
},
{
name: "test required, but missing string header",
args: args{
headerName: "X-Blab",
headerRef: newHeaderRef(openapi3.NewStringSchema(), true),
},
isHeaderPresent: false,
headerVals: nil,
wantErr: true,
wantErrMsg: `response header "X-Blab" missing`,
},
{
name: "test integer header with single integer value",
args: args{
headerName: "X-Blab",
headerRef: newHeaderRef(openapi3.NewIntegerSchema(), true),
},
isHeaderPresent: true,
headerVals: []string{"88"},
wantErr: false,
},
{
name: "test integer header with single string value",
args: args{
headerName: "X-Blab",
headerRef: newHeaderRef(openapi3.NewIntegerSchema(), true),
},
isHeaderPresent: true,
headerVals: []string{"blab"},
wantErr: true,
wantErrMsg: `unable to decode header "X-Blab" value: value blab: an invalid integer: invalid syntax`,
},
{
name: "test int64 header with single int64 value",
args: args{
headerName: "X-Blab",
headerRef: newHeaderRef(openapi3.NewInt64Schema(), true),
},
isHeaderPresent: true,
headerVals: []string{"88"},
wantErr: false,
},
{
name: "test int32 header with single int32 value",
args: args{
headerName: "X-Blab",
headerRef: newHeaderRef(openapi3.NewInt32Schema(), true),
},
isHeaderPresent: true,
headerVals: []string{"88"},
wantErr: false,
},
{
name: "test float64 header with single float64 value",
args: args{
headerName: "X-Blab",
headerRef: newHeaderRef(openapi3.NewFloat64Schema(), true),
},
isHeaderPresent: true,
headerVals: []string{"88.87"},
wantErr: false,
},
{
name: "test integer header with multiple csv integer values",
args: args{
headerName: "X-blab",
headerRef: newHeaderRef(newArraySchema(openapi3.NewIntegerSchema()), true),
},
isHeaderPresent: true,
headerVals: []string{"87,88"},
wantErr: false,
},
{
name: "test integer header with multiple integer values",
args: args{
headerName: "X-blab",
headerRef: newHeaderRef(newArraySchema(openapi3.NewIntegerSchema()), true),
},
isHeaderPresent: true,
headerVals: []string{"87", "88"},
wantErr: false,
},
{
name: "test non-typed, nullable header with single string value",
args: args{
headerName: "X-blab",
headerRef: newHeaderRef(&openapi3.Schema{Nullable: true}, true),
},
isHeaderPresent: true,
headerVals: []string{"blab"},
wantErr: false,
},
{
name: "test required non-typed, nullable header not present",
args: args{
headerName: "X-blab",
headerRef: newHeaderRef(&openapi3.Schema{Nullable: true}, true),
},
isHeaderPresent: false,
headerVals: []string{"blab"},
wantErr: true,
wantErrMsg: `response header "X-blab" missing`,
},
{
name: "test non-typed, non-nullable header with single string value",
args: args{
headerName: "X-blab",
headerRef: newHeaderRef(&openapi3.Schema{Nullable: false}, true),
},
isHeaderPresent: true,
headerVals: []string{"blab"},
wantErr: true,
wantErrMsg: `response header "X-blab" doesn't match schema: Value is not nullable`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
input := newInputDefault()
opts := []openapi3.SchemaValidationOption(nil)
if tt.isHeaderPresent {
input.Header = map[string][]string{http.CanonicalHeaderKey(tt.args.headerName): tt.headerVals}
}

err := validateResponseHeader(tt.args.headerName, tt.args.headerRef, input, opts)
if tt.wantErr {
require.NotEmpty(t, tt.wantErrMsg, "wanted error message is not populated")
require.Error(t, err)
require.Contains(t, err.Error(), tt.wantErrMsg)
} else {
require.NoError(t, err)
}
})
}
}

func newInputDefault() *ResponseValidationInput {
return &ResponseValidationInput{
RequestValidationInput: &RequestValidationInput{
Request: nil,
PathParams: nil,
Route: nil,
},
Status: 200,
Header: nil,
Body: io.NopCloser(strings.NewReader(`{}`)),
}
}

func newHeaderRef(schema *openapi3.Schema, required bool) *openapi3.HeaderRef {
return &openapi3.HeaderRef{Value: &openapi3.Header{Parameter: openapi3.Parameter{Schema: &openapi3.SchemaRef{Value: schema}, Required: required}}}
}

func newArraySchema(schema *openapi3.Schema) *openapi3.Schema {
arraySchema := openapi3.NewArraySchema()
arraySchema.Items = openapi3.NewSchemaRef("", schema)

return arraySchema
}

0 comments on commit 3be535f

Please sign in to comment.