Skip to content

Commit 69c8af1

Browse files
k1LoWdaveshanley
authored andcommitted
add OpenAPI version support for request/response schema validation
1 parent a49e0b8 commit 69c8af1

File tree

6 files changed

+220
-11
lines changed

6 files changed

+220
-11
lines changed

requests/validate_body.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ func (v *requestBodyValidator) ValidateRequestBodyWithPathItem(request *http.Req
108108
})
109109
}
110110

111-
validationSucceeded, validationErrors := ValidateRequestSchema(request, schema, renderedInline, renderedJSON, config.WithExistingOpts(v.options))
111+
validationSucceeded, validationErrors := ValidateRequestSchema(request, schema, renderedInline, renderedJSON, helpers.VersionToFloat(v.document.Version), config.WithExistingOpts(v.options))
112112

113113
errors.PopulateValidationErrors(validationErrors, request, pathValue)
114114

requests/validate_request.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func ValidateRequestSchema(
3434
schema *base.Schema,
3535
renderedSchema,
3636
jsonSchema []byte,
37+
version float32,
3738
opts ...config.Option,
3839
) (bool, []*errors.ValidationError) {
3940
validationOptions := config.NewValidationOptions(opts...)
@@ -110,7 +111,7 @@ func ValidateRequestSchema(
110111
}
111112

112113
// Attempt to compile the JSON schema
113-
jsch, err := helpers.NewCompiledSchema("requestBody", jsonSchema, validationOptions)
114+
jsch, err := helpers.NewCompiledSchemaWithVersion("requestBody", jsonSchema, validationOptions, version)
114115
if err != nil {
115116
validationErrors = append(validationErrors, &errors.ValidationError{
116117
ValidationType: helpers.RequestBodyValidation,

requests/validate_request_test.go

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ func TestValidateRequestSchema(t *testing.T) {
1515
request *http.Request
1616
schema *base.Schema
1717
renderedSchema, jsonSchema []byte
18+
version float32
1819
assertValidRequestSchema assert.BoolAssertionFunc
1920
expectedErrorsCount int
2021
}{
@@ -31,6 +32,7 @@ properties:
3132
exclusiveMinimum: true
3233
minimum: !!float 10`),
3334
jsonSchema: []byte(`{"properties":{"exclusiveNumber":{"description":"This number starts its journey where most numbers are too scared to begin!","exclusiveMinimum":true,"minimum":10,"type":"number"}},"type":"object"}`),
35+
version: 3.1,
3436
assertValidRequestSchema: assert.False,
3537
expectedErrorsCount: 1,
3638
},
@@ -47,6 +49,7 @@ properties:
4749
exclusiveMinimum: 12
4850
minimum: 12`),
4951
jsonSchema: []byte(`{"properties":{"exclusiveNumber":{"type":"number","description":"This number is properly constrained by a numeric exclusive minimum.","exclusiveMinimum":12,"minimum":12}},"type":"object"}`),
52+
version: 3.1,
5053
assertValidRequestSchema: assert.True,
5154
expectedErrorsCount: 0,
5255
},
@@ -57,20 +60,58 @@ properties:
5760
},
5861
renderedSchema: []byte(`type: object
5962
properties:
60-
greeting:
61-
type: string
62-
description: A simple greeting
63-
example: "Hello, world!"`),
63+
greeting:
64+
type: string
65+
description: A simple greeting
66+
example: "Hello, world!"`),
6467
jsonSchema: []byte(`{"properties":{"greeting":{"type":"string","description":"A simple greeting","example":"Hello, world!"}},"type":"object"}`),
68+
version: 3.1,
6569
assertValidRequestSchema: assert.True,
6670
expectedErrorsCount: 0,
6771
},
72+
"PassWithNullablePropertyInOpenAPI30": {
73+
request: postRequestWithBody(`{"name": "John", "middleName": null}`),
74+
schema: &base.Schema{
75+
Type: []string{"object"},
76+
},
77+
renderedSchema: []byte(`type: object
78+
properties:
79+
name:
80+
type: string
81+
description: User's first name
82+
middleName:
83+
type: string
84+
nullable: true
85+
description: User's middle name (optional)`),
86+
jsonSchema: []byte(`{"properties":{"name":{"type":"string","description":"User's first name"},"middleName":{"type":"string","nullable":true,"description":"User's middle name (optional)"}},"type":"object"}`),
87+
version: 3.0,
88+
assertValidRequestSchema: assert.True,
89+
expectedErrorsCount: 0,
90+
},
91+
"PassWithNullablePropertyInOpenAPI31": {
92+
request: postRequestWithBody(`{"name": "John", "middleName": null}`),
93+
schema: &base.Schema{
94+
Type: []string{"object"},
95+
},
96+
renderedSchema: []byte(`type: object
97+
properties:
98+
name:
99+
type: string
100+
description: User's first name
101+
middleName:
102+
type: string
103+
nullable: true
104+
description: User's middle name (optional)`),
105+
jsonSchema: []byte(`{"properties":{"name":{"type":"string","description":"User's first name"},"middleName":{"type":"string","nullable":true,"description":"User's middle name (optional)"}},"type":"object"}`),
106+
version: 3.1,
107+
assertValidRequestSchema: assert.False,
108+
expectedErrorsCount: 1,
109+
},
68110
} {
69-
tc := tc
70111
t.Run(name, func(t *testing.T) {
71112
t.Parallel()
72113

73-
valid, errors := ValidateRequestSchema(tc.request, tc.schema, tc.renderedSchema, tc.jsonSchema)
114+
valid, errors := ValidateRequestSchema(tc.request, tc.schema, tc.renderedSchema, tc.jsonSchema, tc.version)
74115

75116
tc.assertValidRequestSchema(t, valid)
76117
assert.Len(t, errors, tc.expectedErrorsCount)
@@ -98,7 +139,7 @@ properties:
98139

99140
valid, errors := ValidateRequestSchema(postRequestWithBody(`{"exclusiveNumber": 13}`), &base.Schema{
100141
Type: []string{"object"},
101-
}, renderedSchema, jsonSchema)
142+
}, renderedSchema, jsonSchema, 3.1)
102143

103144
assert.False(t, valid)
104145
assert.Len(t, errors, 1)

responses/validate_body.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ func (v *responseBodyValidator) checkResponseSchema(
186186

187187
if len(renderedInline) > 0 && len(renderedJSON) > 0 && schema != nil {
188188
// render the schema, to be used for validation
189-
valid, vErrs := ValidateResponseSchema(request, response, schema, renderedInline, renderedJSON, config.WithRegexEngine(v.options.RegexEngine))
189+
valid, vErrs := ValidateResponseSchema(request, response, schema, renderedInline, renderedJSON, helpers.VersionToFloat(v.document.Version), config.WithRegexEngine(v.options.RegexEngine))
190190
if !valid {
191191
validationErrors = append(validationErrors, vErrs...)
192192
}

responses/validate_response.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func ValidateResponseSchema(
3838
schema *base.Schema,
3939
renderedSchema,
4040
jsonSchema []byte,
41+
version float32,
4142
opts ...config.Option,
4243
) (bool, []*errors.ValidationError) {
4344
options := config.NewValidationOptions(opts...)
@@ -128,7 +129,7 @@ func ValidateResponseSchema(
128129
}
129130

130131
// create a new jsonschema compiler and add in the rendered JSON schema.
131-
jsch, err := helpers.NewCompiledSchema(helpers.ResponseBodyValidation, jsonSchema, options)
132+
jsch, err := helpers.NewCompiledSchemaWithVersion(helpers.ResponseBodyValidation, jsonSchema, options, version)
132133
if err != nil {
133134
// schema compilation failed, return validation error instead of panicking
134135
violation := &errors.SchemaValidationFailure{
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package responses
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"net/http"
7+
"strings"
8+
"testing"
9+
10+
"github.com/pb33f/libopenapi/datamodel/high/base"
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
func TestValidateResponseSchema(t *testing.T) {
15+
for name, tc := range map[string]struct {
16+
request *http.Request
17+
response *http.Response
18+
schema *base.Schema
19+
renderedSchema, jsonSchema []byte
20+
version float32
21+
assertValidResponseSchema assert.BoolAssertionFunc
22+
expectedErrorsCount int
23+
}{
24+
"FailOnBooleanExclusiveMinimum": {
25+
request: postRequest(),
26+
response: responseWithBody(`{"exclusiveNumber": 13}`),
27+
schema: &base.Schema{
28+
Type: []string{"object"},
29+
},
30+
renderedSchema: []byte(`type: object
31+
properties:
32+
exclusiveNumber:
33+
type: number
34+
description: This number starts its journey where most numbers are too scared to begin!
35+
exclusiveMinimum: true
36+
minimum: !!float 10`),
37+
jsonSchema: []byte(`{"properties":{"exclusiveNumber":{"description":"This number starts its journey where most numbers are too scared to begin!","exclusiveMinimum":true,"minimum":10,"type":"number"}},"type":"object"}`),
38+
version: 3.1,
39+
assertValidResponseSchema: assert.False,
40+
expectedErrorsCount: 1,
41+
},
42+
"PassWithCorrectExclusiveMinimum": {
43+
request: postRequest(),
44+
response: responseWithBody(`{"exclusiveNumber": 15}`),
45+
schema: &base.Schema{
46+
Type: []string{"object"},
47+
},
48+
renderedSchema: []byte(`type: object
49+
properties:
50+
exclusiveNumber:
51+
type: number
52+
description: This number is properly constrained by a numeric exclusive minimum.
53+
exclusiveMinimum: 12
54+
minimum: 12`),
55+
jsonSchema: []byte(`{"properties":{"exclusiveNumber":{"type":"number","description":"This number is properly constrained by a numeric exclusive minimum.","exclusiveMinimum":12,"minimum":12}},"type":"object"}`),
56+
version: 3.1,
57+
assertValidResponseSchema: assert.True,
58+
expectedErrorsCount: 0,
59+
},
60+
"PassWithValidStringType": {
61+
request: postRequest(),
62+
response: responseWithBody(`{"greeting": "Hello, world!"}`),
63+
schema: &base.Schema{
64+
Type: []string{"object"},
65+
},
66+
renderedSchema: []byte(`type: object
67+
properties:
68+
greeting:
69+
type: string
70+
description: A simple greeting
71+
example: "Hello, world!"`),
72+
jsonSchema: []byte(`{"properties":{"greeting":{"type":"string","description":"A simple greeting","example":"Hello, world!"}},"type":"object"}`),
73+
version: 3.1,
74+
assertValidResponseSchema: assert.True,
75+
expectedErrorsCount: 0,
76+
},
77+
"PassWithNullablePropertyInOpenAPI30": {
78+
request: postRequest(),
79+
response: responseWithBody(`{"name": "John", "middleName": null}`),
80+
schema: &base.Schema{
81+
Type: []string{"object"},
82+
},
83+
renderedSchema: []byte(`type: object
84+
properties:
85+
name:
86+
type: string
87+
description: User's first name
88+
middleName:
89+
type: string
90+
nullable: true
91+
description: User's middle name (optional)`),
92+
jsonSchema: []byte(`{"properties":{"name":{"type":"string","description":"User's first name"},"middleName":{"type":"string","nullable":true,"description":"User's middle name (optional)"}},"type":"object"}`),
93+
version: 3.0,
94+
assertValidResponseSchema: assert.True,
95+
expectedErrorsCount: 0,
96+
},
97+
"PassWithNullablePropertyInOpenAPI31": {
98+
request: postRequest(),
99+
response: responseWithBody(`{"name": "John", "middleName": null}`),
100+
schema: &base.Schema{
101+
Type: []string{"object"},
102+
},
103+
renderedSchema: []byte(`type: object
104+
properties:
105+
name:
106+
type: string
107+
description: User's first name
108+
middleName:
109+
type: string
110+
nullable: true
111+
description: User's middle name (optional)`),
112+
jsonSchema: []byte(`{"properties":{"name":{"type":"string","description":"User's first name"},"middleName":{"type":"string","nullable":true,"description":"User's middle name (optional)"}},"type":"object"}`),
113+
version: 3.1,
114+
assertValidResponseSchema: assert.False,
115+
expectedErrorsCount: 1,
116+
},
117+
} {
118+
t.Run(name, func(t *testing.T) {
119+
t.Parallel()
120+
121+
valid, errors := ValidateResponseSchema(tc.request, tc.response, tc.schema, tc.renderedSchema, tc.jsonSchema, tc.version)
122+
123+
tc.assertValidResponseSchema(t, valid)
124+
assert.Len(t, errors, tc.expectedErrorsCount)
125+
})
126+
}
127+
}
128+
129+
func postRequest() *http.Request {
130+
req, _ := http.NewRequest(http.MethodPost, "/test", io.NopCloser(strings.NewReader("")))
131+
return req
132+
}
133+
134+
func responseWithBody(payload string) *http.Response {
135+
return &http.Response{
136+
StatusCode: http.StatusOK,
137+
Body: io.NopCloser(bytes.NewReader([]byte(payload))),
138+
Header: http.Header{"Content-Type": []string{"application/json"}},
139+
}
140+
}
141+
142+
func TestInvalidMin(t *testing.T) {
143+
renderedSchema := []byte(`type: object
144+
properties:
145+
exclusiveNumber:
146+
type: number
147+
description: This number starts its journey where most numbers are too scared to begin!
148+
exclusiveMinimum: true
149+
minimum: !!float 10`)
150+
151+
jsonSchema := []byte(`{"properties":{"exclusiveNumber":{"description":"This number starts its journey where most numbers are too scared to begin!","exclusiveMinimum":true,"minimum":10,"type":"number"}},"type":"object"}`)
152+
153+
valid, errors := ValidateResponseSchema(
154+
postRequest(),
155+
responseWithBody(`{"exclusiveNumber": 13}`),
156+
&base.Schema{
157+
Type: []string{"object"},
158+
},
159+
renderedSchema,
160+
jsonSchema,
161+
3.1,
162+
)
163+
164+
assert.False(t, valid)
165+
assert.Len(t, errors, 1)
166+
}

0 commit comments

Comments
 (0)