openapi-core/tests/unit/unmarshalling/test_unmarshal.py
2021-05-02 22:07:45 +01:00

767 lines
21 KiB
Python

import datetime
import uuid
from isodate.tzinfo import UTC, FixedOffset
import pytest
from openapi_core.spec.paths import SpecPath
from openapi_core.types import NoValue
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
from openapi_core.unmarshalling.schemas.exceptions import (
InvalidSchemaFormatValue, InvalidSchemaValue, UnmarshalError,
FormatterNotFoundError,
)
from openapi_core.unmarshalling.schemas.factories import (
SchemaUnmarshallersFactory,
)
from openapi_core.unmarshalling.schemas.formatters import Formatter
from openapi_core.unmarshalling.schemas.util import build_format_checker
@pytest.fixture
def unmarshaller_factory():
def create_unmarshaller(schema, custom_formatters=None, context=None):
custom_formatters = custom_formatters or {}
format_checker = build_format_checker(**custom_formatters)
return SchemaUnmarshallersFactory(
format_checker=format_checker,
custom_formatters=custom_formatters, context=context).create(
schema)
return create_unmarshaller
class TestUnmarshal(object):
def test_no_schema(self, unmarshaller_factory):
schema = None
value = 'test'
with pytest.raises(TypeError):
unmarshaller_factory(schema).unmarshal(value)
def test_schema_type_invalid(self, unmarshaller_factory):
spec = {
'type': 'integer',
}
schema = SpecPath.from_spec(spec)
value = 'test'
with pytest.raises(InvalidSchemaFormatValue):
unmarshaller_factory(schema).unmarshal(value)
def test_schema_custom_format_invalid(self, unmarshaller_factory):
class CustomFormatter(Formatter):
def unmarshal(self, value):
raise ValueError
formatter = CustomFormatter()
custom_format = 'custom'
custom_formatters = {
custom_format: formatter,
}
spec = {
'type': 'string',
'format': 'custom',
}
schema = SpecPath.from_spec(spec)
value = 'test'
with pytest.raises(InvalidSchemaFormatValue):
unmarshaller_factory(
schema,
custom_formatters=custom_formatters,
).unmarshal(value)
class TestSchemaUnmarshallerCall(object):
def test_deprecated(self, unmarshaller_factory):
spec = {
'type': 'string',
'deprecated': True,
}
schema = SpecPath.from_spec(spec)
value = 'test'
with pytest.warns(DeprecationWarning):
result = unmarshaller_factory(schema)(value)
assert result == value
@pytest.mark.parametrize('schema_type', [
'boolean', 'array', 'integer', 'number',
])
def test_non_string_empty_value(self, schema_type, unmarshaller_factory):
spec = {
'type': schema_type,
}
schema = SpecPath.from_spec(spec)
value = ''
with pytest.raises(InvalidSchemaValue):
unmarshaller_factory(schema)(value)
def test_string_valid(self, unmarshaller_factory):
spec = {
'type': 'string',
}
schema = SpecPath.from_spec(spec)
value = 'test'
result = unmarshaller_factory(schema)(value)
assert result == value
def test_string_format_uuid_valid(self, unmarshaller_factory):
spec = {
'type': 'string',
'format': 'uuid',
}
schema = SpecPath.from_spec(spec)
value = str(uuid.uuid4())
result = unmarshaller_factory(schema)(value)
assert result == uuid.UUID(value)
def test_string_format_uuid_uuid_quirks_invalid(
self, unmarshaller_factory):
spec = {
'type': 'string',
'format': 'uuid',
}
schema = SpecPath.from_spec(spec)
value = uuid.uuid4()
with pytest.raises(InvalidSchemaValue):
unmarshaller_factory(schema)(value)
def test_string_format_password(self, unmarshaller_factory):
spec = {
'type': 'string',
'format': 'password',
}
schema = SpecPath.from_spec(spec)
value = 'password'
result = unmarshaller_factory(schema)(value)
assert result == 'password'
def test_string_float_invalid(self, unmarshaller_factory):
spec = {
'type': 'string',
}
schema = SpecPath.from_spec(spec)
value = 1.23
with pytest.raises(InvalidSchemaValue):
unmarshaller_factory(schema)(value)
def test_string_default(self, unmarshaller_factory):
default_value = 'default'
spec = {
'type': 'string',
'default': default_value,
}
schema = SpecPath.from_spec(spec)
value = NoValue
result = unmarshaller_factory(schema)(value)
assert result == default_value
@pytest.mark.parametrize('default_value', ['default', None])
def test_string_default_nullable(
self, default_value, unmarshaller_factory):
spec = {
'type': 'string',
'default': default_value,
'nullable': True,
}
schema = SpecPath.from_spec(spec)
value = NoValue
result = unmarshaller_factory(schema)(value)
assert result == default_value
def test_string_format_date(self, unmarshaller_factory):
spec = {
'type': 'string',
'format': 'date',
}
schema = SpecPath.from_spec(spec)
value = '2018-01-02'
result = unmarshaller_factory(schema)(value)
assert result == datetime.date(2018, 1, 2)
def test_string_format_datetime_invalid(self, unmarshaller_factory):
spec = {
'type': 'string',
'format': 'date-time',
}
schema = SpecPath.from_spec(spec)
value = '2018-01-02T00:00:00'
with pytest.raises(InvalidSchemaValue):
unmarshaller_factory(schema)(value)
def test_string_format_datetime_utc(self, unmarshaller_factory):
spec = {
'type': 'string',
'format': 'date-time',
}
schema = SpecPath.from_spec(spec)
value = '2018-01-02T00:00:00Z'
result = unmarshaller_factory(schema)(value)
tzinfo = UTC
assert result == datetime.datetime(2018, 1, 2, 0, 0, tzinfo=tzinfo)
def test_string_format_datetime_tz(self, unmarshaller_factory):
spec = {
'type': 'string',
'format': 'date-time',
}
schema = SpecPath.from_spec(spec)
value = '2020-04-01T12:00:00+02:00'
result = unmarshaller_factory(schema)(value)
tzinfo = FixedOffset(2)
assert result == datetime.datetime(2020, 4, 1, 12, 0, 0, tzinfo=tzinfo)
def test_string_format_custom(self, unmarshaller_factory):
formatted = 'x-custom'
class CustomFormatter(Formatter):
def unmarshal(self, value):
return formatted
custom_format = 'custom'
spec = {
'type': 'string',
'format': custom_format,
}
schema = SpecPath.from_spec(spec)
value = 'x'
formatter = CustomFormatter()
custom_formatters = {
custom_format: formatter,
}
result = unmarshaller_factory(
schema, custom_formatters=custom_formatters)(value)
assert result == formatted
def test_string_format_custom_value_error(self, unmarshaller_factory):
class CustomFormatter(Formatter):
def unmarshal(self, value):
raise ValueError
custom_format = 'custom'
spec = {
'type': 'string',
'format': custom_format,
}
schema = SpecPath.from_spec(spec)
value = 'x'
formatter = CustomFormatter()
custom_formatters = {
custom_format: formatter,
}
with pytest.raises(InvalidSchemaFormatValue):
unmarshaller_factory(schema, custom_formatters=custom_formatters)(
value)
def test_string_format_unknown(self, unmarshaller_factory):
unknown_format = 'unknown'
spec = {
'type': 'string',
'format': unknown_format,
}
schema = SpecPath.from_spec(spec)
value = 'x'
with pytest.raises(FormatterNotFoundError):
unmarshaller_factory(schema)(value)
def test_string_format_invalid_value(self, unmarshaller_factory):
custom_format = 'custom'
spec = {
'type': 'string',
'format': custom_format,
}
schema = SpecPath.from_spec(spec)
value = 'x'
with pytest.raises(
FormatterNotFoundError,
message=(
'Formatter not found for custom format'
),
):
unmarshaller_factory(schema)(value)
def test_integer_valid(self, unmarshaller_factory):
spec = {
'type': 'integer',
}
schema = SpecPath.from_spec(spec)
value = 123
result = unmarshaller_factory(schema)(value)
assert result == int(value)
def test_integer_string_invalid(self, unmarshaller_factory):
spec = {
'type': 'integer',
}
schema = SpecPath.from_spec(spec)
value = '123'
with pytest.raises(InvalidSchemaValue):
unmarshaller_factory(schema)(value)
def test_integer_enum_invalid(self, unmarshaller_factory):
spec = {
'type': 'integer',
'enum': [1, 2, 3],
}
schema = SpecPath.from_spec(spec)
value = '123'
with pytest.raises(UnmarshalError):
unmarshaller_factory(schema)(value)
def test_integer_enum(self, unmarshaller_factory):
spec = {
'type': 'integer',
'enum': [1, 2, 3],
}
schema = SpecPath.from_spec(spec)
value = 2
result = unmarshaller_factory(schema)(value)
assert result == int(value)
def test_integer_enum_string_invalid(self, unmarshaller_factory):
spec = {
'type': 'integer',
'enum': [1, 2, 3],
}
schema = SpecPath.from_spec(spec)
value = '2'
with pytest.raises(UnmarshalError):
unmarshaller_factory(schema)(value)
def test_integer_default(self, unmarshaller_factory):
default_value = 123
spec = {
'type': 'integer',
'default': default_value,
}
schema = SpecPath.from_spec(spec)
value = NoValue
result = unmarshaller_factory(schema)(value)
assert result == default_value
def test_integer_default_nullable(self, unmarshaller_factory):
default_value = 123
spec = {
'type': 'integer',
'default': default_value,
'nullable': True,
}
schema = SpecPath.from_spec(spec)
value = None
result = unmarshaller_factory(schema)(value)
assert result is None
def test_integer_invalid(self, unmarshaller_factory):
spec = {
'type': 'integer',
}
schema = SpecPath.from_spec(spec)
value = 'abc'
with pytest.raises(InvalidSchemaValue):
unmarshaller_factory(schema)(value)
def test_array_valid(self, unmarshaller_factory):
spec = {
'type': 'array',
'items': {
'type': 'integer',
}
}
schema = SpecPath.from_spec(spec)
value = [1, 2, 3]
result = unmarshaller_factory(schema)(value)
assert result == value
def test_array_null(self, unmarshaller_factory):
spec = {
'type': 'array',
'items': {
'type': 'integer',
}
}
schema = SpecPath.from_spec(spec)
value = None
with pytest.raises(TypeError):
unmarshaller_factory(schema)(value)
def test_array_nullable(self, unmarshaller_factory):
spec = {
'type': 'array',
'items': {
'type': 'integer',
},
'nullable': True,
}
schema = SpecPath.from_spec(spec)
value = None
result = unmarshaller_factory(schema)(value)
assert result is None
def test_array_of_string_string_invalid(self, unmarshaller_factory):
spec = {
'type': 'array',
'items': {
'type': 'string',
}
}
schema = SpecPath.from_spec(spec)
value = '123'
with pytest.raises(InvalidSchemaValue):
unmarshaller_factory(schema)(value)
def test_array_of_integer_string_invalid(self, unmarshaller_factory):
spec = {
'type': 'array',
'items': {
'type': 'integer',
}
}
schema = SpecPath.from_spec(spec)
value = '123'
with pytest.raises(InvalidSchemaValue):
unmarshaller_factory(schema)(value)
def test_boolean_valid(self, unmarshaller_factory):
spec = {
'type': 'boolean',
}
schema = SpecPath.from_spec(spec)
value = True
result = unmarshaller_factory(schema)(value)
assert result == value
def test_boolean_string_invalid(self, unmarshaller_factory):
spec = {
'type': 'boolean',
}
schema = SpecPath.from_spec(spec)
value = 'True'
with pytest.raises(InvalidSchemaValue):
unmarshaller_factory(schema)(value)
def test_number_valid(self, unmarshaller_factory):
spec = {
'type': 'number',
}
schema = SpecPath.from_spec(spec)
value = 1.23
result = unmarshaller_factory(schema)(value)
assert result == value
def test_number_string_invalid(self, unmarshaller_factory):
spec = {
'type': 'number',
}
schema = SpecPath.from_spec(spec)
value = '1.23'
with pytest.raises(InvalidSchemaValue):
unmarshaller_factory(schema)(value)
def test_number_int(self, unmarshaller_factory):
spec = {
'type': 'number',
}
schema = SpecPath.from_spec(spec)
value = 1
result = unmarshaller_factory(schema)(value)
assert result == 1
assert type(result) == int
def test_number_float(self, unmarshaller_factory):
spec = {
'type': 'number',
}
schema = SpecPath.from_spec(spec)
value = 1.2
result = unmarshaller_factory(schema)(value)
assert result == 1.2
assert type(result) == float
def test_number_format_float(self, unmarshaller_factory):
spec = {
'type': 'number',
'format': 'float',
}
schema = SpecPath.from_spec(spec)
value = 1.2
result = unmarshaller_factory(schema)(value)
assert result == 1.2
def test_number_format_double(self, unmarshaller_factory):
spec = {
'type': 'number',
'format': 'double',
}
schema = SpecPath.from_spec(spec)
value = 1.2
result = unmarshaller_factory(schema)(value)
assert result == 1.2
def test_object_nullable(self, unmarshaller_factory):
spec = {
'type': 'object',
'properties': {
'foo': {
'type': 'object',
'nullable': True,
}
},
}
schema = SpecPath.from_spec(spec)
value = {'foo': None}
result = unmarshaller_factory(schema)(value)
assert result == {'foo': None}
def test_schema_any_one_of(self, unmarshaller_factory):
spec = {
'oneOf': [
{
'type': 'string',
},
{
'type': 'array',
'items': {
'type': 'string',
}
}
],
}
schema = SpecPath.from_spec(spec)
assert unmarshaller_factory(schema)(['hello']) == ['hello']
def test_schema_any_all_of(self, unmarshaller_factory):
spec = {
'allOf': [
{
'type': 'array',
'items': {
'type': 'string',
}
}
],
}
schema = SpecPath.from_spec(spec)
assert unmarshaller_factory(schema)(['hello']) == ['hello']
@pytest.mark.parametrize('value', [
{
'somestr': {},
'someint': 123,
},
{
'somestr': [
'content1', 'content2'
],
'someint': 123,
},
{
'somestr': 123,
'someint': 123,
},
{
'somestr': 'content',
'someint': 123,
'not_in_scheme_prop': 123,
},
])
def test_schema_any_all_of_invalid_properties(
self, value, unmarshaller_factory):
spec = {
'allOf': [
{
'type': 'object',
'required': ['somestr'],
'properties': {
'somestr': {
'type': 'string',
},
},
},
{
'type': 'object',
'required': ['someint'],
'properties': {
'someint': {
'type': 'integer',
},
},
}
],
'additionalProperties': False,
}
schema = SpecPath.from_spec(spec)
with pytest.raises(InvalidSchemaValue):
unmarshaller_factory(schema)(value)
def test_schema_any_all_of_any(self, unmarshaller_factory):
spec = {
'allOf': [
{},
{
'type': 'string',
'format': 'date',
},
],
}
schema = SpecPath.from_spec(spec)
value = '2018-01-02'
result = unmarshaller_factory(schema)(value)
assert result == datetime.date(2018, 1, 2)
def test_schema_any(self, unmarshaller_factory):
spec = {}
schema = SpecPath.from_spec(spec)
assert unmarshaller_factory(schema)('string') == 'string'
@pytest.mark.parametrize('value', [
{'additional': 1},
{'foo': 'bar', 'bar': 'foo'},
{'additional': {'bar': 1}},
])
@pytest.mark.parametrize('additional_properties', [True, {}])
def test_schema_free_form_object(
self, value, additional_properties, unmarshaller_factory):
spec = {
'type': 'object',
'additionalProperties': additional_properties,
}
schema = SpecPath.from_spec(spec)
result = unmarshaller_factory(schema)(value)
assert result == value
def test_read_only_properties(self, unmarshaller_factory):
spec = {
'type': 'object',
'required': ['id'],
'properties': {
'id': {
'type': 'integer',
'readOnly': True,
}
},
}
obj_schema = SpecPath.from_spec(spec)
# readOnly properties may be admitted in a Response context
result = unmarshaller_factory(
obj_schema, context=UnmarshalContext.RESPONSE)({"id": 10})
assert result == {
'id': 10,
}
def test_read_only_properties_invalid(self, unmarshaller_factory):
spec = {
'type': 'object',
'required': ['id'],
'properties': {
'id': {
'type': 'integer',
'readOnly': True,
}
},
}
obj_schema = SpecPath.from_spec(spec)
# readOnly properties are not admitted on a Request context
with pytest.raises(InvalidSchemaValue):
unmarshaller_factory(
obj_schema, context=UnmarshalContext.REQUEST)({"id": 10})
def test_write_only_properties(self, unmarshaller_factory):
spec = {
'type': 'object',
'required': ['id'],
'properties': {
'id': {
'type': 'integer',
'writeOnly': True,
}
},
}
obj_schema = SpecPath.from_spec(spec)
# readOnly properties may be admitted in a Response context
result = unmarshaller_factory(
obj_schema, context=UnmarshalContext.REQUEST)({"id": 10})
assert result == {
'id': 10,
}
def test_write_only_properties_invalid(self, unmarshaller_factory):
spec = {
'type': 'object',
'required': ['id'],
'properties': {
'id': {
'type': 'integer',
'writeOnly': True,
}
},
}
obj_schema = SpecPath.from_spec(spec)
# readOnly properties are not admitted on a Request context
with pytest.raises(InvalidSchemaValue):
unmarshaller_factory(
obj_schema, context=UnmarshalContext.RESPONSE)({"id": 10})