diff --git a/openapi_core/schema/paths/exceptions.py b/openapi_core/schema/paths/exceptions.py new file mode 100644 index 0000000..6a28773 --- /dev/null +++ b/openapi_core/schema/paths/exceptions.py @@ -0,0 +1,15 @@ +import attr + +from openapi_core.schema.exceptions import OpenAPIMappingError + + +class OpenAPIPathError(OpenAPIMappingError): + pass + + +@attr.s(hash=True) +class InvalidPath(OpenAPIPathError): + path_pattern = attr.ib() + + def __str__(self): + return "Unknown path {0}".format(self.path_pattern) diff --git a/openapi_core/schema/specs/models.py b/openapi_core/schema/specs/models.py index e1aa893..7e7c4e1 100644 --- a/openapi_core/schema/specs/models.py +++ b/openapi_core/schema/specs/models.py @@ -4,6 +4,7 @@ import logging from openapi_core.compat import partialmethod from openapi_core.schema.operations.exceptions import InvalidOperation +from openapi_core.schema.paths.exceptions import InvalidPath from openapi_core.schema.servers.exceptions import InvalidServer @@ -19,8 +20,8 @@ class Spec(object): self.servers = servers or [] self.components = components - def __getitem__(self, path_name): - return self.paths[path_name] + def __getitem__(self, path_pattern): + return self.get_path(path_pattern) @property def default_url(self): @@ -36,6 +37,12 @@ class Spec(object): def get_server_url(self, index=0): return self.servers[index].default_url + def get_path(self, path_pattern): + try: + return self.paths[path_pattern] + except KeyError: + raise InvalidPath(path_pattern) + def get_operation(self, path_pattern, http_method): try: return self.paths[path_pattern].operations[http_method] diff --git a/openapi_core/validation/request/models.py b/openapi_core/validation/request/models.py index 55d4887..85ebc11 100644 --- a/openapi_core/validation/request/models.py +++ b/openapi_core/validation/request/models.py @@ -16,6 +16,16 @@ class RequestParameters(dict): def __setitem__(self, location, value): raise NotImplementedError + def __add__(self, other): + if not isinstance(other, self.__class__): + raise ValueError("Invalid type") + + for location in self.valid_locations: + if location in other: + self[location].update(other[location]) + + return self + @classmethod def validate_location(cls, location): if location not in cls.valid_locations: diff --git a/openapi_core/validation/request/validators.py b/openapi_core/validation/request/validators.py index 08593f6..c36b6a2 100644 --- a/openapi_core/validation/request/validators.py +++ b/openapi_core/validation/request/validators.py @@ -26,6 +26,14 @@ class RequestValidator(object): server.default_url, request.full_url_pattern ) + try: + path = self.spec[operation_pattern] + # don't process if operation errors + except OpenAPIMappingError as exc: + return RequestValidationResult([exc, ], None, None) + + path_params, path_params_errors = self._get_parameters(request, path) + try: operation = self.spec.get_operation( operation_pattern, request.method) @@ -33,11 +41,11 @@ class RequestValidator(object): except OpenAPIMappingError as exc: return RequestValidationResult([exc, ], None, None) - params, params_errors = self._get_parameters(request, operation) + op_params, op_params_errors = self._get_parameters(request, operation) body, body_errors = self._get_body(request, operation) - errors = params_errors + body_errors - return RequestValidationResult(errors, body, params) + errors = path_params_errors + op_params_errors + body_errors + return RequestValidationResult(errors, body, path_params + op_params) def _get_parameters(self, request, operation): errors = [] diff --git a/tests/integration/test_minimal.py b/tests/integration/test_minimal.py index 1dcc79d..a307985 100644 --- a/tests/integration/test_minimal.py +++ b/tests/integration/test_minimal.py @@ -1,6 +1,7 @@ import pytest from openapi_core.schema.operations.exceptions import InvalidOperation +from openapi_core.schema.paths.exceptions import InvalidPath from openapi_core.shortcuts import create_spec from openapi_core.validation.request.validators import RequestValidator from openapi_core.wrappers.mock import MockRequest @@ -39,7 +40,7 @@ class TestMinimal(object): spec_dict = factory.spec_from_file(spec_path) spec = create_spec(spec_dict) validator = RequestValidator(spec) - request = MockRequest(server, "get", "/nonexistent") + request = MockRequest(server, "post", "/status") result = validator.validate(request) @@ -47,3 +48,18 @@ class TestMinimal(object): assert isinstance(result.errors[0], InvalidOperation) assert result.body is None assert result.parameters == {} + + @pytest.mark.parametrize("server", servers) + @pytest.mark.parametrize("spec_path", spec_paths) + def test_invalid_path(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], InvalidPath) + assert result.body is None + assert result.parameters == {} diff --git a/tests/integration/test_validators.py b/tests/integration/test_validators.py index 37b4832..d6e6a5a 100644 --- a/tests/integration/test_validators.py +++ b/tests/integration/test_validators.py @@ -9,6 +9,8 @@ from openapi_core.schema.media_types.exceptions import ( from openapi_core.extensions.models.models import BaseModel from openapi_core.schema.operations.exceptions import InvalidOperation from openapi_core.schema.parameters.exceptions import MissingRequiredParameter +from openapi_core.schema.parameters.exceptions import InvalidParameterValue +from openapi_core.schema.paths.exceptions import InvalidPath from openapi_core.schema.request_bodies.exceptions import MissingRequestBody from openapi_core.schema.responses.exceptions import ( MissingResponseContent, InvalidResponse, @@ -54,11 +56,21 @@ class TestRequestValidator(object): assert result.body is None assert result.parameters == {} - def test_invalid_operation(self, validator): + def test_invalid_path(self, validator): request = MockRequest(self.host_url, 'get', '/v1') result = validator.validate(request) + assert len(result.errors) == 1 + assert type(result.errors[0]) == InvalidPath + assert result.body is None + assert result.parameters == {} + + def test_invalid_operation(self, validator): + request = MockRequest(self.host_url, 'patch', '/v1/pets') + + result = validator.validate(request) + assert len(result.errors) == 1 assert type(result.errors[0]) == InvalidOperation assert result.body is None @@ -220,6 +232,80 @@ class TestRequestValidator(object): } +class TestPathItemParamsValidator(object): + + @pytest.fixture + def spec_dict(self, factory): + return { + "openapi": "3.0.0", + "info": { + "title": "Test path item parameter validation", + "version": "0.1", + }, + "paths": { + "/resource": { + "parameters": [ + { + "name": "resId", + "in": "query", + "required": True, + "schema": { + "type": "integer", + }, + }, + ], + "get": { + "responses": { + "default": { + "description": "Return the resource." + } + } + } + } + } + } + + @pytest.fixture + def spec(self, spec_dict): + return create_spec(spec_dict) + + @pytest.fixture + def validator(self, spec): + return RequestValidator(spec) + + def test_request_missing_param(self, validator): + request = MockRequest('http://example.com', 'get', '/resource') + result = validator.validate(request) + + assert len(result.errors) == 1 + assert type(result.errors[0]) == MissingRequiredParameter + assert result.body is None + assert result.parameters == {} + + def test_request_invalid_param(self, validator): + request = MockRequest( + 'http://example.com', 'get', '/resource', + args={'resId': 'invalid'}, + ) + result = validator.validate(request) + + assert len(result.errors) == 1 + assert type(result.errors[0]) == InvalidParameterValue + assert result.body is None + assert result.parameters == {} + + def test_request_valid_param(self, validator): + request = MockRequest( + 'http://example.com', 'get', '/resource', + args={'resId': '10'}, + ) + result = validator.validate(request) + + assert len(result.errors) == 0 + assert result.body is None + assert result.parameters == {'query': {'resId': 10}} + + class TestResponseValidator(object): host_url = 'http://petstore.swagger.io'