Skip to content

Commit a01edc7

Browse files
authored
Merge pull request #6 from p1c2u/feature/schema_strict_validation
Schema strict validation
2 parents aaab71c + be7f55d commit a01edc7

File tree

3 files changed

+140
-19
lines changed

3 files changed

+140
-19
lines changed

openapi_core/exceptions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,21 @@ class MissingParameterError(OpenAPIMappingError):
1313
pass
1414

1515

16+
class MissingPropertyError(OpenAPIMappingError):
17+
pass
18+
19+
1620
class InvalidContentTypeError(OpenAPIMappingError):
1721
pass
1822

1923

2024
class InvalidServerError(OpenAPIMappingError):
2125
pass
26+
27+
28+
class InvalidValueType(OpenAPIMappingError):
29+
pass
30+
31+
32+
class UndefinedSchemaProperty(OpenAPIMappingError):
33+
pass

openapi_core/schemas.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
from json import loads
88
from six import iteritems
99

10+
from openapi_core.exceptions import (
11+
InvalidValueType, UndefinedSchemaProperty, MissingPropertyError,
12+
)
1013
from openapi_core.models import ModelFactory
1114

1215
log = logging.getLogger(__name__)
@@ -23,13 +26,14 @@ class Schema(object):
2326

2427
def __init__(
2528
self, schema_type, model=None, properties=None, items=None,
26-
spec_format=None, required=False):
29+
spec_format=None, required=False, default=None):
2730
self.type = schema_type
2831
self.model = model
2932
self.properties = properties and dict(properties) or {}
3033
self.items = items
3134
self.format = spec_format
3235
self.required = required
36+
self.default = default
3337

3438
def __getitem__(self, name):
3539
return self.properties[name]
@@ -57,10 +61,9 @@ def cast(self, value):
5761
try:
5862
return cast_callable(value)
5963
except ValueError:
60-
log.warning(
64+
raise InvalidValueType(
6165
"Failed to cast value of %s to %s", value, self.type,
6266
)
63-
return value
6467

6568
def unmarshal(self, value):
6669
"""Unmarshal parameter from the value."""
@@ -78,9 +81,24 @@ def _unmarshal_object(self, value):
7881
if isinstance(value, (str, bytes)):
7982
value = loads(value)
8083

84+
properties_keys = self.properties.keys()
85+
value_keys = value.keys()
86+
87+
extra_props = set(value_keys) - set(properties_keys)
88+
89+
if extra_props:
90+
raise UndefinedSchemaProperty(
91+
"Undefined properties in schema: {0}".format(extra_props))
92+
8193
properties = {}
8294
for prop_name, prop in iteritems(self.properties):
83-
prop_value = value.get(prop_name)
95+
try:
96+
prop_value = value[prop_name]
97+
except KeyError:
98+
if prop_name in self.required:
99+
raise MissingPropertyError(
100+
"Missing schema property {0}".format(prop_name))
101+
prop_value = prop.default
84102
properties[prop_name] = prop.unmarshal(prop_value)
85103
return ModelFactory().create(properties, name=self.model)
86104

tests/integration/test_petstore.py

Lines changed: 106 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from openapi_core.exceptions import (
66
MissingParameterError, InvalidContentTypeError, InvalidServerError,
7+
InvalidValueType, UndefinedSchemaProperty, MissingPropertyError,
78
)
89
from openapi_core.media_types import MediaType
910
from openapi_core.operations import Operation
@@ -139,41 +140,38 @@ def test_get_pets(self, spec):
139140
}
140141
assert body is None
141142

142-
def test_get_pets_raises_missing_required_param(self, spec):
143+
def test_get_pets_wrong_parameter_type(self, spec):
143144
host_url = 'http://petstore.swagger.io/v1'
144145
path_pattern = '/v1/pets'
146+
query_params = {
147+
'limit': 'twenty',
148+
}
149+
145150
request = RequestMock(
146151
host_url, 'GET', '/pets',
147-
path_pattern=path_pattern,
152+
path_pattern=path_pattern, args=query_params,
148153
)
149154

150-
with pytest.raises(MissingParameterError):
155+
with pytest.raises(InvalidValueType):
151156
request.get_parameters(spec)
152157

153158
body = request.get_body(spec)
154159

155160
assert body is None
156161

