Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/pythonapp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ jobs:
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]
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 ]
steps:
- uses: actions/checkout@v4
- name: Set up Python
Expand All @@ -23,6 +26,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install -e .[test]
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
Expand Down Expand Up @@ -59,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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ env/*

# ignore http server log
atests/http_server/http_server.log
.claude/
7 changes: 7 additions & 0 deletions atests/secretvar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# inject secret into robot suite.. doing this via python
# to ensure this can also run in older robot versions
try:
from robot.api.types import Secret
SECRET_PASSWORD = Secret("passwd")
except (ImportError, ModuleNotFoundError):
SECRET_PASSWORD = "not-supported"
46 changes: 45 additions & 1 deletion atests/test_authentication.robot
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
*** Settings ***
Library RequestsLibrary
Library customAuthenticator.py

Variables secretvar.py

*** Test Cases ***
Get With Auth
Expand Down Expand Up @@ -32,3 +32,47 @@ 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 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

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
12 changes: 9 additions & 3 deletions src/RequestsLibrary/RequestsKeywords.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand All @@ -30,6 +31,11 @@ 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")
if auth is not None and isinstance(auth, (list, tuple)):
kwargs["auth"] = process_secrets(auth)

self._capture_output()

resp = request_function(
Expand Down Expand Up @@ -59,7 +65,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]
Expand All @@ -68,10 +74,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):
"""
Expand Down
9 changes: 5 additions & 4 deletions src/RequestsLibrary/SessionKeywords.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
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

Expand Down Expand Up @@ -172,7 +172,7 @@ def create_session(
Note that max_retries must be greater than 0.

"""
auth = requests.auth.HTTPBasicAuth(*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, \
Expand Down Expand Up @@ -262,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(*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, \
Expand Down Expand Up @@ -452,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(*auth) if auth else None
digest_auth = requests.auth.HTTPDigestAuth(*process_secrets(auth)) if auth else None

return self._create_session(
alias=alias,
Expand Down Expand Up @@ -543,6 +543,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, \
Expand Down
22 changes: 22 additions & 0 deletions src/RequestsLibrary/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -74,9 +78,27 @@ 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))


def process_secrets(auth):
"""
Process robot's Secret types in auth tuples by extracting their values.
"""
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")
Expand Down
34 changes: 33 additions & 1 deletion utests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)