Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

openapi3filter: add missing response headers validation #650

Merged
merged 2 commits into from Oct 28, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Expand Up @@ -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
)
9 changes: 7 additions & 2 deletions go.sum
Expand Up @@ -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=
138 changes: 138 additions & 0 deletions 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)
}
})
}
}
42 changes: 35 additions & 7 deletions openapi3filter/validate_response.go
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
"sort"

"github.com/getkin/kin-openapi/openapi3"
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down