From 29451ffbf63cb7d658c903fd5e136f077e24abfa Mon Sep 17 00:00:00 2001 From: Paul Brand Date: Tue, 30 Apr 2024 22:55:52 +0200 Subject: [PATCH 01/11] Adds support for TARCE and CONNECT http methods --- src/RequestsLibrary/RequestsKeywords.py | 71 +++++++++++++++---- .../RequestsOnSessionKeywords.py | 66 ++++++++++++++--- 2 files changed, 115 insertions(+), 22 deletions(-) diff --git a/src/RequestsLibrary/RequestsKeywords.py b/src/RequestsLibrary/RequestsKeywords.py index 84a43a1..c103b19 100644 --- a/src/RequestsLibrary/RequestsKeywords.py +++ b/src/RequestsLibrary/RequestsKeywords.py @@ -28,13 +28,14 @@ def _common_request( **kwargs): if session: - method_function = getattr(session, method) + request_function = getattr(session, "request") else: - method_function = getattr(requests, method) + request_function = getattr(requests, "request") self._capture_output() - resp = method_function( + resp = request_function( + method, self._merge_url(session, uri), timeout=self._get_timeout(kwargs.pop('timeout', None)), cookies=kwargs.pop('cookies', self.cookies), @@ -177,7 +178,7 @@ def session_less_get(self, url, params=None, | ``files`` | Dictionary of file-like-objects (or ``{'name': file-tuple}``) for multipart encoding upload. ``file-tuple`` can be a 2-tuple ``('filename', fileobj)``, 3-tuple ``('filename', fileobj, 'content_type')`` or a 4-tuple ``('filename', fileobj, 'content_type', custom_headers)``, where ``'content-type'`` is a string defining the content type of the given file and ``custom_headers`` a dict-like object containing additional headers to add for the file. | | ``auth`` | Auth tuple to enable Basic/Digest/Custom HTTP Auth. | | ``timeout`` | How many seconds to wait for the server to send data before giving up, as a float, or a ``(connect timeout, read timeout)`` tuple. | - | ``allow_redirects`` | Boolean. Enable/disable (values ``${True}`` or ``${False}``) GET/OPTIONS/POST/PUT/PATCH/DELETE/HEAD redirection. Defaults to ``${True}``. | + | ``allow_redirects`` | Boolean. Enable/disable (values ``${True}`` or ``${False}``) GET/POST/PUT/DELETE/PATCH/TRACE/CONNECT redirection. Defaults to ``${True}``. | | ``proxies`` | Dictionary mapping protocol or protocol and host to the URL of the proxy (e.g. {'http': 'foo.bar:3128', 'http://host.name': 'foo.bar:4012'}) | | ``verify`` | Either a boolean, in which case it controls whether we verify the server's TLS certificate, or a string, in which case it must be a path to a CA bundle to use. Defaults to ``${True}``. Warning: if a session has been created with ``verify=${False}`` any other requests will not verify the SSL certificate. | | ``stream`` | if ``${False}``, the response content will be immediately downloaded. | @@ -187,7 +188,7 @@ def session_less_get(self, url, params=None, https://requests.readthedocs.io/en/latest/api/ """ - response = self._common_request('get', None, url, + response = self._common_request('GET', None, url, params=params, **kwargs) self._check_status(expected_status, response, msg) return response @@ -214,7 +215,7 @@ def session_less_post(self, url, data=None, json=None, Other optional requests arguments can be passed using ``**kwargs`` see the `GET` keyword for the complete list. """ - response = self._common_request('post', None, url, + response = self._common_request('POST', None, url, data=data, json=json, **kwargs) self._check_status(expected_status, response, msg) return response @@ -242,7 +243,7 @@ def session_less_put(self, url, data=None, json=None, see the `GET` keyword for the complete list. """ - response = self._common_request("put", None, url, + response = self._common_request("PUT", None, url, data=data, json=json, **kwargs) self._check_status(expected_status, response, msg) return response @@ -250,7 +251,7 @@ def session_less_put(self, url, data=None, json=None, @keyword('HEAD') @warn_if_equal_symbol_in_url_session_less def session_less_head(self, url, - expected_status=None, msg=None, **kwargs): + expected_status=None, msg=None, allow_redirects=False, **kwargs): """ Sends a HEAD request. @@ -268,7 +269,7 @@ def session_less_head(self, url, Other optional requests arguments can be passed using ``**kwargs`` see the `GET` keyword for the complete list. """ - response = self._common_request('head', None, url, **kwargs) + response = self._common_request('HEAD', None, url, allow_redirects=allow_redirects, **kwargs) self._check_status(expected_status, response, msg) return response @@ -294,7 +295,7 @@ def session_less_patch(self, url, data=None, json=None, Other optional requests arguments can be passed using ``**kwargs`` see the `GET` keyword for the complete list. """ - response = self._common_request('patch', None, url, + response = self._common_request('PATCH', None, url, data=data, json=json, **kwargs) self._check_status(expected_status, response, msg) return response @@ -317,14 +318,14 @@ def session_less_delete(self, url, Other optional requests arguments can be passed using ``**kwargs`` see the `GET` keyword for the complete list. """ - response = self._common_request("delete", None, url, **kwargs) + response = self._common_request("DELETE", None, url, **kwargs) self._check_status(expected_status, response, msg) return response @keyword('OPTIONS') @warn_if_equal_symbol_in_url_session_less def session_less_options(self, url, - expected_status=None, msg=None, **kwargs): + expected_status=None, msg=None, allow_redirects=False, **kwargs): """ Sends a OPTIONS request. @@ -339,6 +340,50 @@ def session_less_options(self, url, Other optional requests arguments can be passed using ``**kwargs`` see the `GET` keyword for the complete list. """ - response = self._common_request("options", None, url, **kwargs) + response = self._common_request("OPTIONS", None, url, allow_redirects=allow_redirects, **kwargs) + self._check_status(expected_status, response, msg) + return response + + @keyword('CONNECT') + @warn_if_equal_symbol_in_url_session_less + def session_less_connect(self, url, + expected_status=None, msg=None, **kwargs): + """ + Sends a CONNECT request. + + The endpoint used to retrieve the resource is the ``url``. + + By default this keyword fails if a status code with error values is returned in the response, + this behavior can be modified using the ``expected_status`` and ``msg`` parameters, + read more about it in `Status Should Be` keyword documentation. + In order to disable this implicit assert mechanism you can pass as ``expected_status`` the values ``any`` or + ``anything``. + + Other optional requests arguments can be passed using ``**kwargs`` + see the `GET` keyword for the complete list. + """ + response = self._common_request("CONNECT", None, url, **kwargs) self._check_status(expected_status, response, msg) return response + + @keyword('TRACE') + @warn_if_equal_symbol_in_url_session_less + def session_less_trace(self, url, + expected_status=None, msg=None, **kwargs): + """ + Sends a TRACE request. + + The endpoint used to retrieve the resource is the ``url``. + + By default this keyword fails if a status code with error values is returned in the response, + this behavior can be modified using the ``expected_status`` and ``msg`` parameters, + read more about it in `Status Should Be` keyword documentation. + In order to disable this implicit assert mechanism you can pass as ``expected_status`` the values ``any`` or + ``anything``. + + Other optional requests arguments can be passed using ``**kwargs`` + see the `GET` keyword for the complete list. + """ + response = self._common_request("TRACE", None, url, **kwargs) + self._check_status(expected_status, response, msg) + return response \ No newline at end of file diff --git a/src/RequestsLibrary/RequestsOnSessionKeywords.py b/src/RequestsLibrary/RequestsOnSessionKeywords.py index ea34e59..13a0618 100644 --- a/src/RequestsLibrary/RequestsOnSessionKeywords.py +++ b/src/RequestsLibrary/RequestsOnSessionKeywords.py @@ -28,7 +28,7 @@ def get_on_session(self, alias, url, params=None, see the `GET` keyword for the complete list. """ session = self._cache.switch(alias) - response = self._common_request("get", session, url, + response = self._common_request("GET", session, url, params=params, **kwargs) self._check_status(expected_status, response, msg) return response @@ -57,7 +57,7 @@ def post_on_session(self, alias, url, data=None, json=None, see the `GET` keyword for the complete list. """ session = self._cache.switch(alias) - response = self._common_request("post", session, url, + response = self._common_request("POST", session, url, data=data, json=json, **kwargs) self._check_status(expected_status, response, msg) return response @@ -86,7 +86,7 @@ def patch_on_session(self, alias, url, data=None, json=None, see the `GET` keyword for the complete list. """ session = self._cache.switch(alias) - response = self._common_request("patch", session, url, + response = self._common_request("PATCH", session, url, data=data, json=json, **kwargs) self._check_status(expected_status, response, msg) return response @@ -115,7 +115,7 @@ def put_on_session(self, alias, url, data=None, json=None, see the `GET` keyword for the complete list. """ session = self._cache.switch(alias) - response = self._common_request("put", session, url, + response = self._common_request("PUT", session, url, data=data, json=json, **kwargs) self._check_status(expected_status, response, msg) return response @@ -140,14 +140,14 @@ def delete_on_session(self, alias, url, see the `GET` keyword for the complete list. """ session = self._cache.switch(alias) - response = self._common_request("delete", session, url, **kwargs) + response = self._common_request("DELETE", session, url, **kwargs) self._check_status(expected_status, response, msg) return response @keyword("HEAD On Session") @warn_if_equal_symbol_in_url_on_session def head_on_session(self, alias, url, - expected_status=None, msg=None, **kwargs): + expected_status=None, msg=None, allow_redirects=False, **kwargs): """ Sends a HEAD request on a previously created HTTP Session. @@ -167,14 +167,14 @@ def head_on_session(self, alias, url, see the `GET` keyword for the complete list. """ session = self._cache.switch(alias) - response = self._common_request("head", session, url, **kwargs) + response = self._common_request("HEAD", session, url, allow_redirects=allow_redirects, **kwargs) self._check_status(expected_status, response, msg) return response @keyword("OPTIONS On Session") @warn_if_equal_symbol_in_url_on_session def options_on_session(self, alias, url, - expected_status=None, msg=None, **kwargs): + expected_status=None, msg=None, allow_redirects=False, **kwargs): """ Sends a OPTIONS request on a previously created HTTP Session. @@ -191,6 +191,54 @@ def options_on_session(self, alias, url, see the `GET` keyword for the complete list. """ session = self._cache.switch(alias) - response = self._common_request("options", session, url, **kwargs) + response = self._common_request("OPTIONS", session, url, allow_redirects=allow_redirects, **kwargs) self._check_status(expected_status, response, msg) return response + + @keyword("CONNECT On Session") + @warn_if_equal_symbol_in_url_on_session + def connect_on_session(self, alias, url, + expected_status=None, msg=None, **kwargs): + """ + Sends a CONNECT request on a previously created HTTP Session. + + Session will be identified using the ``alias`` name. + The endpoint used to retrieve the resource is the ``url``. + + By default this keyword fails if a status code with error values is returned in the response, + this behavior can be modified using the ``expected_status`` and ``msg`` parameters, + read more about it in `Status Should Be` keyword documentation. + In order to disable this implicit assert mechanism you can pass as ``expected_status`` the values ``any`` or + ``anything``. + + Other optional requests arguments can be passed using ``**kwargs`` + see the `GET` keyword for the complete list. + """ + session = self._cache.switch(alias) + response = self._common_request("CONNECT", session, url, **kwargs) + self._check_status(expected_status, response, msg) + return response + + @keyword("TRACE On Session") + @warn_if_equal_symbol_in_url_on_session + def trace_on_session(self, alias, url, + expected_status=None, msg=None, **kwargs): + """ + Sends a TRACE request on a previously created HTTP Session. + + Session will be identified using the ``alias`` name. + The endpoint used to retrieve the resource is the ``url``. + + By default this keyword fails if a status code with error values is returned in the response, + this behavior can be modified using the ``expected_status`` and ``msg`` parameters, + read more about it in `Status Should Be` keyword documentation. + In order to disable this implicit assert mechanism you can pass as ``expected_status`` the values ``any`` or + ``anything``. + + Other optional requests arguments can be passed using ``**kwargs`` + see the `GET` keyword for the complete list. + """ + session = self._cache.switch(alias) + response = self._common_request("TRACE", session, url, **kwargs) + self._check_status(expected_status, response, msg) + return response \ No newline at end of file From 9466c485689700a9df0c29394caf21ee2708eb0f Mon Sep 17 00:00:00 2001 From: Paul Brand Date: Wed, 1 May 2024 00:03:31 +0200 Subject: [PATCH 02/11] Adds acceptance tests for CONNECT and TRACE http methods Limits versions of flask and werkzeug to prevent error when starting http_server for testing --- atests/http_server/core.py | 6 +++--- atests/test_redirect.robot | 28 +++++++++++++++++++++++++-- atests/test_requests.robot | 3 ++- atests/test_requests_on_session.robot | 2 +- setup.py | 4 ++-- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/atests/http_server/core.py b/atests/http_server/core.py index 05e8ac7..babf561 100644 --- a/atests/http_server/core.py +++ b/atests/http_server/core.py @@ -39,7 +39,7 @@ def view_headers(): return jsonify(get_dict('headers')) -@app.route("/anything", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "TRACE"]) +@app.route("/anything", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "TRACE", "HEAD", "CONNECT"]) def view_anything(anything=None): """Returns anything passed in request data. --- @@ -68,7 +68,7 @@ def view_anything(anything=None): @app.route( - "/status/", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "TRACE"] + "/status/", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "TRACE", "HEAD", "CONNECT"] ) def view_status_code(codes): """Return status code or random status code if more than one are given @@ -118,7 +118,7 @@ def view_status_code(codes): return status_code(code) -@app.route("/redirect-to", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "TRACE"]) +@app.route("/redirect-to", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "TRACE", "HEAD", "CONNECT", "OPTIONS"]) def redirect_to(): """302/3XX Redirects to the given URL. --- diff --git a/atests/test_redirect.robot b/atests/test_redirect.robot index 5a07924..3a6dc81 100644 --- a/atests/test_redirect.robot +++ b/atests/test_redirect.robot @@ -28,7 +28,7 @@ Get Request Without Redirection Options Request Without Redirection By Default [Tags] options ${resp}= OPTIONS On Session ${GLOBAL_SESSION} url=/redirect-to?url=anything - Status Should Be OK ${resp} + Status Should Be 302 ${resp} Length Should Be ${resp.history} 0 # TODO understand whether this is the right behavior or not @@ -36,7 +36,7 @@ Options Request With Redirection [Tags] options ${resp}= OPTIONS On Session ${GLOBAL_SESSION} url=/redirect-to?url=anything allow_redirects=${true} Status Should Be OK ${resp} - Length Should Be ${resp.history} 0 + Length Should Be ${resp.history} 1 Head Request With Redirection [Tags] head @@ -94,3 +94,27 @@ Put Request Without Redirection [Tags] put ${resp}= PUT On Session ${GLOBAL_SESSION} url=/redirect-to?url=anything allow_redirects=${false} Status Should be 302 ${resp} + +CONNECT Request Without Redirection + [Tags] connect + ${resp}= CONNECT On Session ${GLOBAL_SESSION} url=/redirect-to?url=anything allow_redirects=${false} + Status Should be 302 ${resp} + Length Should Be ${resp.history} 0 + +CONNECT Request With Redirection + [Tags] connect + ${resp}= CONNECT On Session ${GLOBAL_SESSION} url=/redirect-to?url=anything allow_redirects=${true} + Status Should be OK ${resp} + Length Should Be ${resp.history} 1 + +TRACE Request Without Redirection + [Tags] trace + ${resp}= TRACE On Session ${GLOBAL_SESSION} url=/redirect-to?url=anything allow_redirects=${false} + Status Should be 302 ${resp} + Length Should Be ${resp.history} 0 + +TRACE Request With Redirection + [Tags] trace + ${resp}= TRACE On Session ${GLOBAL_SESSION} url=/redirect-to?url=anything allow_redirects=${true} + Status Should be OK ${resp} + Length Should Be ${resp.history} 1 \ No newline at end of file diff --git a/atests/test_requests.robot b/atests/test_requests.robot index afd00a9..45ab9a0 100644 --- a/atests/test_requests.robot +++ b/atests/test_requests.robot @@ -259,7 +259,7 @@ Options Request On Existing Session Options Request Check Allow Header [Tags] options - ${allow_header}= Create List POST HEAD PATCH GET TRACE DELETE OPTIONS PUT + ${allow_header}= Create List GET POST PUT DELETE PATCH TRACE HEAD CONNECT OPTIONS ${resp}= OPTIONS ${HTTP_LOCAL_SERVER}/anything Status Should Be OK ${resp} ${allow_response_header}= Get From Dictionary ${resp.headers} Allow @@ -275,3 +275,4 @@ Options Request Expect A Success On Unauthorized Request [Tags] options ${resp}= OPTIONS ${HTTP_LOCAL_SERVER}/status/401 expected_status=200 Status Should Be OK ${resp} + diff --git a/atests/test_requests_on_session.robot b/atests/test_requests_on_session.robot index 8fb7e44..c1719fd 100644 --- a/atests/test_requests_on_session.robot +++ b/atests/test_requests_on_session.robot @@ -257,7 +257,7 @@ Options Request On Existing Session Options Request Check Allow Header [Tags] options - ${allow_header}= Create List POST HEAD PATCH GET TRACE DELETE OPTIONS PUT + ${allow_header}= Create List POST HEAD PATCH GET TRACE DELETE OPTIONS PUT CONNECT ${resp}= OPTIONS On Session ${GLOBAL_SESSION} /anything Status Should Be OK ${resp} ${allow_response_header}= Get From Dictionary ${resp.headers} Allow diff --git a/setup.py b/setup.py index edd4009..3dbd194 100644 --- a/setup.py +++ b/setup.py @@ -26,8 +26,8 @@ Topic :: Software Development :: Testing """[1:-1] -TEST_REQUIRE = ['robotframework>=3.2.1', 'pytest', 'flask', 'six', 'coverage', 'flake8'] if PY3 \ - else ['robotframework>=3.2.1', 'pytest', 'flask', 'coverage', 'flake8', 'mock'] +TEST_REQUIRE = ['robotframework>=3.2.1', 'pytest', 'flask==2.3.3', 'werkzeug==2.3.8', 'six', 'coverage', 'flake8'] if PY3 \ + else ['robotframework>=3.2.1', 'pytest', 'flask==2.3.3', 'werkzeug==2.3.8', 'coverage', 'flake8', 'mock'] VERSION = None version_file = join(dirname(abspath(__file__)), 'src', 'RequestsLibrary', 'version.py') From 877d9e60f05d9e90f9c87e4150582ba7afe2e0b1 Mon Sep 17 00:00:00 2001 From: Paul Brand Date: Wed, 1 May 2024 00:41:20 +0200 Subject: [PATCH 03/11] Updates documentation --- doc/RequestsLibrary.html | 104 +++++++++++++++++++++++++-------------- 1 file changed, 66 insertions(+), 38 deletions(-) diff --git a/doc/RequestsLibrary.html b/doc/RequestsLibrary.html index a8c0a72..37fc561 100644 --- a/doc/RequestsLibrary.html +++ b/doc/RequestsLibrary.html @@ -6,9 +6,9 @@ - + - - - - -