diff --git a/go.mod b/go.mod index 8d93cf754..942b3195c 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/gorilla/mux v1.8.0 github.com/invopop/yaml v0.1.0 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 - github.com/stretchr/testify v1.5.1 + github.com/stretchr/testify v1.8.1 gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 074de2aa8..4982bc738 100644 --- a/go.sum +++ b/go.sum @@ -22,15 +22,20 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwd github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/openapi3filter/issue201_test.go b/openapi3filter/issue201_test.go new file mode 100644 index 000000000..8db8620ae --- /dev/null +++ b/openapi3filter/issue201_test.go @@ -0,0 +1,138 @@ +package openapi3filter + +import ( + "context" + "io" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +func TestIssue201(t *testing.T) { + loader := openapi3.NewLoader() + ctx := loader.Context + spec := ` +openapi: '3' +info: + version: 1.0.0 + title: Sample API +paths: + /_: + get: + description: '' + responses: + default: + description: '' + content: + application/json: + schema: + type: object + headers: + X-Blip: + description: '' + required: true + schema: + pattern: '^blip$' + x-blop: + description: '' + schema: + pattern: '^blop$' + X-Blap: + description: '' + required: true + schema: + pattern: '^blap$' + X-Blup: + description: '' + required: true + schema: + pattern: '^blup$' +`[1:] + + doc, err := loader.LoadFromData([]byte(spec)) + require.NoError(t, err) + + err = doc.Validate(ctx) + require.NoError(t, err) + + for name, testcase := range map[string]struct { + headers map[string]string + err string + }{ + + "no error": { + headers: map[string]string{ + "X-Blip": "blip", + "x-blop": "blop", + "X-Blap": "blap", + "X-Blup": "blup", + }, + }, + + "missing non-required header": { + headers: map[string]string{ + "X-Blip": "blip", + // "x-blop": "blop", + "X-Blap": "blap", + "X-Blup": "blup", + }, + }, + + "missing required header": { + err: `response header "X-Blip" missing`, + headers: map[string]string{ + // "X-Blip": "blip", + "x-blop": "blop", + "X-Blap": "blap", + "X-Blup": "blup", + }, + }, + + "invalid required header": { + err: `response header "X-Blup" doesn't match the schema: string doesn't match the regular expression "^blup$"`, + headers: map[string]string{ + "X-Blip": "blip", + "x-blop": "blop", + "X-Blap": "blap", + "X-Blup": "bluuuuuup", + }, + }, + } { + t.Run(name, func(t *testing.T) { + router, err := gorillamux.NewRouter(doc) + require.NoError(t, err) + + r, err := http.NewRequest(http.MethodGet, `/_`, nil) + require.NoError(t, err) + + r.Header.Add(headerCT, "application/json") + for k, v := range testcase.headers { + r.Header.Add(k, v) + } + + route, pathParams, err := router.FindRoute(r) + require.NoError(t, err) + + err = ValidateResponse(context.Background(), &ResponseValidationInput{ + RequestValidationInput: &RequestValidationInput{ + Request: r, + PathParams: pathParams, + Route: route, + }, + Status: 200, + Header: r.Header, + Body: io.NopCloser(strings.NewReader(`{}`)), + }) + if e := testcase.err; e != "" { + require.ErrorContains(t, err, e) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go index ffb7a1f5a..e90b5d60e 100644 --- a/openapi3filter/validate_response.go +++ b/openapi3filter/validate_response.go @@ -7,6 +7,7 @@ import ( "fmt" "io/ioutil" "net/http" + "sort" "github.com/getkin/kin-openapi/openapi3" ) @@ -61,6 +62,39 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error return &ResponseError{Input: input, Reason: "response has not been resolved"} } + opts := make([]openapi3.SchemaValidationOption, 0, 2) + if options.MultiError { + opts = append(opts, openapi3.MultiErrors()) + } + + headers := make([]string, 0, len(response.Headers)) + for k := range response.Headers { + if k != headerCT { + headers = append(headers, k) + } + } + 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, + } + } + } + if options.ExcludeResponseBody { // A user turned off validation of a response's body. return nil @@ -120,14 +154,8 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error } } - opts := make([]openapi3.SchemaValidationOption, 0, 2) // 2 potential opts here - opts = append(opts, openapi3.VisitAsResponse()) - if options.MultiError { - opts = append(opts, openapi3.MultiErrors()) - } - // Validate data with the schema. - if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil { + if err := contentType.Schema.Value.VisitJSON(value, append(opts, openapi3.VisitAsResponse())...); err != nil { return &ResponseError{ Input: input, Reason: "response body doesn't match the schema",