From 1d43d03c216debdcbaf46e958846a9a28eb8b0ab Mon Sep 17 00:00:00 2001 From: Adrien YHUEL Date: Sun, 16 Aug 2026 19:08:21 +0200 Subject: [PATCH] fix: compare MediaType with wildcards --- responses/validate_body.go | 22 ++- responses/validate_body_test.go | 294 ++++++++++++++++++++++++++++++++ 2 files changed, 314 insertions(+), 2 deletions(-) diff --git a/responses/validate_body.go b/responses/validate_body.go index 06c7c7a..c51e3b8 100644 --- a/responses/validate_body.go +++ b/responses/validate_body.go @@ -80,7 +80,7 @@ func (v *responseBodyValidator) ValidateResponseBodyWithPathItem(request *http.R if foundResponse != nil { if v.options.ValidateResponseBody && foundResponse.Content != nil { // only validate if we have content types. // check content type has been defined in the contract - if mediaType, ok := foundResponse.Content.Get(mediaTypeSting); ok { + if mediaType, ok := v.extractContentType(mediaTypeSting, foundResponse); ok { validationErrors = append(validationErrors, v.checkResponseSchema(request, response, contentType, mediaType, operation)...) } else { @@ -98,7 +98,7 @@ func (v *responseBodyValidator) ValidateResponseBodyWithPathItem(request *http.R // check content type has been defined in the contract if !v.options.ValidateResponseBody { foundResponse = operation.Responses.Default - } else if mediaType, ok := operation.Responses.Default.Content.Get(mediaTypeSting); ok { + } else if mediaType, ok := v.extractContentType(mediaTypeSting, operation.Responses.Default); ok { foundResponse = operation.Responses.Default validationErrors = append(validationErrors, v.checkResponseSchema(request, response, contentType, mediaType, operation)...) @@ -239,3 +239,21 @@ func (v *responseBodyValidator) checkResponseSchema( return validationErrors } + +func (v *responseBodyValidator) extractContentType(contentType string, response *v3.Response) (*v3.MediaType, bool) { + mediaType, ok := response.Content.Get(contentType) + if ok { + return mediaType, true + } + ctMediaRange := strings.SplitN(contentType, "/", 2) + for contentPair := response.Content.First(); contentPair != nil; contentPair = contentPair.Next() { + s := contentPair.Key() + mediaTypeValue := contentPair.Value() + opMediaRange := strings.SplitN(s, "/", 2) + if (opMediaRange[0] == "*" || opMediaRange[0] == ctMediaRange[0]) && + (opMediaRange[1] == "*" || opMediaRange[1] == ctMediaRange[1]) { + return mediaTypeValue, true + } + } + return nil, false +} diff --git a/responses/validate_body_test.go b/responses/validate_body_test.go index 8de2675..49d2342 100644 --- a/responses/validate_body_test.go +++ b/responses/validate_body_test.go @@ -945,6 +945,153 @@ paths: assert.Len(t, errors, 0) } +func TestValidateBody_ValidBasicSchema_WithContentTypeWildcards(t *testing.T) { + tb := newvalidateResponseTestBed( + t, + []byte(`openapi: 3.1.0 +paths: + /burgers/createBurger: + post: + responses: + '200': + content: + "*/*": + schema: + type: object + properties: + name: + type: string + patties: + type: integer + vegetarian: + type: boolean`, + ), + ) + + req, res := tb.makeRequestWithReponse( + t, + http.MethodPost, + "/burgers/createBurger", + func(w http.ResponseWriter, r *http.Request) { + bodyBytes, err := json.Marshal(map[string]interface{}{ + "name": "Big Mac", + "patties": 2, + "vegetarian": false, + }) + + require.NoError(t, err, "failed to marshal body") + + w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) + w.WriteHeader(http.StatusOK) + _, _ = w.Write(bodyBytes) + }, + ) + + // validate! + valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) + + assert.True(t, valid) + assert.Len(t, errors, 0) +} + +func TestValidateBody_ValidBasicSchema_WithContentTypeWildcardEnd(t *testing.T) { + tb := newvalidateResponseTestBed( + t, + []byte(`openapi: 3.1.0 +paths: + /burgers/createBurger: + post: + responses: + '200': + content: + "application/*": + schema: + type: object + properties: + name: + type: string + patties: + type: integer + vegetarian: + type: boolean`, + ), + ) + + req, res := tb.makeRequestWithReponse( + t, + http.MethodPost, + "/burgers/createBurger", + func(w http.ResponseWriter, r *http.Request) { + bodyBytes, err := json.Marshal(map[string]interface{}{ + "name": "Big Mac", + "patties": 2, + "vegetarian": false, + }) + + require.NoError(t, err, "failed to marshal body") + + w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) + w.WriteHeader(http.StatusOK) + _, _ = w.Write(bodyBytes) + }, + ) + + // validate! + valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) + + assert.True(t, valid) + assert.Len(t, errors, 0) +} + +func TestValidateBody_ValidBasicSchema_WithContentTypeWildcardStart(t *testing.T) { + tb := newvalidateResponseTestBed( + t, + []byte(`openapi: 3.1.0 +paths: + /burgers/createBurger: + post: + responses: + '200': + content: + "*/json": + schema: + type: object + properties: + name: + type: string + patties: + type: integer + vegetarian: + type: boolean`, + ), + ) + + req, res := tb.makeRequestWithReponse( + t, + http.MethodPost, + "/burgers/createBurger", + func(w http.ResponseWriter, r *http.Request) { + bodyBytes, err := json.Marshal(map[string]interface{}{ + "name": "Big Mac", + "patties": 2, + "vegetarian": false, + }) + + require.NoError(t, err, "failed to marshal body") + + w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) + w.WriteHeader(http.StatusOK) + _, _ = w.Write(bodyBytes) + }, + ) + + // validate! + valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) + + assert.True(t, valid) + assert.Len(t, errors, 0) +} + func TestValidateBody_ValidBasicSchemaUsingDefault(t *testing.T) { tb := newvalidateResponseTestBed( t, @@ -994,6 +1141,153 @@ paths: assert.Len(t, errors, 0) } +func TestValidateBody_ValidBasicSchemaUsingDefault_WithContentTypeWildcards(t *testing.T) { + tb := newvalidateResponseTestBed( + t, + []byte(`openapi: 3.1.0 +paths: + /burgers/createBurger: + post: + responses: + default: + content: + "*/*": + schema: + type: object + properties: + name: + type: string + patties: + type: integer + vegetarian: + type: boolean`, + ), + ) + + req, res := tb.makeRequestWithReponse( + t, + http.MethodPost, + "/burgers/createBurger", + func(w http.ResponseWriter, r *http.Request) { + bodyBytes, err := json.Marshal(map[string]interface{}{ + "name": "Big Mac", + "patties": 2, + "vegetarian": false, + }) + + require.NoError(t, err, "failed to marshal body") + + w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) + w.WriteHeader(http.StatusOK) + _, _ = w.Write(bodyBytes) + }, + ) + + // validate! + valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) + + assert.True(t, valid) + assert.Len(t, errors, 0) +} + +func TestValidateBody_ValidBasicSchemaUsingDefault_WithContentTypeWildcardEnd(t *testing.T) { + tb := newvalidateResponseTestBed( + t, + []byte(`openapi: 3.1.0 +paths: + /burgers/createBurger: + post: + responses: + default: + content: + "application/*": + schema: + type: object + properties: + name: + type: string + patties: + type: integer + vegetarian: + type: boolean`, + ), + ) + + req, res := tb.makeRequestWithReponse( + t, + http.MethodPost, + "/burgers/createBurger", + func(w http.ResponseWriter, r *http.Request) { + bodyBytes, err := json.Marshal(map[string]interface{}{ + "name": "Big Mac", + "patties": 2, + "vegetarian": false, + }) + + require.NoError(t, err, "failed to marshal body") + + w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) + w.WriteHeader(http.StatusOK) + _, _ = w.Write(bodyBytes) + }, + ) + + // validate! + valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) + + assert.True(t, valid) + assert.Len(t, errors, 0) +} + +func TestValidateBody_ValidBasicSchemaUsingDefault_WithContentTypeWildcardStart(t *testing.T) { + tb := newvalidateResponseTestBed( + t, + []byte(`openapi: 3.1.0 +paths: + /burgers/createBurger: + post: + responses: + default: + content: + "*/json": + schema: + type: object + properties: + name: + type: string + patties: + type: integer + vegetarian: + type: boolean`, + ), + ) + + req, res := tb.makeRequestWithReponse( + t, + http.MethodPost, + "/burgers/createBurger", + func(w http.ResponseWriter, r *http.Request) { + bodyBytes, err := json.Marshal(map[string]interface{}{ + "name": "Big Mac", + "patties": 2, + "vegetarian": false, + }) + + require.NoError(t, err, "failed to marshal body") + + w.Header().Set(helpers.ContentTypeHeader, helpers.JSONContentType) + w.WriteHeader(http.StatusOK) + _, _ = w.Write(bodyBytes) + }, + ) + + // validate! + valid, errors := tb.responseBodyValidator.ValidateResponseBody(req, res) + + assert.True(t, valid) + assert.Len(t, errors, 0) +} + func TestValidateBody_InvalidBasicSchemaUsingDefault_MissingContentType(t *testing.T) { tb := newvalidateResponseTestBed( t,