From 0df9a4dbcda2ca36f5c8bb7e2f6f3be8d384d325 Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Tue, 28 Oct 2025 07:03:22 +0100 Subject: [PATCH 01/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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() From ffa89ec6c4ff8a6faeca51b54f7c4b2264dcce5e Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Fri, 12 Dec 2025 19:24:50 +0100 Subject: [PATCH 18/27] reference released 7.4 in pipeline --- .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 8f30560..8909d6e 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -14,7 +14,7 @@ jobs: python-version: [ 3.8, 3.12 ] # test with robot without and with Secret support? Not sure if # it is worth it? - robot-version: [ 7.3.2, 7.4b1 ] + robot-version: [ 7.3.2, 7.4 ] steps: - uses: actions/checkout@v4 - name: Set up Python From abae2e7ca5734fb051b9f9a4290535f5a2445f8d Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Wed, 17 Dec 2025 22:07:26 +0100 Subject: [PATCH 19/27] adjust auth tests to local http server --- atests/test_authentication.robot | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/atests/test_authentication.robot b/atests/test_authentication.robot index b092b10..df205d0 100644 --- a/atests/test_authentication.robot +++ b/atests/test_authentication.robot @@ -23,13 +23,13 @@ Get With Custom Auth Get With Digest Auth [Tags] get get-cert - ${auth}= Create List user pass + ${auth}= Create List user passwd Create Digest Session ... authsession ... ${HTTP_LOCAL_SERVER} ... auth=${auth} ... debug=3 - ${resp}= GET On Session authsession /digest-auth/auth/user/pass + ${resp}= GET On Session authsession /digest-auth/auth/user/passwd Should Be Equal As Strings ${resp.status_code} 200 Should Be Equal As Strings ${resp.json()['authenticated']} True @@ -38,8 +38,8 @@ Get With Auth with Robot Secrets 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 + Create Session authsession ${HTTP_LOCAL_SERVER} auth=${auth} + ${resp}= GET On Session authsession /basic-auth/user/passwd Should Be Equal As Strings ${resp.status_code} 200 Should Be Equal As Strings ${resp.json()['authenticated']} True @@ -53,8 +53,7 @@ Get With Digest Auth with Robot Secrets ... ${HTTP_LOCAL_SERVER} ... auth=${auth} ... debug=3 - ... verify=${CURDIR}${/}cacert.pem - ${resp}= GET On Session httpbin /digest-auth/auth/user/passwd + ${resp}= GET On Session authsession /digest-auth/auth/user/passwd Should Be Equal As Strings ${resp.status_code} 200 Should Be Equal As Strings ${resp.json()['authenticated']} True @@ -63,16 +62,6 @@ Session-less GET With Auth with Robot Secrets 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 + ${resp}= GET ${HTTP_LOCAL_SERVER}/basic-auth/user/passwd auth=${auth} 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 From fd38b0672f69e311d94905b62ccfc16b834aac96 Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Wed, 17 Dec 2025 22:16:43 +0100 Subject: [PATCH 20/27] use flag to determine if secrets are supported --- src/RequestsLibrary/utils.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/RequestsLibrary/utils.py b/src/RequestsLibrary/utils.py index af3a165..c173403 100644 --- a/src/RequestsLibrary/utils.py +++ b/src/RequestsLibrary/utils.py @@ -7,8 +7,9 @@ from robot.api import logger try: from robot.api.types import Secret + robot_has_secret = True except (ImportError, ModuleNotFoundError): - pass + robot_has_secret = False from RequestsLibrary.compat import urlencode from RequestsLibrary.exceptions import UnknownStatusError @@ -87,15 +88,13 @@ def process_secrets(auth): """ Process robot's Secret types in auth tuples by extracting their values. """ - try: - Secret - except NameError: - new_auth = auth - else: + if robot_has_secret: new_auth = tuple( a.value if isinstance(a, Secret) else a for a in auth ) + else: + new_auth = auth return new_auth From 74d0b120176ae5ff1c64256761213f266501ce0d Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Wed, 24 Dec 2025 09:34:10 +0100 Subject: [PATCH 21/27] prevent secrets to be logged, even in TRACE/DEBUG log level --- src/RequestsLibrary/RequestsKeywords.py | 5 ++++- src/RequestsLibrary/log.py | 11 ++++++++--- src/RequestsLibrary/utils.py | 12 +++++++++--- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/RequestsLibrary/RequestsKeywords.py b/src/RequestsLibrary/RequestsKeywords.py index 5ef6ed6..1dbec85 100644 --- a/src/RequestsLibrary/RequestsKeywords.py +++ b/src/RequestsLibrary/RequestsKeywords.py @@ -6,6 +6,7 @@ from RequestsLibrary import log from RequestsLibrary.compat import urljoin from RequestsLibrary.utils import ( + has_secrets, is_list_or_tuple, is_file_descriptor, process_secrets, @@ -33,7 +34,9 @@ def _common_request(self, method, session, uri, **kwargs): # Process robot's Secret types included in auth auth = kwargs.get("auth") + contains_secrets = False if auth is not None and isinstance(auth, (list, tuple)): + contains_secrets = has_secrets(auth) kwargs["auth"] = process_secrets(auth) self._capture_output() @@ -46,7 +49,7 @@ def _common_request(self, method, session, uri, **kwargs): **kwargs ) - log.log_request(resp) + log.log_request(resp, has_secrets=contains_secrets) self._print_debug() log.log_response(resp) diff --git a/src/RequestsLibrary/log.py b/src/RequestsLibrary/log.py index 09807f4..d12ba23 100644 --- a/src/RequestsLibrary/log.py +++ b/src/RequestsLibrary/log.py @@ -17,7 +17,7 @@ def log_response(response): ) -def log_request(response): +def log_request(response, has_secrets=False): request = response.request if response.history: original_request = response.history[0].request @@ -25,9 +25,14 @@ def log_request(response): else: original_request = request redirected = "" + + # Mask Authorization header based on whether secrets were used safe_headers = dict(original_request.headers) - if logger.LOGLEVEL not in ['TRACE', 'DEBUG'] and AUTHORIZATION in safe_headers: - safe_headers[AUTHORIZATION] = '*****' + if AUTHORIZATION in safe_headers: + # If secrets were used, always mask. Otherwise, only mask if not in DEBUG/TRACE + if has_secrets or logger.LOGLEVEL not in ['TRACE', 'DEBUG']: + safe_headers[AUTHORIZATION] = '*****' + logger.info( "%s Request : " % original_request.method.upper() + "url=%s %s\n " % (original_request.url, redirected) diff --git a/src/RequestsLibrary/utils.py b/src/RequestsLibrary/utils.py index c173403..756fa57 100644 --- a/src/RequestsLibrary/utils.py +++ b/src/RequestsLibrary/utils.py @@ -7,9 +7,9 @@ from robot.api import logger try: from robot.api.types import Secret - robot_has_secret = True + robot_supports_secrets = True except (ImportError, ModuleNotFoundError): - robot_has_secret = False + robot_supports_secrets = False from RequestsLibrary.compat import urlencode from RequestsLibrary.exceptions import UnknownStatusError @@ -84,11 +84,17 @@ def is_list_or_tuple(data): return isinstance(data, (list, tuple)) +def has_secrets(auth): + if robot_supports_secrets and auth is not None and isinstance(auth, (list, tuple)): + return any(isinstance(a, Secret) for a in auth) + return False + + def process_secrets(auth): """ Process robot's Secret types in auth tuples by extracting their values. """ - if robot_has_secret: + if robot_supports_secrets: new_auth = tuple( a.value if isinstance(a, Secret) else a for a in auth From de8ebf6c314bc741149a7d5ed6754e02a13b7bc8 Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Thu, 25 Dec 2025 10:32:23 +0100 Subject: [PATCH 22/27] use different password value for secret passwrds --- atests/secretvar.py | 2 +- atests/test_authentication.robot | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/atests/secretvar.py b/atests/secretvar.py index 9ec8e4d..0139422 100644 --- a/atests/secretvar.py +++ b/atests/secretvar.py @@ -2,6 +2,6 @@ # to ensure this can also run in older robot versions try: from robot.api.types import Secret - SECRET_PASSWORD = Secret("passwd") + SECRET_PASSWORD = Secret("secret_passwd") except (ImportError, ModuleNotFoundError): SECRET_PASSWORD = "not-supported" diff --git a/atests/test_authentication.robot b/atests/test_authentication.robot index df205d0..a15ad37 100644 --- a/atests/test_authentication.robot +++ b/atests/test_authentication.robot @@ -39,7 +39,7 @@ Get With Auth with Robot Secrets ... msg=robot version does not support secrets ${auth}= Create List user ${SECRET_PASSWORD} Create Session authsession ${HTTP_LOCAL_SERVER} auth=${auth} - ${resp}= GET On Session authsession /basic-auth/user/passwd + ${resp}= GET On Session authsession /basic-auth/user/secret_passwd Should Be Equal As Strings ${resp.status_code} 200 Should Be Equal As Strings ${resp.json()['authenticated']} True @@ -53,7 +53,7 @@ Get With Digest Auth with Robot Secrets ... ${HTTP_LOCAL_SERVER} ... auth=${auth} ... debug=3 - ${resp}= GET On Session authsession /digest-auth/auth/user/passwd + ${resp}= GET On Session authsession /digest-auth/auth/user/secret_passwd Should Be Equal As Strings ${resp.status_code} 200 Should Be Equal As Strings ${resp.json()['authenticated']} True @@ -62,6 +62,6 @@ Session-less GET With Auth with Robot Secrets Skip If $SECRET_PASSWORD == "not-supported" ... msg=robot version does not support secrets ${auth}= Create List user ${SECRET_PASSWORD} - ${resp}= GET ${HTTP_LOCAL_SERVER}/basic-auth/user/passwd auth=${auth} + ${resp}= GET ${HTTP_LOCAL_SERVER}/basic-auth/user/secret_passwd auth=${auth} Should Be Equal As Strings ${resp.status_code} 200 Should Be Equal As Strings ${resp.json()['authenticated']} True From d8ddff8e7bcc6609806d379905219166286c0545 Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Thu, 25 Dec 2025 10:49:23 +0100 Subject: [PATCH 23/27] mask Authorization header in HTTP debug output when secrets are used MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prevent exposure of credentials in Robot Framework logs at DEBUG/TRACE levels by masking Authorization headers in HTTP connection debug output when Robot Secret types are detected. Changes: - Add check_and_process_secrets() to detect and process secrets in one pass - Track secret usage in sessions via _has_secrets attribute - Mask Authorization header in _print_debug() when secrets present - Import AUTHORIZATION constant from log module for consistency This ensures credentials are never logged even with debug=3, while still allowing Authorization headers to be visible for debugging when no secrets are used (e.g., test credentials). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/RequestsLibrary/RequestsKeywords.py | 17 +++++--- src/RequestsLibrary/SessionKeywords.py | 58 +++++++++++++++++++++---- src/RequestsLibrary/utils.py | 24 ++++++++++ 3 files changed, 84 insertions(+), 15 deletions(-) diff --git a/src/RequestsLibrary/RequestsKeywords.py b/src/RequestsLibrary/RequestsKeywords.py index 1dbec85..6acb86f 100644 --- a/src/RequestsLibrary/RequestsKeywords.py +++ b/src/RequestsLibrary/RequestsKeywords.py @@ -6,10 +6,9 @@ from RequestsLibrary import log from RequestsLibrary.compat import urljoin from RequestsLibrary.utils import ( - has_secrets, + check_and_process_secrets, is_list_or_tuple, is_file_descriptor, - process_secrets, warn_if_equal_symbol_in_url_session_less, ) @@ -24,6 +23,7 @@ def __init__(self): self.timeout = None self.cookies = None self.last_response = None + self._request_has_secrets = False def _common_request(self, method, session, uri, **kwargs): @@ -32,12 +32,17 @@ def _common_request(self, method, session, uri, **kwargs): else: request_function = getattr(requests, "request") - # Process robot's Secret types included in auth auth = kwargs.get("auth") - contains_secrets = False if auth is not None and isinstance(auth, (list, tuple)): - contains_secrets = has_secrets(auth) - kwargs["auth"] = process_secrets(auth) + kwargs["auth"], contains_secrets = check_and_process_secrets(auth) + else: + contains_secrets = False + + if session and hasattr(session, '_has_secrets'): + contains_secrets = contains_secrets or session._has_secrets + + # Store secrets flag for _print_debug to access + self._request_has_secrets = contains_secrets self._capture_output() diff --git a/src/RequestsLibrary/SessionKeywords.py b/src/RequestsLibrary/SessionKeywords.py index be8c472..5e55d97 100644 --- a/src/RequestsLibrary/SessionKeywords.py +++ b/src/RequestsLibrary/SessionKeywords.py @@ -1,4 +1,5 @@ import logging +import re import sys import requests @@ -12,7 +13,8 @@ from RequestsLibrary import utils from RequestsLibrary.compat import RetryAdapter, httplib from RequestsLibrary.exceptions import InvalidExpectedStatus, InvalidResponse -from RequestsLibrary.utils import is_string_type, process_secrets +from RequestsLibrary.log import AUTHORIZATION +from RequestsLibrary.utils import is_string_type, check_and_process_secrets from .RequestsKeywords import RequestsKeywords @@ -172,7 +174,12 @@ def create_session( Note that max_retries must be greater than 0. """ - auth = requests.auth.HTTPBasicAuth(*process_secrets(auth)) if auth else None + # Check if auth contains secrets and process in one pass + if auth: + processed_auth, session_has_secrets = check_and_process_secrets(auth) + auth = requests.auth.HTTPBasicAuth(*processed_auth) + else: + session_has_secrets = False logger.info( "Creating Session using : alias=%s, url=%s, headers=%s, \ @@ -180,7 +187,7 @@ def create_session( debug=%s " % (alias, url, headers, cookies, auth, timeout, proxies, verify, debug) ) - return self._create_session( + session = self._create_session( alias=alias, url=url, headers=headers, @@ -196,6 +203,9 @@ def create_session( retry_status_list=retry_status_list, retry_method_list=retry_method_list, ) + # Store whether this session has secrets + session._has_secrets = session_has_secrets + return session @keyword("Create Client Cert Session") def create_client_cert_session( @@ -262,7 +272,12 @@ 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 + # Check if auth contains secrets and process in one pass + if auth: + processed_auth, session_has_secrets = check_and_process_secrets(auth) + auth = requests.auth.HTTPBasicAuth(*processed_auth) + else: + session_has_secrets = False logger.info( "Creating Session using : alias=%s, url=%s, headers=%s, \ @@ -300,6 +315,8 @@ def create_client_cert_session( ) session.cert = tuple(client_certs) + # Store whether this session has secrets + session._has_secrets = session_has_secrets return session @keyword("Create Custom Session") @@ -452,9 +469,15 @@ 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 + # Check if auth contains secrets and process in one pass + if auth: + processed_auth, session_has_secrets = check_and_process_secrets(auth) + digest_auth = requests.auth.HTTPDigestAuth(*processed_auth) + else: + digest_auth = None + session_has_secrets = False - return self._create_session( + session = self._create_session( alias=alias, url=url, headers=headers, @@ -470,6 +493,9 @@ def create_digest_session( retry_status_list=retry_status_list, retry_method_list=retry_method_list, ) + # Store whether this session has secrets + session._has_secrets = session_has_secrets + return session @keyword("Create Ntlm Session") def create_ntlm_session( @@ -543,8 +569,9 @@ 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]) + # Check if auth contains secrets and process in one pass + processed_auth, session_has_secrets = check_and_process_secrets(auth) + ntlm_auth = HttpNtlmAuth("{}\\{}".format(processed_auth[0], processed_auth[1]), processed_auth[2]) logger.info( "Creating NTLM Session using : alias=%s, url=%s, \ headers=%s, cookies=%s, ntlm_auth=%s, timeout=%s, \ @@ -562,7 +589,7 @@ def create_ntlm_session( ) ) - return self._create_session( + session = self._create_session( alias=alias, url=url, headers=headers, @@ -578,6 +605,9 @@ def create_ntlm_session( retry_status_list=retry_status_list, retry_method_list=retry_method_list, ) + # Store whether this session has secrets + session._has_secrets = session_has_secrets + return session @keyword("Session Exists") def session_exists(self, alias): @@ -657,4 +687,14 @@ def _print_debug(self): debug_info = "\n".join( [ll.rstrip() for ll in debug_info.splitlines() if ll.strip()] ) + + # Mask Authorization header in debug output when secrets are used + if self._request_has_secrets: + debug_info = re.sub( + rf'({AUTHORIZATION}:)\s*([^\n]+)', + r'\1 *****', + debug_info, + flags=re.IGNORECASE + ) + logger.debug(debug_info) diff --git a/src/RequestsLibrary/utils.py b/src/RequestsLibrary/utils.py index 756fa57..cf0175f 100644 --- a/src/RequestsLibrary/utils.py +++ b/src/RequestsLibrary/utils.py @@ -104,6 +104,30 @@ def process_secrets(auth): return new_auth +def check_and_process_secrets(auth): + """ + Check if auth contains secrets and process them in a single pass. + + Returns: + tuple: (processed_auth, has_secrets_flag) + """ + if not auth or not isinstance(auth, (list, tuple)): + return auth, False + + if robot_supports_secrets: + has_secrets_flag = False + processed = [] + for a in auth: + if isinstance(a, Secret): + has_secrets_flag = True + processed.append(a.value) + else: + processed.append(a) + return tuple(processed), has_secrets_flag + else: + return auth, False + + def utf8_urlencode(data): if is_string_type(data): return data.encode("utf-8") From 7e96c4836a92df579c2c3474eda7f4b26ff9f502 Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Thu, 25 Dec 2025 10:59:11 +0100 Subject: [PATCH 24/27] adjust unit test --- utests/test_utils.py | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/utests/test_utils.py b/utests/test_utils.py index 1042677..46182ae 100644 --- a/utests/test_utils.py +++ b/utests/test_utils.py @@ -4,7 +4,7 @@ from requests import Session from RequestsLibrary import RequestsLibrary -from RequestsLibrary.utils import is_file_descriptor, merge_headers, process_secrets +from RequestsLibrary.utils import is_file_descriptor, merge_headers, check_and_process_secrets from utests import SCRIPT_DIR from utests import mock @@ -80,27 +80,42 @@ def test_warn_that_url_is_missing(mocked_logger, mocked_keywords): mocked_logger.warn.assert_called() -def test_process_secrets_with_no_secrets(): +def test_check_and_process_secrets_with_no_secrets(): auth = ('user', 'password') - result = process_secrets(auth) - assert result == ('user', 'password') + processed_auth, has_secrets = check_and_process_secrets(auth) + assert processed_auth == ('user', 'password') + assert has_secrets is False @pytest.mark.skipif(not secret_type_supported, reason="Running on pre-7.4 robot") -def test_process_secrets_with_secrets(): +def test_check_and_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) + processed_auth, has_secrets = check_and_process_secrets(auth) + assert processed_auth == ('user', 'mypassword') + assert has_secrets is True + assert not isinstance(processed_auth[1], Secret) @pytest.mark.skipif(not secret_type_supported, reason="Running on pre-7.4 robot") -def test_process_secrets_with_mixed_secrets(): +def test_check_and_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) + processed_auth, has_secrets = check_and_process_secrets(auth) + assert processed_auth == ('myuser', 'mypassword') + assert has_secrets is True + assert not isinstance(processed_auth[0], Secret) + assert not isinstance(processed_auth[1], Secret) + + +def test_check_and_process_secrets_with_none(): + processed_auth, has_secrets = check_and_process_secrets(None) + assert processed_auth is None + assert has_secrets is False + + +def test_check_and_process_secrets_with_empty_list(): + processed_auth, has_secrets = check_and_process_secrets([]) + assert processed_auth == [] + assert has_secrets is False From 9907dec2d80b9613331e79ed40b55c119809bab7 Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Thu, 25 Dec 2025 11:05:06 +0100 Subject: [PATCH 25/27] remove unneeded functions --- src/RequestsLibrary/utils.py | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/RequestsLibrary/utils.py b/src/RequestsLibrary/utils.py index cf0175f..c7ee0d5 100644 --- a/src/RequestsLibrary/utils.py +++ b/src/RequestsLibrary/utils.py @@ -84,29 +84,9 @@ def is_list_or_tuple(data): return isinstance(data, (list, tuple)) -def has_secrets(auth): - if robot_supports_secrets and auth is not None and isinstance(auth, (list, tuple)): - return any(isinstance(a, Secret) for a in auth) - return False - - -def process_secrets(auth): - """ - Process robot's Secret types in auth tuples by extracting their values. - """ - if robot_supports_secrets: - new_auth = tuple( - a.value if isinstance(a, Secret) else a - for a in auth - ) - else: - new_auth = auth - return new_auth - - def check_and_process_secrets(auth): """ - Check if auth contains secrets and process them in a single pass. + Check if auth contains secrets and process them Returns: tuple: (processed_auth, has_secrets_flag) From 15298bfe3d897eb1d39c2b6dcee3ab5b3f2717b3 Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Thu, 25 Dec 2025 14:24:30 +0100 Subject: [PATCH 26/27] remove redundant comments --- src/RequestsLibrary/SessionKeywords.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/RequestsLibrary/SessionKeywords.py b/src/RequestsLibrary/SessionKeywords.py index 5e55d97..7be5e84 100644 --- a/src/RequestsLibrary/SessionKeywords.py +++ b/src/RequestsLibrary/SessionKeywords.py @@ -174,7 +174,6 @@ def create_session( Note that max_retries must be greater than 0. """ - # Check if auth contains secrets and process in one pass if auth: processed_auth, session_has_secrets = check_and_process_secrets(auth) auth = requests.auth.HTTPBasicAuth(*processed_auth) @@ -203,7 +202,6 @@ def create_session( retry_status_list=retry_status_list, retry_method_list=retry_method_list, ) - # Store whether this session has secrets session._has_secrets = session_has_secrets return session @@ -272,7 +270,6 @@ 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. """ - # Check if auth contains secrets and process in one pass if auth: processed_auth, session_has_secrets = check_and_process_secrets(auth) auth = requests.auth.HTTPBasicAuth(*processed_auth) @@ -315,7 +312,6 @@ def create_client_cert_session( ) session.cert = tuple(client_certs) - # Store whether this session has secrets session._has_secrets = session_has_secrets return session @@ -469,7 +465,6 @@ 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. """ - # Check if auth contains secrets and process in one pass if auth: processed_auth, session_has_secrets = check_and_process_secrets(auth) digest_auth = requests.auth.HTTPDigestAuth(*processed_auth) @@ -493,7 +488,6 @@ def create_digest_session( retry_status_list=retry_status_list, retry_method_list=retry_method_list, ) - # Store whether this session has secrets session._has_secrets = session_has_secrets return session @@ -569,7 +563,6 @@ def create_ntlm_session( " - expected 3, got {}".format(len(auth)) ) else: - # Check if auth contains secrets and process in one pass processed_auth, session_has_secrets = check_and_process_secrets(auth) ntlm_auth = HttpNtlmAuth("{}\\{}".format(processed_auth[0], processed_auth[1]), processed_auth[2]) logger.info( @@ -605,7 +598,6 @@ def create_ntlm_session( retry_status_list=retry_status_list, retry_method_list=retry_method_list, ) - # Store whether this session has secrets session._has_secrets = session_has_secrets return session From 4d726b0475d5903bb54e85cdfcda65944f2a2de2 Mon Sep 17 00:00:00 2001 From: Oliver Boehmer Date: Fri, 26 Dec 2025 13:27:32 +0100 Subject: [PATCH 27/27] replace session attribute with helper function and tracking dict --- src/RequestsLibrary/RequestsKeywords.py | 12 +- src/RequestsLibrary/SessionKeywords.py | 12 +- utests/test_session_secrets.py | 289 ++++++++++++++++++++++++ 3 files changed, 307 insertions(+), 6 deletions(-) create mode 100644 utests/test_session_secrets.py diff --git a/src/RequestsLibrary/RequestsKeywords.py b/src/RequestsLibrary/RequestsKeywords.py index 6acb86f..38fa90d 100644 --- a/src/RequestsLibrary/RequestsKeywords.py +++ b/src/RequestsLibrary/RequestsKeywords.py @@ -24,6 +24,13 @@ def __init__(self): self.cookies = None self.last_response = None self._request_has_secrets = False + self._session_secrets = {} # Maps session object ID to secrets flag + + def _get_session_secrets_flag(self, session): + """Get the secrets flag for a session object""" + if not session: + return False + return self._session_secrets.get(id(session), False) def _common_request(self, method, session, uri, **kwargs): @@ -38,8 +45,9 @@ def _common_request(self, method, session, uri, **kwargs): else: contains_secrets = False - if session and hasattr(session, '_has_secrets'): - contains_secrets = contains_secrets or session._has_secrets + if session: + # Check if the session was created with robot secrets + contains_secrets = contains_secrets or self._get_session_secrets_flag(session) # Store secrets flag for _print_debug to access self._request_has_secrets = contains_secrets diff --git a/src/RequestsLibrary/SessionKeywords.py b/src/RequestsLibrary/SessionKeywords.py index 7be5e84..07518b9 100644 --- a/src/RequestsLibrary/SessionKeywords.py +++ b/src/RequestsLibrary/SessionKeywords.py @@ -27,6 +27,10 @@ class SessionKeywords(RequestsKeywords): DEFAULT_RETRY_METHOD_LIST = RetryAdapter.get_default_allowed_methods() + def _set_session_secrets_flag(self, session, has_secrets): + """Store the secrets flag for a session object using its id as key""" + self._session_secrets[id(session)] = has_secrets + def _create_session( self, alias, @@ -202,7 +206,7 @@ def create_session( retry_status_list=retry_status_list, retry_method_list=retry_method_list, ) - session._has_secrets = session_has_secrets + self._set_session_secrets_flag(session, session_has_secrets) return session @keyword("Create Client Cert Session") @@ -312,7 +316,7 @@ def create_client_cert_session( ) session.cert = tuple(client_certs) - session._has_secrets = session_has_secrets + self._set_session_secrets_flag(session, session_has_secrets) return session @keyword("Create Custom Session") @@ -488,7 +492,7 @@ def create_digest_session( retry_status_list=retry_status_list, retry_method_list=retry_method_list, ) - session._has_secrets = session_has_secrets + self._set_session_secrets_flag(session, session_has_secrets) return session @keyword("Create Ntlm Session") @@ -598,7 +602,7 @@ def create_ntlm_session( retry_status_list=retry_status_list, retry_method_list=retry_method_list, ) - session._has_secrets = session_has_secrets + self._set_session_secrets_flag(session, session_has_secrets) return session @keyword("Session Exists") diff --git a/utests/test_session_secrets.py b/utests/test_session_secrets.py new file mode 100644 index 0000000..f523765 --- /dev/null +++ b/utests/test_session_secrets.py @@ -0,0 +1,289 @@ +import pytest + +from RequestsLibrary import RequestsLibrary +from utests import mock + +try: + from robot.api.types import Secret + secret_type_supported = True +except (ImportError, ModuleNotFoundError): + secret_type_supported = False + + +def test_get_session_secrets_flag_with_none_session(): + """Test _get_session_secrets_flag returns False for None session""" + lib = RequestsLibrary() + assert lib._get_session_secrets_flag(None) is False + + +def test_get_session_secrets_flag_for_session_without_secrets(): + """Test _get_session_secrets_flag returns False for session created without secrets""" + lib = RequestsLibrary() + session = lib.create_session('test', 'http://example.com') + # Session created without secrets should return False + assert lib._get_session_secrets_flag(session) is False + + +def test_get_session_secrets_flag_for_unknown_session(): + """Test _get_session_secrets_flag returns False for untracked session object""" + lib = RequestsLibrary() + # Create a session but don't register it + import requests + untracked_session = requests.Session() + # Unknown session should return False + assert lib._get_session_secrets_flag(untracked_session) is False + + +@pytest.mark.skipif(not secret_type_supported, reason="Requires Robot 7.4+") +def test_create_session_with_secrets_sets_flag(): + """Test that creating a session with secrets properly sets the flag""" + lib = RequestsLibrary() + secret_pwd = Secret('mypassword') + auth = ['user', secret_pwd] + + # Create session with secrets + session = lib.create_session('test', 'http://example.com', auth=auth) + + # Verify secret flag is tracked + assert lib._get_session_secrets_flag(session) is True + + +@pytest.mark.skipif(not secret_type_supported, reason="Requires Robot 7.4+") +def test_create_digest_session_with_secrets_sets_flag(): + """Test that creating a digest session with secrets properly sets the flag""" + lib = RequestsLibrary() + secret_pwd = Secret('mypassword') + auth = ['user', secret_pwd] + + # Create digest session with secrets + session = lib.create_digest_session('test', 'http://example.com', auth=auth) + + # Verify secret flag is tracked + assert lib._get_session_secrets_flag(session) is True + + +@pytest.mark.skipif(not secret_type_supported, reason="Requires Robot 7.4+") +def test_create_client_cert_session_with_secrets_sets_flag(): + """Test that creating a client cert session with secrets properly sets the flag""" + lib = RequestsLibrary() + secret_pwd = Secret('mypassword') + auth = ['user', secret_pwd] + + # Create client cert session with secrets + session = lib.create_client_cert_session( + 'test', 'http://example.com', + auth=auth, + client_certs=('cert.pem', 'key.pem') + ) + + # Verify secret flag is tracked + assert lib._get_session_secrets_flag(session) is True + + +@pytest.mark.skipif(not secret_type_supported, reason="Requires Robot 7.4+") +def test_common_request_with_session_secrets_sets_request_flag(): + """Test _common_request properly checks session secret flag and sets request flag""" + lib = RequestsLibrary() + secret_pwd = Secret('mypassword') + + # Create session with secrets in auth + session = lib.create_session('test', 'http://example.com', auth=['user', secret_pwd]) + + # Mock the session.request to prevent actual HTTP call + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.headers = {} + mock_response.text = '' + mock_response.content = b'' + mock_response.url = 'http://example.com/test' + session.request = mock.MagicMock(return_value=mock_response) + + # Make a request without any auth parameter + lib._common_request('GET', session, '/test') + + # Verify _request_has_secrets is set to True due to session having secrets + assert lib._request_has_secrets is True + + +@pytest.mark.skipif(not secret_type_supported, reason="Requires Robot 7.4+") +def test_common_request_without_session_secrets_does_not_set_request_flag(): + """Test _common_request doesn't set request flag when session has no secrets""" + lib = RequestsLibrary() + + # Create session without secrets + session = lib.create_session('test', 'http://example.com', auth=['user', 'plaintext']) + + # Mock the session.request to prevent actual HTTP call + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.headers = {} + mock_response.text = '' + mock_response.content = b'' + mock_response.url = 'http://example.com/test' + session.request = mock.MagicMock(return_value=mock_response) + + # Make a request without any auth parameter + lib._common_request('GET', session, '/test') + + # Verify _request_has_secrets is False + assert lib._request_has_secrets is False + + +@pytest.mark.skipif(not secret_type_supported, reason="Requires Robot 7.4+") +def test_common_request_with_request_level_secrets(): + """Test _common_request sets flag when secrets are in request auth (not session)""" + lib = RequestsLibrary() + request_secret = Secret('request_password') + + # Create session without secrets + session = lib.create_session('test', 'http://example.com') + + # Mock the session.request to prevent actual HTTP call + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.headers = {} + mock_response.text = '' + mock_response.content = b'' + mock_response.url = 'http://example.com/test' + session.request = mock.MagicMock(return_value=mock_response) + + # Make a request with secrets in auth parameter + lib._common_request('GET', session, '/test', auth=['user', request_secret]) + + # Verify _request_has_secrets is True due to request auth having secrets + assert lib._request_has_secrets is True + + +@pytest.mark.skipif(not secret_type_supported, reason="Requires Robot 7.4+") +def test_common_request_with_both_session_and_request_secrets(): + """Test _common_request with secrets in both session and request auth""" + lib = RequestsLibrary() + session_secret = Secret('session_pwd') + request_secret = Secret('request_pwd') + + # Create session with secrets + session = lib.create_session('test', 'http://example.com', auth=['user1', session_secret]) + + # Mock the session.request to prevent actual HTTP call + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.headers = {} + mock_response.text = '' + mock_response.content = b'' + mock_response.url = 'http://example.com/test' + session.request = mock.MagicMock(return_value=mock_response) + + # Make a request with different secrets in auth + lib._common_request('GET', session, '/test', auth=['user2', request_secret]) + + # Verify _request_has_secrets is True (secrets from both sources) + assert lib._request_has_secrets is True + + +@pytest.mark.skipif(not secret_type_supported, reason="Requires Robot 7.4+") +def test_session_secrets_flag_persists_across_requests(): + """Test that session secret flag persists across multiple requests""" + lib = RequestsLibrary() + secret_pwd = Secret('mypassword') + + # Create session with secrets + session = lib.create_session('test', 'http://example.com', auth=['user', secret_pwd]) + + # Mock the session.request + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.headers = {} + mock_response.text = '' + mock_response.content = b'' + mock_response.url = 'http://example.com/test' + session.request = mock.MagicMock(return_value=mock_response) + + # Make first request + lib._common_request('GET', session, '/test1') + assert lib._request_has_secrets is True + + # Make second request + lib._common_request('GET', session, '/test2') + assert lib._request_has_secrets is True + + # Session should still have the flag set + assert lib._get_session_secrets_flag(session) is True + + +def test_common_request_sessionless_with_no_secrets(): + """Test session-less request without secrets doesn't set flag""" + lib = RequestsLibrary() + + # Mock requests.request for session-less call + with mock.patch('requests.request') as mock_request: + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.headers = {} + mock_response.text = '' + mock_response.content = b'' + mock_response.url = 'http://example.com/test' + mock_request.return_value = mock_response + + # Make session-less request with plain auth + lib._common_request('GET', None, 'http://example.com/test', auth=['user', 'password']) + + # Should not have secrets flag set + assert lib._request_has_secrets is False + + +@pytest.mark.skipif(not secret_type_supported, reason="Requires Robot 7.4+") +def test_common_request_sessionless_with_secrets(): + """Test session-less request with secrets sets flag""" + lib = RequestsLibrary() + secret_pwd = Secret('mypassword') + + # Mock requests.request for session-less call + with mock.patch('requests.request') as mock_request: + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.headers = {} + mock_response.text = '' + mock_response.content = b'' + mock_response.url = 'http://example.com/test' + mock_request.return_value = mock_response + + # Make session-less request with secret auth + lib._common_request('GET', None, 'http://example.com/test', auth=['user', secret_pwd]) + + # Should have secrets flag set + assert lib._request_has_secrets is True + + +@pytest.mark.skipif(not secret_type_supported, reason="Requires Robot 7.4+") +def test_multiple_sessions_with_different_secret_flags(): + """Test that multiple sessions can have different secret flags""" + lib = RequestsLibrary() + secret_pwd = Secret('mypassword') + + # Create session with secrets + session1 = lib.create_session('session1', 'http://example1.com', auth=['user', secret_pwd]) + + # Create session without secrets + session2 = lib.create_session('session2', 'http://example2.com', auth=['user', 'plaintext']) + + # Verify flags are tracked independently + assert lib._get_session_secrets_flag(session1) is True + assert lib._get_session_secrets_flag(session2) is False + + +@pytest.mark.skipif(not secret_type_supported, reason="Requires Robot 7.4+") +def test_set_session_secrets_flag_directly(): + """Test _set_session_secrets_flag method directly""" + lib = RequestsLibrary() + session = lib.create_session('test', 'http://example.com') + + # Initially should be False + assert lib._get_session_secrets_flag(session) is False + + # Set to True + lib._set_session_secrets_flag(session, True) + assert lib._get_session_secrets_flag(session) is True + + # Set back to False + lib._set_session_secrets_flag(session, False) + assert lib._get_session_secrets_flag(session) is False