Skip to content

Commit 98029fc

Browse files
committed
Allow empty content type to pass validation.
empty content is valid Signed-off-by: Dave Shanley <[email protected]>
1 parent 8e071d3 commit 98029fc

File tree

2 files changed

+184
-141
lines changed

2 files changed

+184
-141
lines changed

responses/validate_body.go

Lines changed: 141 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -4,151 +4,151 @@
44
package responses
55

66
import (
7-
"github.com/pb33f/libopenapi-validator/errors"
8-
"github.com/pb33f/libopenapi-validator/helpers"
9-
"github.com/pb33f/libopenapi-validator/paths"
10-
"github.com/pb33f/libopenapi/datamodel/high/base"
11-
"github.com/pb33f/libopenapi/datamodel/high/v3"
12-
"github.com/pb33f/libopenapi/utils"
13-
"net/http"
14-
"strconv"
15-
"strings"
7+
"github.com/pb33f/libopenapi-validator/errors"
8+
"github.com/pb33f/libopenapi-validator/helpers"
9+
"github.com/pb33f/libopenapi-validator/paths"
10+
"github.com/pb33f/libopenapi/datamodel/high/base"
11+
"github.com/pb33f/libopenapi/datamodel/high/v3"
12+
"github.com/pb33f/libopenapi/utils"
13+
"net/http"
14+
"strconv"
15+
"strings"
1616
)
1717

