diff --git a/openapi_core/contrib/django/compat.py b/openapi_core/contrib/django/compat.py index 7459c9b0..fa521179 100644 --- a/openapi_core/contrib/django/compat.py +++ b/openapi_core/contrib/django/compat.py @@ -4,12 +4,18 @@ ) -def get_headers(req): +def get_request_headers(req): # in Django 1 headers is not defined return req.headers if hasattr(req, 'headers') else \ HttpHeaders(req.META) +def get_response_headers(resp): + # in Django 2 headers is not defined + return resp.headers if hasattr(resp, 'headers') else \ + dict(resp._headers.values()) + + def get_current_scheme_host(req): # in Django 1 _current_scheme_host is not defined return req._current_scheme_host if hasattr(req, '_current_scheme_host') \ diff --git a/openapi_core/contrib/django/requests.py b/openapi_core/contrib/django/requests.py index d674657c..ace403ea 100644 --- a/openapi_core/contrib/django/requests.py +++ b/openapi_core/contrib/django/requests.py @@ -4,7 +4,7 @@ from six.moves.urllib.parse import urljoin from openapi_core.contrib.django.compat import ( - get_headers, get_current_scheme_host, + get_request_headers, get_current_scheme_host, ) from openapi_core.validation.request.datatypes import ( RequestParameters, OpenAPIRequest, @@ -39,7 +39,7 @@ def create(cls, request): path_pattern = '/' + route path = request.resolver_match and request.resolver_match.kwargs or {} - headers = get_headers(request) + headers = get_request_headers(request) parameters = RequestParameters( path=path, query=request.GET, diff --git a/openapi_core/contrib/django/responses.py b/openapi_core/contrib/django/responses.py index efbe69d3..8c436897 100644 --- a/openapi_core/contrib/django/responses.py +++ b/openapi_core/contrib/django/responses.py @@ -1,4 +1,5 @@ """OpenAPI core contrib django responses module""" +from openapi_core.contrib.django.compat import get_response_headers from openapi_core.validation.response.datatypes import OpenAPIResponse @@ -7,8 +8,10 @@ class DjangoOpenAPIResponseFactory(object): @classmethod def create(cls, response): mimetype = response["Content-Type"] + headers = get_response_headers(response) return OpenAPIResponse( data=response.content, status_code=response.status_code, + headers=headers.items(), mimetype=mimetype, ) diff --git a/openapi_core/contrib/falcon/responses.py b/openapi_core/contrib/falcon/responses.py index cc996920..8b28f09a 100644 --- a/openapi_core/contrib/falcon/responses.py +++ b/openapi_core/contrib/falcon/responses.py @@ -19,5 +19,6 @@ def create(cls, response): return OpenAPIResponse( data=data, status_code=status_code, + headers=response.headers, mimetype=mimetype, ) diff --git a/openapi_core/contrib/flask/responses.py b/openapi_core/contrib/flask/responses.py index 73e7605b..c6a1cffb 100644 --- a/openapi_core/contrib/flask/responses.py +++ b/openapi_core/contrib/flask/responses.py @@ -9,5 +9,6 @@ def create(cls, response): return OpenAPIResponse( data=response.data, status_code=response._status_code, + headers=response.headers, mimetype=response.mimetype, ) diff --git a/openapi_core/contrib/requests/responses.py b/openapi_core/contrib/requests/responses.py index 502d6b9b..6f3c324a 100644 --- a/openapi_core/contrib/requests/responses.py +++ b/openapi_core/contrib/requests/responses.py @@ -7,8 +7,10 @@ class RequestsOpenAPIResponseFactory(object): @classmethod def create(cls, response): mimetype = response.headers.get('Content-Type') + headers = dict(response.headers) return OpenAPIResponse( data=response.content, status_code=response.status_code, + headers=headers, mimetype=mimetype, ) diff --git a/tests/integration/contrib/django/data/djangoproject/testapp/views.py b/tests/integration/contrib/django/data/djangoproject/testapp/views.py index fa8448a6..e28a80af 100644 --- a/tests/integration/contrib/django/data/djangoproject/testapp/views.py +++ b/tests/integration/contrib/django/data/djangoproject/testapp/views.py @@ -30,6 +30,7 @@ def get(self, request, pk): "test": "test_val", } django_response = JsonResponse(response_dict) + django_response['X-Rate-Limit'] = '12' openapi_response = DjangoOpenAPIResponse(django_response) validator = ResponseValidator(spec) diff --git a/tests/integration/contrib/django/data/openapi.yaml b/tests/integration/contrib/django/data/openapi.yaml index 37ba7be6..58c8ec57 100644 --- a/tests/integration/contrib/django/data/openapi.yaml +++ b/tests/integration/contrib/django/data/openapi.yaml @@ -17,6 +17,12 @@ paths: type: string required: - test + headers: + X-Rate-Limit: + description: Rate limit + schema: + type: integer + required: true parameters: - required: true in: path diff --git a/tests/integration/contrib/falcon/conftest.py b/tests/integration/contrib/falcon/conftest.py index 5ad05030..66d209de 100644 --- a/tests/integration/contrib/falcon/conftest.py +++ b/tests/integration/contrib/falcon/conftest.py @@ -40,11 +40,13 @@ def create_request( @pytest.fixture def response_factory(environ_factory): def create_response( - data, status_code=200, content_type='application/json'): + data, status_code=200, headers=None, + content_type='application/json'): options = ResponseOptions() resp = Response(options) resp.body = data resp.content_type = content_type resp.status = HTTP_200 + resp.set_headers(headers or {}) return resp return create_response diff --git a/tests/integration/contrib/falcon/data/v3.0/falcon_factory.yaml b/tests/integration/contrib/falcon/data/v3.0/falcon_factory.yaml index d6b5e4be..295f3670 100644 --- a/tests/integration/contrib/falcon/data/v3.0/falcon_factory.yaml +++ b/tests/integration/contrib/falcon/data/v3.0/falcon_factory.yaml @@ -32,6 +32,12 @@ paths: properties: data: type: string + headers: + X-Rate-Limit: + description: Rate limit + schema: + type: integer + required: true default: description: Return errors. content: diff --git a/tests/integration/contrib/falcon/test_falcon_middlewares.py b/tests/integration/contrib/falcon/test_falcon_middlewares.py index a2348357..cbfce002 100644 --- a/tests/integration/contrib/falcon/test_falcon_middlewares.py +++ b/tests/integration/contrib/falcon/test_falcon_middlewares.py @@ -68,6 +68,7 @@ def view_response_callable(request, response, id): response.content_type = MEDIA_HTML response.status = HTTP_200 response.body = 'success' + response.set_header('X-Rate-Limit', '12') self.view_response_callable = view_response_callable headers = {'Content-Type': 'application/json'} result = client.simulate_get( @@ -193,6 +194,7 @@ def view_response_callable(request, response, id): response.body = dumps({ 'data': 'data', }) + response.set_header('X-Rate-Limit', '12') self.view_response_callable = view_response_callable headers = {'Content-Type': 'application/json'} diff --git a/tests/integration/contrib/falcon/test_falcon_validation.py b/tests/integration/contrib/falcon/test_falcon_validation.py index 9e5466cf..c54f448c 100644 --- a/tests/integration/contrib/falcon/test_falcon_validation.py +++ b/tests/integration/contrib/falcon/test_falcon_validation.py @@ -21,7 +21,10 @@ def test_response_validator_path_pattern(self, validator = ResponseValidator(spec) request = request_factory('GET', '/browse/12', subdomain='kb') openapi_request = FalconOpenAPIRequestFactory.create(request) - response = response_factory('{"data": "data"}', status_code=200) + response = response_factory( + '{"data": "data"}', + status_code=200, headers={'X-Rate-Limit': '12'}, + ) openapi_response = FalconOpenAPIResponseFactory.create(response) result = validator.validate(openapi_request, openapi_response) assert not result.errors diff --git a/tests/integration/contrib/flask/conftest.py b/tests/integration/contrib/flask/conftest.py index 4e86bcdc..c737f009 100644 --- a/tests/integration/contrib/flask/conftest.py +++ b/tests/integration/contrib/flask/conftest.py @@ -44,6 +44,9 @@ def create_request(method, path, subdomain=None, query_string=None): @pytest.fixture def response_factory(): def create_response( - data, status_code=200, content_type='application/json'): - return Response(data, status=status_code, content_type=content_type) + data, status_code=200, headers=None, + content_type='application/json'): + return Response( + data, status=status_code, headers=headers, + content_type=content_type) return create_response diff --git a/tests/integration/contrib/flask/data/v3.0/flask_factory.yaml b/tests/integration/contrib/flask/data/v3.0/flask_factory.yaml index 6ed6d563..3b674c7c 100644 --- a/tests/integration/contrib/flask/data/v3.0/flask_factory.yaml +++ b/tests/integration/contrib/flask/data/v3.0/flask_factory.yaml @@ -26,6 +26,12 @@ paths: properties: data: type: string + headers: + X-Rate-Limit: + description: Rate limit + schema: + type: integer + required: true default: description: Return errors. content: diff --git a/tests/integration/contrib/flask/test_flask_decorator.py b/tests/integration/contrib/flask/test_flask_decorator.py index 31c30c6f..6e6c899c 100644 --- a/tests/integration/contrib/flask/test_flask_decorator.py +++ b/tests/integration/contrib/flask/test_flask_decorator.py @@ -62,7 +62,9 @@ def view_response_callable(*args, **kwargs): assert request.openapi.parameters == RequestParameters(path={ 'id': 12, }) - return make_response('success', 200) + resp = make_response('success', 200) + resp.headers['X-Rate-Limit'] = '12' + return resp self.view_response_callable = view_response_callable result = client.get('/browse/12/') @@ -172,7 +174,9 @@ def view_response_callable(*args, **kwargs): assert request.openapi.parameters == RequestParameters(path={ 'id': 12, }) - return jsonify(data='data') + resp = jsonify(data='data') + resp.headers['X-Rate-Limit'] = '12' + return resp self.view_response_callable = view_response_callable result = client.get('/browse/12/') diff --git a/tests/integration/contrib/flask/test_flask_validation.py b/tests/integration/contrib/flask/test_flask_validation.py index 672df583..95170f37 100644 --- a/tests/integration/contrib/flask/test_flask_validation.py +++ b/tests/integration/contrib/flask/test_flask_validation.py @@ -22,7 +22,10 @@ def test_response_validator_path_pattern(self, validator = ResponseValidator(flask_spec) request = request_factory('GET', '/browse/12/', subdomain='kb') openapi_request = FlaskOpenAPIRequest(request) - response = response_factory('{"data": "data"}', status_code=200) + response = response_factory( + '{"data": "data"}', + status_code=200, headers={'X-Rate-Limit': '12'}, + ) openapi_response = FlaskOpenAPIResponse(response) result = validator.validate(openapi_request, openapi_response) assert not result.errors diff --git a/tests/integration/contrib/flask/test_flask_views.py b/tests/integration/contrib/flask/test_flask_views.py index 4134a0f7..2933e505 100644 --- a/tests/integration/contrib/flask/test_flask_views.py +++ b/tests/integration/contrib/flask/test_flask_views.py @@ -55,6 +55,7 @@ def view(self, app, details_view_func, list_view_func): def test_invalid_content_type(self, client): self.view_response = make_response('success', 200) + self.view_response.headers['X-Rate-Limit'] = '12' result = client.get('/browse/12/') @@ -158,8 +159,31 @@ def test_endpoint_error(self, client): assert result.status_code == 400 assert result.json == expected_data + def test_missing_required_header(self, client): + self.view_response = jsonify(data='data') + + result = client.get('/browse/12/') + + expected_data = { + 'errors': [ + { + 'class': ( + "" + ), + 'status': 400, + 'title': ( + "Missing required header: X-Rate-Limit" + ) + } + ] + } + assert result.status_code == 400 + assert result.json == expected_data + def test_valid(self, client): self.view_response = jsonify(data='data') + self.view_response.headers['X-Rate-Limit'] = '12' result = client.get('/browse/12/') diff --git a/tests/integration/contrib/requests/data/v3.0/requests_factory.yaml b/tests/integration/contrib/requests/data/v3.0/requests_factory.yaml index c3f73cd2..c7ea6c3a 100644 --- a/tests/integration/contrib/requests/data/v3.0/requests_factory.yaml +++ b/tests/integration/contrib/requests/data/v3.0/requests_factory.yaml @@ -44,6 +44,12 @@ paths: properties: data: type: string + headers: + X-Rate-Limit: + description: Rate limit + schema: + type: integer + required: true default: description: Return errors. content: diff --git a/tests/integration/contrib/requests/test_requests_validation.py b/tests/integration/contrib/requests/test_requests_validation.py index 997a1a43..c15bf361 100644 --- a/tests/integration/contrib/requests/test_requests_validation.py +++ b/tests/integration/contrib/requests/test_requests_validation.py @@ -22,6 +22,7 @@ def test_response_validator_path_pattern(self, spec): responses.add( responses.POST, 'http://localhost/browse/12/?q=string', json={"data": "data"}, status=200, match_querystring=True, + headers={'X-Rate-Limit': '12'}, ) validator = ResponseValidator(spec) request = requests.Request(