157-
def test_get_pets_failed_to_cast(self, spec):
162+
def test_get_pets_raises_missing_required_param(self, spec):
158163
host_url = 'http://petstore.swagger.io/v1'
159164
path_pattern = '/v1/pets'
160-
query_params = {
161-
'limit': 'non_integer_value',
162-
}
163-
164165
request = RequestMock(
165166
host_url, 'GET', '/pets',
166-
path_pattern=path_pattern, args=query_params,
167+
path_pattern=path_pattern,
167168
)
168169

169-
parameters = request.get_parameters(spec)
170+
with pytest.raises(MissingParameterError):
171+
request.get_parameters(spec)
172+
170173
body = request.get_body(spec)
171174

172-
assert parameters == {
173-
'query': {
174-
'limit': 'non_integer_value',
175-
}
176-
}
177175
assert body is None
178176

179177
def test_get_pets_empty_value(self, spec):
@@ -260,6 +258,99 @@ def test_post_pets(self, spec, spec_dict):
260258
assert body.address.street == pet_street
261259
assert body.address.city == pet_city
262260

261+
def test_post_pets_empty_body(self, spec, spec_dict):
262+
host_url = 'http://petstore.swagger.io/v1'
263+
path_pattern = '/v1/pets'
264+
data_json = {}
265+
data = json.dumps(data_json)
266+
267+
request = RequestMock(
268+
host_url, 'POST', '/pets',
269+
path_pattern=path_pattern, data=data,
270+
)
271+
272+
parameters = request.get_parameters(spec)
273+
274+
assert parameters == {}
275+
276+
with pytest.raises(MissingPropertyError):
277+
request.get_body(spec)
278+
279+
def test_post_pets_extra_body_properties(self, spec, spec_dict):
280+
host_url = 'http://petstore.swagger.io/v1'
281+
path_pattern = '/v1/pets'
282+
pet_name = 'Cat'
283+
alias = 'kitty'
284+
data_json = {
285+
'name': pet_name,
286+
'alias': alias,
287+
}
288+
data = json.dumps(data_json)
289+
290+
request = RequestMock(
291+
host_url, 'POST', '/pets',
292+
path_pattern=path_pattern, data=data,
293+
)
294+
295+
parameters = request.get_parameters(spec)
296+
297+
assert parameters == {}
298+
299+
with pytest.raises(UndefinedSchemaProperty):
300+
request.get_body(spec)
301+
302+
def test_post_pets_only_required_body(self, spec, spec_dict):
303+
host_url = 'http://petstore.swagger.io/v1'
304+
path_pattern = '/v1/pets'
305+
pet_name = 'Cat'
306+
data_json = {
307+
'name': pet_name,
308+
}
309+
data = json.dumps(data_json)
310+
311+
request = RequestMock(
312+
host_url, 'POST', '/pets',
313+
path_pattern=path_pattern, data=data,
314+
)
315+
316+
parameters = request.get_parameters(spec)
317+
318+
assert parameters == {}
319+
320+
body = request.get_body(spec)
321+
322+
schemas = spec_dict['components']['schemas']
323+
pet_model = schemas['PetCreate']['x-model']
324+
assert body.__class__.__name__ == pet_model
325+
assert body.name == pet_name
326+
assert body.tag is None
327+
assert body.address is None
328+
329+
def test_get_pets_wrong_body_type(self, spec):
330+
host_url = 'http://petstore.swagger.io/v1'
331+
path_pattern = '/v1/pets'
332+
pet_name = 'Cat'
333+
pet_tag = 'cats'
334+
pet_address = 'address text'
335+
data_json = {
336+
'name': pet_name,
337+
'tag': pet_tag,
338+
'address': pet_address,
339+
}
340+
data = json.dumps(data_json)
341+
342+
request = RequestMock(
343+
host_url, 'POST', '/pets',
344+
path_pattern=path_pattern, data=data,
345+
)
346+
347+
parameters = request.get_parameters(spec)
348+
349+
assert parameters == {}
350+
351+
with pytest.raises(InvalidValueType):
352+
request.get_body(spec)
353+
263354
def test_post_pets_raises_invalid_content_type(self, spec):
264355
host_url = 'http://petstore.swagger.io/v1'
265356
path_pattern = '/v1/pets'

0 commit comments

Comments
 (0)