From baf042cbb688f3988a25b1d46776f243c6e46c36 Mon Sep 17 00:00:00 2001 From: Pedro Peixoto Date: Sat, 13 Jul 2019 08:42:44 -0300 Subject: [PATCH 1/6] Creating tests that comply with readOnly and writeOnly specifications --- .../data/v3.0/read_only_write_only.yaml | 39 +++++++++++ tests/integration/test_read_only.py | 65 +++++++++++++++++++ tests/integration/test_write_only.py | 63 ++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 tests/integration/data/v3.0/read_only_write_only.yaml create mode 100644 tests/integration/test_read_only.py create mode 100644 tests/integration/test_write_only.py diff --git a/tests/integration/data/v3.0/read_only_write_only.yaml b/tests/integration/data/v3.0/read_only_write_only.yaml new file mode 100644 index 0000000..be5a06a --- /dev/null +++ b/tests/integration/data/v3.0/read_only_write_only.yaml @@ -0,0 +1,39 @@ +openapi: "3.0.0" +info: + title: Specification Containing readOnly + version: "0.1" +paths: + /users: + post: + operationId: createUser + requestBody: + description: Post data for creating a user + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/User' + responses: + default: + description: Create a user + content: + application/json: + schema: + $ref: '#/components/schemas/User' +components: + schemas: + User: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int32 + readOnly: true + name: + type: string + hidden: + type: boolean + writeOnly: true \ No newline at end of file diff --git a/tests/integration/test_read_only.py b/tests/integration/test_read_only.py new file mode 100644 index 0000000..976aa68 --- /dev/null +++ b/tests/integration/test_read_only.py @@ -0,0 +1,65 @@ +import json + +import pytest + +from openapi_core.schema.media_types.exceptions import InvalidMediaTypeValue +from openapi_core.schema.schemas.enums import UnmarshalContext +from openapi_core.schema.schemas.exceptions import InvalidSchemaProperty +from openapi_core.shortcuts import create_spec +from openapi_core.validation.response.validators import ResponseValidator +from openapi_core.validation.request.validators import RequestValidator +from openapi_core.wrappers.mock import MockRequest, MockResponse + + +@pytest.fixture +def response_validator(spec): + return ResponseValidator(spec) + + +@pytest.fixture +def request_validator(spec): + return RequestValidator(spec) + + +@pytest.fixture('class') +def spec(factory): + spec_dict = factory.spec_from_file("data/v3.0/read_only_write_only.yaml") + return create_spec(spec_dict) + + +class TestReadOnly(object): + + def test_write_a_read_only_property(self, request_validator): + data = json.dumps({ + 'id': 10, + 'name': "Pedro" + }) + + request = MockRequest(host_url='', method='POST', + path='/users', data=data) + + with pytest.raises(InvalidMediaTypeValue) as ex: + request_validator.validate(request).raise_for_errors() + assert isinstance(ex.value.original_exception, InvalidSchemaProperty) + ex = ex.value.original_exception + + assert ex.property_name == 'id' + assert UnmarshalContext.REQUEST.value in str(ex.original_exception) + + def test_read_only_property_response(self, response_validator): + data = json.dumps({ + 'id': 10, + 'name': "Pedro" + }) + + request = MockRequest(host_url='', method='POST', + path='/users') + + response = MockResponse(data) + + is_valid = response_validator.validate(request, response) + is_valid.raise_for_errors() + + assert len(is_valid.errors) == 0 + assert is_valid.data.id == 10 + assert is_valid.data.name == "Pedro" diff --git a/tests/integration/test_write_only.py b/tests/integration/test_write_only.py new file mode 100644 index 0000000..695fd26 --- /dev/null +++ b/tests/integration/test_write_only.py @@ -0,0 +1,63 @@ +import json + +import pytest + +from openapi_core.schema.media_types.exceptions import InvalidMediaTypeValue +from openapi_core.schema.schemas.enums import UnmarshalContext +from openapi_core.schema.schemas.exceptions import InvalidSchemaProperty +from openapi_core.shortcuts import create_spec +from openapi_core.validation.response.validators import ResponseValidator +from openapi_core.validation.request.validators import RequestValidator +from openapi_core.wrappers.mock import MockRequest, MockResponse + + +@pytest.fixture +def response_validator(spec): + return ResponseValidator(spec) + + +@pytest.fixture +def request_validator(spec): + return RequestValidator(spec) + + +@pytest.fixture('class') +def spec(factory): + spec_dict = factory.spec_from_file("data/v3.0/read_only_write_only.yaml") + return create_spec(spec_dict) + + +class TestWriteOnly(object): + + def test_write_only_property(self, request_validator): + data = json.dumps({ + 'name': "Pedro", + 'hidden': False + }) + + request = MockRequest(host_url='', method='POST', + path='/users', data=data) + + is_valid = request_validator.validate(request) + is_valid.raise_for_errors() + assert is_valid.body.name == "Pedro" + assert is_valid.body.hidden is False + + def test_read_a_write_only_property(self, response_validator): + data = json.dumps({ + 'id': 10, + 'name': "Pedro", + 'hidden': True + }) + + request = MockRequest(host_url='', method='POST', + path='/users') + response = MockResponse(data) + + with pytest.raises(InvalidMediaTypeValue) as ex: + response_validator.validate(request, response).raise_for_errors() + assert isinstance(ex.value.original_exception, InvalidSchemaProperty) + ex = ex.value.original_exception + + assert ex.property_name == 'hidden' + assert UnmarshalContext.RESPONSE.value in str(ex.original_exception) From ca369f795a0c539ee7b3995af55ac032e3879fe1 Mon Sep 17 00:00:00 2001 From: Pedro Peixoto Date: Sat, 13 Jul 2019 08:46:18 -0300 Subject: [PATCH 2/6] Creating read_only and write_only properties on the Schema --- openapi_core/schema/schemas/factories.py | 4 +++- openapi_core/schema/schemas/models.py | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/openapi_core/schema/schemas/factories.py b/openapi_core/schema/schemas/factories.py index f92d598..55b48fe 100644 --- a/openapi_core/schema/schemas/factories.py +++ b/openapi_core/schema/schemas/factories.py @@ -46,6 +46,8 @@ class SchemaFactory(object): exclusive_maximum = schema_deref.get('exclusiveMaximum', False) min_properties = schema_deref.get('minProperties', None) max_properties = schema_deref.get('maxProperties', None) + read_only = schema_deref.get('readOnly', False) + write_only = schema_deref.get('writeOnly', False) extensions = self.extensions_generator.generate(schema_deref) @@ -81,7 +83,7 @@ class SchemaFactory(object): exclusive_maximum=exclusive_maximum, exclusive_minimum=exclusive_minimum, min_properties=min_properties, max_properties=max_properties, - extensions=extensions, + read_only=read_only, write_only=write_only, extensions=extensions, _source=schema_deref, ) diff --git a/openapi_core/schema/schemas/models.py b/openapi_core/schema/schemas/models.py index 37fe06f..15e409c 100644 --- a/openapi_core/schema/schemas/models.py +++ b/openapi_core/schema/schemas/models.py @@ -26,7 +26,8 @@ class Schema(object): min_length=None, max_length=None, pattern=None, unique_items=False, minimum=None, maximum=None, multiple_of=None, exclusive_minimum=False, exclusive_maximum=False, - min_properties=None, max_properties=None, extensions=None, + min_properties=None, max_properties=None, + read_only=False, write_only=False, extensions=None, _source=None): self.type = SchemaType(schema_type) self.properties = properties and dict(properties) or {} @@ -56,6 +57,11 @@ class Schema(object): if min_properties is not None else None self.max_properties = int(max_properties)\ if max_properties is not None else None + self.read_only = read_only + self.write_only = write_only + + if self.read_only and self.write_only: + raise OpenAPISchemaError("Schema cannot be readOnly AND writeOnly") self.extensions = extensions and dict(extensions) or {} From aa95bc5d02aeb2f52c7e983ff4fe7f28772f4380 Mon Sep 17 00:00:00 2001 From: Pedro Peixoto Date: Sat, 13 Jul 2019 08:51:45 -0300 Subject: [PATCH 3/6] Providing a context whenever unmarshalling objects on Request and Responses (readOnly and writeOnly applies on to properties) --- openapi_core/schema/schemas/enums.py | 5 +++++ openapi_core/validation/request/validators.py | 3 +++ openapi_core/validation/response/validators.py | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/openapi_core/schema/schemas/enums.py b/openapi_core/schema/schemas/enums.py index 8b77e7c..3cfe035 100644 --- a/openapi_core/schema/schemas/enums.py +++ b/openapi_core/schema/schemas/enums.py @@ -26,3 +26,8 @@ class SchemaFormat(Enum): DATETIME = 'date-time' PASSWORD = 'password' UUID = 'uuid' + + +class UnmarshalContext(Enum): + REQUEST = 'request' + RESPONSE = 'response' diff --git a/openapi_core/validation/request/validators.py b/openapi_core/validation/request/validators.py index ccfe380..df07e05 100644 --- a/openapi_core/validation/request/validators.py +++ b/openapi_core/validation/request/validators.py @@ -11,6 +11,7 @@ from openapi_core.schema.parameters.exceptions import ( ) from openapi_core.schema.paths.exceptions import InvalidPath from openapi_core.schema.request_bodies.exceptions import MissingRequestBody +from openapi_core.schema.schemas.enums import UnmarshalContext from openapi_core.schema.servers.exceptions import InvalidServer from openapi_core.security.exceptions import SecurityError from openapi_core.unmarshalling.schemas.exceptions import ( @@ -23,6 +24,8 @@ from openapi_core.validation.request.datatypes import ( from openapi_core.validation.util import get_operation_pattern +_CONTEXT = UnmarshalContext.REQUEST + class RequestValidator(object): def __init__( diff --git a/openapi_core/validation/response/validators.py b/openapi_core/validation/response/validators.py index 241e8d9..8e93057 100644 --- a/openapi_core/validation/response/validators.py +++ b/openapi_core/validation/response/validators.py @@ -6,6 +6,7 @@ from openapi_core.schema.media_types.exceptions import InvalidContentType from openapi_core.schema.responses.exceptions import ( InvalidResponse, MissingResponseContent, ) +from openapi_core.schema.schemas.enums import UnmarshalContext from openapi_core.schema.servers.exceptions import InvalidServer from openapi_core.unmarshalling.schemas.exceptions import ( UnmarshalError, ValidateError, @@ -13,6 +14,8 @@ from openapi_core.unmarshalling.schemas.exceptions import ( from openapi_core.validation.response.datatypes import ResponseValidationResult from openapi_core.validation.util import get_operation_pattern +_CONTEXT = UnmarshalContext.RESPONSE + class ResponseValidator(object): @@ -73,6 +76,7 @@ class ResponseValidator(object): try: media_type = operation_response[response.mimetype] + except InvalidContentType as exc: return None, [exc, ] From 8260426b9177862ffa11558b82f556ba675077d7 Mon Sep 17 00:00:00 2001 From: Pedro Peixoto Date: Sun, 14 Jul 2019 00:25:40 -0300 Subject: [PATCH 4/6] Creating a decorator that warns the developer whenever he fails to set a context. --- openapi_core/schema/schemas/util.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openapi_core/schema/schemas/util.py b/openapi_core/schema/schemas/util.py index dcbf2fd..17eb670 100644 --- a/openapi_core/schema/schemas/util.py +++ b/openapi_core/schema/schemas/util.py @@ -3,6 +3,9 @@ from distutils.util import strtobool from six import string_types from json import dumps +from openapi_core.schema.schemas.enums import UnmarshalContext +from openapi_core.schema.schemas.exceptions import UnmarshalContextNotSet + def forcebool(val): if isinstance(val, string_types): From 975ac0d7c9982523e880ca0ccd7df3302068d4f6 Mon Sep 17 00:00:00 2001 From: Pedro Peixoto Date: Sun, 14 Jul 2019 00:39:00 -0300 Subject: [PATCH 5/6] Updating unmarshal and validation methods' signatures to propagate the context until reaching the properties. --- openapi_core/schema/parameters/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openapi_core/schema/parameters/models.py b/openapi_core/schema/parameters/models.py index e99194f..18598df 100644 --- a/openapi_core/schema/parameters/models.py +++ b/openapi_core/schema/parameters/models.py @@ -9,6 +9,9 @@ from openapi_core.schema.schemas.enums import SchemaType log = logging.getLogger(__name__) +_CONTEXT = UnmarshalContext.REQUEST + + class Parameter(object): """Represents an OpenAPI operation Parameter.""" From 1bea6013c3a70895330594575a1e7a9d3f2a0a72 Mon Sep 17 00:00:00 2001 From: Artur Maciag Date: Mon, 17 Feb 2020 12:40:32 +0000 Subject: [PATCH 6/6] Property read-only and write-only support --- openapi_core/schema/parameters/models.py | 3 - openapi_core/schema/schemas/enums.py | 5 - openapi_core/schema/schemas/models.py | 3 - openapi_core/schema/schemas/util.py | 3 - openapi_core/schema_validator/_validators.py | 29 ++++++ openapi_core/schema_validator/validators.py | 11 ++- openapi_core/unmarshalling/schemas/enums.py | 7 ++ .../unmarshalling/schemas/factories.py | 32 ++++-- .../unmarshalling/schemas/unmarshallers.py | 10 +- openapi_core/validation/request/validators.py | 8 +- .../validation/response/validators.py | 9 +- tests/integration/schema/test_spec.py | 6 +- tests/integration/test_read_only.py | 65 ------------- tests/integration/test_write_only.py | 63 ------------ .../validation/test_read_only_write_only.py | 97 +++++++++++++++++++ tests/unit/unmarshalling/test_unmarshal.py | 48 ++++++++- 16 files changed, 233 insertions(+), 166 deletions(-) create mode 100644 openapi_core/unmarshalling/schemas/enums.py delete mode 100644 tests/integration/test_read_only.py delete mode 100644 tests/integration/test_write_only.py create mode 100644 tests/integration/validation/test_read_only_write_only.py diff --git a/openapi_core/schema/parameters/models.py b/openapi_core/schema/parameters/models.py index 18598df..e99194f 100644 --- a/openapi_core/schema/parameters/models.py +++ b/openapi_core/schema/parameters/models.py @@ -9,9 +9,6 @@ from openapi_core.schema.schemas.enums import SchemaType log = logging.getLogger(__name__) -_CONTEXT = UnmarshalContext.REQUEST - - class Parameter(object): """Represents an OpenAPI operation Parameter.""" diff --git a/openapi_core/schema/schemas/enums.py b/openapi_core/schema/schemas/enums.py index 3cfe035..8b77e7c 100644 --- a/openapi_core/schema/schemas/enums.py +++ b/openapi_core/schema/schemas/enums.py @@ -26,8 +26,3 @@ class SchemaFormat(Enum): DATETIME = 'date-time' PASSWORD = 'password' UUID = 'uuid' - - -class UnmarshalContext(Enum): - REQUEST = 'request' - RESPONSE = 'response' diff --git a/openapi_core/schema/schemas/models.py b/openapi_core/schema/schemas/models.py index 15e409c..a4109c4 100644 --- a/openapi_core/schema/schemas/models.py +++ b/openapi_core/schema/schemas/models.py @@ -60,9 +60,6 @@ class Schema(object): self.read_only = read_only self.write_only = write_only - if self.read_only and self.write_only: - raise OpenAPISchemaError("Schema cannot be readOnly AND writeOnly") - self.extensions = extensions and dict(extensions) or {} self._all_required_properties_cache = None diff --git a/openapi_core/schema/schemas/util.py b/openapi_core/schema/schemas/util.py index 17eb670..dcbf2fd 100644 --- a/openapi_core/schema/schemas/util.py +++ b/openapi_core/schema/schemas/util.py @@ -3,9 +3,6 @@ from distutils.util import strtobool from six import string_types from json import dumps -from openapi_core.schema.schemas.enums import UnmarshalContext -from openapi_core.schema.schemas.exceptions import UnmarshalContextNotSet - def forcebool(val): if isinstance(val, string_types): diff --git a/openapi_core/schema_validator/_validators.py b/openapi_core/schema_validator/_validators.py index fc5a4ba..fdcdeae 100644 --- a/openapi_core/schema_validator/_validators.py +++ b/openapi_core/schema_validator/_validators.py @@ -35,6 +35,19 @@ def nullable(validator, is_nullable, instance, schema): yield ValidationError("None for not nullable") +def required(validator, required, instance, schema): + if not validator.is_type(instance, "object"): + return + for property in required: + if property not in instance: + prop_schema = schema['properties'][property] + read_only = prop_schema.get('readOnly', False) + write_only = prop_schema.get('writeOnly', False) + if validator.write and read_only or validator.read and write_only: + continue + yield ValidationError("%r is a required property" % property) + + def additionalProperties(validator, aP, instance, schema): if not validator.is_type(instance, "object"): return @@ -54,5 +67,21 @@ def additionalProperties(validator, aP, instance, schema): yield ValidationError(error % extras_msg(extras)) +def readOnly(validator, ro, instance, schema): + if not validator.write or not ro: + return + + yield ValidationError( + "Tried to write read-only proparty with %s" % (instance)) + + +def writeOnly(validator, wo, instance, schema): + if not validator.read or not wo: + return + + yield ValidationError( + "Tried to read write-only proparty with %s" % (instance)) + + def not_implemented(validator, value, instance, schema): pass diff --git a/openapi_core/schema_validator/validators.py b/openapi_core/schema_validator/validators.py index 14f0402..198d42a 100644 --- a/openapi_core/schema_validator/validators.py +++ b/openapi_core/schema_validator/validators.py @@ -21,7 +21,6 @@ BaseOAS30Validator = create( u"uniqueItems": _validators.uniqueItems, u"maxProperties": _validators.maxProperties, u"minProperties": _validators.minProperties, - u"required": _validators.required, u"enum": _validators.enum, # adjusted to OAS u"type": oas_validators.type, @@ -31,6 +30,7 @@ BaseOAS30Validator = create( u"not": _validators.not_, u"items": oas_validators.items, u"properties": _validators.properties, + u"required": oas_validators.required, u"additionalProperties": oas_validators.additionalProperties, # TODO: adjust description u"format": oas_validators.format, @@ -39,8 +39,8 @@ BaseOAS30Validator = create( # fixed OAS fields u"nullable": oas_validators.nullable, u"discriminator": oas_validators.not_implemented, - u"readOnly": oas_validators.not_implemented, - u"writeOnly": oas_validators.not_implemented, + u"readOnly": oas_validators.readOnly, + u"writeOnly": oas_validators.writeOnly, u"xml": oas_validators.not_implemented, u"externalDocs": oas_validators.not_implemented, u"example": oas_validators.not_implemented, @@ -54,6 +54,11 @@ BaseOAS30Validator = create( class OAS30Validator(BaseOAS30Validator): + def __init__(self, *args, **kwargs): + self.read = kwargs.pop('read', None) + self.write = kwargs.pop('write', None) + super(OAS30Validator, self).__init__(*args, **kwargs) + def iter_errors(self, instance, _schema=None): if _schema is None: _schema = self.schema diff --git a/openapi_core/unmarshalling/schemas/enums.py b/openapi_core/unmarshalling/schemas/enums.py new file mode 100644 index 0000000..ffe4ed5 --- /dev/null +++ b/openapi_core/unmarshalling/schemas/enums.py @@ -0,0 +1,7 @@ +"""OpenAPI core unmarshalling schemas enums module""" +from enum import Enum + + +class UnmarshalContext(Enum): + REQUEST = 'request' + RESPONSE = 'response' diff --git a/openapi_core/unmarshalling/schemas/factories.py b/openapi_core/unmarshalling/schemas/factories.py index 2222420..3c699a0 100644 --- a/openapi_core/unmarshalling/schemas/factories.py +++ b/openapi_core/unmarshalling/schemas/factories.py @@ -5,6 +5,7 @@ from openapi_core.schema.schemas.enums import SchemaType, SchemaFormat from openapi_core.schema.schemas.models import Schema from openapi_core.schema_validator import OAS30Validator from openapi_core.schema_validator import oas30_format_checker +from openapi_core.unmarshalling.schemas.enums import UnmarshalContext from openapi_core.unmarshalling.schemas.exceptions import ( FormatterNotFoundError, ) @@ -29,11 +30,17 @@ class SchemaUnmarshallersFactory(object): SchemaType.ANY: AnyUnmarshaller, } - def __init__(self, resolver=None, custom_formatters=None): + CONTEXT_VALIDATION = { + UnmarshalContext.REQUEST: 'write', + UnmarshalContext.RESPONSE: 'read', + } + + def __init__(self, resolver=None, custom_formatters=None, context=None): self.resolver = resolver if custom_formatters is None: custom_formatters = {} self.custom_formatters = custom_formatters + self.context = context def create(self, schema, type_override=None): """Create unmarshaller from the schema.""" @@ -50,7 +57,9 @@ class SchemaUnmarshallersFactory(object): elif schema_type in self.COMPLEX_UNMARSHALLERS: klass = self.COMPLEX_UNMARSHALLERS[schema_type] kwargs = dict( - schema=schema, unmarshallers_factory=self) + schema=schema, unmarshallers_factory=self, + context=self.context, + ) formatter = self.get_formatter(klass.FORMATTERS, schema.format) @@ -70,10 +79,17 @@ class SchemaUnmarshallersFactory(object): return default_formatters.get(schema_format) def get_validator(self, schema): - format_checker = deepcopy(oas30_format_checker) + format_checker = self._get_format_checker() + kwargs = { + 'resolver': self.resolver, + 'format_checker': format_checker, + } + if self.context is not None: + kwargs[self.CONTEXT_VALIDATION[self.context]] = True + return OAS30Validator(schema.__dict__, **kwargs) + + def _get_format_checker(self): + fc = deepcopy(oas30_format_checker) for name, formatter in self.custom_formatters.items(): - format_checker.checks(name)(formatter.validate) - return OAS30Validator( - schema.__dict__, - resolver=self.resolver, format_checker=format_checker, - ) + fc.checks(name)(formatter.validate) + return fc diff --git a/openapi_core/unmarshalling/schemas/unmarshallers.py b/openapi_core/unmarshalling/schemas/unmarshallers.py index c275a39..95913db 100644 --- a/openapi_core/unmarshalling/schemas/unmarshallers.py +++ b/openapi_core/unmarshalling/schemas/unmarshallers.py @@ -13,6 +13,7 @@ from openapi_core.schema_validator._types import ( is_object, is_number, is_string, ) from openapi_core.schema_validator._format import oas30_format_checker +from openapi_core.unmarshalling.schemas.enums import UnmarshalContext from openapi_core.unmarshalling.schemas.exceptions import ( UnmarshalError, ValidateError, InvalidSchemaValue, InvalidSchemaFormatValue, @@ -120,9 +121,12 @@ class BooleanUnmarshaller(PrimitiveTypeUnmarshaller): class ComplexUnmarshaller(PrimitiveTypeUnmarshaller): - def __init__(self, formatter, validator, schema, unmarshallers_factory): + def __init__( + self, formatter, validator, schema, unmarshallers_factory, + context=None): super(ComplexUnmarshaller, self).__init__(formatter, validator, schema) self.unmarshallers_factory = unmarshallers_factory + self.context = context class ArrayUnmarshaller(ComplexUnmarshaller): @@ -206,6 +210,10 @@ class ObjectUnmarshaller(ComplexUnmarshaller): properties[prop_name] = prop_value for prop_name, prop in iteritems(all_props): + if self.context == UnmarshalContext.REQUEST and prop.read_only: + continue + if self.context == UnmarshalContext.RESPONSE and prop.write_only: + continue try: prop_value = value[prop_name] except KeyError: diff --git a/openapi_core/validation/request/validators.py b/openapi_core/validation/request/validators.py index df07e05..cab719d 100644 --- a/openapi_core/validation/request/validators.py +++ b/openapi_core/validation/request/validators.py @@ -11,9 +11,9 @@ from openapi_core.schema.parameters.exceptions import ( ) from openapi_core.schema.paths.exceptions import InvalidPath from openapi_core.schema.request_bodies.exceptions import MissingRequestBody -from openapi_core.schema.schemas.enums import UnmarshalContext from openapi_core.schema.servers.exceptions import InvalidServer from openapi_core.security.exceptions import SecurityError +from openapi_core.unmarshalling.schemas.enums import UnmarshalContext from openapi_core.unmarshalling.schemas.exceptions import ( UnmarshalError, ValidateError, ) @@ -24,8 +24,6 @@ from openapi_core.validation.request.datatypes import ( from openapi_core.validation.util import get_operation_pattern -_CONTEXT = UnmarshalContext.REQUEST - class RequestValidator(object): def __init__( @@ -259,7 +257,9 @@ class RequestValidator(object): SchemaUnmarshallersFactory, ) unmarshallers_factory = SchemaUnmarshallersFactory( - self.spec._resolver, self.custom_formatters) + self.spec._resolver, self.custom_formatters, + context=UnmarshalContext.REQUEST, + ) unmarshaller = unmarshallers_factory.create( param_or_media_type.schema) return unmarshaller(value) diff --git a/openapi_core/validation/response/validators.py b/openapi_core/validation/response/validators.py index 8e93057..0bf9913 100644 --- a/openapi_core/validation/response/validators.py +++ b/openapi_core/validation/response/validators.py @@ -6,16 +6,14 @@ from openapi_core.schema.media_types.exceptions import InvalidContentType from openapi_core.schema.responses.exceptions import ( InvalidResponse, MissingResponseContent, ) -from openapi_core.schema.schemas.enums import UnmarshalContext from openapi_core.schema.servers.exceptions import InvalidServer +from openapi_core.unmarshalling.schemas.enums import UnmarshalContext from openapi_core.unmarshalling.schemas.exceptions import ( UnmarshalError, ValidateError, ) from openapi_core.validation.response.datatypes import ResponseValidationResult from openapi_core.validation.util import get_operation_pattern -_CONTEXT = UnmarshalContext.RESPONSE - class ResponseValidator(object): @@ -76,7 +74,6 @@ class ResponseValidator(object): try: media_type = operation_response[response.mimetype] - except InvalidContentType as exc: return None, [exc, ] @@ -143,7 +140,9 @@ class ResponseValidator(object): SchemaUnmarshallersFactory, ) unmarshallers_factory = SchemaUnmarshallersFactory( - self.spec._resolver, self.custom_formatters) + self.spec._resolver, self.custom_formatters, + context=UnmarshalContext.RESPONSE, + ) unmarshaller = unmarshallers_factory.create( param_or_media_type.schema) return unmarshaller(value) diff --git a/tests/integration/schema/test_spec.py b/tests/integration/schema/test_spec.py index 7456589..a2e31f0 100644 --- a/tests/integration/schema/test_spec.py +++ b/tests/integration/schema/test_spec.py @@ -268,5 +268,9 @@ class TestPetstore(object): if not spec.components: return - for _, schema in iteritems(spec.components.schemas): + for schema_name, schema in iteritems(spec.components.schemas): assert type(schema) == Schema + + schema_spec = spec_dict['components']['schemas'][schema_name] + assert schema.read_only == schema_spec.get('readOnly', False) + assert schema.write_only == schema_spec.get('writeOnly', False) diff --git a/tests/integration/test_read_only.py b/tests/integration/test_read_only.py deleted file mode 100644 index 976aa68..0000000 --- a/tests/integration/test_read_only.py +++ /dev/null @@ -1,65 +0,0 @@ -import json - -import pytest - -from openapi_core.schema.media_types.exceptions import InvalidMediaTypeValue -from openapi_core.schema.schemas.enums import UnmarshalContext -from openapi_core.schema.schemas.exceptions import InvalidSchemaProperty -from openapi_core.shortcuts import create_spec -from openapi_core.validation.response.validators import ResponseValidator -from openapi_core.validation.request.validators import RequestValidator -from openapi_core.wrappers.mock import MockRequest, MockResponse - - -@pytest.fixture -def response_validator(spec): - return ResponseValidator(spec) - - -@pytest.fixture -def request_validator(spec): - return RequestValidator(spec) - - -@pytest.fixture('class') -def spec(factory): - spec_dict = factory.spec_from_file("data/v3.0/read_only_write_only.yaml") - return create_spec(spec_dict) - - -class TestReadOnly(object): - - def test_write_a_read_only_property(self, request_validator): - data = json.dumps({ - 'id': 10, - 'name': "Pedro" - }) - - request = MockRequest(host_url='', method='POST', - path='/users', data=data) - - with pytest.raises(InvalidMediaTypeValue) as ex: - request_validator.validate(request).raise_for_errors() - assert isinstance(ex.value.original_exception, InvalidSchemaProperty) - ex = ex.value.original_exception - - assert ex.property_name == 'id' - assert UnmarshalContext.REQUEST.value in str(ex.original_exception) - - def test_read_only_property_response(self, response_validator): - data = json.dumps({ - 'id': 10, - 'name': "Pedro" - }) - - request = MockRequest(host_url='', method='POST', - path='/users') - - response = MockResponse(data) - - is_valid = response_validator.validate(request, response) - is_valid.raise_for_errors() - - assert len(is_valid.errors) == 0 - assert is_valid.data.id == 10 - assert is_valid.data.name == "Pedro" diff --git a/tests/integration/test_write_only.py b/tests/integration/test_write_only.py deleted file mode 100644 index 695fd26..0000000 --- a/tests/integration/test_write_only.py +++ /dev/null @@ -1,63 +0,0 @@ -import json - -import pytest - -from openapi_core.schema.media_types.exceptions import InvalidMediaTypeValue -from openapi_core.schema.schemas.enums import UnmarshalContext -from openapi_core.schema.schemas.exceptions import InvalidSchemaProperty -from openapi_core.shortcuts import create_spec -from openapi_core.validation.response.validators import ResponseValidator -from openapi_core.validation.request.validators import RequestValidator -from openapi_core.wrappers.mock import MockRequest, MockResponse - - -@pytest.fixture -def response_validator(spec): - return ResponseValidator(spec) - - -@pytest.fixture -def request_validator(spec): - return RequestValidator(spec) - - -@pytest.fixture('class') -def spec(factory): - spec_dict = factory.spec_from_file("data/v3.0/read_only_write_only.yaml") - return create_spec(spec_dict) - - -class TestWriteOnly(object): - - def test_write_only_property(self, request_validator): - data = json.dumps({ - 'name': "Pedro", - 'hidden': False - }) - - request = MockRequest(host_url='', method='POST', - path='/users', data=data) - - is_valid = request_validator.validate(request) - is_valid.raise_for_errors() - assert is_valid.body.name == "Pedro" - assert is_valid.body.hidden is False - - def test_read_a_write_only_property(self, response_validator): - data = json.dumps({ - 'id': 10, - 'name': "Pedro", - 'hidden': True - }) - - request = MockRequest(host_url='', method='POST', - path='/users') - response = MockResponse(data) - - with pytest.raises(InvalidMediaTypeValue) as ex: - response_validator.validate(request, response).raise_for_errors() - assert isinstance(ex.value.original_exception, InvalidSchemaProperty) - ex = ex.value.original_exception - - assert ex.property_name == 'hidden' - assert UnmarshalContext.RESPONSE.value in str(ex.original_exception) diff --git a/tests/integration/validation/test_read_only_write_only.py b/tests/integration/validation/test_read_only_write_only.py new file mode 100644 index 0000000..08cc689 --- /dev/null +++ b/tests/integration/validation/test_read_only_write_only.py @@ -0,0 +1,97 @@ +import json + +import pytest + +from openapi_core.shortcuts import create_spec +from openapi_core.unmarshalling.schemas.exceptions import InvalidSchemaValue +from openapi_core.validation.response.validators import ResponseValidator +from openapi_core.validation.request.validators import RequestValidator +from openapi_core.testing import MockRequest, MockResponse + + +@pytest.fixture +def response_validator(spec): + return ResponseValidator(spec) + + +@pytest.fixture +def request_validator(spec): + return RequestValidator(spec) + + +@pytest.fixture('class') +def spec(factory): + spec_dict = factory.spec_from_file("data/v3.0/read_only_write_only.yaml") + return create_spec(spec_dict) + + +class TestReadOnly(object): + + def test_write_a_read_only_property(self, request_validator): + data = json.dumps({ + 'id': 10, + 'name': "Pedro", + }) + + request = MockRequest(host_url='', method='POST', + path='/users', data=data) + + result = request_validator.validate(request) + + assert type(result.errors[0]) == InvalidSchemaValue + assert result.body is None + + def test_read_only_property_response(self, response_validator): + data = json.dumps({ + 'id': 10, + 'name': "Pedro", + }) + + request = MockRequest(host_url='', method='POST', + path='/users') + + response = MockResponse(data) + + result = response_validator.validate(request, response) + + assert not result.errors + assert result.data == { + 'id': 10, + 'name': "Pedro", + } + + +class TestWriteOnly(object): + + def test_write_only_property(self, request_validator): + data = json.dumps({ + 'name': "Pedro", + 'hidden': False, + }) + + request = MockRequest(host_url='', method='POST', + path='/users', data=data) + + result = request_validator.validate(request) + + assert not result.errors + assert result.body == { + 'name': "Pedro", + 'hidden': False, + } + + def test_read_a_write_only_property(self, response_validator): + data = json.dumps({ + 'id': 10, + 'name': "Pedro", + 'hidden': True, + }) + + request = MockRequest(host_url='', method='POST', + path='/users') + response = MockResponse(data) + + result = response_validator.validate(request, response) + + assert type(result.errors[0]) == InvalidSchemaValue + assert result.data is None diff --git a/tests/unit/unmarshalling/test_unmarshal.py b/tests/unit/unmarshalling/test_unmarshal.py index ae6c88c..e8c7609 100644 --- a/tests/unit/unmarshalling/test_unmarshal.py +++ b/tests/unit/unmarshalling/test_unmarshal.py @@ -8,6 +8,7 @@ from openapi_core.schema.parameters.models import Parameter from openapi_core.schema.schemas.enums import SchemaType from openapi_core.schema.schemas.models import Schema from openapi_core.schema.schemas.types import NoValue +from openapi_core.unmarshalling.schemas.enums import UnmarshalContext from openapi_core.unmarshalling.schemas.exceptions import ( InvalidSchemaFormatValue, InvalidSchemaValue, UnmarshalError, FormatterNotFoundError, @@ -20,9 +21,10 @@ from openapi_core.unmarshalling.schemas.formatters import Formatter @pytest.fixture def unmarshaller_factory(): - def create_unmarshaller(schema, custom_formatters=None): + def create_unmarshaller(schema, custom_formatters=None, context=None): return SchemaUnmarshallersFactory( - custom_formatters=custom_formatters).create(schema) + custom_formatters=custom_formatters, context=context).create( + schema) return create_unmarshaller @@ -429,3 +431,45 @@ class TestSchemaUnmarshallerCall(object): result = unmarshaller_factory(schema)(value) assert result == value + + def test_read_only_properties(self, unmarshaller_factory): + id_property = Schema('integer', read_only=True) + + def properties(): + yield ('id', id_property) + + obj_schema = Schema('object', properties=properties(), required=['id']) + + # readOnly properties may be admitted in a Response context + result = unmarshaller_factory( + obj_schema, context=UnmarshalContext.RESPONSE)({"id": 10}) + assert result == { + 'id': 10, + } + + # readOnly properties are not admitted on a Request context + result = unmarshaller_factory( + obj_schema, context=UnmarshalContext.REQUEST)({"id": 10}) + + assert result == {} + + def test_write_only_properties(self, unmarshaller_factory): + id_property = Schema('integer', write_only=True) + + def properties(): + yield ('id', id_property) + + obj_schema = Schema('object', properties=properties(), required=['id']) + + # readOnly properties may be admitted in a Response context + result = unmarshaller_factory( + obj_schema, context=UnmarshalContext.REQUEST)({"id": 10}) + assert result == { + 'id': 10, + } + + # readOnly properties are not admitted on a Request context + result = unmarshaller_factory( + obj_schema, context=UnmarshalContext.RESPONSE)({"id": 10}) + + assert result == {}