From 5a91425c68040aa5d4f2e253c5a94f9c460abb5c Mon Sep 17 00:00:00 2001 From: Elisha Yadgaran Date: Tue, 17 Nov 2020 00:12:56 -0800 Subject: [PATCH 1/3] use prepared request to format payload before converting --- openapi_core/contrib/requests/requests.py | 50 +++++++++++++++++++---- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/openapi_core/contrib/requests/requests.py b/openapi_core/contrib/requests/requests.py index 12921d9d..c25ad678 100644 --- a/openapi_core/contrib/requests/requests.py +++ b/openapi_core/contrib/requests/requests.py @@ -1,5 +1,8 @@ """OpenAPI core contrib requests requests module""" +from typing import Union from werkzeug.datastructures import ImmutableMultiDict +from requests import Request, PreparedRequest +from urllib.parse import urlparse, parse_qs from openapi_core.validation.request.datatypes import ( RequestParameters, OpenAPIRequest, @@ -9,26 +12,57 @@ class RequestsOpenAPIRequestFactory(object): @classmethod - def create(cls, request): + def create(cls, request: Union[Request, PreparedRequest]) -> OpenAPIRequest: + """ + Converts a requests request to an OpenAPI one + + Internally converts to a `PreparedRequest` first to parse the exact + payload being sent + """ + if isinstance(request, Request): + request = request.prepare() + + # Method method = request.method.lower() - cookie = request.cookies or {} + # Cookies + if request._cookies is not None: + # cookies are stored in a cookiejar object + cookie = request._cookies.get_dict() + else: + cookie = {} + + # Preparing a request formats the URL with params, strip them out again + o = urlparse(request.url) + params = parse_qs(o.query) + # extract the URL without query parameters + url = o._replace(query=None).geturl() # gets deduced by path finder against spec path = {} - mimetype = request.headers.get('Accept') or \ - request.headers.get('Content-Type') + # Order matters because all python requests issued from a session include + # Accept */* which does not necessarily match the content type + mimetype = request.headers.get('Content-Type') or \ + request.headers.get('Accept') + + # Headers - request.headers is not an instance of dict, which is expected + header = dict(request.headers) + + # Body + # TODO: figure out if request._body_position is relevant + body = request.body + parameters = RequestParameters( - query=ImmutableMultiDict(request.params), - header=request.headers, + query=ImmutableMultiDict(params), + header=header, cookie=cookie, path=path, ) return OpenAPIRequest( - full_url_pattern=request.url, + full_url_pattern=url, method=method, parameters=parameters, - body=request.data, + body=body, mimetype=mimetype, ) From f316b81ba395d09ff0b9b3fd61a7dae1925e22a9 Mon Sep 17 00:00:00 2001 From: p1c2u Date: Wed, 24 Mar 2021 12:56:55 +0000 Subject: [PATCH 2/3] use prepared request tests --- openapi_core/contrib/requests/requests.py | 3 +- .../requests/data/v3.0/requests_factory.yaml | 20 ++++++++++- .../requests/test_requests_requests.py | 13 ++++--- .../requests/test_requests_validation.py | 35 +++++++++++++++---- 4 files changed, 58 insertions(+), 13 deletions(-) diff --git a/openapi_core/contrib/requests/requests.py b/openapi_core/contrib/requests/requests.py index c25ad678..b2eeb64e 100644 --- a/openapi_core/contrib/requests/requests.py +++ b/openapi_core/contrib/requests/requests.py @@ -26,11 +26,10 @@ def create(cls, request: Union[Request, PreparedRequest]) -> OpenAPIRequest: method = request.method.lower() # Cookies + cookie = {} if request._cookies is not None: # cookies are stored in a cookiejar object cookie = request._cookies.get_dict() - else: - cookie = {} # Preparing a request formats the URL with params, strip them out again o = urlparse(request.url) 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 abef7eb6..c3f73cd2 100644 --- a/tests/integration/contrib/requests/data/v3.0/requests_factory.yaml +++ b/tests/integration/contrib/requests/data/v3.0/requests_factory.yaml @@ -13,7 +13,25 @@ paths: description: the ID of the resource to retrieve schema: type: integer - get: + - name: q + in: query + required: true + description: query key + schema: + type: string + post: + requestBody: + description: request data + required: True + content: + application/json: + schema: + type: object + required: + - param1 + properties: + param1: + type: integer responses: 200: description: Return the resource. diff --git a/tests/integration/contrib/requests/test_requests_requests.py b/tests/integration/contrib/requests/test_requests_requests.py index 45e0258a..bd0556b6 100644 --- a/tests/integration/contrib/requests/test_requests_requests.py +++ b/tests/integration/contrib/requests/test_requests_requests.py @@ -15,6 +15,7 @@ def test_simple(self, request_factory, request): query = ImmutableMultiDict([]) headers = request.headers cookies = {} + prepared = request.prepare() assert openapi_request.parameters == RequestParameters( path=path, query=query, @@ -23,7 +24,7 @@ def test_simple(self, request_factory, request): ) assert openapi_request.method == request.method.lower() assert openapi_request.full_url_pattern == 'http://localhost/' - assert openapi_request.body == request.data + assert openapi_request.body == prepared.body assert openapi_request.mimetype == 'application/json' def test_multiple_values(self, request_factory, request): @@ -44,9 +45,10 @@ def test_multiple_values(self, request_factory, request): header=headers, cookie=cookies, ) + prepared = request.prepare() assert openapi_request.method == request.method.lower() assert openapi_request.full_url_pattern == 'http://localhost/' - assert openapi_request.body == request.data + assert openapi_request.body == prepared.body assert openapi_request.mimetype == 'application/json' def test_url_rule(self, request_factory, request): @@ -57,7 +59,9 @@ def test_url_rule(self, request_factory, request): # empty when not bound to spec path = {} query = ImmutableMultiDict([]) - headers = request.headers + headers = ( + ('Content-Type', 'application/json'), + ) cookies = {} assert openapi_request.parameters == RequestParameters( path=path, @@ -65,8 +69,9 @@ def test_url_rule(self, request_factory, request): header=headers, cookie=cookies, ) + prepared = request.prepare() assert openapi_request.method == request.method.lower() assert openapi_request.full_url_pattern == \ 'http://localhost/browse/12/' - assert openapi_request.body == request.data + assert openapi_request.body == prepared.body assert openapi_request.mimetype == 'application/json' diff --git a/tests/integration/contrib/requests/test_requests_validation.py b/tests/integration/contrib/requests/test_requests_validation.py index 6812d93f..997a1a43 100644 --- a/tests/integration/contrib/requests/test_requests_validation.py +++ b/tests/integration/contrib/requests/test_requests_validation.py @@ -10,7 +10,7 @@ from openapi_core.validation.response.validators import ResponseValidator -class TestFlaskOpenAPIValidation(object): +class TestRequestsOpenAPIValidation(object): @pytest.fixture def spec(self, factory): @@ -20,10 +20,16 @@ def spec(self, factory): @responses.activate def test_response_validator_path_pattern(self, spec): responses.add( - responses.GET, 'http://localhost/browse/12/', - json={"data": "data"}, status=200) + responses.POST, 'http://localhost/browse/12/?q=string', + json={"data": "data"}, status=200, match_querystring=True, + ) validator = ResponseValidator(spec) - request = requests.Request('GET', 'http://localhost/browse/12/') + request = requests.Request( + 'POST', 'http://localhost/browse/12/', + params={'q': 'string'}, + headers={'content-type': 'application/json'}, + json={'param1': 1}, + ) request_prepared = request.prepare() session = requests.Session() response = session.send(request_prepared) @@ -32,10 +38,27 @@ def test_response_validator_path_pattern(self, spec): result = validator.validate(openapi_request, openapi_response) assert not result.errors - @responses.activate def test_request_validator_path_pattern(self, spec): validator = RequestValidator(spec) - request = requests.Request('GET', 'http://localhost/browse/12/') + request = requests.Request( + 'POST', 'http://localhost/browse/12/', + params={'q': 'string'}, + headers={'content-type': 'application/json'}, + json={'param1': 1}, + ) openapi_request = RequestsOpenAPIRequest(request) result = validator.validate(openapi_request) assert not result.errors + + def test_request_validator_prepared_request(self, spec): + validator = RequestValidator(spec) + request = requests.Request( + 'POST', 'http://localhost/browse/12/', + params={'q': 'string'}, + headers={'content-type': 'application/json'}, + json={'param1': 1}, + ) + request_prepared = request.prepare() + openapi_request = RequestsOpenAPIRequest(request_prepared) + result = validator.validate(openapi_request) + assert not result.errors From ff4a6c81eeda0e2274aa9dc03597779c141e5728 Mon Sep 17 00:00:00 2001 From: p1c2u Date: Wed, 24 Mar 2021 13:06:35 +0000 Subject: [PATCH 3/3] requests request typing fix --- openapi_core/contrib/requests/requests.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openapi_core/contrib/requests/requests.py b/openapi_core/contrib/requests/requests.py index b2eeb64e..4d0490fe 100644 --- a/openapi_core/contrib/requests/requests.py +++ b/openapi_core/contrib/requests/requests.py @@ -1,8 +1,8 @@ """OpenAPI core contrib requests requests module""" -from typing import Union +from __future__ import absolute_import from werkzeug.datastructures import ImmutableMultiDict -from requests import Request, PreparedRequest -from urllib.parse import urlparse, parse_qs +from requests import Request +from six.moves.urllib.parse import urlparse, parse_qs from openapi_core.validation.request.datatypes import ( RequestParameters, OpenAPIRequest, @@ -12,7 +12,7 @@ class RequestsOpenAPIRequestFactory(object): @classmethod - def create(cls, request: Union[Request, PreparedRequest]) -> OpenAPIRequest: + def create(cls, request): """ Converts a requests request to an OpenAPI one @@ -40,12 +40,13 @@ def create(cls, request: Union[Request, PreparedRequest]) -> OpenAPIRequest: # gets deduced by path finder against spec path = {} - # Order matters because all python requests issued from a session include - # Accept */* which does not necessarily match the content type + # Order matters because all python requests issued from a session + # include Accept */* which does not necessarily match the content type mimetype = request.headers.get('Content-Type') or \ request.headers.get('Accept') - # Headers - request.headers is not an instance of dict, which is expected + # Headers - request.headers is not an instance of dict + # which is expected header = dict(request.headers) # Body