diff --git a/README.rst b/README.rst index 7764f6a6..8dd3d87d 100644 --- a/README.rst +++ b/README.rst @@ -18,13 +18,9 @@ OpenAPI Spec validator About ##### -OpenAPI Spec Validator is a Python library that validates OpenAPI Specs -against the `OpenAPI 2.0 (aka -Swagger) `__ -and `OpenAPI -3.0 `__ -specification. The validator aims to check for full compliance with the -Specification. +OpenAPI Spec Validator is a Python library that validates OpenAPI Specs against the `OpenAPI 2.0 (aka Swagger) `__, `OpenAPI 3.0 `__ and `OpenAPI 3.1 `__ specification. + +The validator aims to check for full compliance with the Specification. Installation ############ diff --git a/openapi_spec_validator/__init__.py b/openapi_spec_validator/__init__.py index 6ffd2540..2cc71745 100644 --- a/openapi_spec_validator/__init__.py +++ b/openapi_spec_validator/__init__.py @@ -1,10 +1,14 @@ # -*- coding: utf-8 -*- +from jsonschema.validators import Draft4Validator, Draft7Validator + from openapi_spec_validator.shortcuts import ( validate_spec_factory, validate_spec_url_factory, ) from openapi_spec_validator.handlers import UrlHandler, FileObjectHandler from openapi_spec_validator.schemas import get_openapi_schema -from openapi_spec_validator.factories import JSONSpecValidatorFactory +from openapi_spec_validator.factories import ( + OpenAPIValidatorFactory, OpenAPISpecValidatorFactory, +) from openapi_spec_validator.validators import SpecValidator __author__ = 'Artur Maciag' @@ -14,9 +18,16 @@ __license__ = 'Apache License, Version 2.0' __all__ = [ - 'openapi_v2_spec_validator', 'openapi_v3_spec_validator', - 'validate_v2_spec', 'validate_v3_spec', 'validate_spec', - 'validate_v2_spec_url', 'validate_v3_spec_url', 'validate_spec_url', + 'validate_spec', 'validate_spec_url', + 'openapi_spec_validator', + 'openapi_v2_spec_validator', + 'validate_v2_spec', 'validate_v2_spec_url', + 'openapi_v3_spec_validator', + 'validate_v3_spec', 'validate_v3_spec_url', + 'openapi_v3_0_spec_validator', + 'validate_v3_0_spec', 'validate_v3_0_spec_url', + 'openapi_v3_1_spec_validator', + 'validate_v3_1_spec', 'validate_v3_1_spec_url', ] file_object_handler = FileObjectHandler() @@ -28,9 +39,14 @@ 'file': UrlHandler('file'), } +# schemas +draft4_validator_factory = OpenAPIValidatorFactory(Draft4Validator) +draft7_validator_factory = OpenAPIValidatorFactory(Draft7Validator) + # v2.0 spec -schema_v2, schema_v2_url = get_openapi_schema('2.0') -openapi_v2_validator_factory = JSONSpecValidatorFactory( +schema_v2, schema_v2_url = get_openapi_schema('2.0', Draft4Validator) +openapi_v2_validator_factory = OpenAPISpecValidatorFactory( + draft4_validator_factory, schema_v2, schema_v2_url, resolver_handlers=default_handlers, ) @@ -40,25 +56,51 @@ ) # v3.0 spec -schema_v3, schema_v3_url = get_openapi_schema('3.0') -openapi_v3_validator_factory = JSONSpecValidatorFactory( - schema_v3, schema_v3_url, +schema_v3_0, schema_v3_0_url = get_openapi_schema('3.0', Draft4Validator) +openapi_v3_0_validator_factory = OpenAPISpecValidatorFactory( + draft4_validator_factory, + schema_v3_0, schema_v3_0_url, resolver_handlers=default_handlers, ) -openapi_v3_spec_validator = SpecValidator( - openapi_v3_validator_factory, +openapi_v3_0_spec_validator = SpecValidator( + openapi_v3_0_validator_factory, + resolver_handlers=default_handlers, +) + +# v3.1 spec +# @TODO: change to 2020-12 validator when released +schema_v3_1, schema_v3_1_url = get_openapi_schema('3.1', Draft7Validator) +openapi_v3_1_validator_factory = OpenAPISpecValidatorFactory( + draft7_validator_factory, + schema_v3_1, schema_v3_1_url, + resolver_handlers=default_handlers, +) +openapi_v3_1_spec_validator = SpecValidator( + openapi_v3_1_validator_factory, resolver_handlers=default_handlers, ) # shortcuts -validate_v2_spec = validate_spec_factory(openapi_v2_spec_validator.validate) +validate_v2_spec = validate_spec_factory( + openapi_v2_spec_validator.validate) validate_v2_spec_url = validate_spec_url_factory( openapi_v2_spec_validator.validate, default_handlers) -validate_v3_spec = validate_spec_factory(openapi_v3_spec_validator.validate) -validate_v3_spec_url = validate_spec_url_factory( - openapi_v3_spec_validator.validate, default_handlers) +validate_v3_0_spec = validate_spec_factory( + openapi_v3_0_spec_validator.validate) +validate_v3_0_spec_url = validate_spec_url_factory( + openapi_v3_0_spec_validator.validate, default_handlers) + +validate_v3_1_spec = validate_spec_factory( + openapi_v3_1_spec_validator.validate) +validate_v3_1_spec_url = validate_spec_url_factory( + openapi_v3_1_spec_validator.validate, default_handlers) # aliases to the latest version -validate_spec = validate_v3_spec -validate_spec_url = validate_v3_spec_url +schema_v3, schema_v3_url = schema_v3_0, schema_v3_0_url +openapi_v3_validator_factory = openapi_v3_0_validator_factory +openapi_v3_spec_validator = openapi_v3_0_spec_validator +validate_v3_spec = validate_v3_0_spec +validate_v3_spec_url = validate_v3_0_spec_url +validate_spec = validate_v3_0_spec +validate_spec_url = validate_v3_0_spec_url diff --git a/openapi_spec_validator/__main__.py b/openapi_spec_validator/__main__.py index eb2884aa..7d31c3ae 100644 --- a/openapi_spec_validator/__main__.py +++ b/openapi_spec_validator/__main__.py @@ -3,7 +3,8 @@ import sys from openapi_spec_validator import ( - openapi_v2_spec_validator, openapi_v3_spec_validator, + openapi_v2_spec_validator, openapi_v3_0_spec_validator, + openapi_v3_1_spec_validator ) from openapi_spec_validator.exceptions import ValidationError from openapi_spec_validator.readers import read_from_stdin, read_from_filename @@ -20,10 +21,10 @@ def main(args=None): parser.add_argument('filename', help="Absolute or relative path to file") parser.add_argument( '--schema', - help="OpenAPI schema (default: 3.0.0)", + help="OpenAPI schema (default: 3.1)", type=str, - choices=['2.0', '3.0.0'], - default='3.0.0' + choices=['2.0', '3.0', '3.1'], + default='3.0' ) args = parser.parse_args(args) @@ -42,7 +43,8 @@ def main(args=None): # choose the validator validators = { '2.0': openapi_v2_spec_validator, - '3.0.0': openapi_v3_spec_validator, + '3.0': openapi_v3_0_spec_validator, + '3.1': openapi_v3_1_spec_validator, } validator = validators[args.schema] diff --git a/openapi_spec_validator/decorators.py b/openapi_spec_validator/decorators.py index 27eba969..b609e0e3 100644 --- a/openapi_spec_validator/decorators.py +++ b/openapi_spec_validator/decorators.py @@ -1,49 +1,5 @@ """OpenAPI spec validator decorators module.""" from functools import wraps -import logging - -from openapi_spec_validator.managers import VisitingManager - -log = logging.getLogger(__name__) - - -class DerefValidatorDecorator: - """Dereferences instance if it is a $ref before passing it for validation. - - :param instance_resolver: Resolves refs in the openapi service spec - """ - def __init__(self, instance_resolver): - self.instance_resolver = instance_resolver - self.visiting = VisitingManager() - - def __call__(self, func): - def wrapped(validator, schema_element, instance, schema): - if not isinstance(instance, dict) or '$ref' not in instance: - for res in func(validator, schema_element, instance, schema): - yield res - return - - ref = instance['$ref'] - - # ref already visited - if ref in self.visiting: - return - - self._attach_scope(instance) - with self.visiting.visit(ref): - with self.instance_resolver.resolving(ref) as target: - for res in func(validator, schema_element, target, schema): - yield res - - return wrapped - - def _attach_scope(self, instance): - log.debug('Attaching x-scope to %s', instance) - if 'x-scope' in instance: - log.debug('Ref %s already has scope attached', instance['$ref']) - return - - instance['x-scope'] = list(self.instance_resolver._scopes_stack) class ValidationErrorWrapper(object): diff --git a/openapi_spec_validator/dereferencing/decorators.py b/openapi_spec_validator/dereferencing/decorators.py new file mode 100644 index 00000000..55f7540b --- /dev/null +++ b/openapi_spec_validator/dereferencing/decorators.py @@ -0,0 +1,23 @@ +"""OpenAPI spec validator dereferencing decorators module.""" +from openapi_spec_validator.dereferencing.utils import is_ref + + +class DerefValidatorDecorator: + """Dereferences instance if it is a $ref before passing it for validation. + + :param instance_resolver: Resolves refs in the openapi service spec + """ + def __init__(self, spec_dereferencer): + self.spec_dereferencer = spec_dereferencer + + def __call__(self, func): + def wrapped(validator, schema_element, instance, schema): + if is_ref(instance): + with self.spec_dereferencer.dereference(instance) as deref: + for res in func(validator, schema_element, deref, schema): + yield res + else: + for res in func(validator, schema_element, instance, schema): + yield res + + return wrapped diff --git a/openapi_spec_validator/dereferencing/dereferencers.py b/openapi_spec_validator/dereferencing/dereferencers.py new file mode 100644 index 00000000..68352c75 --- /dev/null +++ b/openapi_spec_validator/dereferencing/dereferencers.py @@ -0,0 +1,22 @@ +import logging + +from openapi_spec_validator.dereferencing.managers import ResolverManager +from openapi_spec_validator.dereferencing.utils import is_ref + +log = logging.getLogger(__name__) + + +class Dereferencer(object): + + def __init__(self, spec_resolver): + self.resolver_manager = ResolverManager(spec_resolver) + + def dereference(self, item): + log.info("Dereferencing %s", item) + if item is None or not is_ref(item): + return item + + ref = item['$ref'] + with self.resolver_manager.in_scope(item) as resolver: + with resolver.resolving(ref) as target: + return target diff --git a/openapi_spec_validator/dereferencing/managers.py b/openapi_spec_validator/dereferencing/managers.py new file mode 100644 index 00000000..4f94e8bb --- /dev/null +++ b/openapi_spec_validator/dereferencing/managers.py @@ -0,0 +1,73 @@ +"""OpenAPI spec validator dereferencing managers module.""" +from contextlib import contextmanager +import logging + +log = logging.getLogger(__name__) + + +class VisitingManager(dict): + """Visiting manager. Mark keys which being visited.""" + + @contextmanager + def visit(self, key): + """Visits key and marks as visited. + Support context manager interface. + + :param key: key being visited. + """ + self[key] = key + try: + yield key + finally: + del self[key] + + +class SpecDereferencer: + """Dereferences instance if it is a $ref before passing it for validation. + + :param instance_resolver: Resolves refs in the openapi service spec + :param visiting_manager: Visiting manager + """ + def __init__(self, instance_resolver, visiting_manager, scope='x-scope'): + self.instance_resolver = instance_resolver + self.visiting_manager = visiting_manager + self.scope = scope + + @contextmanager + def dereference(self, instance): + ref = instance['$ref'] + if ref in self.visiting_manager: + log.debug('Ref %s already visited', ref) + return + + self._attach_scope(instance) + with self.visiting_manager.visit(ref): + with self.instance_resolver.resolving(ref) as target: + yield target + + def _attach_scope(self, instance): + log.info('Attaching scope to %s', instance) + if self.scope in instance: + log.debug('Ref %s already has scope attached', instance['$ref']) + return + + log.debug('Attaching scope to %s', instance) + instance[self.scope] = list(self.instance_resolver._scopes_stack) + + +class ResolverManager(object): + def __init__(self, resolver, scope='x-scope'): + self.resolver = resolver + self.scope = scope + + @contextmanager + def in_scope(self, item): + if self.scope not in item: + yield self.resolver + else: + saved_scope_stack = self.resolver._scopes_stack + try: + self.resolver._scopes_stack = item[self.scope] + yield self.resolver + finally: + self.resolver._scopes_stack = saved_scope_stack diff --git a/openapi_spec_validator/dereferencing/utils.py b/openapi_spec_validator/dereferencing/utils.py new file mode 100644 index 00000000..3dd72e9f --- /dev/null +++ b/openapi_spec_validator/dereferencing/utils.py @@ -0,0 +1,2 @@ +def is_ref(spec): + return isinstance(spec, dict) and '$ref' in spec diff --git a/openapi_spec_validator/factories.py b/openapi_spec_validator/factories.py index 956235a6..75c2be75 100644 --- a/openapi_spec_validator/factories.py +++ b/openapi_spec_validator/factories.py @@ -1,54 +1,80 @@ """OpenAPI spec validator factories module.""" -from jsonschema import validators -from jsonschema.validators import Draft4Validator, RefResolver +from jsonschema.validators import extend, Draft4Validator, RefResolver +from six import iteritems -from openapi_spec_validator.generators import ( - SpecValidatorsGeneratorFactory, +from openapi_spec_validator.dereferencing.decorators import ( + DerefValidatorDecorator, +) +from openapi_spec_validator.dereferencing.managers import ( + VisitingManager, SpecDereferencer, ) -class Draft4ExtendedValidatorFactory(Draft4Validator): - """Draft4Validator with extra validators factory that follows $refs +class OpenAPIValidatorFactory(object): + """Validator factory with extra validators that follows $refs in the schema being validated.""" - @classmethod - def from_resolver(cls, spec_resolver): - """Creates a customized Draft4ExtendedValidator. + validators = { + '$ref', + 'properties', + 'additionalProperties', + 'patternProperties', + 'type', + 'dependencies', + 'required', + 'minProperties', + 'maxProperties', + 'allOf', + 'oneOf', + 'anyOf', + 'not', + } + + def __init__(self, schema_validator_class): + self.schema_validator_class = schema_validator_class + + def from_resolver(self, spec_resolver): + """Creates an OpenAPIValidator. :param spec_resolver: resolver for the spec :type resolver: :class:`jsonschema.RefResolver` """ - spec_validators = cls._get_spec_validators(spec_resolver) - return validators.extend(Draft4Validator, spec_validators) - - @classmethod - def _get_spec_validators(cls, spec_resolver): - generator = SpecValidatorsGeneratorFactory.from_spec_resolver( - spec_resolver) - return dict(list(generator)) - - -class JSONSpecValidatorFactory: + visiting = VisitingManager() + dereferencer = SpecDereferencer(spec_resolver, visiting) + spec_validators = self._get_spec_validators(dereferencer) + return extend(self.schema_validator_class, spec_validators) + + def _get_spec_validators(self, dereferencer): + deref = DerefValidatorDecorator(dereferencer) + validators_iter = iteritems(self.schema_validator_class.VALIDATORS) + return dict( + (key, deref(validator_callable)) + for key, validator_callable in validators_iter + if key in self.validators + ) + + +class OpenAPISpecValidatorFactory: """ - Json documents validator factory against a json schema. + OpenAPI Spec validator factory against a spec schema. :param schema: schema for validation. :param schema_url: schema base uri. """ - schema_validator_class = Draft4Validator - spec_validator_factory = Draft4ExtendedValidatorFactory - - def __init__(self, schema, schema_url='', resolver_handlers=None): + def __init__( + self, spec_validator_factory, schema, + schema_url='', resolver_handlers=None, + ): + self.spec_validator_factory = spec_validator_factory self.schema = schema self.schema_url = schema_url self.resolver_handlers = resolver_handlers or () - self.schema_validator_class.check_schema(self.schema) - @property def schema_resolver(self): - return self._get_resolver(self.schema_url, self.schema) + return RefResolver( + self.schema_url, self.schema, handlers=self.resolver_handlers) def create(self, spec_resolver): """Creates json documents validator from spec resolver. @@ -63,7 +89,3 @@ def create(self, spec_resolver): return validator_cls( self.schema, resolver=self.schema_resolver) - - def _get_resolver(self, base_uri, referrer): - return RefResolver( - base_uri, referrer, handlers=self.resolver_handlers) diff --git a/openapi_spec_validator/generators.py b/openapi_spec_validator/generators.py deleted file mode 100644 index 74c31435..00000000 --- a/openapi_spec_validator/generators.py +++ /dev/null @@ -1,42 +0,0 @@ -"""OpenAPI spec validator generators module.""" -import logging - -from six import iteritems -from jsonschema.validators import Draft4Validator - -from openapi_spec_validator.decorators import DerefValidatorDecorator - -log = logging.getLogger(__name__) - - -class SpecValidatorsGeneratorFactory: - """Generator factory for customized validators that follows $refs - in the schema being validated. - """ - validators = { - '$ref', - 'properties', - 'additionalProperties', - 'patternProperties', - 'type', - 'dependencies', - 'required', - 'minProperties', - 'maxProperties', - 'allOf', - 'oneOf', - 'anyOf', - 'not', - } - - @classmethod - def from_spec_resolver(cls, spec_resolver): - """Creates validators generator for the spec resolver. - - :param spec_resolver: resolver for the spec - :type instance_resolver: :class:`jsonschema.RefResolver` - """ - deref = DerefValidatorDecorator(spec_resolver) - for key, validator_callable in iteritems(Draft4Validator.VALIDATORS): - if key in cls.validators: - yield key, deref(validator_callable) diff --git a/openapi_spec_validator/managers.py b/openapi_spec_validator/managers.py deleted file mode 100644 index a4810e18..00000000 --- a/openapi_spec_validator/managers.py +++ /dev/null @@ -1,36 +0,0 @@ -"""OpenAPI spec validator managers module.""" -from contextlib import contextmanager - - -class VisitingManager(dict): - """Visiting manager. Mark keys which being visited.""" - - @contextmanager - def visit(self, key): - """Visits key and marks as visited. - Support context manager interface. - - :param key: key being visited. - """ - self[key] = key - try: - yield key - finally: - del self[key] - - -class ResolverManager(object): - def __init__(self, resolver): - self.resolver = resolver - - @contextmanager - def in_scope(self, item, scope='x-scope'): - if scope not in item: - yield self.resolver - else: - saved_scope_stack = self.resolver._scopes_stack - try: - self.resolver._scopes_stack = item[scope] - yield self.resolver - finally: - self.resolver._scopes_stack = saved_scope_stack diff --git a/openapi_spec_validator/resources/schemas/v3.1/schema.json b/openapi_spec_validator/resources/schemas/v3.1/schema.json new file mode 100644 index 00000000..f78d1f88 --- /dev/null +++ b/openapi_spec_validator/resources/schemas/v3.1/schema.json @@ -0,0 +1,1478 @@ +{ + "title": "A JSON Schema for OpenAPI 3.1.X.", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Validation schema for OpenAPI Specification 3.1.X.", + "type": "object", + "required": [ + "openapi", + "info" + ], + "anyOf": [ + { + "required": [ + "components" + ] + }, + { + "required": [ + "paths" + ] + }, + { + "required": [ + "webhooks" + ] + } + ], + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "properties": { + "openapi": { + "type": "string", + "pattern": "^3\\.1\\.\\d(-.+)?$" + }, + "jsonSchemaDialect": { + "type": "string", + "format": "uri" + }, + "info": { + "$ref": "#/$defs/Objects/$defs/Info" + }, + "externalDocs": { + "$ref": "#/$defs/Objects/$defs/ExternalDocumentation" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/$defs/Objects/$defs/Server" + } + }, + "security": { + "type": "array", + "items": { + "$ref": "#SecurityRequirement" + } + }, + "tags": { + "type": "array", + "items": { + "$ref": "#Tag" + } + }, + "paths": { + "$ref": "#/$defs/Objects/$defs/Paths" + }, + "webhooks": { + "$comment": "TODO: Implement meta-schema for webhooks" + }, + "components": { + "$ref": "#/$defs/Objects/$defs/Components" + } + }, + "unevaluatedProperties": false, + "$defs": { + "Mixins": { + "$defs": { + "Describable": { + "properties": { + "description": { + "type": "string" + } + } + }, + "Requireable": { + "properties": { + "required": { + "type": "boolean", + "default": false + } + } + }, + "Deprecatable": { + "properties": { + "deprecated": { + "type": "boolean", + "default": false + } + } + }, + "DescReqDep": { + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/Describable" + }, + { + "$ref": "#/$defs/Mixins/$defs/Requireable" + }, + { + "$ref": "#/$defs/Mixins/$defs/Deprecatable" + } + ] + }, + "Extensible": { + "patternProperties": { + "^x-": true + } + }, + "WithSingleExample": { + "properties": { + "example": true, + "examples": false + } + }, + "WithExampleObjects": { + "properties": { + "example": false, + "examples": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Objects/$defs/Example" + } + } + } + }, + "WithExamples": { + "oneOf": [ + { + "$ref": "#/$defs/Mixins/$defs/WithSingleExample" + }, + { + "$ref": "#/$defs/Mixins/$defs/WithExampleObjects" + } + ] + }, + "WithSchema": { + "properties": { + "schema": { + "$ref": "#Schema" + }, + "content": false + } + }, + "WithSchemaAndExamples": { + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/WithSchema" + }, + { + "$ref": "#/$defs/Mixins/$defs/WithExamples" + } + ] + }, + "WithContent": { + "properties": { + "schema": false, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#MediaType" + }, + "minProperties": 1, + "maxProperties": 1 + } + } + }, + "WithNameAndLocation": { + "required": [ + "name", + "in" + ], + "properties": { + "name": { + "type": "string" + }, + "in": { + "type": "string", + "enum": [ + "path", + "query", + "header", + "cookie" + ] + } + } + }, + "WithExplodeAndReserved": { + "properties": { + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean", + "default": false + } + } + }, + "WithEmpty": { + "properties": { + "allowEmptyValue": { + "type": "boolean", + "default": false + } + } + }, + "WithExplodeReservedAndEmpty": { + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/WithExplodeAndReserved" + }, + { + "$ref": "#/$defs/Mixins/$defs/WithEmpty" + } + ] + }, + "StyledSimple": { + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/WithExplodeReservedAndEmpty" + } + ], + "properties": { + "style": { + "type": "string", + "const": "simple", + "default": "simple" + } + } + }, + "StyledMatrix": { + "properties": { + "style": { + "type": "string", + "enum": [ + "matrix", + "label", + "simple" + ], + "default": "simple" + } + } + }, + "StyledFormOnly": { + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/WithExplodeReservedAndEmpty" + } + ], + "properties": { + "style": { + "type": "string", + "const": "form", + "default": "form" + } + } + }, + "StyledFormComplexNoDefaultOrEmpty": { + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/WithExplodeAndReserved" + } + ], + "properties": { + "style": { + "type": "string", + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ] + } + } + }, + "StyledFormComplex": { + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/StyledFormComplexNoDefaultOrEmpty" + }, + { + "$ref": "#/$defs/Mixins/$defs/WithExplodeReservedAndEmpty" + } + ], + "properties": { + "style": { + "default": "form" + } + } + } + } + }, + "Objects": { + "$defs": { + "Reference": { + "$anchor": "Reference", + "type": "object", + "required": [ + "$ref" + ], + "properties": { + "$ref": { + "type": "string", + "format": "uri-reference" + } + } + }, + "Info": { + "$anchor": "Info", + "type": "object", + "required": [ + "title", + "version" + ], + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/Describable" + }, + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "properties": { + "title": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "termsOfService": { + "type": "string", + "format": "uri-reference" + }, + "contact": { + "$ref": "#/$defs/Objects/$defs/Info/$defs/Contact" + }, + "license": { + "$ref": "#/$defs/Objects/$defs/Info/$defs/License" + } + }, + "$defs": { + "Contact": { + "$anchor": "Contact", + "type": "object", + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + }, + "email": { + "type": "string", + "format": "email" + } + }, + "unevaluatedProperties": false + }, + "License": { + "$anchor": "License", + "type": "object", + "required": [ + "name" + ], + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + }, + "identifier": { + "type": "string" + } + }, + "unevaluatedProperties": false + } + } + }, + "Server": { + "$anchor": "Server", + "type": "object", + "required": [ + "url" + ], + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/Describable" + }, + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "properties": { + "url": { + "type": "string" + }, + "variables": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": [ + "default" + ], + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/Describable" + }, + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "properties": { + "enum": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "default": { + "type": "string" + } + }, + "unevaluatedProperties": false + } + } + }, + "unevaluatedProperties": false + }, + "Response": { + "$anchor": "Response", + "oneOf": [ + { + "$ref": "#/$defs/Objects/$defs/Reference" + }, + { + "type": "object", + "required": [ + "description" + ], + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/Describable" + }, + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "properties": { + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#Header" + } + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#MediaType" + } + }, + "links": { + "type": "object", + "additionalProperteis": { + "$ref": "#Link" + } + } + }, + "unevaluatedProperties": false + } + ] + }, + "MediaType": { + "$anchor": "MediaType", + "type": "object", + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/WithSchemaAndExamples" + } + ], + "properties": { + "encoding": { + "type": "object", + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/StyledFormComplexNoDefaultOrEmpty" + } + ], + "properties": { + "contentType": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#Header" + } + } + } + } + }, + "unevaluatedProperties": false + }, + "Example": { + "$anchor": "Example", + "oneOf": [ + { + "$ref": "#/$defs/Objects/$defs/Reference" + }, + { + "type": "object", + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/Describable" + }, + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "properties": { + "summary": { + "type": "string" + }, + "value": true, + "externalValue": { + "type": "string", + "format": "uri-reference" + } + }, + "unevaluatedProperties": false + } + ] + }, + "Header": { + "$anchor": "Header", + "oneOf": [ + { + "$ref": "#/$defs/Objects/$defs/Reference" + }, + { + "type": "object", + "required": [ + "schema" + ], + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/DescReqDep" + }, + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "oneOf": [ + { + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/WithSchemaAndExamples" + }, + { + "$ref": "#/$defs/Mixins/$defs/StyledSimple" + } + ] + }, + { + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/WithContent" + }, + { + "$ref": "#/$defs/Mixins/$defs/WithEmpty" + } + ] + } + ], + "unevaluatedProperties": false + } + ] + }, + "Paths": { + "$anchor": "Paths", + "type": "object", + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "patternProperties": { + "^/": { + "type": "object", + "oneOf": [ + { + "$ref": "#/$defs/Objects/$defs/Reference" + }, + { + "$ref": "#/$defs/Objects/$defs/PathItem" + } + ] + } + }, + "unevaluatedProperties": false + }, + "PathItem": { + "$anchor": "PathItem", + "type": "object", + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/Describable" + }, + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "properties": { + "summary": { + "type": "string" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/$defs/Objects/$defs/Server" + } + }, + "parameters": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/$defs/Objects/$defs/Parameter" + }, + { + "$ref": "#/$defs/Objects/$defs/Reference" + } + ] + }, + "uniqueItems": true + } + } + }, + "Operation": { + "$anchor": "Operation", + "type": "object", + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/Describable" + }, + { + "$ref": "#/$defs/Mixins/$defs/Deprecatable" + }, + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "summary": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/$defs/Objects/$defs/ExternalDocumentation" + }, + "operationId": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/$defs/Objects/$defs/Parameter" + } + }, + "requestBody": { + "$ref": "#/$defs/Objects/$defs/RequestBody" + }, + "responses": { + "$ref": "#/$defs/Objects/$defs/Responses" + }, + "callback": { + "$ref": "#/$defs/Objects/$defs/Callback" + }, + "security": { + "type": "array", + "items": { + "$ref": "#/$defs/Objects/$defs/SecurityRequirement" + } + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/$defs/Objects/$defs/Server" + } + } + }, + "unevaluatedProperties": false + }, + "Responses": { + "$anchor": "Responses", + "type": "object", + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "properties": { + "default": { + "$ref": "#Response" + } + }, + "patternProperties": { + "[1-5](?:\\d{2}|XX)": { + "$ref": "#Response" + } + }, + "minProperties": 1, + "unevaluatedProperties": false + }, + "SecurityRequirement": { + "$anchor": "SecurityRequirement", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "Tag": { + "$anchor": "Tag", + "type": "object", + "required": [ + "name" + ], + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/Describable" + }, + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "properties": { + "name": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/$defs/Objects/$defs/ExternalDocumentation" + } + }, + "unevaluatedProperties": false + }, + "ExternalDocumentation": { + "$anchor": "ExternalDocumentation", + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "type": "object", + "required": [ + "url" + ], + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + } + } + }, + "Parameter": { + "$anchor": "Parameter", + "oneOf": [ + { + "$ref": "#/$defs/Objects/$defs/Reference" + }, + { + "type": "object", + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/WithNameAndLocation" + }, + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + }, + { + "oneOf": [ + { + "allOf": [ + { + "$ref": "#PathParam" + }, + { + "$ref": "#/$defs/Mixins/$defs/Describable" + }, + { + "$ref": "#/$defs/Mixins/$defs/Deprecatable" + }, + { + "required": [ + "required" + ], + "properties": { + "required": { + "const": true, + "default": true + } + } + } + ] + }, + { + "allOf": [ + { + "anyOf": [ + { + "$ref": "#QueryParam" + }, + { + "$ref": "#HeaderParam" + }, + { + "$ref": "#CookieParam" + } + ] + }, + { + "$ref": "#/$defs/Mixins/$defs/DescReqDep" + } + ] + } + ] + } + ], + "oneOf": [ + { + "allOf": [ + { + "required": [ + "schema" + ] + }, + { + "$ref": "#/$defs/Mixins/$defs/WithSchemaAndExamples" + }, + { + "oneOf": [ + { + "allOf": [ + { + "$ref": "#PathParam" + }, + { + "$ref": "#/$defs/Mixins/$defs/StyledMatrix" + } + ] + }, + { + "allOf": [ + { + "$ref": "#QueryParam" + }, + { + "$ref": "#/$defs/Mixins/$defs/StyledFormComplex" + } + ] + }, + { + "allOf": [ + { + "$ref": "#HeaderParam" + }, + { + "$ref": "#/$defs/Mixins/$defs/StyledSimple" + } + ] + }, + { + "allOf": [ + { + "$ref": "#CookieParam" + }, + { + "$ref": "#/$defs/Mixins/$defs/StyledFormOnly" + } + ] + } + ] + } + ] + }, + { + "allOf": [ + { + "required": [ + "content" + ] + }, + { + "$ref": "#/$defs/Mixins/$defs/WithContent" + }, + { + "$ref": "#/$defs/Mixins/$defs/WithEmpty" + } + ] + } + ], + "unevaluatedProperties": false + } + ], + "$defs": { + "PathParam": { + "$anchor": "PathParam", + "properties": { + "in": { + "const": "path" + } + } + }, + "QueryParam": { + "$anchor": "QueryParam", + "properties": { + "in": { + "const": "query" + } + } + }, + "HeaderParam": { + "$anchor": "HeaderParam", + "properties": { + "in": { + "const": "header" + } + } + }, + "CookieParam": { + "$anchor": "CookieParam", + "properties": { + "in": { + "const": "cookie" + } + } + } + } + }, + "RequestBody": { + "$anchor": "RequestBody", + "required": [ + "content" + ], + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/Describable" + }, + { + "$ref": "#/$defs/Mixins/$defs/Requireable" + }, + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "properties": { + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#MediaType" + } + } + }, + "unevaluatedProperties": false + }, + "SecurityScheme": { + "$anchor": "SecurityScheme", + "oneOf": [ + { + "$ref": "#/$defs/Objects/$defs/Reference" + }, + { + "type": "object", + "required": [ + "type" + ], + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/Describable" + }, + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "properties": { + "type": { + "string": null, + "enum": [ + "apiKey", + "http", + "bearer", + "mutualTLS", + "oauth2", + "openIdConnect" + ], + "oneOf": [ + { + "$ref": "#APIKey" + }, + { + "$ref": "#HTTP" + }, + { + "$ref": "#MutualTLS" + }, + { + "$ref": "#OAuth2" + }, + { + "$ref": "#OpenIdConnect" + } + ] + } + }, + "unevaluatedProperties": false + } + ], + "$defs": { + "APIKey": { + "$anchor": "APIKey", + "properties": { + "type": { + "const": "apiKey" + } + }, + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/WithNameAndLocation" + } + ], + "not": { + "$ref": "#PathParam" + } + }, + "HTTP": { + "$anchor": "HTTP", + "required": [ + "scheme" + ], + "properties": { + "scheme": { + "type": "string" + } + }, + "oneOf": [ + { + "properties": { + "type": { + "const": "http" + } + } + }, + { + "properties": { + "type": { + "const": "bearer" + }, + "bearerFormat": { + "type": "string" + } + } + } + ] + }, + "MutalTLS": { + "$anchor": "MutualTLS", + "properties": { + "type": { + "const": "mutualTLS" + } + } + }, + "OAuth2": { + "$anchor": "OAuth2", + "required": [ + "flows" + ], + "properties": { + "type": { + "const": "oauth2" + }, + "flows": { + "$ref": "#OAuthFlows" + } + } + }, + "OpenIdConnect": { + "$anchor": "OpenIdConnect", + "required": [ + "openIdConnectUrl" + ], + "properties": { + "type": { + "const": "openIdConnect" + }, + "openIdConnectUrl": { + "type": "string", + "format": "uri" + } + } + } + } + }, + "OAuthFlows": { + "$anchor": "OAuthFlows", + "type": "object", + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "properties": { + "implicit": { + "allOf": [ + { + "required": [ + "scopes" + ] + }, + { + "$ref": "#CommonFlow" + }, + { + "$ref": "#AuthorizationFlow" + }, + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "unevaluatedProperties": false + }, + "password": { + "allOf": [ + { + "$ref": "#CommonFlow" + }, + { + "$ref": "#TokenFlow" + }, + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "unevaluatedProperties": false + }, + "clientCredentials": { + "allOf": [ + { + "$ref": "#CommonFlow" + }, + { + "$ref": "#TokenFlow" + }, + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "unevaluatedProperties": false + }, + "authorizationCode": { + "allOf": [ + { + "$ref": "#CommonFlow" + }, + { + "$ref": "#TokenFlow" + }, + { + "$ref": "#AuthorizationFlow" + }, + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "unevaluatedProperties": false + } + }, + "unevaluatedProperties": false, + "$defs": { + "Common": { + "$anchor": "CommonFlow", + "properties": { + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "Token": { + "$anchor": "TokenFlow", + "required": [ + "tokenUrl" + ], + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri-reference" + } + } + }, + "Authorization": { + "$anchor": "AuthorizationFlow", + "required": [ + "authorizationUrl" + ], + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri-reference" + } + } + } + } + }, + "Link": { + "$anchor": "Link", + "oneOf": [ + { + "$ref": "#/$defs/Objects/$defs/Reference" + }, + { + "type": "object", + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/Describable" + }, + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "oneOf": [ + { + "properties": { + "operationRef": { + "type": "string", + "format": "uri-reference" + }, + "operationId": false + } + }, + { + "properties": { + "operationRef": false, + "operationId": { + "type": "string" + } + } + } + ], + "properties": { + "parameters": { + "type": "object", + "additionalProperties": true + }, + "requestBody": true, + "server": { + "$ref": "#/$defs/Objects/$defs/Server" + } + }, + "unevaluatedProperties": false + } + ] + }, + "Callback": { + "$anchor": "Callback", + "oneOf": [ + { + "$ref": "#/$defs/Objects/$defs/Reference" + }, + { + "type": "object", + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "unevaluatedProperties": false + } + ] + }, + "Schema": { + "$anchor": "Schema", + "oneOf": [ + { + "$ref": "#/$defs/Objects/$defs/Reference" + }, + { + "type": "object", + "allOf": [ + { + "$ref": "https://json-schema.org/draft/2020-12/schema" + }, + { + "$ref": "#/$defs/Objects/$defs/Schema/$defs/Extensions" + }, + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + } + ], + "unevaluatedProperties": false + } + ], + "$defs": { + "Extensions": { + "$anchor": "SchemaExtensions", + "properties": { + "discriminator": { + "$ref": "#/$defs/Objects/$defs/Discriminator" + }, + "externalDocs": { + "$ref": "#/$defs/Objects/$defs/ExternalDocumentation" + }, + "xml": { + "$ref": "#XML" + } + } + }, + "Discriminator": { + "$anchor": "Discriminator", + "type": "object", + "required": [ + "propertyName" + ], + "properties": { + "propertyName": { + "type": "string" + }, + "mapping": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "XML": { + "$anchor": "XML", + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + }, + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string", + "format": "url" + }, + "prefix": { + "type": "string" + }, + "attribute": { + "type": "boolean", + "default": false + }, + "wrapped": { + "type": "boolean", + "default": false + } + } + } + ], + "additionalProperties": false + } + } + }, + "Components": { + "$anchor": "Components", + "type": "object", + "allOf": [ + { + "$ref": "#/$defs/Mixins/$defs/Extensible" + }, + { + "additionalProperties": { + "$comment": "All sub-objects have the same property name constraints", + "type": "object", + "propertyNames": { + "pattern": "^[a-zA-Z0-9\\.\\-_]+$" + } + } + } + ], + "properties": { + "schemas": { + "additionalProperties": { + "$ref": "#/$defs/Objects/$defs/Schema" + } + }, + "responses": { + "additionalProperties": { + "$ref": "#/$defs/Objects/$defs/Responses" + } + }, + "parameters": { + "additionalProperties": { + "$ref": "#/$defs/Objects/$defs/Parameter" + } + }, + "pathItems": { + "additionalProperties": { + "$ref": "#/$defs/Objects/$defs/PathItem" + } + }, + "examples": { + "additionalProperties": { + "$ref": "#/$defs/Objects/$defs/Example" + } + }, + "requestBodies": { + "additionalProperties": { + "$ref": "#/$defs/Objects/$defs/RequestBody" + } + }, + "headers": { + "additionalProperties": { + "$ref": "#/$defs/Objects/$defs/Header" + } + }, + "links": { + "additionalProperties": { + "$ref": "#/$defs/Objects/$defs/Link" + } + }, + "callbacks": { + "additionalProperties": { + "$ref": "#/$defs/Objects/$defs/Callback" + } + } + }, + "unevaluatedProperties": false + } + } + } + } +} diff --git a/openapi_spec_validator/schemas.py b/openapi_spec_validator/schemas.py index c2857c00..deae6808 100644 --- a/openapi_spec_validator/schemas.py +++ b/openapi_spec_validator/schemas.py @@ -8,12 +8,13 @@ from openapi_spec_validator.loaders import ExtendedSafeLoader -def get_openapi_schema(version): +def get_openapi_schema(version, schema_validator_class): path = 'resources/schemas/v{0}/schema.json'.format(version) path_resource = resource_filename('openapi_spec_validator', path) path_full = os.path.join(os.path.dirname(__file__), path_resource) schema = read_yaml_file(path_full) schema_url = parse.urljoin('file:', request.pathname2url(path_full)) + schema_validator_class.check_schema(schema) return schema, schema_url diff --git a/openapi_spec_validator/validators.py b/openapi_spec_validator/validators.py index 5bedea81..86433cb7 100644 --- a/openapi_spec_validator/validators.py +++ b/openapi_spec_validator/validators.py @@ -5,38 +5,19 @@ from openapi_schema_validator import OAS30Validator, oas30_format_checker from six import iteritems +from openapi_spec_validator.dereferencing.dereferencers import Dereferencer +from openapi_spec_validator.dereferencing.managers import ResolverManager from openapi_spec_validator.exceptions import ( ParameterDuplicateError, ExtraParametersError, UnresolvableParameterError, OpenAPIValidationError, DuplicateOperationIDError, ) from openapi_spec_validator.decorators import ValidationErrorWrapper -from openapi_spec_validator.managers import ResolverManager log = logging.getLogger(__name__) wraps_errors = ValidationErrorWrapper(OpenAPIValidationError) -def is_ref(spec): - return isinstance(spec, dict) and '$ref' in spec - - -class Dereferencer(object): - - def __init__(self, spec_resolver): - self.resolver_manager = ResolverManager(spec_resolver) - - def dereference(self, item): - log.debug("Dereferencing %s", item) - if item is None or not is_ref(item): - return item - - ref = item['$ref'] - with self.resolver_manager.in_scope(item) as resolver: - with resolver.resolving(ref) as target: - return target - - class SpecValidator(object): def __init__(self, validator_factory, resolver_handlers): diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index a83ced19..74c7f23e 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -5,8 +5,10 @@ from six.moves.urllib.parse import urlunparse from yaml import safe_load -from openapi_spec_validator import (openapi_v3_spec_validator, - openapi_v2_spec_validator) +from openapi_spec_validator import ( + openapi_v2_spec_validator, openapi_v3_0_spec_validator, + openapi_v3_1_spec_validator, +) from openapi_spec_validator.schemas import read_yaml_file @@ -42,8 +44,13 @@ def factory(): @pytest.fixture -def validator(): - return openapi_v3_spec_validator +def v3_0_validator(): + return openapi_v3_0_spec_validator + + +@pytest.fixture +def v3_1_validator(): + return openapi_v3_1_spec_validator @pytest.fixture diff --git a/tests/integration/data/v3.1/empty.yaml b/tests/integration/data/v3.1/empty.yaml new file mode 100644 index 00000000..9151fcbb --- /dev/null +++ b/tests/integration/data/v3.1/empty.yaml @@ -0,0 +1 @@ +openapi: "3.1.0" \ No newline at end of file diff --git a/tests/integration/data/v3.1/petstore.yaml b/tests/integration/data/v3.1/petstore.yaml new file mode 100644 index 00000000..ea4218d0 --- /dev/null +++ b/tests/integration/data/v3.1/petstore.yaml @@ -0,0 +1,109 @@ +openapi: "3.1.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + 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/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" +components: + schemas: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string \ No newline at end of file diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index bbc1e1f0..3c196f0d 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -7,18 +7,25 @@ def test_schema_default(): - """Test default schema is 3.0.0""" + """Test default schema is 3.0""" testargs = ['./tests/integration/data/v3.0/petstore.yaml'] main(testargs) -def test_schema_v3(): - """No errors when calling proper v3 file.""" - testargs = ['--schema', '3.0.0', +def test_schema_v3_0(): + """No errors when calling proper v3.0 file.""" + testargs = ['--schema', '3.0', './tests/integration/data/v3.0/petstore.yaml'] main(testargs) +def test_schema_v3_1(): + """No errors when calling proper v3.1 file.""" + testargs = ['--schema', '3.1', + './tests/integration/data/v3.1/petstore.yaml'] + main(testargs) + + def test_schema_v2(): """No errors when calling with proper v2 file.""" testargs = ['--schema', '2.0', @@ -36,19 +43,19 @@ def test_schema_unknown(): def test_validation_error(): """SystemExit on running with ValidationError.""" - testargs = ['--schema', '3.0.0', + testargs = ['--schema', '3.0', './tests/integration/data/v2.0/petstore.yaml'] with pytest.raises(SystemExit): main(testargs) @mock.patch( - 'openapi_spec_validator.__main__.openapi_v3_spec_validator.validate', + 'openapi_spec_validator.__main__.openapi_v3_0_spec_validator.validate', side_effect=Exception, ) def test_unknown_error(m_validate): """SystemExit on running with unknown error.""" - testargs = ['--schema', '3.0.0', + testargs = ['--schema', '3.0', './tests/integration/data/v2.0/petstore.yaml'] with pytest.raises(SystemExit): main(testargs) @@ -62,12 +69,12 @@ def test_nonexisting_file(): def test_schema_stdin(): - """Test schema from STDIN""" + """Test schema is 3.0 from STDIN""" spes_path = './tests/integration/data/v3.0/petstore.yaml' with open(spes_path, 'r') as spec_file: spec_lines = spec_file.readlines() spec_io = StringIO("".join(spec_lines)) - testargs = ['-'] + testargs = ['--schema', '3.0', '-'] with mock.patch('openapi_spec_validator.__main__.sys.stdin', spec_io): main(testargs) diff --git a/tests/integration/test_shortcuts.py b/tests/integration/test_v2_shortcuts.py similarity index 53% rename from tests/integration/test_shortcuts.py rename to tests/integration/test_v2_shortcuts.py index f27aa18f..e54f1292 100644 --- a/tests/integration/test_shortcuts.py +++ b/tests/integration/test_v2_shortcuts.py @@ -1,10 +1,8 @@ import pytest from openapi_spec_validator import ( - validate_spec, validate_spec_url, validate_v2_spec, validate_v2_spec_url, - validate_spec_url_factory, - openapi_v2_spec_validator, openapi_v3_spec_validator, + validate_spec_url_factory, openapi_v2_spec_validator, ) from openapi_spec_validator.exceptions import OpenAPIValidationError from openapi_spec_validator.handlers.urllib import UrllibHandler @@ -23,19 +21,6 @@ def test_failed(self, spec): validate_v2_spec(spec) -class BaseTestValidValidteSpec: - - def test_valid(self, spec): - validate_spec(spec) - - -class BaseTestFaliedValidateSpec: - - def test_failed(self, spec): - with pytest.raises(OpenAPIValidationError): - validate_spec(spec) - - class BaseTestValidValidateSpecUrl: @pytest.fixture @@ -70,34 +55,6 @@ def test_failed(self, spec_url): validate_v2_spec_url(spec_url) -class BaseTestValidValidateV3SpecUrl(BaseTestValidValidateSpecUrl): - - @pytest.fixture - def validate_spec_url_callable(self, urllib_handlers): - return validate_spec_url_factory( - openapi_v3_spec_validator.validate, urllib_handlers) - - def test_default_valid(self, spec_url): - validate_spec_url(spec_url) - - def test_urllib_valid(self, validate_spec_url_callable, spec_url): - validate_spec_url_callable(spec_url) - - -class BaseTestFaliedValidateSpecUrl: - - def test_failed(self, spec_url): - with pytest.raises(OpenAPIValidationError): - validate_spec_url(spec_url) - - -class TestLocalEmptyExample(BaseTestFaliedValidateSpec): - - @pytest.fixture - def spec(self, factory): - return factory.spec_from_file("data/v3.0/empty.yaml") - - class TestLocalPetstoreV2Example(BaseTestValidValidteV2Spec): @pytest.fixture @@ -105,13 +62,6 @@ def spec(self, factory): return factory.spec_from_file("data/v2.0/petstore.yaml") -class TestLocalPetstoreExample(BaseTestValidValidteSpec): - - @pytest.fixture - def spec(self, factory): - return factory.spec_from_file("data/v3.0/petstore.yaml") - - class TestPetstoreV2Example(BaseTestValidValidateV2SpecUrl): @pytest.fixture @@ -143,36 +93,3 @@ def spec_url(self): 'f25a1d44cff9669703257173e562376cc5bd0ec6/examples/v2.0/' 'yaml/petstore-expanded.yaml' ) - - -class TestPetstoreExample(BaseTestValidValidateV3SpecUrl): - - @pytest.fixture - def spec_url(self): - return ( - 'https://raw.githubusercontent.com/OAI/OpenAPI-Specification/' - 'f75f8486a1aae1a7ceef92fbc63692cb2556c0cd/examples/v3.0/' - 'petstore.yaml' - ) - - -class TestApiWithExampe(BaseTestValidValidateV3SpecUrl): - - @pytest.fixture - def spec_url(self): - return ( - 'https://raw.githubusercontent.com/OAI/OpenAPI-Specification/' - 'f75f8486a1aae1a7ceef92fbc63692cb2556c0cd/examples/v3.0/' - 'api-with-examples.yaml' - ) - - -class TestPetstoreExpandedExample(BaseTestValidValidateV3SpecUrl): - - @pytest.fixture - def spec_url(self): - return ( - 'https://raw.githubusercontent.com/OAI/OpenAPI-Specification/' - '970566d5ca236a5ce1a02fb7d617fdbd07df88db/examples/v3.0/' - 'api-with-examples.yaml' - ) diff --git a/tests/integration/test_v2_validator.py b/tests/integration/test_v2_validator.py new file mode 100644 index 00000000..be187a38 --- /dev/null +++ b/tests/integration/test_v2_validator.py @@ -0,0 +1,42 @@ +from openapi_spec_validator.exceptions import OpenAPIValidationError + + +class TestSpecValidatorIterErrors(object): + + def test_parameter_default_value_wrong_type(self, swagger_validator): + spec = { + 'swagger': '2.0', + 'info': { + 'title': 'Test Api', + 'version': '0.0.1', + }, + 'paths': { + '/test/': { + 'get': { + 'responses': { + '200': { + 'description': 'OK', + 'schema': {'type': 'object'}, + }, + }, + 'parameters': [ + { + 'name': 'param1', + 'in': 'query', + 'type': 'integer', + 'default': 'invaldtype', + }, + ], + }, + }, + }, + } + + errors = swagger_validator.iter_errors(spec) + + errors_list = list(errors) + assert len(errors_list) == 1 + assert errors_list[0].__class__ == OpenAPIValidationError + assert errors_list[0].message == ( + "'invaldtype' is not of type integer" + ) diff --git a/tests/integration/test_v3_0_shortcuts.py b/tests/integration/test_v3_0_shortcuts.py new file mode 100644 index 00000000..4262f7b7 --- /dev/null +++ b/tests/integration/test_v3_0_shortcuts.py @@ -0,0 +1,102 @@ +import pytest + +from openapi_spec_validator import ( + validate_v3_0_spec, validate_v3_0_spec_url, + validate_spec_url_factory, openapi_v3_0_spec_validator, +) +from openapi_spec_validator.exceptions import OpenAPIValidationError +from openapi_spec_validator.handlers.urllib import UrllibHandler + + +class BaseTestValidValidteSpec: + + def test_valid(self, spec): + validate_v3_0_spec(spec) + + +class BaseTestFaliedValidateSpec: + + def test_failed(self, spec): + with pytest.raises(OpenAPIValidationError): + validate_v3_0_spec(spec) + + +class BaseTestValidValidateSpecUrl: + + @pytest.fixture + def urllib_handlers(self): + all_urls_handler = UrllibHandler('http', 'https', 'file') + return { + '': all_urls_handler, + 'http': UrllibHandler('http'), + 'https': UrllibHandler('https'), + 'file': UrllibHandler('file'), + } + + +class BaseTestValidValidateV3SpecUrl(BaseTestValidValidateSpecUrl): + + @pytest.fixture + def validate_spec_url_callable(self, urllib_handlers): + return validate_spec_url_factory( + openapi_v3_0_spec_validator.validate, urllib_handlers) + + def test_default_valid(self, spec_url): + validate_v3_0_spec_url(spec_url) + + def test_urllib_valid(self, validate_spec_url_callable, spec_url): + validate_spec_url_callable(spec_url) + + +class BaseTestFaliedValidateSpecUrl: + + def test_failed(self, spec_url): + with pytest.raises(OpenAPIValidationError): + validate_v3_0_spec_url(spec_url) + + +class TestLocalEmptyv3Example(BaseTestFaliedValidateSpec): + + @pytest.fixture + def spec(self, factory): + return factory.spec_from_file("data/v3.0/empty.yaml") + + +class TestLocalPetstoreV3Example(BaseTestValidValidteSpec): + + @pytest.fixture + def spec(self, factory): + return factory.spec_from_file("data/v3.0/petstore.yaml") + + +class TestPetstoreV3Example(BaseTestValidValidateV3SpecUrl): + + @pytest.fixture + def spec_url(self): + return ( + 'https://raw.githubusercontent.com/OAI/OpenAPI-Specification/' + 'f75f8486a1aae1a7ceef92fbc63692cb2556c0cd/examples/v3.0/' + 'petstore.yaml' + ) + + +class TestApiWithV3Exampe(BaseTestValidValidateV3SpecUrl): + + @pytest.fixture + def spec_url(self): + return ( + 'https://raw.githubusercontent.com/OAI/OpenAPI-Specification/' + 'f75f8486a1aae1a7ceef92fbc63692cb2556c0cd/examples/v3.0/' + 'api-with-examples.yaml' + ) + + +class TestPetstoreV3ExpandedExample(BaseTestValidValidateV3SpecUrl): + + @pytest.fixture + def spec_url(self): + return ( + 'https://raw.githubusercontent.com/OAI/OpenAPI-Specification/' + '970566d5ca236a5ce1a02fb7d617fdbd07df88db/examples/v3.0/' + 'api-with-examples.yaml' + ) diff --git a/tests/integration/test_validate.py b/tests/integration/test_v3_0_validate.py similarity index 91% rename from tests/integration/test_validate.py rename to tests/integration/test_v3_0_validate.py index 9bb3e08f..e96f6c85 100644 --- a/tests/integration/test_validate.py +++ b/tests/integration/test_v3_0_validate.py @@ -9,8 +9,8 @@ class BaseTestValidOpeAPIv3Validator(object): def spec_url(self): return '' - def test_valid(self, validator, spec, spec_url): - return validator.validate(spec, spec_url=spec_url) + def test_valid(self, v3_0_validator, spec, spec_url): + return v3_0_validator.validate(spec, spec_url=spec_url) class BaseTestFailedOpeAPIv3Validator(object): @@ -19,9 +19,9 @@ class BaseTestFailedOpeAPIv3Validator(object): def spec_url(self): return '' - def test_failed(self, validator, spec, spec_url): + def test_failed(self, v3_0_validator, spec, spec_url): with pytest.raises(OpenAPIValidationError): - validator.validate(spec, spec_url=spec_url) + v3_0_validator.validate(spec, spec_url=spec_url) class TestLocalEmptyExample(BaseTestFailedOpeAPIv3Validator): diff --git a/tests/integration/test_validators.py b/tests/integration/test_v3_0_validator.py similarity index 89% rename from tests/integration/test_validators.py rename to tests/integration/test_v3_0_validator.py index 5172b889..a3954276 100644 --- a/tests/integration/test_validators.py +++ b/tests/integration/test_v3_0_validator.py @@ -1,3 +1,5 @@ +import pytest + from openapi_spec_validator.exceptions import ( ExtraParametersError, UnresolvableParameterError, OpenAPIValidationError, DuplicateOperationIDError, @@ -6,6 +8,10 @@ class TestSpecValidatorIterErrors(object): + @pytest.fixture + def validator(self, v3_0_validator): + return v3_0_validator + def test_empty(self, validator): spec = {} @@ -296,45 +302,6 @@ def test_parameter_default_value_wrong_type(self, validator): "'invaldtype' is not of type integer" ) - def test_parameter_default_value_wrong_type_swagger(self, - swagger_validator): - spec = { - 'swagger': '2.0', - 'info': { - 'title': 'Test Api', - 'version': '0.0.1', - }, - 'paths': { - '/test/': { - 'get': { - 'responses': { - '200': { - 'description': 'OK', - 'schema': {'type': 'object'}, - }, - }, - 'parameters': [ - { - 'name': 'param1', - 'in': 'query', - 'type': 'integer', - 'default': 'invaldtype', - }, - ], - }, - }, - }, - } - - errors = swagger_validator.iter_errors(spec) - - errors_list = list(errors) - assert len(errors_list) == 1 - assert errors_list[0].__class__ == OpenAPIValidationError - assert errors_list[0].message == ( - "'invaldtype' is not of type integer" - ) - def test_parameter_default_value_with_reference(self, validator): spec = { 'openapi': '3.0.0', diff --git a/tests/integration/test_v3_1_shortcuts.py b/tests/integration/test_v3_1_shortcuts.py new file mode 100644 index 00000000..496e02dd --- /dev/null +++ b/tests/integration/test_v3_1_shortcuts.py @@ -0,0 +1,80 @@ +import pytest + +from openapi_spec_validator import ( + validate_v3_1_spec, validate_v3_1_spec_url, + validate_spec_url_factory, openapi_v3_1_spec_validator, +) +from openapi_spec_validator.exceptions import OpenAPIValidationError +from openapi_spec_validator.handlers.urllib import UrllibHandler + + +class BaseTestValidValidteSpec: + + def test_valid(self, spec): + validate_v3_1_spec(spec) + + +class BaseTestFaliedValidateSpec: + + def test_failed(self, spec): + with pytest.raises(OpenAPIValidationError): + validate_v3_1_spec(spec) + + +class BaseTestValidValidateSpecUrl: + + @pytest.fixture + def urllib_handlers(self): + all_urls_handler = UrllibHandler('http', 'https', 'file') + return { + '': all_urls_handler, + 'http': UrllibHandler('http'), + 'https': UrllibHandler('https'), + 'file': UrllibHandler('file'), + } + + +class BaseTestValidValidateV3SpecUrl(BaseTestValidValidateSpecUrl): + + @pytest.fixture + def validate_spec_url_callable(self, urllib_handlers): + return validate_spec_url_factory( + openapi_v3_1_spec_validator.validate, urllib_handlers) + + def test_default_valid(self, spec_url): + validate_v3_1_spec_url(spec_url) + + def test_urllib_valid(self, validate_spec_url_callable, spec_url): + validate_spec_url_callable(spec_url) + + +class BaseTestFaliedValidateSpecUrl: + + def test_failed(self, spec_url): + with pytest.raises(OpenAPIValidationError): + validate_v3_1_spec_url(spec_url) + + +class TestLocalEmptyv3Example(BaseTestFaliedValidateSpec): + + @pytest.fixture + def spec(self, factory): + return factory.spec_from_file("data/v3.1/empty.yaml") + + +class TestLocalPetstoreV3Example(BaseTestValidValidteSpec): + + @pytest.fixture + def spec(self, factory): + return factory.spec_from_file("data/v3.1/petstore.yaml") + + +class TestRemoteV3WebhookExampe(BaseTestValidValidateV3SpecUrl): + + @pytest.fixture + def spec_url(self): + return ( + 'https://raw.githubusercontent.com/OAI/OpenAPI-Specification/' + 'f1adc846131b33be72df6a0c87e5e5da59dde0ff/examples/v3.1/' + 'webhook-example.yaml' + ) diff --git a/tests/integration/test_v3_1_validate.py b/tests/integration/test_v3_1_validate.py new file mode 100644 index 00000000..f5fee6d7 --- /dev/null +++ b/tests/integration/test_v3_1_validate.py @@ -0,0 +1,50 @@ +import pytest + +from openapi_spec_validator.exceptions import OpenAPIValidationError + + +class BaseTestValidOpeAPIv3Validator(object): + + @pytest.fixture + def spec_url(self): + return '' + + def test_valid(self, v3_1_validator, spec, spec_url): + return v3_1_validator.validate(spec, spec_url=spec_url) + + +class BaseTestFailedOpeAPIv3Validator(object): + + @pytest.fixture + def spec_url(self): + return '' + + def test_failed(self, v3_1_validator, spec, spec_url): + with pytest.raises(OpenAPIValidationError): + v3_1_validator.validate(spec, spec_url=spec_url) + + +class TestLocalEmptyExample(BaseTestFailedOpeAPIv3Validator): + + @pytest.fixture + def spec(self, factory): + return factory.spec_from_file("data/v3.1/empty.yaml") + + +class TestLocalPetstoreExample(BaseTestValidOpeAPIv3Validator): + + @pytest.fixture + def spec(self, factory): + return factory.spec_from_file("data/v3.1/petstore.yaml") + + +class TestRemoteV3WebhookExampe(BaseTestValidOpeAPIv3Validator): + + @pytest.fixture + def spec(self, factory): + url = ( + 'https://raw.githubusercontent.com/OAI/OpenAPI-Specification/' + 'f1adc846131b33be72df6a0c87e5e5da59dde0ff/examples/v3.1/' + 'webhook-example.yaml' + ) + return factory.spec_from_url(url) diff --git a/tests/integration/test_v3_1_validator.py b/tests/integration/test_v3_1_validator.py new file mode 100644 index 00000000..7718d53a --- /dev/null +++ b/tests/integration/test_v3_1_validator.py @@ -0,0 +1,351 @@ +import pytest + +from openapi_spec_validator.exceptions import ( + ExtraParametersError, UnresolvableParameterError, OpenAPIValidationError, + DuplicateOperationIDError, +) + + +class TestSpecValidatorIterErrors(object): + + @pytest.fixture + def validator(self, v3_1_validator): + return v3_1_validator + + def test_empty(self, validator): + spec = {} + + errors = validator.iter_errors(spec) + + errors_list = list(errors) + assert len(errors_list) == 3 + assert errors_list[0].__class__ == OpenAPIValidationError + assert errors_list[0].message == "'openapi' is a required property" + assert errors_list[1].__class__ == OpenAPIValidationError + assert errors_list[1].message == "'info' is a required property" + assert errors_list[2].__class__ == OpenAPIValidationError + # no paths, components or webhooks + assert errors_list[2].message == \ + "{} is not valid under any of the given schemas" + + def test_info_empty(self, validator): + spec = { + 'openapi': '3.1.0', + 'info': {}, + 'paths': {}, + } + + errors = validator.iter_errors(spec) + + errors_list = list(errors) + assert errors_list[0].__class__ == OpenAPIValidationError + assert errors_list[0].message == "'title' is a required property" + + @pytest.mark.parametrize('field', ['paths', 'components', 'webhooks']) + def test_minimalistic(self, validator, field): + spec = { + 'openapi': '3.1.0', + 'info': { + 'title': 'Test Api', + 'version': '0.0.1', + }, + field: {}, + } + + errors = validator.iter_errors(spec) + + errors_list = list(errors) + assert errors_list == [] + + def test_same_parameters_names(self, validator): + spec = { + 'openapi': '3.1.0', + 'info': { + 'title': 'Test Api', + 'version': '0.0.1', + }, + 'paths': { + '/test/{param1}': { + 'parameters': [ + { + 'name': 'param1', + 'in': 'query', + 'schema': { + 'type': 'integer', + }, + }, + { + 'name': 'param1', + 'in': 'path', + 'schema': { + 'type': 'integer', + }, + 'required': True, + }, + ], + }, + }, + } + + errors = validator.iter_errors(spec) + + errors_list = list(errors) + assert errors_list == [] + + def test_same_operation_ids(self, validator): + spec = { + 'openapi': '3.1.0', + 'info': { + 'title': 'Test Api', + 'version': '0.0.1', + }, + 'paths': { + '/test': { + 'get': { + 'operationId': 'operation1', + 'responses': { + 'default': { + 'description': 'default response', + }, + }, + }, + 'post': { + 'operationId': 'operation1', + 'responses': { + 'default': { + 'description': 'default response', + }, + }, + }, + }, + '/test2': { + 'get': { + 'operationId': 'operation1', + 'responses': { + 'default': { + 'description': 'default response', + }, + }, + }, + }, + }, + } + + errors = validator.iter_errors(spec) + + errors_list = list(errors) + assert len(errors_list) == 2 + assert errors_list[0].__class__ == DuplicateOperationIDError + assert errors_list[1].__class__ == DuplicateOperationIDError + + def test_allow_allof_required_no_properties(self, validator): + spec = { + 'openapi': '3.1.0', + 'info': { + 'title': 'Test Api', + 'version': '0.0.1', + }, + 'paths': {}, + 'components': { + 'schemas': { + 'Credit': { + 'type': 'object', + 'properties': { + 'clientId': {'type': 'string'}, + } + }, + 'CreditCreate': { + 'allOf': [ + { + '$ref': '#/components/schemas/Credit' + }, + { + 'required': ['clientId'] + } + ] + } + }, + }, + } + + errors = validator.iter_errors(spec) + errors_list = list(errors) + assert errors_list == [] + + def test_extra_parameters_in_required(self, validator): + spec = { + 'openapi': '3.1.0', + 'info': { + 'title': 'Test Api', + 'version': '0.0.1', + }, + 'paths': {}, + 'components': { + 'schemas': { + 'testSchema': { + 'type': 'object', + 'required': [ + 'testparam1', + ] + } + }, + }, + } + + errors = validator.iter_errors(spec) + + errors_list = list(errors) + assert errors_list[0].__class__ == ExtraParametersError + assert errors_list[0].message == ( + "Required list has not defined properties: ['testparam1']" + ) + + def test_undocumented_parameter(self, validator): + spec = { + 'openapi': '3.1.0', + 'info': { + 'title': 'Test Api', + 'version': '0.0.1', + }, + 'paths': { + '/test/{param1}/{param2}': { + 'get': { + 'responses': { + 'default': { + 'description': 'default response', + }, + }, + }, + 'parameters': [ + { + 'name': 'param1', + 'in': 'path', + 'schema': { + 'type': 'integer', + }, + 'required': True, + }, + ], + }, + }, + } + + errors = validator.iter_errors(spec) + + errors_list = list(errors) + assert errors_list[0].__class__ == UnresolvableParameterError + assert errors_list[0].message == ( + "Path parameter 'param2' for 'get' operation in " + "'/test/{param1}/{param2}' was not resolved" + ) + + def test_default_value_wrong_type(self, validator): + spec = { + 'openapi': '3.1.0', + 'info': { + 'title': 'Test Api', + 'version': '0.0.1', + }, + 'paths': {}, + 'components': { + 'schemas': { + 'test': { + 'type': 'integer', + 'default': 'invaldtype', + }, + }, + }, + } + + errors = validator.iter_errors(spec) + + errors_list = list(errors) + assert len(errors_list) == 1 + assert errors_list[0].__class__ == OpenAPIValidationError + assert errors_list[0].message == ( + "'invaldtype' is not of type integer" + ) + + def test_parameter_default_value_wrong_type(self, validator): + spec = { + 'openapi': '3.1.0', + 'info': { + 'title': 'Test Api', + 'version': '0.0.1', + }, + 'paths': { + '/test/{param1}': { + 'get': { + 'responses': { + 'default': { + 'description': 'default response', + }, + }, + }, + 'parameters': [ + { + 'name': 'param1', + 'in': 'path', + 'schema': { + 'type': 'integer', + 'default': 'invaldtype', + }, + 'required': True, + }, + ], + }, + }, + } + + errors = validator.iter_errors(spec) + + errors_list = list(errors) + assert len(errors_list) == 1 + assert errors_list[0].__class__ == OpenAPIValidationError + assert errors_list[0].message == ( + "'invaldtype' is not of type integer" + ) + + def test_parameter_default_value_with_reference(self, validator): + spec = { + 'openapi': '3.1.0', + 'info': { + 'title': 'Test Api', + 'version': '0.0.1', + }, + 'paths': { + '/test/': { + 'get': { + 'responses': { + 'default': { + 'description': 'default response', + }, + }, + 'parameters': [ + { + 'name': 'param1', + 'in': 'query', + 'schema': { + 'allOf': [{ + '$ref': '#/components/schemas/type', + }], + 'default': 1, + }, + }, + ], + }, + }, + }, + 'components': { + 'schemas': { + 'type': { + 'type': 'integer', + } + }, + }, + } + + errors = validator.iter_errors(spec) + + errors_list = list(errors) + assert errors_list == []