Skip to content

Commit 9ea22ae

Browse files
authored
openapi3filter: add missing response headers validation (#650)
1 parent 0a4abfc commit 9ea22ae

File tree

4 files changed

+181
-10
lines changed

4 files changed

+181
-10
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ require (
77
github.com/gorilla/mux v1.8.0
88
github.com/invopop/yaml v0.1.0
99
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
10-
github.com/stretchr/testify v1.5.1
10+
github.com/stretchr/testify v1.8.1
1111
gopkg.in/yaml.v2 v2.4.0 // indirect
1212
gopkg.in/yaml.v3 v3.0.1
1313
)

go.sum

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,20 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwd
2222
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
2323
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
2424
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
25+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
26+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
2527
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
26-
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
27-
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
28+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
29+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
30+
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
31+
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
2832
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
2933
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
3034
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
3135
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
3236
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
3337
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
38+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
3439
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
3540
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
3641
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

openapi3filter/issue201_test.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package openapi3filter
2+
3+
import (
4+
"context"
5+
"io"
6+
"net/http"
7+
"strings"
8+
"testing"
9+
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/getkin/kin-openapi/openapi3"
13+
"github.com/getkin/kin-openapi/routers/gorillamux"
14+
)
15+
16+
func TestIssue201(t *testing.T) {
17+
loader := openapi3.NewLoader()
18+
ctx := loader.Context
19+
spec := `
20+
openapi: '3'
21+
info:
22+
version: 1.0.0
23+
title: Sample API
24+
paths:
25+
/_:
26+
get:
27+
description: ''
28+
responses:
29+
default:
30+
description: ''
31+
content:
32+
application/json:
33+
schema:
34+
type: object
35+
headers:
36+
X-Blip:
37+
description: ''
38+
required: true
39+
schema:
40+
pattern: '^blip$'
41+
x-blop:
42+
description: ''
43+
schema:
44+
pattern: '^blop$'
45+
X-Blap:
46+
description: ''
47+
required: true
48+
schema:
49+
pattern: '^blap$'
50+
X-Blup:
51+
description: ''
52+
required: true
53+
schema:
54+
pattern: '^blup$'
55+
`[1:]
56+
57+
doc, err := loader.LoadFromData([]byte(spec))
58+
require.NoError(t, err)
59+
60+
err = doc.Validate(ctx)
61+
require.NoError(t, err)
62+
63+
for name, testcase := range map[string]struct {
64+
headers map[string]string
65+
err string
66+
}{
67+
68+
"no error": {
69+
headers: map[string]string{
70+
"X-Blip": "blip",
71+
"x-blop": "blop",
72+
"X-Blap": "blap",
73+
"X-Blup": "blup",
74+
},
75+
},
76+
77+
"missing non-required header": {
78+
headers: map[string]string{
79+
"X-Blip": "blip",
80+
// "x-blop": "blop",
81+
"X-Blap": "blap",
82+
"X-Blup": "blup",
83+
},
84+
},
85+
86+
"missing required header": {
87+
err: `response header "X-Blip" missing`,
88+
headers: map[string]string{
89+
// "X-Blip": "blip",
90+
"x-blop": "blop",
91+
"X-Blap": "blap",
92+
"X-Blup": "blup",
93+
},
94+
},
95+
96+
"invalid required header": {
97+
err: `response header "X-Blup" doesn't match the schema: string doesn't match the regular expression "^blup$"`,
98+
headers: map[string]string{
99+
"X-Blip": "blip",
100+
"x-blop": "blop",
101+
"X-Blap": "blap",
102+
"X-Blup": "bluuuuuup",
103+
},
104+
},
105+
} {
106+
t.Run(name, func(t *testing.T) {
107+
router, err := gorillamux.NewRouter(doc)
108+
require.NoError(t, err)
109+
110+
r, err := http.NewRequest(http.MethodGet, `/_`, nil)
111+
require.NoError(t, err)
112+
113+
r.Header.Add(headerCT, "application/json")
114+
for k, v := range testcase.headers {
115+
r.Header.Add(k, v)
116+
}
117+
118+
route, pathParams, err := router.FindRoute(r)
119+
require.NoError(t, err)
120+
121+
err = ValidateResponse(context.Background(), &ResponseValidationInput{
122+
RequestValidationInput: &RequestValidationInput{
123+
Request: r,
124+
PathParams: pathParams,
125+
Route: route,
126+
},
127+
Status: 200,
128+
Header: r.Header,
129+
Body: io.NopCloser(strings.NewReader(`{}`)),
130+
})
131+
if e := testcase.err; e != "" {
132+
require.ErrorContains(t, err, e)
133+
} else {
134+
require.NoError(t, err)
135+
}
136+
})
137+
}
138+
}

openapi3filter/validate_response.go

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"io/ioutil"
99
"net/http"
10+
"sort"
1011

1112
"github.com/getkin/kin-openapi/openapi3"
1213
)
@@ -61,6 +62,39 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error
6162
return &ResponseError{Input: input, Reason: "response has not been resolved"}
6263
}
6364

65+
opts := make([]openapi3.SchemaValidationOption, 0, 2)
66+
if options.MultiError {
67+
opts = append(opts, openapi3.MultiErrors())
68+
}
69+
70+
headers := make([]string, 0, len(response.Headers))
71+
for k := range response.Headers {
72+
if k != headerCT {
73+
headers = append(headers, k)
74+
}
75+
}
76+
sort.Strings(headers)
77+
for _, k := range headers {
78+
s := response.Headers[k]
79+
h := input.Header.Get(k)
80+
if h == "" {
81+
if s.Value.Required {
82+
return &ResponseError{
83+
Input: input,
84+
Reason: fmt.Sprintf("response header %q missing", k),
85+
}
86+
}
87+
continue
88+
}
89+
if err := s.Value.Schema.Value.VisitJSON(h, opts...); err != nil {
90+
return &ResponseError{
91+
Input: input,
92+
Reason: fmt.Sprintf("response header %q doesn't match the schema", k),
93+
Err: err,
94+
}
95+
}
96+
}
97+
6498
if options.ExcludeResponseBody {
6599
// A user turned off validation of a response's body.
66100
return nil
@@ -120,14 +154,8 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error
120154
}
121155
}
122156

123-
opts := make([]openapi3.SchemaValidationOption, 0, 2) // 2 potential opts here
124-
opts = append(opts, openapi3.VisitAsResponse())
125-
if options.MultiError {
126-
opts = append(opts, openapi3.MultiErrors())
127-
}
128-
129157
// Validate data with the schema.
130-
if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil {
158+
if err := contentType.Schema.Value.VisitJSON(value, append(opts, openapi3.VisitAsResponse())...); err != nil {
131159
return &ResponseError{
132160
Input: input,
133161
Reason: "response body doesn't match the schema",

0 commit comments

Comments
 (0)