Raw value type strict validation

This commit is contained in:
Artur Maciąg 2019-03-22 01:51:47 +00:00 committed by Artur Maciag
parent 395f68b234
commit cd0ca5d26f
6 changed files with 317 additions and 42 deletions

View file

@ -109,7 +109,11 @@ class Parameter(object):
raise InvalidParameterValue(self.name, exc)
try:
unmarshalled = self.schema.unmarshal(deserialized, custom_formatters=custom_formatters)
unmarshalled = self.schema.unmarshal(
deserialized,
custom_formatters=custom_formatters,
strict=False,
)
except OpenAPISchemaError as exc:
raise InvalidParameterValue(self.name, exc)

View file

@ -38,9 +38,6 @@ class Schema(object):
"""Represents an OpenAPI Schema."""
DEFAULT_CAST_CALLABLE_GETTER = {
SchemaType.INTEGER: int,
SchemaType.NUMBER: float,
SchemaType.BOOLEAN: forcebool,
}
STRING_FORMAT_CALLABLE_GETTER = {
@ -148,12 +145,15 @@ class Schema(object):
return set(required)
def get_cast_mapping(self, custom_formatters=None):
def get_cast_mapping(self, custom_formatters=None, strict=True):
pass_defaults = lambda f: functools.partial(
f, custom_formatters=custom_formatters)
f, custom_formatters=custom_formatters, strict=strict)
mapping = self.DEFAULT_CAST_CALLABLE_GETTER.copy()
mapping.update({
SchemaType.STRING: pass_defaults(self._unmarshal_string),
SchemaType.BOOLEAN: pass_defaults(self._unmarshal_boolean),
SchemaType.INTEGER: pass_defaults(self._unmarshal_integer),
SchemaType.NUMBER: pass_defaults(self._unmarshal_number),
SchemaType.ANY: pass_defaults(self._unmarshal_any),
SchemaType.ARRAY: pass_defaults(self._unmarshal_collection),
SchemaType.OBJECT: pass_defaults(self._unmarshal_object),
@ -161,14 +161,15 @@ class Schema(object):
return defaultdict(lambda: lambda x: x, mapping)
def cast(self, value, custom_formatters=None):
def cast(self, value, custom_formatters=None, strict=True):
"""Cast value to schema type"""
if value is None:
if not self.nullable:
raise InvalidSchemaValue("Null value for non-nullable schema", value, self.type)
return self.default
cast_mapping = self.get_cast_mapping(custom_formatters=custom_formatters)
cast_mapping = self.get_cast_mapping(
custom_formatters=custom_formatters, strict=strict)
if self.type is not SchemaType.STRING and value == '':
return None
@ -180,12 +181,12 @@ class Schema(object):
raise InvalidSchemaValue(
"Failed to cast value {value} to type {type}", value, self.type)
def unmarshal(self, value, custom_formatters=None):
def unmarshal(self, value, custom_formatters=None, strict=True):
"""Unmarshal parameter from the value."""
if self.deprecated:
warnings.warn("The schema is deprecated", DeprecationWarning)
casted = self.cast(value, custom_formatters=custom_formatters)
casted = self.cast(value, custom_formatters=custom_formatters, strict=strict)
if casted is None and not self.required:
return None
@ -196,7 +197,10 @@ class Schema(object):
return casted
def _unmarshal_string(self, value, custom_formatters=None):
def _unmarshal_string(self, value, custom_formatters=None, strict=True):
if strict and not isinstance(value, (text_type, binary_type)):
raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type)
try:
schema_format = SchemaFormat(self.format)
except ValueError:
@ -216,7 +220,25 @@ class Schema(object):
raise InvalidCustomFormatSchemaValue(
"Failed to format value {value} to format {type}: {exception}", value, self.format, exc)
def _unmarshal_any(self, value, custom_formatters=None):
def _unmarshal_integer(self, value, custom_formatters=None, strict=True):
if strict and not isinstance(value, (integer_types, )):
raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type)
return int(value)
def _unmarshal_number(self, value, custom_formatters=None, strict=True):
if strict and not isinstance(value, (float, )):
raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type)
return float(value)
def _unmarshal_boolean(self, value, custom_formatters=None, strict=True):
if strict and not isinstance(value, (bool, )):
raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type)
return forcebool(value)
def _unmarshal_any(self, value, custom_formatters=None, strict=True):
types_resolve_order = [
SchemaType.OBJECT, SchemaType.ARRAY, SchemaType.BOOLEAN,
SchemaType.INTEGER, SchemaType.NUMBER, SchemaType.STRING,
@ -232,16 +254,21 @@ class Schema(object):
raise NoValidSchema(value)
def _unmarshal_collection(self, value, custom_formatters=None):
def _unmarshal_collection(self, value, custom_formatters=None, strict=True):
if not isinstance(value, (list, tuple)):
raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type)
if self.items is None:
raise UndefinedItemsSchema(self.type)
f = functools.partial(self.items.unmarshal,
custom_formatters=custom_formatters)
f = functools.partial(
self.items.unmarshal,
custom_formatters=custom_formatters, strict=strict,
)
return list(map(f, value))
def _unmarshal_object(self, value, model_factory=None,
custom_formatters=None):
custom_formatters=None, strict=True):
if not isinstance(value, (dict, )):
raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type)
@ -270,7 +297,7 @@ class Schema(object):
return model_factory.create(properties, name=self.model)
def _unmarshal_properties(self, value, one_of_schema=None,
custom_formatters=None):
custom_formatters=None, strict=True):
all_props = self.get_all_properties()
all_props_names = self.get_all_properties_names()
all_req_props_names = self.get_all_required_properties_names()

