Skip to content

Commit a4cba7c

Browse files
committed
Response headers support in contrib
1 parent 10675ca commit a4cba7c

19 files changed

+90
-10
lines changed

openapi_core/contrib/django/compat.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,18 @@
44
)
55

66

7-
def get_headers(req):
7+
def get_request_headers(req):
88
# in Django 1 headers is not defined
99
return req.headers if hasattr(req, 'headers') else \
1010
HttpHeaders(req.META)
1111

1212

13+
def get_response_headers(resp):
14+
# in Django 2 headers is not defined
15+
return resp.headers if hasattr(resp, 'headers') else \
16+
dict(resp._headers.values())
17+
18+
1319
def get_current_scheme_host(req):
1420
# in Django 1 _current_scheme_host is not defined
1521
return req._current_scheme_host if hasattr(req, '_current_scheme_host') \

openapi_core/contrib/django/requests.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from six.moves.urllib.parse import urljoin
55

66
from openapi_core.contrib.django.compat import (
7-
get_headers, get_current_scheme_host,
7+
get_request_headers, get_current_scheme_host,
88
)
99
from openapi_core.validation.request.datatypes import (
1010
RequestParameters, OpenAPIRequest,
@@ -39,7 +39,7 @@ def create(cls, request):
3939
path_pattern = '/' + route
4040

4141
path = request.resolver_match and request.resolver_match.kwargs or {}
42-
headers = get_headers(request)
42+
headers = get_request_headers(request)
4343
parameters = RequestParameters(
4444
path=path,
4545
query=request.GET,

openapi_core/contrib/django/responses.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""OpenAPI core contrib django responses module"""
2+
from openapi_core.contrib.django.compat import get_response_headers
23
from openapi_core.validation.response.datatypes import OpenAPIResponse
34

45

@@ -7,8 +8,10 @@ class DjangoOpenAPIResponseFactory(object):
78
@classmethod
89
def create(cls, response):
910
mimetype = response["Content-Type"]
11+
headers = get_response_headers(response)
1012
return OpenAPIResponse(
1113
data=response.content,
1214
status_code=response.status_code,
15+
headers=headers.items(),
1316
mimetype=mimetype,
1417
)

openapi_core/contrib/falcon/responses.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ def create(cls, response):
1919
return OpenAPIResponse(
2020
data=data,
2121
status_code=status_code,
22+
headers=response.headers,
2223
mimetype=mimetype,
2324
)

openapi_core/contrib/flask/responses.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ def create(cls, response):
99
return OpenAPIResponse(
1010
data=response.data,
1111
status_code=response._status_code,
12+
headers=response.headers,
1213
mimetype=response.mimetype,
1314
)

openapi_core/contrib/requests/responses.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ class RequestsOpenAPIResponseFactory(object):
77
@classmethod
88
def create(cls, response):
99
mimetype = response.headers.get('Content-Type')
10+
headers = dict(response.headers)
1011
return OpenAPIResponse(
1112
data=response.content,
1213
status_code=response.status_code,
14+
headers=headers,
1315
mimetype=mimetype,
1416
)

tests/integration/contrib/django/data/djangoproject/testapp/views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def get(self, request, pk):
3030
"test": "test_val",
3131
}
3232
django_response = JsonResponse(response_dict)
33+
django_response['X-Rate-Limit'] = '12'
3334

3435
openapi_response = DjangoOpenAPIResponse(django_response)
3536
validator = ResponseValidator(spec)

tests/integration/contrib/django/data/openapi.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ paths:
1717
type: string
1818
required:
1919
- test
20+
headers:
21+
X-Rate-Limit:
22+
description: Rate limit
23+
schema:
24+
type: integer
25+
required: true
2026
parameters:
2127
- required: true
2228
in: path

tests/integration/contrib/falcon/conftest.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,13 @@ def create_request(
4040
@pytest.fixture
4141
def response_factory(environ_factory):
4242
def create_response(
43-
data, status_code=200, content_type='application/json'):
43+
data, status_code=200, headers=None,
44+
content_type='application/json'):
4445
options = ResponseOptions()
4546
resp = Response(options)
4647
resp.body = data
4748
resp.content_type = content_type
4849
resp.status = HTTP_200
50+
resp.set_headers(headers or {})
4951
return resp
5052
return create_response

tests/integration/contrib/falcon/data/v3.0/falcon_factory.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ paths:
3232
properties:
3333
data:
3434
type: string
35+
headers:
36+
X-Rate-Limit:
37+
description: Rate limit
38+
schema:
39+
type: integer
40+
required: true
3541
default:
3642
description: Return errors.
3743
content:

tests/integration/contrib/falcon/test_falcon_middlewares.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def view_response_callable(request, response, id):
6868
response.content_type = MEDIA_HTML
6969
response.status = HTTP_200
7070
response.body = 'success'
71+
response.set_header('X-Rate-Limit', '12')
7172
self.view_response_callable = view_response_callable
7273
headers = {'Content-Type': 'application/json'}
7374
result = client.simulate_get(
@@ -193,6 +194,7 @@ def view_response_callable(request, response, id):
193194
response.body = dumps({
194195
'data': 'data',
195196
})
197+
response.set_header('X-Rate-Limit', '12')
196198
self.view_response_callable = view_response_callable
197199

198200
headers = {'Content-Type': 'application/json'}

tests/integration/contrib/falcon/test_falcon_validation.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ def test_response_validator_path_pattern(self,
2121
validator = ResponseValidator(spec)
2222
request = request_factory('GET', '/browse/12', subdomain='kb')
2323
openapi_request = FalconOpenAPIRequestFactory.create(request)
24-
response = response_factory('{"data": "data"}', status_code=200)
24+
response = response_factory(
25+
'{"data": "data"}',
26+
status_code=200, headers={'X-Rate-Limit': '12'},
27+
)
2528
openapi_response = FalconOpenAPIResponseFactory.create(response)
2629
result = validator.validate(openapi_request, openapi_response)
2730
assert not result.errors

tests/integration/contrib/flask/conftest.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ def create_request(method, path, subdomain=None, query_string=None):
4444
@pytest.fixture
4545
def response_factory():
4646
def create_response(
47-
data, status_code=200, content_type='application/json'):
48-
return Response(data, status=status_code, content_type=content_type)
47+
data, status_code=200, headers=None,
48+
content_type='application/json'):
49+
return Response(
50+
data, status=status_code, headers=headers,
51+
content_type=content_type)
4952
return create_response

tests/integration/contrib/flask/data/v3.0/flask_factory.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ paths:
2626
properties:
2727
data:
2828
type: string
29+
headers:
30+
X-Rate-Limit:
31+
description: Rate limit
32+
schema:
33+
type: integer
34+
required: true
2935
default:
3036
description: Return errors.
3137
content:

tests/integration/contrib/flask/test_flask_decorator.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ def view_response_callable(*args, **kwargs):
6262
assert request.openapi.parameters == RequestParameters(path={
6363
'id': 12,
6464
})
65-
return make_response('success', 200)
65+
resp = make_response('success', 200)
66+
resp.headers['X-Rate-Limit'] = '12'
67+
return resp
6668
self.view_response_callable = view_response_callable
6769
result = client.get('/browse/12/')
6870

@@ -172,7 +174,9 @@ def view_response_callable(*args, **kwargs):
172174
assert request.openapi.parameters == RequestParameters(path={
173175
'id': 12,
174176
})
175-
return jsonify(data='data')
177+
resp = jsonify(data='data')
178+
resp.headers['X-Rate-Limit'] = '12'
179+
return resp
176180
self.view_response_callable = view_response_callable
177181

178182
result = client.get('/browse/12/')

tests/integration/contrib/flask/test_flask_validation.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ def test_response_validator_path_pattern(self,
2222
validator = ResponseValidator(flask_spec)
2323
request = request_factory('GET', '/browse/12/', subdomain='kb')
2424
openapi_request = FlaskOpenAPIRequest(request)
25-
response = response_factory('{"data": "data"}', status_code=200)
25+
response = response_factory(
26+
'{"data": "data"}',
27+
status_code=200, headers={'X-Rate-Limit': '12'},
28+
)
2629
openapi_response = FlaskOpenAPIResponse(response)
2730
result = validator.validate(openapi_request, openapi_response)
2831
assert not result.errors

tests/integration/contrib/flask/test_flask_views.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def view(self, app, details_view_func, list_view_func):
5555

5656
def test_invalid_content_type(self, client):
5757
self.view_response = make_response('success', 200)
58+
self.view_response.headers['X-Rate-Limit'] = '12'
5859

5960
result = client.get('/browse/12/')
6061

@@ -158,8 +159,31 @@ def test_endpoint_error(self, client):
158159
assert result.status_code == 400
159160
assert result.json == expected_data
160161

162+
def test_missing_required_header(self, client):
163+
self.view_response = jsonify(data='data')
164+
165+
result = client.get('/browse/12/')
166+
167+
expected_data = {
168+
'errors': [
169+
{
170+
'class': (
171+
"<class 'openapi_core.exceptions."
172+
"MissingRequiredHeader'>"
173+
),
174+
'status': 400,
175+
'title': (
176+
"Missing required header: X-Rate-Limit"
177+
)
178+
}
179+
]
180+
}
181+
assert result.status_code == 400
182+
assert result.json == expected_data
183+
161184
def test_valid(self, client):
162185
self.view_response = jsonify(data='data')
186+
self.view_response.headers['X-Rate-Limit'] = '12'
163187

164188
result = client.get('/browse/12/')
165189

tests/integration/contrib/requests/data/v3.0/requests_factory.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ paths:
4444
properties:
4545
data:
4646
type: string
47+
headers:
48+
X-Rate-Limit:
49+
description: Rate limit
50+
schema:
51+
type: integer
52+
required: true
4753
default:
4854
description: Return errors.
4955
content:

tests/integration/contrib/requests/test_requests_validation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def test_response_validator_path_pattern(self, spec):
2222
responses.add(
2323
responses.POST, 'http://localhost/browse/12/?q=string',
2424
json={"data": "data"}, status=200, match_querystring=True,
25+
headers={'X-Rate-Limit': '12'},
2526
)
2627
validator = ResponseValidator(spec)
2728
request = requests.Request(

0 commit comments

Comments
 (0)