diff --git a/openapi_core/components.py b/openapi_core/components.py index 978aef2f..39a955a8 100644 --- a/openapi_core/components.py +++ b/openapi_core/components.py @@ -24,10 +24,10 @@ def __init__(self, dereferencer, schemas_registry): def create(self, components_spec): components_deref = self.dereferencer.dereference(components_spec) - schemas_spec = components_deref.get('schemas', []) - responses_spec = components_deref.get('responses', []) - parameters_spec = components_deref.get('parameters', []) - request_bodies_spec = components_deref.get('request_bodies', []) + schemas_spec = components_deref.get('schemas', {}) + responses_spec = components_deref.get('responses', {}) + parameters_spec = components_deref.get('parameters', {}) + request_bodies_spec = components_deref.get('request_bodies', {}) schemas = self.schemas_generator.generate(schemas_spec) responses = self._generate_response(responses_spec) diff --git a/openapi_core/operations.py b/openapi_core/operations.py index 3e82ca37..bf9d520b 100644 --- a/openapi_core/operations.py +++ b/openapi_core/operations.py @@ -4,6 +4,7 @@ from functools import lru_cache from six import iteritems +from openapi_spec_validator.validators import PathItemValidator from openapi_core.exceptions import InvalidResponse from openapi_core.parameters import ParametersGenerator @@ -56,7 +57,7 @@ def __init__(self, dereferencer, schemas_registry): def generate(self, path_name, path): path_deref = self.dereferencer.dereference(path) for http_method, operation in iteritems(path_deref): - if http_method.startswith('x-') or http_method == 'parameters': + if http_method not in PathItemValidator.OPERATIONS: continue operation_deref = self.dereferencer.dereference(operation) diff --git a/openapi_core/servers.py b/openapi_core/servers.py index 68e1dc3b..91b3508a 100644 --- a/openapi_core/servers.py +++ b/openapi_core/servers.py @@ -41,6 +41,9 @@ def __init__(self, dereferencer): def generate(self, servers_spec): servers_deref = self.dereferencer.dereference(servers_spec) + if not servers_deref: + yield Server('/') + return for server_spec in servers_deref: url = server_spec['url'] variables_spec = server_spec.get('variables', {}) @@ -65,9 +68,6 @@ def __init__(self, dereferencer): def generate(self, variables_spec): variables_deref = self.dereferencer.dereference(variables_spec) - if not variables_deref: - return [Server('/'), ] - for variable_name, variable_spec in iteritems(variables_deref): default = variable_spec['default'] enum = variable_spec.get('enum') diff --git a/openapi_core/specs.py b/openapi_core/specs.py index 4edf5e4d..906ff2e2 100644 --- a/openapi_core/specs.py +++ b/openapi_core/specs.py @@ -77,10 +77,10 @@ def create(self, spec_dict, spec_url=''): spec_dict_deref = self.dereferencer.dereference(spec_dict) - info_spec = spec_dict_deref.get('info', []) + info_spec = spec_dict_deref.get('info', {}) servers_spec = spec_dict_deref.get('servers', []) - paths = spec_dict_deref.get('paths', []) - components_spec = spec_dict_deref.get('components', []) + paths = spec_dict_deref.get('paths', {}) + components_spec = spec_dict_deref.get('components', {}) info = self.info_factory.create(info_spec) servers = self.servers_generator.generate(servers_spec) diff --git a/openapi_core/validators.py b/openapi_core/validators.py index 53c4ce61..0f7fa6b4 100644 --- a/openapi_core/validators.py +++ b/openapi_core/validators.py @@ -1,5 +1,6 @@ """OpenAPI core validators module""" from six import iteritems +from yarl import URL from openapi_core.exceptions import ( OpenAPIMappingError, MissingParameter, MissingBody, InvalidResponse, @@ -51,6 +52,16 @@ def __init__(self, errors, data=None, headers=None): self.headers = headers +def get_operation_pattern(server_url, request_url_pattern): + """Return an updated request URL pattern with the server URL removed.""" + if server_url[-1] == "/": + # operations have to start with a slash, so do not remove it + server_url = server_url[:-1] + if URL(server_url).is_absolute(): + return request_url_pattern.replace(server_url, "", 1) + return URL(request_url_pattern).path_qs.replace(server_url, "", 1) + + class RequestValidator(object): def __init__(self, spec): @@ -68,8 +79,9 @@ def validate(self, request): errors.append(exc) return RequestValidationResult(errors, body, parameters) - operation_pattern = request.full_url_pattern.replace( - server.default_url, '') + operation_pattern = get_operation_pattern( + server.default_url, request.full_url_pattern + ) try: operation = self.spec.get_operation( @@ -154,8 +166,9 @@ def validate(self, request, response): errors.append(exc) return ResponseValidationResult(errors, data, headers) - operation_pattern = request.full_url_pattern.replace( - server.default_url, '') + operation_pattern = get_operation_pattern( + server.default_url, request.full_url_pattern + ) try: operation = self.spec.get_operation( diff --git a/requirements.txt b/requirements.txt index b5a3675b..86471695 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ openapi-spec-validator six +yarl diff --git a/tests/integration/data/v3.0/minimal.yaml b/tests/integration/data/v3.0/minimal.yaml new file mode 100644 index 00000000..9e28534c --- /dev/null +++ b/tests/integration/data/v3.0/minimal.yaml @@ -0,0 +1,10 @@ +openapi: "3.0.0" +info: + title: Minimal valid OpenAPI specification + version: "0.1" +paths: + /status: + get: + responses: + default: + description: Return the API status. diff --git a/tests/integration/data/v3.0/minimal_with_servers.yaml b/tests/integration/data/v3.0/minimal_with_servers.yaml new file mode 100644 index 00000000..1e7ce11f --- /dev/null +++ b/tests/integration/data/v3.0/minimal_with_servers.yaml @@ -0,0 +1,12 @@ +openapi: "3.0.0" +info: + title: Minimal valid OpenAPI specification with explicit 'servers' array + version: "0.1" +servers: + - url: / +paths: + /status: + get: + responses: + default: + description: Return the API status. diff --git a/tests/integration/test_minimal.py b/tests/integration/test_minimal.py new file mode 100644 index 00000000..deb96435 --- /dev/null +++ b/tests/integration/test_minimal.py @@ -0,0 +1,49 @@ +import pytest + +from openapi_core.exceptions import InvalidOperation +from openapi_core.shortcuts import create_spec +from openapi_core.validators import RequestValidator +from openapi_core.wrappers import MockRequest + + +class TestMinimal(object): + + servers = [ + "http://minimal.test/", + "https://bad.remote.domain.net/", + "http://localhost", + "http://localhost:8080", + "https://u:p@a.b:1337" + ] + + spec_paths = [ + "data/v3.0/minimal_with_servers.yaml", + "data/v3.0/minimal.yaml" + ] + + @pytest.mark.parametrize("server", servers) + @pytest.mark.parametrize("spec_path", spec_paths) + def test_hosts(self, factory, server, spec_path): + spec_dict = factory.spec_from_file(spec_path) + spec = create_spec(spec_dict) + validator = RequestValidator(spec) + request = MockRequest(server, "get", "/status") + + result = validator.validate(request) + + assert not result.errors + + @pytest.mark.parametrize("server", servers) + @pytest.mark.parametrize("spec_path", spec_paths) + def test_invalid_operation(self, factory, server, spec_path): + spec_dict = factory.spec_from_file(spec_path) + spec = create_spec(spec_dict) + validator = RequestValidator(spec) + request = MockRequest(server, "get", "/nonexistent") + + result = validator.validate(request) + + assert len(result.errors) == 1 + assert isinstance(result.errors[0], InvalidOperation) + assert result.body is None + assert result.parameters == {}