diff --git a/openapi_core/servers.py b/openapi_core/servers.py index 68e1dc3..91b3508 100644 --- a/openapi_core/servers.py +++ b/openapi_core/servers.py @@ -41,6 +41,9 @@ class ServersGenerator(object): 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 @@ class ServerVariablesGenerator(object): 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/validators.py b/openapi_core/validators.py index 53c4ce6..0f7fa6b 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 @@ class ResponseValidationResult(BaseValidationResult): 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 @@ class RequestValidator(object): 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 @@ class ResponseValidator(object): 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 b5a3675..8647169 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 0000000..9e28534 --- /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 0000000..1e7ce11 --- /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 0000000..deb9643 --- /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 == {}