View file

@ -59,16 +59,7 @@ paths:
explode: false
responses:
'200':
description: An paged array of pets
headers:
x-next:
description: A link to the next page of responses
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/PetsData"
$ref: "#/components/responses/PetsResponse"
post:
summary: Create a pet
operationId: createPets
@ -327,9 +318,20 @@ components:
additionalProperties:
type: string
responses:
ErrorResponse:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/ExtendedError"
ErrorResponse:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/ExtendedError"
PetsResponse:
description: An paged array of pets
headers:
x-next:
description: A link to the next page of responses
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/PetsData"

View file

@ -17,8 +17,9 @@ from openapi_core.schema.parameters.models import Parameter
from openapi_core.schema.paths.models import Path
from openapi_core.schema.request_bodies.models import RequestBody
from openapi_core.schema.responses.models import Response
from openapi_core.schema.schemas.enums import SchemaType
from openapi_core.schema.schemas.exceptions import (
NoValidSchema,
NoValidSchema, InvalidSchemaProperty, InvalidSchemaValue,
)
from openapi_core.schema.schemas.models import Schema
from openapi_core.schema.servers.exceptions import InvalidServer
@ -234,6 +235,105 @@ class TestPetstore(object):
assert isinstance(response_result.data, BaseModel)
assert response_result.data.data == []
def test_get_pets_response(self, spec, response_validator):
host_url = 'http://petstore.swagger.io/v1'
path_pattern = '/v1/pets'
query_params = {
'limit': '20',
}
request = MockRequest(
host_url, 'GET', '/pets',
path_pattern=path_pattern, args=query_params,
)
parameters = request.get_parameters(spec)
body = request.get_body(spec)
assert parameters == {
'query': {
'limit': 20,
'page': 1,
'search': '',
}
}
assert body is None
data_json = {
'data': [
{
'id': 1,
'name': 'Cat',
}
],
}
data = json.dumps(data_json)
response = MockResponse(data)
response_result = response_validator.validate(request, response)
assert response_result.errors == []
assert isinstance(response_result.data, BaseModel)
assert len(response_result.data.data) == 1
assert response_result.data.data[0].id == 1
assert response_result.data.data[0].name == 'Cat'
def test_get_pets_invalid_response(self, spec, response_validator):
host_url = 'http://petstore.swagger.io/v1'
path_pattern = '/v1/pets'
query_params = {
'limit': '20',
}
request = MockRequest(
host_url, 'GET', '/pets',
path_pattern=path_pattern, args=query_params,
)
parameters = request.get_parameters(spec)
body = request.get_body(spec)
assert parameters == {
'query': {
'limit': 20,
'page': 1,
'search': '',
}
}
assert body is None
data_json = {
'data': [
{
'id': 1,
'name': {
'first_name': 'Cat',
},
}
],
}
data = json.dumps(data_json)
response = MockResponse(data)
response_result = response_validator.validate(request, response)
assert response_result.errors == [
InvalidMediaTypeValue(
original_exception=InvalidSchemaProperty(
property_name='data',
original_exception=InvalidSchemaProperty(
property_name='name',
original_exception=InvalidSchemaValue(
msg="Value {value} is not of type {type}",
type=SchemaType.STRING,
value={'first_name': 'Cat'},
),
),
),
),
]
assert response_result.data is None
def test_get_pets_ids_param(self, spec, response_validator):
host_url = 'http://petstore.swagger.io/v1'
path_pattern = '/v1/pets'
@ -419,7 +519,7 @@ class TestPetstore(object):
data_json = {
'name': pet_name,
'tag': pet_tag,
'position': '2',
'position': 2,
'address': {
'street': pet_street,
'city': pet_city,
@ -479,7 +579,7 @@ class TestPetstore(object):
data_json = {
'name': pet_name,
'tag': pet_tag,
'position': '2',
'position': 2,
'address': {
'street': pet_street,
'city': pet_city,
@ -535,11 +635,11 @@ class TestPetstore(object):
pet_tag = 'cats'
pet_street = 'Piekna'
pet_city = 'Warsaw'
pet_healthy = 'false'
pet_healthy = False
data_json = {
'name': pet_name,
'tag': pet_tag,
'position': '2',
'position': 2,
'address': {
'street': pet_street,
'city': pet_city,

View file

@ -155,7 +155,7 @@ class TestRequestValidator(object):
data_json = {
'name': pet_name,
'tag': pet_tag,
'position': '2',
'position': 2,
'address': {
'street': pet_street,
'city': pet_city,

View file

@ -48,6 +48,13 @@ class TestSchemaUnmarshal(object):
assert result == value
def test_string_float_invalid(self):
schema = Schema('string')
value = 1.23
with pytest.raises(InvalidSchemaValue):
schema.unmarshal(value)
def test_string_none(self):
schema = Schema('string')
value = None
@ -117,7 +124,7 @@ class TestSchemaUnmarshal(object):
value = 'x'
with mock.patch.dict(
Schema.STRING_FORMAT_CAST_CALLABLE_GETTER,
Schema.STRING_FORMAT_CALLABLE_GETTER,
{custom_format: mock.Mock(side_effect=ValueError())},
), pytest.raises(
InvalidSchemaValue, message='Failed to format value'
@ -126,12 +133,19 @@ class TestSchemaUnmarshal(object):
def test_integer_valid(self):
schema = Schema('integer')
value = '123'
value = 123
result = schema.unmarshal(value)
assert result == int(value)
def test_integer_string_invalid(self):
schema = Schema('integer')
value = '123'
with pytest.raises(InvalidSchemaValue):
schema.unmarshal(value)
def test_integer_enum_invalid(self):
schema = Schema('integer', enum=[1, 2, 3])
value = '123'
@ -141,12 +155,19 @@ class TestSchemaUnmarshal(object):
def test_integer_enum(self):
schema = Schema('integer', enum=[1, 2, 3])
value = '2'
value = 2
result = schema.unmarshal(value)
assert result == int(value)
def test_integer_enum_string_invalid(self):
schema = Schema('integer', enum=[1, 2, 3])
value = '2'
with pytest.raises(InvalidSchemaValue):
schema.unmarshal(value)
def test_integer_default(self):
default_value = '123'
schema = Schema('integer', default=default_value)
@ -171,6 +192,65 @@ class TestSchemaUnmarshal(object):
with pytest.raises(InvalidSchemaValue):
schema.unmarshal(value)
def test_array_valid(self):
schema = Schema('array', items=Schema('integer'))
value = [1, 2, 3]
result = schema.unmarshal(value)
assert result == value
def test_array_of_string_string_invalid(self):
schema = Schema('array', items=Schema('string'))
value = '123'
with pytest.raises(InvalidSchemaValue):
schema.unmarshal(value)
def test_array_of_integer_string_invalid(self):
schema = Schema('array', items=Schema('integer'))
value = '123'
with pytest.raises(InvalidSchemaValue):
schema.unmarshal(value)
def test_boolean_valid(self):
schema = Schema('boolean')
value = True
result = schema.unmarshal(value)
assert result == value
def test_boolean_string_invalid(self):
schema = Schema('boolean')
value = 'True'
with pytest.raises(InvalidSchemaValue):
schema.unmarshal(value)
def test_number_valid(self):
schema = Schema('number')
value = 1.23
result = schema.unmarshal(value)
assert result == value
def test_number_string_invalid(self):
schema = Schema('number')
value = '1.23'
with pytest.raises(InvalidSchemaValue):
schema.unmarshal(value)
def test_number_int_invalid(self):
schema = Schema('number')
value = 1
with pytest.raises(InvalidSchemaValue):
schema.unmarshal(value)
class TestSchemaValidate(object):
@ -763,3 +843,65 @@ class TestSchemaValidate(object):
with pytest.raises(Exception):
schema.validate(value)
@pytest.mark.parametrize('value', [
Model({
'someint': 123,
}),
Model({
'somestr': u('content'),
}),
Model({
'somestr': u('content'),
'someint': 123,
}),
])
def test_object_with_properties(self, value):
schema = Schema(
'object',
properties={
'somestr': Schema('string'),
'someint': Schema('integer'),
},
)
result = schema.validate(value)
assert result == value
@pytest.mark.parametrize('value', [
Model({
'somestr': Model(),
'someint': 123,
}),
Model({
'somestr': {},
'someint': 123,
}),
Model({
'somestr': [
'content1', 'content2'
],
'someint': 123,
}),
Model({
'somestr': 123,
'someint': 123,
}),
Model({
'somestr': 'content',
'someint': 123,
'not_in_scheme_prop': 123,
}),
])
def test_object_with_invalid_properties(self, value):
schema = Schema(
'object',
properties={
'somestr': Schema('string'),
'someint': Schema('integer'),
},
)
with pytest.raises(Exception):
schema.validate(value)