Skip to content

Commit

Permalink
openapi3filter: add missing response headers validation (#650)
Browse files Browse the repository at this point in the history
  • Loading branch information
fenollp committed Oct 28, 2022
1 parent 0a4abfc commit 9ea22ae
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 10 deletions.
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

0 comments on commit 9ea22ae

Please sign in to comment.