1818
func (v *responseBodyValidator) ValidateResponseBody(
19-
request *http.Request,
20-
response *http.Response) (bool, []*errors.ValidationError) {
21-
22-
// find path
23-
var pathItem *v3.PathItem
24-
var errs []*errors.ValidationError
25-
if v.pathItem == nil {
26-
pathItem, errs, _ = paths.FindPath(request, v.document)
27-
if pathItem == nil || errs != nil {
28-
v.errors = errs
29-
return false, errs
30-
}
31-
} else {
32-
pathItem = v.pathItem
33-
}
34-
35-
var validationErrors []*errors.ValidationError
36-
operation := helpers.ExtractOperation(request, pathItem)
37-
38-
// extract the response code from the response
39-
httpCode := response.StatusCode
40-
contentType := response.Header.Get(helpers.ContentTypeHeader)
41-
42-
// extract the media type from the content type header.
43-
mediaTypeSting, _, _ := helpers.ExtractContentType(contentType)
44-
45-
// check if the response code is in the contract
46-
foundResponse := operation.Responses.FindResponseByCode(httpCode)
47-
if foundResponse != nil {
48-
49-
// check content type has been defined in the contract
50-
if mediaType, ok := foundResponse.Content[mediaTypeSting]; ok {
51-
52-
validationErrors = append(validationErrors,
53-
v.checkResponseSchema(request, response, mediaTypeSting, mediaType)...)
54-
55-
} else {
56-
57-
// check that the operation *actually* returns a body. (i.e. a 204 response)
58-
if foundResponse.Content != nil {
59-
60-
// content type not found in the contract
61-
codeStr := strconv.Itoa(httpCode)
62-
validationErrors = append(validationErrors,
63-
errors.ResponseContentTypeNotFound(operation, request, response, codeStr, false))
64-
65-
}
66-
}
67-
} else {
68-
69-
// no code match, check for default response
70-
if operation.Responses.Default != nil {
71-
72-
// check content type has been defined in the contract
73-
if mediaType, ok := operation.Responses.Default.Content[mediaTypeSting]; ok {
74-
75-
validationErrors = append(validationErrors,
76-
v.checkResponseSchema(request, response, contentType, mediaType)...)
77-
78-
} else {
79-
80-
// check that the operation *actually* returns a body. (i.e. a 204 response)
81-
if operation.Responses.Default.Content != nil {
82-
83-
// content type not found in the contract
84-
codeStr := strconv.Itoa(httpCode)
85-
validationErrors = append(validationErrors,
86-
errors.ResponseContentTypeNotFound(operation, request, response, codeStr, true))
87-
}
88-
}
89-
90-
} else {
91-
// TODO: add support for '2XX' and '3XX' responses in the contract
92-
// no default, no code match, nothing!
93-
validationErrors = append(validationErrors,
94-
errors.ResponseCodeNotFound(operation, request, httpCode))
95-
}
96-
}
97-
if len(validationErrors) > 0 {
98-
return false, validationErrors
99-
}
100-
return true, nil
19+
request *http.Request,
20+
response *http.Response) (bool, []*errors.ValidationError) {
21+
22+
// find path
23+
var pathItem *v3.PathItem
24+
var errs []*errors.ValidationError
25+
if v.pathItem == nil {
26+
pathItem, errs, _ = paths.FindPath(request, v.document)
27+
if pathItem == nil || errs != nil {
28+
v.errors = errs
29+
return false, errs
30+
}
31+
} else {
32+
pathItem = v.pathItem
33+
}
34+
35+
var validationErrors []*errors.ValidationError
36+
operation := helpers.ExtractOperation(request, pathItem)
37+
38+
// extract the response code from the response
39+
httpCode := response.StatusCode
40+
contentType := response.Header.Get(helpers.ContentTypeHeader)
41+
42+
// extract the media type from the content type header.
43+
mediaTypeSting, _, _ := helpers.ExtractContentType(contentType)
44+
45+
// check if the response code is in the contract
46+
foundResponse := operation.Responses.FindResponseByCode(httpCode)
47+
if foundResponse != nil {
48+
49+
// check content type has been defined in the contract
50+
if mediaType, ok := foundResponse.Content[mediaTypeSting]; ok {
51+
52+
validationErrors = append(validationErrors,
53+
v.checkResponseSchema(request, response, mediaTypeSting, mediaType)...)
54+
55+
} else {
56+
57+
// check that the operation *actually* returns a body. (i.e. a 204 response)
58+
if foundResponse.Content != nil && len(foundResponse.Content) > 0 {
59+
60+
// content type not found in the contract
61+
codeStr := strconv.Itoa(httpCode)
62+
validationErrors = append(validationErrors,
63+
errors.ResponseContentTypeNotFound(operation, request, response, codeStr, false))
64+
65+
}
66+
}
67+
} else {
68+
69+
// no code match, check for default response
70+
if operation.Responses.Default != nil {
71+
72+
// check content type has been defined in the contract
73+
if mediaType, ok := operation.Responses.Default.Content[mediaTypeSting]; ok {
74+
75+
validationErrors = append(validationErrors,
76+
v.checkResponseSchema(request, response, contentType, mediaType)...)
77+
78+
} else {
79+
80+
// check that the operation *actually* returns a body. (i.e. a 204 response)
81+
if operation.Responses.Default.Content != nil && len(operation.Responses.Default.Content) > 0 {
82+
83+
// content type not found in the contract
84+
codeStr := strconv.Itoa(httpCode)
85+
validationErrors = append(validationErrors,
86+
errors.ResponseContentTypeNotFound(operation, request, response, codeStr, true))
87+
}
88+
}
89+
90+
} else {
91+
// TODO: add support for '2XX' and '3XX' responses in the contract
92+
// no default, no code match, nothing!
93+
validationErrors = append(validationErrors,
94+
errors.ResponseCodeNotFound(operation, request, httpCode))
95+
}
96+
}
97+
if len(validationErrors) > 0 {
98+
return false, validationErrors
99+
}
100+
return true, nil
101101
}
102102

