From 0df9a4dbcda2ca36f5c8bb7e2f6f3be8d384d325 Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Tue, 28 Oct 2025 07:03:22 +0100 Subject: [PATCH 01/17] adjust session keywords and tests to support secrets --- atests/test_authentication.robot | 38 +++++++++++++++++++++++++- src/RequestsLibrary/SessionKeywords.py | 35 ++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/atests/test_authentication.robot b/atests/test_authentication.robot index 8bc7e4e..a45a3ed 100644 --- a/atests/test_authentication.robot +++ b/atests/test_authentication.robot @@ -1,7 +1,7 @@ *** Settings *** Library RequestsLibrary Library customAuthenticator.py - +Variables secretvar.py *** Test Cases *** Get With Auth @@ -32,3 +32,39 @@ Get With Digest Auth ${resp}= GET On Session httpbin /digest-auth/auth/user/pass Should Be Equal As Strings ${resp.status_code} 200 Should Be Equal As Strings ${resp.json()['authenticated']} True + +Get With Auth with Robot Secrets + [Tags] robot-74 get get-cert + Skip If $SECRET_PASSWORD == "not-supported" + ... msg=robot version does not support secrets + ${auth}= Create List user ${SECRET_PASSWORD} + Create Session httpbin https://httpbin.org auth=${auth} verify=${CURDIR}${/}cacert.pem + ${resp}= GET On Session httpbin /basic-auth/user/passwd + Should Be Equal As Strings ${resp.status_code} 200 + Should Be Equal As Strings ${resp.json()['authenticated']} True + + +Get With Custom Auth with Robot Secrets + [Tags] robot-74 get + Skip If $SECRET_PASSWORD == "not-supported" + ... msg=robot version does not support secrets + ${auth}= Create List user ${SECRET_PASSWORD} + Create Custom Session httpbin https://httpbin.org auth=${auth} verify=${CURDIR}${/}cacert.pem + ${resp}= GET On Session httpbin /basic-auth/user/passwd + Should Be Equal As Strings ${resp.status_code} 200 + Should Be Equal As Strings ${resp.json()['authenticated']} True + +Get With Digest Auth with Robot Secrets + [Tags] robot-74 get get-cert + Skip If $SECRET_PASSWORD == "not-supported" + ... msg=robot version does not support secrets + ${auth}= Create List user ${SECRET_PASSWORD} + Create Digest Session + ... httpbin + ... https://httpbin.org + ... auth=${auth} + ... debug=3 + ... verify=${CURDIR}${/}cacert.pem + ${resp}= GET On Session httpbin /digest-auth/auth/user/passwd + Should Be Equal As Strings ${resp.status_code} 200 + Should Be Equal As Strings ${resp.json()['authenticated']} True diff --git a/src/RequestsLibrary/SessionKeywords.py b/src/RequestsLibrary/SessionKeywords.py index 0b19cb7..158b683 100644 --- a/src/RequestsLibrary/SessionKeywords.py +++ b/src/RequestsLibrary/SessionKeywords.py @@ -7,6 +7,10 @@ from requests.sessions import merge_setting from robot.api import logger from robot.api.deco import keyword +try: + from robot.api.types import Secret +except (ImportError, ModuleNotFoundError): + pass from robot.utils.asserts import assert_equal from RequestsLibrary import utils @@ -21,6 +25,17 @@ except ImportError: pass +def _process_secrets(auth): + try: + Secret + except NameError: + new_auth = auth + else: + new_auth = tuple( + a.value if isinstance(a, Secret) else a + for a in auth + ) + return new_auth class SessionKeywords(RequestsKeywords): DEFAULT_RETRY_METHOD_LIST = RetryAdapter.get_default_allowed_methods() @@ -172,7 +187,11 @@ def create_session( Note that max_retries must be greater than 0. """ - auth = requests.auth.HTTPBasicAuth(*auth) if auth else None + if auth: + auth = _process_secrets(auth) + auth = requests.auth.HTTPBasicAuth(*auth) + else: + auth = None logger.info( "Creating Session using : alias=%s, url=%s, headers=%s, \ @@ -262,7 +281,11 @@ def create_client_cert_session( eg. set to [502, 503] to retry requests if those status are returned. Note that max_retries must be greater than 0. """ - auth = requests.auth.HTTPBasicAuth(*auth) if auth else None + if auth: + auth = _process_secrets(auth) + auth = requests.auth.HTTPBasicAuth(*auth) + else: + auth = None logger.info( "Creating Session using : alias=%s, url=%s, headers=%s, \ @@ -372,6 +395,7 @@ def create_custom_session( debug=%s " % (alias, url, headers, cookies, auth, timeout, proxies, verify, debug) ) + auth = _process_secrets(auth) return self._create_session( alias=alias, @@ -452,7 +476,11 @@ def create_digest_session( eg. set to [502, 503] to retry requests if those status are returned. Note that max_retries must be greater than 0. """ - digest_auth = requests.auth.HTTPDigestAuth(*auth) if auth else None + if auth: + auth = _process_secrets(auth) + digest_auth = requests.auth.HTTPDigestAuth(*auth) + else: + digest_auth = None return self._create_session( alias=alias, @@ -543,6 +571,7 @@ def create_ntlm_session( " - expected 3, got {}".format(len(auth)) ) else: + auth = _process_secrets(auth) ntlm_auth = HttpNtlmAuth("{}\\{}".format(auth[0], auth[1]), auth[2]) logger.info( "Creating NTLM Session using : alias=%s, url=%s, \ From 7b498716b8c5614a99407a5e28b69bab6129f84f Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Tue, 28 Oct 2025 07:05:20 +0100 Subject: [PATCH 02/17] temp upgrade of robot in pipeline --- .github/workflows/pythonapp.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index 37d08c0..558be72 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -23,6 +23,7 @@ jobs: - name: Install dependencies run: | python -m pip install -e .[test] + python -m pip install --pre robotframework - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names From 663f6da29a5079070f185ad075b50a09ab43e1c8 Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Tue, 28 Oct 2025 07:30:58 +0100 Subject: [PATCH 03/17] fix custom auth, make it more concicse --- atests/test_authentication.robot | 11 ----------- src/RequestsLibrary/SessionKeywords.py | 24 ++++++++---------------- 2 files changed, 8 insertions(+), 27 deletions(-) diff --git a/atests/test_authentication.robot b/atests/test_authentication.robot index a45a3ed..ba06936 100644 --- a/atests/test_authentication.robot +++ b/atests/test_authentication.robot @@ -43,17 +43,6 @@ Get With Auth with Robot Secrets Should Be Equal As Strings ${resp.status_code} 200 Should Be Equal As Strings ${resp.json()['authenticated']} True - -Get With Custom Auth with Robot Secrets - [Tags] robot-74 get - Skip If $SECRET_PASSWORD == "not-supported" - ... msg=robot version does not support secrets - ${auth}= Create List user ${SECRET_PASSWORD} - Create Custom Session httpbin https://httpbin.org auth=${auth} verify=${CURDIR}${/}cacert.pem - ${resp}= GET On Session httpbin /basic-auth/user/passwd - Should Be Equal As Strings ${resp.status_code} 200 - Should Be Equal As Strings ${resp.json()['authenticated']} True - Get With Digest Auth with Robot Secrets [Tags] robot-74 get get-cert Skip If $SECRET_PASSWORD == "not-supported" diff --git a/src/RequestsLibrary/SessionKeywords.py b/src/RequestsLibrary/SessionKeywords.py index 158b683..c5f5e54 100644 --- a/src/RequestsLibrary/SessionKeywords.py +++ b/src/RequestsLibrary/SessionKeywords.py @@ -187,11 +187,12 @@ def create_session( Note that max_retries must be greater than 0. """ - if auth: - auth = _process_secrets(auth) - auth = requests.auth.HTTPBasicAuth(*auth) - else: - auth = None + auth = requests.auth.HTTPBasicAuth(*_process_secrets(auth)) if auth else None + # if auth: + # auth = _process_secrets(auth) + # auth = requests.auth.HTTPBasicAuth(*auth) + # else: + # auth = None logger.info( "Creating Session using : alias=%s, url=%s, headers=%s, \ @@ -281,11 +282,7 @@ def create_client_cert_session( eg. set to [502, 503] to retry requests if those status are returned. Note that max_retries must be greater than 0. """ - if auth: - auth = _process_secrets(auth) - auth = requests.auth.HTTPBasicAuth(*auth) - else: - auth = None + auth = requests.auth.HTTPBasicAuth(*_process_secrets(auth)) if auth else None logger.info( "Creating Session using : alias=%s, url=%s, headers=%s, \ @@ -395,7 +392,6 @@ def create_custom_session( debug=%s " % (alias, url, headers, cookies, auth, timeout, proxies, verify, debug) ) - auth = _process_secrets(auth) return self._create_session( alias=alias, @@ -476,11 +472,7 @@ def create_digest_session( eg. set to [502, 503] to retry requests if those status are returned. Note that max_retries must be greater than 0. """ - if auth: - auth = _process_secrets(auth) - digest_auth = requests.auth.HTTPDigestAuth(*auth) - else: - digest_auth = None + digest_auth = requests.auth.HTTPDigestAuth(*_process_secrets(auth)) if auth else None return self._create_session( alias=alias, From c8c6d2813a3c3292de14eac56175e053b8e928ac Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Tue, 28 Oct 2025 07:36:38 +0100 Subject: [PATCH 04/17] add missing secretvar.py --- atests/secretvar.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 atests/secretvar.py diff --git a/atests/secretvar.py b/atests/secretvar.py new file mode 100644 index 0000000..dde5aee --- /dev/null +++ b/atests/secretvar.py @@ -0,0 +1,8 @@ +# inject secret into robot suite.. doing this via python +# to ensure this can also run in older robot versions +# tests related +try: + from robot.api.types import Secret + SECRET_PASSWORD = Secret("passwd") +except (ImportError, ModuleNotFoundError): + SECRET_PASSWORD = "not-supported" From 7fdb7f05b876de904cb039eca0a9bc7b242cabc2 Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Tue, 28 Oct 2025 07:36:52 +0100 Subject: [PATCH 05/17] remove comments --- src/RequestsLibrary/SessionKeywords.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/RequestsLibrary/SessionKeywords.py b/src/RequestsLibrary/SessionKeywords.py index c5f5e54..0d731fb 100644 --- a/src/RequestsLibrary/SessionKeywords.py +++ b/src/RequestsLibrary/SessionKeywords.py @@ -188,11 +188,6 @@ def create_session( """ auth = requests.auth.HTTPBasicAuth(*_process_secrets(auth)) if auth else None - # if auth: - # auth = _process_secrets(auth) - # auth = requests.auth.HTTPBasicAuth(*auth) - # else: - # auth = None logger.info( "Creating Session using : alias=%s, url=%s, headers=%s, \ From 11f8f192c36836b6646417fad1412b6b4adce497 Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Tue, 28 Oct 2025 09:09:27 +0100 Subject: [PATCH 06/17] test with different robot versions --- .github/workflows/pythonapp.yml | 3 ++- atests/secretvar.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index 558be72..ddb1375 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -12,6 +12,7 @@ jobs: matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] python-version: [ 3.8, 3.12 ] + robot-version: [ 6.1.1, 7.4b1 ] steps: - uses: actions/checkout@v4 - name: Set up Python @@ -23,7 +24,7 @@ jobs: - name: Install dependencies run: | python -m pip install -e .[test] - python -m pip install --pre robotframework + python -m pip install robotframework==${{ matrix.robot-version }} - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names diff --git a/atests/secretvar.py b/atests/secretvar.py index dde5aee..9ec8e4d 100644 --- a/atests/secretvar.py +++ b/atests/secretvar.py @@ -1,6 +1,5 @@ # inject secret into robot suite.. doing this via python # to ensure this can also run in older robot versions -# tests related try: from robot.api.types import Secret SECRET_PASSWORD = Secret("passwd") From fef032ba83a92161889b9ab5650ffbeace8c81ed Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Tue, 28 Oct 2025 09:41:47 +0100 Subject: [PATCH 07/17] add Secret logic also to all other keywords which support auth --- .gitignore | 1 + atests/test_authentication.robot | 19 +++++++++++++ src/RequestsLibrary/RequestsKeywords.py | 7 +++++ src/RequestsLibrary/SessionKeywords.py | 25 ++++------------ src/RequestsLibrary/utils.py | 31 ++++++++++++++++++++ utests/test_RequestsKeywords.py | 38 +++++++++++++++++++++++++ 6 files changed, 101 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 8c500ab..955b6f1 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ env/* # ignore http server log atests/http_server/http_server.log +.claude/ diff --git a/atests/test_authentication.robot b/atests/test_authentication.robot index ba06936..56613fd 100644 --- a/atests/test_authentication.robot +++ b/atests/test_authentication.robot @@ -57,3 +57,22 @@ Get With Digest Auth with Robot Secrets ${resp}= GET On Session httpbin /digest-auth/auth/user/passwd Should Be Equal As Strings ${resp.status_code} 200 Should Be Equal As Strings ${resp.json()['authenticated']} True + +Session-less GET With Auth with Robot Secrets + [Tags] robot-74 get get-cert session-less + Skip If $SECRET_PASSWORD == "not-supported" + ... msg=robot version does not support secrets + ${auth}= Create List user ${SECRET_PASSWORD} + ${resp}= GET https://httpbin.org/basic-auth/user/passwd auth=${auth} verify=${CURDIR}${/}cacert.pem + Should Be Equal As Strings ${resp.status_code} 200 + Should Be Equal As Strings ${resp.json()['authenticated']} True + +Session-less POST With Auth with Robot Secrets + [Tags] robot-74 post post-cert session-less + Skip If $SECRET_PASSWORD == "not-supported" + ... msg=robot version does not support secrets + ${auth}= Create List user ${SECRET_PASSWORD} + ${data}= Create Dictionary test=data + ${resp}= POST https://httpbin.org/post json=${data} auth=${auth} verify=${CURDIR}${/}cacert.pem + Should Be Equal As Strings ${resp.status_code} 200 + Should Be Equal As Strings ${resp.json()['json']['test']} data diff --git a/src/RequestsLibrary/RequestsKeywords.py b/src/RequestsLibrary/RequestsKeywords.py index c74bf83..730acfb 100644 --- a/src/RequestsLibrary/RequestsKeywords.py +++ b/src/RequestsLibrary/RequestsKeywords.py @@ -8,6 +8,7 @@ from RequestsLibrary.utils import ( is_list_or_tuple, is_file_descriptor, + process_secrets, warn_if_equal_symbol_in_url_session_less, ) @@ -30,6 +31,12 @@ def _common_request(self, method, session, uri, **kwargs): else: request_function = getattr(requests, "request") + # Process Secret types in auth parameter if present + if "auth" in kwargs and kwargs["auth"] is not None: + auth = kwargs["auth"] + if isinstance(auth, (list, tuple)): + kwargs["auth"] = process_secrets(auth) + self._capture_output() resp = request_function( diff --git a/src/RequestsLibrary/SessionKeywords.py b/src/RequestsLibrary/SessionKeywords.py index 0d731fb..be8c472 100644 --- a/src/RequestsLibrary/SessionKeywords.py +++ b/src/RequestsLibrary/SessionKeywords.py @@ -7,16 +7,12 @@ from requests.sessions import merge_setting from robot.api import logger from robot.api.deco import keyword -try: - from robot.api.types import Secret -except (ImportError, ModuleNotFoundError): - pass from robot.utils.asserts import assert_equal from RequestsLibrary import utils from RequestsLibrary.compat import RetryAdapter, httplib from RequestsLibrary.exceptions import InvalidExpectedStatus, InvalidResponse -from RequestsLibrary.utils import is_string_type +from RequestsLibrary.utils import is_string_type, process_secrets from .RequestsKeywords import RequestsKeywords @@ -25,17 +21,6 @@ except ImportError: pass -def _process_secrets(auth): - try: - Secret - except NameError: - new_auth = auth - else: - new_auth = tuple( - a.value if isinstance(a, Secret) else a - for a in auth - ) - return new_auth class SessionKeywords(RequestsKeywords): DEFAULT_RETRY_METHOD_LIST = RetryAdapter.get_default_allowed_methods() @@ -187,7 +172,7 @@ def create_session( Note that max_retries must be greater than 0. """ - auth = requests.auth.HTTPBasicAuth(*_process_secrets(auth)) if auth else None + auth = requests.auth.HTTPBasicAuth(*process_secrets(auth)) if auth else None logger.info( "Creating Session using : alias=%s, url=%s, headers=%s, \ @@ -277,7 +262,7 @@ def create_client_cert_session( eg. set to [502, 503] to retry requests if those status are returned. Note that max_retries must be greater than 0. """ - auth = requests.auth.HTTPBasicAuth(*_process_secrets(auth)) if auth else None + auth = requests.auth.HTTPBasicAuth(*process_secrets(auth)) if auth else None logger.info( "Creating Session using : alias=%s, url=%s, headers=%s, \ @@ -467,7 +452,7 @@ def create_digest_session( eg. set to [502, 503] to retry requests if those status are returned. Note that max_retries must be greater than 0. """ - digest_auth = requests.auth.HTTPDigestAuth(*_process_secrets(auth)) if auth else None + digest_auth = requests.auth.HTTPDigestAuth(*process_secrets(auth)) if auth else None return self._create_session( alias=alias, @@ -558,7 +543,7 @@ def create_ntlm_session( " - expected 3, got {}".format(len(auth)) ) else: - auth = _process_secrets(auth) + auth = process_secrets(auth) ntlm_auth = HttpNtlmAuth("{}\\{}".format(auth[0], auth[1]), auth[2]) logger.info( "Creating NTLM Session using : alias=%s, url=%s, \ diff --git a/src/RequestsLibrary/utils.py b/src/RequestsLibrary/utils.py index 78242b0..d4e8adf 100644 --- a/src/RequestsLibrary/utils.py +++ b/src/RequestsLibrary/utils.py @@ -5,6 +5,10 @@ from requests.status_codes import codes from requests.structures import CaseInsensitiveDict from robot.api import logger +try: + from robot.api.types import Secret +except (ImportError, ModuleNotFoundError): + pass from RequestsLibrary.compat import urlencode from RequestsLibrary.exceptions import UnknownStatusError @@ -77,6 +81,33 @@ def is_file_descriptor(fd): def is_list_or_tuple(data): return isinstance(data, (list, tuple)) + +def process_secrets(auth): + """ + Process Secret types in auth tuples by extracting their values. + + This function unwraps Robot Framework Secret objects from authentication + tuples, allowing credentials to be protected from logging while still + being usable for HTTP authentication. + + ``auth`` Tuple or list containing authentication credentials, which may + include Secret objects (available in Robot Framework 7.0+) + + Returns a tuple with Secret values unwrapped. If Secret type is not + available (older Robot Framework versions), returns the auth unchanged. + """ + try: + Secret + except NameError: + new_auth = auth + else: + new_auth = tuple( + a.value if isinstance(a, Secret) else a + for a in auth + ) + return new_auth + + def utf8_urlencode(data): if is_string_type(data): return data.encode("utf-8") diff --git a/utests/test_RequestsKeywords.py b/utests/test_RequestsKeywords.py index 4eae783..7b967a8 100644 --- a/utests/test_RequestsKeywords.py +++ b/utests/test_RequestsKeywords.py @@ -1,3 +1,4 @@ +import pytest from RequestsLibrary import RequestsLibrary from utests import mock @@ -81,3 +82,40 @@ def test_merge_url_with_url_override_base(): session, keywords = build_mocked_session_keywords('http://www.domain.com') url = keywords._merge_url(session, 'https://new.domain.com') assert url == 'https://new.domain.com' + + +def test_process_secrets_with_no_secrets(): + from RequestsLibrary.utils import process_secrets + auth = ('user', 'password') + result = process_secrets(auth) + assert result == ('user', 'password') + + +def test_process_secrets_with_secrets(): + try: + from robot.api.types import Secret + except (ImportError, ModuleNotFoundError): + pytest.skip('Secret type not available in tested robot version') + + from RequestsLibrary.utils import process_secrets + secret_password = Secret('mypassword') + auth = ('user', secret_password) + result = process_secrets(auth) + assert result == ('user', 'mypassword') + assert not isinstance(result[1], Secret) + + +def test_process_secrets_with_mixed_secrets(): + try: + from robot.api.types import Secret + except (ImportError, ModuleNotFoundError): + pytest.skip('Secret type not available in tested robot version') + + from RequestsLibrary.utils import process_secrets + secret_user = Secret('myuser') + secret_password = Secret('mypassword') + auth = (secret_user, secret_password) + result = process_secrets(auth) + assert result == ('myuser', 'mypassword') + assert not isinstance(result[0], Secret) + assert not isinstance(result[1], Secret) From 2827af7ffdf28b0e3f2421b7c92e0dd8a8179fee Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Tue, 28 Oct 2025 09:55:01 +0100 Subject: [PATCH 08/17] use skipif --- utests/test_RequestsKeywords.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/utests/test_RequestsKeywords.py b/utests/test_RequestsKeywords.py index 7b967a8..898fa41 100644 --- a/utests/test_RequestsKeywords.py +++ b/utests/test_RequestsKeywords.py @@ -1,7 +1,13 @@ import pytest from RequestsLibrary import RequestsLibrary +from RequestsLibrary.utils import process_secrets from utests import mock +try: + from robot.api.types import Secret + secret_type_supported = True +except (ImportError, ModuleNotFoundError): + secret_type_supported = False # @mock.patch('RequestsLibrary.RequestsKeywords.requests.get') # def test_common_request_none_session(mocked_get): @@ -90,14 +96,8 @@ def test_process_secrets_with_no_secrets(): result = process_secrets(auth) assert result == ('user', 'password') - +@pytest.mark.skipif(not secret_type_supported, reason="Running on pre-7.4 robot") def test_process_secrets_with_secrets(): - try: - from robot.api.types import Secret - except (ImportError, ModuleNotFoundError): - pytest.skip('Secret type not available in tested robot version') - - from RequestsLibrary.utils import process_secrets secret_password = Secret('mypassword') auth = ('user', secret_password) result = process_secrets(auth) @@ -105,13 +105,8 @@ def test_process_secrets_with_secrets(): assert not isinstance(result[1], Secret) +@pytest.mark.skipif(not secret_type_supported, reason="Running on pre-7.4 robot") def test_process_secrets_with_mixed_secrets(): - try: - from robot.api.types import Secret - except (ImportError, ModuleNotFoundError): - pytest.skip('Secret type not available in tested robot version') - - from RequestsLibrary.utils import process_secrets secret_user = Secret('myuser') secret_password = Secret('mypassword') auth = (secret_user, secret_password) From affb0b7d6bf5b03af8ae810a42081807384aa1da Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Tue, 28 Oct 2025 09:59:50 +0100 Subject: [PATCH 09/17] shorten docstring --- src/RequestsLibrary/utils.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/RequestsLibrary/utils.py b/src/RequestsLibrary/utils.py index d4e8adf..20b3cc2 100644 --- a/src/RequestsLibrary/utils.py +++ b/src/RequestsLibrary/utils.py @@ -84,17 +84,7 @@ def is_list_or_tuple(data): def process_secrets(auth): """ - Process Secret types in auth tuples by extracting their values. - - This function unwraps Robot Framework Secret objects from authentication - tuples, allowing credentials to be protected from logging while still - being usable for HTTP authentication. - - ``auth`` Tuple or list containing authentication credentials, which may - include Secret objects (available in Robot Framework 7.0+) - - Returns a tuple with Secret values unwrapped. If Secret type is not - available (older Robot Framework versions), returns the auth unchanged. + Process robot's Secret types in auth tuples by extracting their values. """ try: Secret From a25afabf29c5d835763fd24084080d6a1998785e Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Tue, 28 Oct 2025 10:03:10 +0100 Subject: [PATCH 10/17] fix some flake8 errors --- src/RequestsLibrary/RequestsKeywords.py | 6 +++--- src/RequestsLibrary/utils.py | 1 + utests/test_RequestsKeywords.py | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/RequestsLibrary/RequestsKeywords.py b/src/RequestsLibrary/RequestsKeywords.py index 730acfb..a8cf91f 100644 --- a/src/RequestsLibrary/RequestsKeywords.py +++ b/src/RequestsLibrary/RequestsKeywords.py @@ -66,7 +66,7 @@ def _close_file_descriptors(files, data): """ Helper method that closes any open file descriptors. """ - + if is_list_or_tuple(files): files_descriptor_to_close = filter( is_file_descriptor, [file[1][1] for file in files] + [data] @@ -75,10 +75,10 @@ def _close_file_descriptors(files, data): files_descriptor_to_close = filter( is_file_descriptor, list(files.values()) + [data] ) - + for file_descriptor in files_descriptor_to_close: file_descriptor.close() - + @staticmethod def _merge_url(session, uri): """ diff --git a/src/RequestsLibrary/utils.py b/src/RequestsLibrary/utils.py index 20b3cc2..af3a165 100644 --- a/src/RequestsLibrary/utils.py +++ b/src/RequestsLibrary/utils.py @@ -78,6 +78,7 @@ def is_string_type(data): def is_file_descriptor(fd): return isinstance(fd, io.IOBase) + def is_list_or_tuple(data): return isinstance(data, (list, tuple)) diff --git a/utests/test_RequestsKeywords.py b/utests/test_RequestsKeywords.py index 898fa41..9e92eb7 100644 --- a/utests/test_RequestsKeywords.py +++ b/utests/test_RequestsKeywords.py @@ -96,6 +96,7 @@ def test_process_secrets_with_no_secrets(): result = process_secrets(auth) assert result == ('user', 'password') + @pytest.mark.skipif(not secret_type_supported, reason="Running on pre-7.4 robot") def test_process_secrets_with_secrets(): secret_password = Secret('mypassword') From f90a13677e3d372609b6c430e0e29a563407dd6b Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Tue, 28 Oct 2025 10:56:38 +0100 Subject: [PATCH 11/17] simplify logic --- src/RequestsLibrary/RequestsKeywords.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/RequestsLibrary/RequestsKeywords.py b/src/RequestsLibrary/RequestsKeywords.py index a8cf91f..5ef6ed6 100644 --- a/src/RequestsLibrary/RequestsKeywords.py +++ b/src/RequestsLibrary/RequestsKeywords.py @@ -31,11 +31,10 @@ def _common_request(self, method, session, uri, **kwargs): else: request_function = getattr(requests, "request") - # Process Secret types in auth parameter if present - if "auth" in kwargs and kwargs["auth"] is not None: - auth = kwargs["auth"] - if isinstance(auth, (list, tuple)): - kwargs["auth"] = process_secrets(auth) + # Process robot's Secret types included in auth + auth = kwargs.get("auth") + if auth is not None and isinstance(auth, (list, tuple)): + kwargs["auth"] = process_secrets(auth) self._capture_output() From c6b7e8513941b6e339676f87e14bf7266896e96f Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Tue, 28 Oct 2025 11:23:01 +0100 Subject: [PATCH 12/17] add comment on robot version --- .github/workflows/pythonapp.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index ddb1375..685237b 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -12,7 +12,9 @@ jobs: matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] python-version: [ 3.8, 3.12 ] - robot-version: [ 6.1.1, 7.4b1 ] + # test with robot without and with Secret support? Not sure if + # it is worth it? + robot-version: [ 7.3.2, 7.4b1 ] steps: - uses: actions/checkout@v4 - name: Set up Python From b0b5d6dfaf42af08ef8a60e2e21fa47359f1e458 Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Tue, 28 Oct 2025 12:48:59 +0100 Subject: [PATCH 13/17] fix artificat pipeline error --- .github/workflows/pythonapp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index 685237b..8f30560 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -63,5 +63,5 @@ jobs: if: ${{ always() }} uses: actions/upload-artifact@v4 with: - name: rf-tests-report-${{ matrix.os }}-${{ matrix.python-version }} + name: rf-tests-report-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.robot-version }} path: ./tests-report From 970acc82625b16c66935b436a1d828c58327a3a4 Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Wed, 29 Oct 2025 15:15:15 +0100 Subject: [PATCH 14/17] trigger pipeline From 011d5341a6506150e9282dd9c4a813eb1f2a0db7 Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Tue, 18 Nov 2025 12:02:47 +0100 Subject: [PATCH 15/17] move unit tests to utests/test_utils.py --- utests/test_RequestsKeywords.py | 33 -------------------------------- utests/test_utils.py | 34 ++++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/utests/test_RequestsKeywords.py b/utests/test_RequestsKeywords.py index 9e92eb7..a24d082 100644 --- a/utests/test_RequestsKeywords.py +++ b/utests/test_RequestsKeywords.py @@ -3,12 +3,6 @@ from RequestsLibrary.utils import process_secrets from utests import mock -try: - from robot.api.types import Secret - secret_type_supported = True -except (ImportError, ModuleNotFoundError): - secret_type_supported = False - # @mock.patch('RequestsLibrary.RequestsKeywords.requests.get') # def test_common_request_none_session(mocked_get): # keywords = RequestsLibrary.RequestsKeywords() @@ -88,30 +82,3 @@ def test_merge_url_with_url_override_base(): session, keywords = build_mocked_session_keywords('http://www.domain.com') url = keywords._merge_url(session, 'https://new.domain.com') assert url == 'https://new.domain.com' - - -def test_process_secrets_with_no_secrets(): - from RequestsLibrary.utils import process_secrets - auth = ('user', 'password') - result = process_secrets(auth) - assert result == ('user', 'password') - - -@pytest.mark.skipif(not secret_type_supported, reason="Running on pre-7.4 robot") -def test_process_secrets_with_secrets(): - secret_password = Secret('mypassword') - auth = ('user', secret_password) - result = process_secrets(auth) - assert result == ('user', 'mypassword') - assert not isinstance(result[1], Secret) - - -@pytest.mark.skipif(not secret_type_supported, reason="Running on pre-7.4 robot") -def test_process_secrets_with_mixed_secrets(): - secret_user = Secret('myuser') - secret_password = Secret('mypassword') - auth = (secret_user, secret_password) - result = process_secrets(auth) - assert result == ('myuser', 'mypassword') - assert not isinstance(result[0], Secret) - assert not isinstance(result[1], Secret) diff --git a/utests/test_utils.py b/utests/test_utils.py index 0ae280d..1042677 100644 --- a/utests/test_utils.py +++ b/utests/test_utils.py @@ -4,10 +4,16 @@ from requests import Session from RequestsLibrary import RequestsLibrary -from RequestsLibrary.utils import is_file_descriptor, merge_headers +from RequestsLibrary.utils import is_file_descriptor, merge_headers, process_secrets from utests import SCRIPT_DIR from utests import mock +try: + from robot.api.types import Secret + secret_type_supported = True +except (ImportError, ModuleNotFoundError): + secret_type_supported = False + def test_none(): assert is_file_descriptor(None) is False @@ -72,3 +78,29 @@ def test_warn_that_url_is_missing(mocked_logger, mocked_keywords): except TypeError: pass mocked_logger.warn.assert_called() + + +def test_process_secrets_with_no_secrets(): + auth = ('user', 'password') + result = process_secrets(auth) + assert result == ('user', 'password') + + +@pytest.mark.skipif(not secret_type_supported, reason="Running on pre-7.4 robot") +def test_process_secrets_with_secrets(): + secret_password = Secret('mypassword') + auth = ('user', secret_password) + result = process_secrets(auth) + assert result == ('user', 'mypassword') + assert not isinstance(result[1], Secret) + + +@pytest.mark.skipif(not secret_type_supported, reason="Running on pre-7.4 robot") +def test_process_secrets_with_mixed_secrets(): + secret_user = Secret('myuser') + secret_password = Secret('mypassword') + auth = (secret_user, secret_password) + result = process_secrets(auth) + assert result == ('myuser', 'mypassword') + assert not isinstance(result[0], Secret) + assert not isinstance(result[1], Secret) From b40719664b97e6be0a7d07627666ba233688b575 Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Tue, 18 Nov 2025 12:05:12 +0100 Subject: [PATCH 16/17] remove imports no longer required --- utests/test_RequestsKeywords.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/utests/test_RequestsKeywords.py b/utests/test_RequestsKeywords.py index a24d082..54e43ef 100644 --- a/utests/test_RequestsKeywords.py +++ b/utests/test_RequestsKeywords.py @@ -1,6 +1,4 @@ -import pytest from RequestsLibrary import RequestsLibrary -from RequestsLibrary.utils import process_secrets from utests import mock # @mock.patch('RequestsLibrary.RequestsKeywords.requests.get') From bd4c74c522c9e608f3e0433fbf2a4c3f57252a0c Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Tue, 18 Nov 2025 12:05:59 +0100 Subject: [PATCH 17/17] add line --- utests/test_RequestsKeywords.py | 1 + 1 file changed, 1 insertion(+) diff --git a/utests/test_RequestsKeywords.py b/utests/test_RequestsKeywords.py index 54e43ef..4eae783 100644 --- a/utests/test_RequestsKeywords.py +++ b/utests/test_RequestsKeywords.py @@ -1,6 +1,7 @@ from RequestsLibrary import RequestsLibrary from utests import mock + # @mock.patch('RequestsLibrary.RequestsKeywords.requests.get') # def test_common_request_none_session(mocked_get): # keywords = RequestsLibrary.RequestsKeywords()