103103
func (v *responseBodyValidator) checkResponseSchema(
104-
request *http.Request,
105-
response *http.Response,
106-
contentType string,
107-
mediaType *v3.MediaType) []*errors.ValidationError {
108-
109-
var validationErrors []*errors.ValidationError
110-
111-
// currently, we can only validate JSON based responses, so check for the presence
112-
// of 'json' in the content type (what ever it may be) so we can perform a schema check on it.
113-
// anything other than JSON, will be ignored.
114-
if strings.Contains(strings.ToLower(contentType), helpers.JSONType) {
115-
116-
// extract schema from media type
117-
if mediaType.Schema != nil {
118-
119-
var schema *base.Schema
120-
var renderedInline, renderedJSON []byte
121-
122-
// have we seen this schema before? let's hash it and check the cache.
123-
hash := mediaType.GoLow().Schema.Value.Hash()
124-
125-
if cacheHit, ch := v.schemaCache[hash]; ch {
126-
127-
// got a hit, use cached values
128-
schema = cacheHit.schema
129-
renderedInline = cacheHit.renderedInline
130-
renderedJSON = cacheHit.renderedJSON
131-
132-
} else {
133-
134-
// render the schema inline and perform the intensive work of rendering and converting
135-
// this is only performed once per schema and cached in the validator.
136-
schema = mediaType.Schema.Schema()
137-
renderedInline, _ = schema.RenderInline()
138-
renderedJSON, _ = utils.ConvertYAMLtoJSON(renderedInline)
139-
v.schemaCache[hash] = &schemaCache{
140-
schema: schema,
141-
renderedInline: renderedInline,
142-
renderedJSON: renderedJSON,
143-
}
144-
}
145-
146-
// render the schema, to be used for validation
147-
valid, vErrs := ValidateResponseSchema(request, response, schema, renderedInline, renderedJSON)
148-
if !valid {
149-
validationErrors = append(validationErrors, vErrs...)
150-
}
151-
}
152-
}
153-
return validationErrors
104+
request *http.Request,
105+
response *http.Response,
106+
contentType string,
107+
mediaType *v3.MediaType) []*errors.ValidationError {
108+
109+
var validationErrors []*errors.ValidationError
110+
111+
// currently, we can only validate JSON based responses, so check for the presence
112+
// of 'json' in the content type (what ever it may be) so we can perform a schema check on it.
113+
// anything other than JSON, will be ignored.
114+
if strings.Contains(strings.ToLower(contentType), helpers.JSONType) {
115+
116+
// extract schema from media type
117+
if mediaType.Schema != nil {
118+
119+
var schema *base.Schema
120+
var renderedInline, renderedJSON []byte
121+
122+
// have we seen this schema before? let's hash it and check the cache.
123+
hash := mediaType.GoLow().Schema.Value.Hash()
124+
125+
if cacheHit, ch := v.schemaCache[hash]; ch {
126+
127+
// got a hit, use cached values
128+
schema = cacheHit.schema
129+
renderedInline = cacheHit.renderedInline
130+
renderedJSON = cacheHit.renderedJSON
131+
132+
} else {
133+
134+
// render the schema inline and perform the intensive work of rendering and converting
135+
// this is only performed once per schema and cached in the validator.
136+
schema = mediaType.Schema.Schema()
137+
renderedInline, _ = schema.RenderInline()
138+
renderedJSON, _ = utils.ConvertYAMLtoJSON(renderedInline)
139+
v.schemaCache[hash] = &schemaCache{
140+
schema: schema,
141+
renderedInline: renderedInline,
142+
renderedJSON: renderedJSON,
143+
}
144+
}
145+
146+
// render the schema, to be used for validation
147+
valid, vErrs := ValidateResponseSchema(request, response, schema, renderedInline, renderedJSON)
148+
if !valid {
149+
validationErrors = append(validationErrors, vErrs...)
150+
}
151+
}
152+
}
153+
return validationErrors
154154
}

responses/validate_body_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -945,3 +945,46 @@ paths:
945945
assert.Len(t, errors[0].SchemaValidationErrors, 2)
946946
assert.Equal(t, "200 response body for '/burgers/createBurger' failed to validate schema", errors[0].Message)
947947
}
948+
949+
func TestValidateBody_EmptyContentType_Valid(t *testing.T) {
950+
spec := `openapi: "3.0.0"
951+
info:
952+
title: Healthcheck
953+
version: '0.1.0'
954+
paths:
955+
/health:
956+
get:
957+
responses:
958+
'200':
959+
description: pet response
960+
content: {}`
961+
962+
doc, _ := libopenapi.NewDocument([]byte(spec))
963+
964+
m, _ := doc.BuildV3Model()
965+
v := NewResponseBodyValidator(&m.Model)
966+
967+
// build a request
968+
request, _ := http.NewRequest(http.MethodGet, "https://things.com/health", nil)
969+
970+
// simulate a request/response
971+
res := httptest.NewRecorder()
972+
handler := func(w http.ResponseWriter, r *http.Request) {
973+
w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType)
974+
w.WriteHeader(http.StatusOK)
975+
_, _ = w.Write(nil)
976+
}
977+
978+
// fire the request
979+
handler(res, request)
980+
981+
// record response
982+
response := res.Result()
983+
984+
// validate!
985+
valid, errors := v.ValidateResponseBody(request, response)
986+
987+
assert.True(t, valid)
988+
assert.Len(t, errors, 0)
989+
990+
}

0 commit comments

Comments
 (0)