diff --git a/.gitignore b/.gitignore index 7f38cff..eb64fa1 100644 --- a/.gitignore +++ b/.gitignore @@ -105,6 +105,10 @@ celerybeat.pid # Environments .env .venv +.venv38 +.venv39 +.venv310 +.venv311 env/ venv/ ENV/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 111ad56..393cd51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ ## [Unreleased] ### Added +- OAuth 2.0 Password Grant authentication, by @HardNorth +### Changed +- Client version updated on [5.6.7](https://github.com/reportportal/client-Python/releases/tag/5.6.7), by @HardNorth +### Fixed +- Some configuration parameter names, which are different in the client, by @HardNorth +### Removed +- `RP_UUID` param support, as it was deprecated pretty while ago, by @HardNorth + +## [5.6.4] +### Added - `RP_DEBUG_MODE` configuration variable, by @HardNorth ## [5.6.3] diff --git a/README.md b/README.md index 33d9752..efae027 100644 --- a/README.md +++ b/README.md @@ -38,17 +38,40 @@ The latest stable version of library is available on PyPI: For reporting results to ReportPortal you need to pass some variables to `robot` run: -REQUIRED: +**Required**: + +These variable should be specified in either case: ``` --listener robotframework_reportportal.listener ---variable RP_API_KEY:"your_user_api_key" --variable RP_ENDPOINT:"your_reportportal_url" --variable RP_LAUNCH:"launch_name" --variable RP_PROJECT:"reportportal_project_name" ``` -NOT REQUIRED: +And also one type of authorization is required: API Key or OAuth 2.0 Password grant: + +``` +--variable RP_API_KEY:"your_user_api_key" + - You can get it in the User Profile section on the UI. +``` +Or: +``` +--variable RP_OAUTH_URI:"https://reportportal.example.com/uat/sso/oauth/token" + - OAuth 2.0 token endpoint URL for password grant authentication. **Required** if API key is not used. +--variable RP_OAUTH_USERNAME:"my_username" + - OAuth 2.0 username for password grant authentication. **Required** if OAuth 2.0 is used. +--variable RP_OAUTH_PASSWORD:"my_password" + - OAuth 2.0 password for password grant authentication. **Required** if OAuth 2.0 is used. +--variable RP_OAUTH_CLIENT_ID:"client_id" + - OAuth 2.0 client identifier. **Required** if OAuth 2.0 is used. +--variable RP_OAUTH_CLIENT_SECRET:"client_id_secret" + - OAuth 2.0 client secret. **Optional** for OAuth 2.0 authentication. +--variable RP_OAUTH_SCOPE:"offline_access" + - OAuth 2.0 access token scope. **Optional** for OAuth 2.0 authentication. +``` + +**Optional**: ``` --variable RP_CLIENT_TYPE:"SYNC" @@ -71,9 +94,8 @@ NOT REQUIRED: - Default value is "10.0", response read timeout for ReportPortal connection. --variable RP_LOG_BATCH_SIZE:"10" - Default value is "20", affects size of async batch log requests ---variable RP_LOG_BATCH_PAYLOAD_SIZE:"10240000" - - Default value is "65000000", maximum payload size of async batch log - requests +--variable RP_LOG_BATCH_PAYLOAD_LIMIT:"10240000" + - Default value is "65000000", maximum payload size of async batch log requests --variable RP_RERUN:"True" - Default is "False". Enables rerun mode for the last launch. --variable RP_RERUN_OF:"xxxxx-xxxx-xxxx-lauch-uuid" diff --git a/requirements-dev.txt b/requirements-dev.txt index c866a0f..f7e35a5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,3 +2,5 @@ pytest pytest-cov robotframework-datadriver +black +isort diff --git a/requirements.txt b/requirements.txt index 577943a..2799ac5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ # Basic dependencies python-dateutil~=2.9.0.post0 -reportportal-client~=5.6.0 +reportportal-client~=5.6.7 robotframework diff --git a/robotframework_reportportal/listener.py b/robotframework_reportportal/listener.py index 57f1829..b723a53 100644 --- a/robotframework_reportportal/listener.py +++ b/robotframework_reportportal/listener.py @@ -18,6 +18,7 @@ import os import re import uuid +import warnings from functools import wraps from mimetypes import guess_type from typing import Any, Dict, List, Optional, Union @@ -299,7 +300,14 @@ def service(self) -> RobotService: """Initialize instance of the RobotService.""" if self.variables.enabled and not self._service: self._service = RobotService() - self._service.init_service(self.variables) + try: + self._service.init_service(self.variables) + except ValueError as e: + # Log warning instead of raising error, since Robot Framework catches all errors + warnings.warn(e.args[0], UserWarning, stacklevel=2) + self.variables.enabled = False + self._service = None + raise e return self._service @property diff --git a/robotframework_reportportal/service.py b/robotframework_reportportal/service.py index ffb8646..42757c8 100644 --- a/robotframework_reportportal/service.py +++ b/robotframework_reportportal/service.py @@ -77,10 +77,7 @@ def init_service(self, variables: Variables) -> None: """ if self.rp is None: self.debug = variables.debug_mode - logger.debug( - f"ReportPortal - Init service: endpoint={variables.endpoint}, " - f"project={variables.project}, api_key={variables.api_key}" - ) + logger.debug(f"ReportPortal - Init service: endpoint={variables.endpoint}, project={variables.project}") self.rp = create_client( client_type=variables.client_type, @@ -92,11 +89,17 @@ def init_service(self, variables: Variables) -> None: retries=5, verify_ssl=variables.verify_ssl, max_pool_size=variables.pool_size, - log_batch_payload_size=variables.log_batch_payload_size, + log_batch_payload_limit=variables.log_batch_payload_limit, launch_uuid=variables.launch_id, launch_uuid_print=variables.launch_uuid_print, print_output=variables.launch_uuid_print_output, http_timeout=variables.http_timeout, + oauth_uri=variables.oauth_uri, + oauth_username=variables.oauth_username, + oauth_password=variables.oauth_password, + oauth_client_id=variables.oauth_client_id, + oauth_client_secret=variables.oauth_client_secret, + oauth_scope=variables.oauth_scope, ) def terminate_service(self) -> None: diff --git a/robotframework_reportportal/variables.py b/robotframework_reportportal/variables.py index dd3c556..d3905c4 100644 --- a/robotframework_reportportal/variables.py +++ b/robotframework_reportportal/variables.py @@ -48,7 +48,18 @@ class Variables: _pabot_pool_id: Optional[int] _pabot_used: Optional[str] project: Optional[str] + + # API key auth parameter api_key: Optional[str] + + # OAuth 2.0 parameters + oauth_uri: Optional[str] + oauth_username: Optional[str] + oauth_password: Optional[str] + oauth_client_id: Optional[str] + oauth_client_secret: Optional[str] + oauth_scope: Optional[str] + attach_log: bool attach_report: bool attach_xunit: bool @@ -62,7 +73,7 @@ class Variables: rerun_of: Optional[str] test_attributes: List[str] skipped_issue: bool - log_batch_payload_size: int + log_batch_payload_limit: int launch_uuid_print: bool launch_uuid_print_output: Optional[OutputType] client_type: ClientType @@ -92,9 +103,25 @@ def __init__(self) -> None: self.rerun_of = get_variable("RP_RERUN_OF", default=None) self.skipped_issue = to_bool(get_variable("RP_SKIPPED_ISSUE", default="True")) self.test_attributes = get_variable("RP_TEST_ATTRIBUTES", default="").split() - self.log_batch_payload_size = int( - get_variable("RP_LOG_BATCH_PAYLOAD_SIZE", default=str(MAX_LOG_BATCH_PAYLOAD_SIZE)) - ) + + batch_payload_size_limit = get_variable("RP_LOG_BATCH_PAYLOAD_LIMIT", default=None) + batch_payload_size = get_variable("RP_LOG_BATCH_PAYLOAD_SIZE", default=None) + if batch_payload_size: + warn( + "Parameter `RP_LOG_BATCH_PAYLOAD_SIZE` is deprecated since 5.6.5 " + "and will be subject for removing in the next major version. Use `RP_LOG_BATCH_PAYLOAD_LIMIT` argument" + " instead.", + DeprecationWarning, + 2, + ) + if not batch_payload_size_limit: + batch_payload_size_limit = batch_payload_size + + if batch_payload_size_limit: + self.log_batch_payload_limit = int(batch_payload_size_limit) + else: + self.log_batch_payload_limit = MAX_LOG_BATCH_PAYLOAD_SIZE + self.launch_uuid_print = to_bool(get_variable("RP_LAUNCH_UUID_PRINT", default="False")) output_type = get_variable("RP_LAUNCH_UUID_PRINT_OUTPUT") self.launch_uuid_print_output = OutputType[output_type.upper()] if output_type else None @@ -116,29 +143,20 @@ def __init__(self) -> None: self.remove_keywords = to_bool(get_variable("RP_REMOVE_KEYWORDS", default="False")) self.flatten_keywords = to_bool(get_variable("RP_FLATTEN_KEYWORDS", default="False")) + # API key auth parameter self.api_key = get_variable("RP_API_KEY") - if not self.api_key: - token = get_variable("RP_UUID") - if token: - warn( - message="Argument `RP_UUID` is deprecated since version 5.3.3 and will be subject for " - "removing in the next major version. Use `RP_API_KEY` argument instead.", - category=DeprecationWarning, - stacklevel=2, - ) - self.api_key = token - else: - warn( - message="Argument `RP_API_KEY` is `None` or empty string, that's not supposed to happen " - "because ReportPortal is usually requires an authorization key. Please check your" - " configuration.", - category=RuntimeWarning, - stacklevel=2, - ) + + # OAuth 2.0 parameters + self.oauth_uri = get_variable("RP_OAUTH_URI") + self.oauth_username = get_variable("RP_OAUTH_USERNAME") + self.oauth_password = get_variable("RP_OAUTH_PASSWORD") + self.oauth_client_id = get_variable("RP_OAUTH_CLIENT_ID") + self.oauth_client_secret = get_variable("RP_OAUTH_CLIENT_SECRET") + self.oauth_scope = get_variable("RP_OAUTH_SCOPE") self.debug_mode = to_bool(get_variable("RP_DEBUG_MODE", default="False")) - cond = (self.endpoint, self.launch_name, self.project, self.api_key) + cond = (self.endpoint, self.launch_name, self.project) self.enabled = all(cond) if not self.enabled: warn( diff --git a/setup.py b/setup.py index 83ed16c..9c1a42e 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ from setuptools import setup -__version__ = "5.6.4" +__version__ = "5.6.5" def read_file(fname): diff --git a/tests/__init__.py b/tests/__init__.py index ea257eb..f4ef062 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -15,4 +15,3 @@ """ REPORT_PORTAL_SERVICE = "reportportal_client.RPClient" -REQUESTS_SERVICE = "reportportal_client.client.requests.Session" diff --git a/tests/integration/test_variables.py b/tests/integration/test_variables.py index 1bb83e6..51e3c59 100644 --- a/tests/integration/test_variables.py +++ b/tests/integration/test_variables.py @@ -19,41 +19,55 @@ import pytest from reportportal_client import BatchedRPClient, OutputType, RPClient, ThreadedRPClient -from tests import REPORT_PORTAL_SERVICE, REQUESTS_SERVICE +from tests import REPORT_PORTAL_SERVICE from tests.helpers import utils +@mock.patch(REPORT_PORTAL_SERVICE) +def test_agent_pass_batch_payload_limit_variable(mock_client_init): + variables = utils.DEFAULT_VARIABLES.copy() + payload_size = 100 + variables["RP_LOG_BATCH_PAYLOAD_LIMIT"] = payload_size + with warnings.catch_warnings(record=True) as w: + result = utils.run_robot_tests(["examples/simple.robot"], variables=variables) + assert result == 0 # the test successfully passed + assert len(w) == 0 + + payload_variable = "log_batch_payload_limit" + assert payload_variable in mock_client_init.call_args_list[0][1] + assert mock_client_init.call_args_list[0][1][payload_variable] == payload_size + + @mock.patch(REPORT_PORTAL_SERVICE) def test_agent_pass_batch_payload_size_variable(mock_client_init): variables = utils.DEFAULT_VARIABLES.copy() payload_size = 100 variables["RP_LOG_BATCH_PAYLOAD_SIZE"] = payload_size - result = utils.run_robot_tests(["examples/simple.robot"], variables=variables) + + with pytest.warns(DeprecationWarning) as w: + result = utils.run_robot_tests(["examples/simple.robot"], variables=variables) assert result == 0 # the test successfully passed + assert len(w) == 1 + assert "Parameter `RP_LOG_BATCH_PAYLOAD_SIZE` is deprecated" in str(w[0].message) - payload_variable = "log_batch_payload_size" + payload_variable = "log_batch_payload_limit" assert payload_variable in mock_client_init.call_args_list[0][1] assert mock_client_init.call_args_list[0][1][payload_variable] == payload_size -@mock.patch(REQUESTS_SERVICE) -def test_agent_pass_launch_uuid_variable(mock_requests_init): +@mock.patch(REPORT_PORTAL_SERVICE) +def test_agent_pass_launch_uuid_variable(mock_client_init): variables = utils.DEFAULT_VARIABLES.copy() test_launch_id = "my_test_launch" variables["RP_LAUNCH_UUID"] = test_launch_id result = utils.run_robot_tests(["examples/simple.robot"], variables=variables) assert result == 0 # the test successfully passed - mock_requests = mock_requests_init.return_value - assert mock_requests.post.call_count == 3 - suite_start = mock_requests.post.call_args_list[0] - assert suite_start[0][0].endswith("/item") - assert suite_start[1]["json"]["launchUuid"] == test_launch_id + assert "launch_uuid" in mock_client_init.call_args_list[0][1] + assert mock_client_init.call_args_list[0][1]["launch_uuid"] == test_launch_id -@pytest.mark.parametrize( - "variable, warn_num", [("RP_PROJECT", 1), ("RP_API_KEY", 2), ("RP_ENDPOINT", 1), ("RP_LAUNCH", 1)] -) +@pytest.mark.parametrize("variable, warn_num", [("RP_PROJECT", 1), ("RP_ENDPOINT", 1), ("RP_LAUNCH", 1)]) @mock.patch(REPORT_PORTAL_SERVICE) def test_no_required_variable_warning(mock_client_init, variable, warn_num): variables = utils.DEFAULT_VARIABLES.copy() @@ -61,10 +75,10 @@ def test_no_required_variable_warning(mock_client_init, variable, warn_num): with warnings.catch_warnings(record=True) as w: result = utils.run_robot_tests(["examples/simple.robot"], variables=variables) - assert result == 0 # the test successfully passed + assert result == 0 # the test successfully passed - assert len(w) == warn_num - assert w[0].category == RuntimeWarning + assert len(w) == warn_num + assert w[0].category == RuntimeWarning mock_client = mock_client_init.return_value assert mock_client.start_launch.call_count == 0 @@ -92,62 +106,24 @@ def test_rp_api_key(mock_client_init): with warnings.catch_warnings(record=True) as w: result = utils.run_robot_tests(["examples/simple.robot"], variables=variables) - assert int(result) == 0, "Exit code should be 0 (no errors)" - - assert mock_client_init.call_count == 1 - - constructor_args = mock_client_init.call_args_list[0][1] - assert constructor_args["api_key"] == api_key - assert len(filter_agent_calls(w)) == 0 - - -@mock.patch(REPORT_PORTAL_SERVICE) -def test_rp_uuid(mock_client_init): - api_key = "rp_api_key" - variables = dict(utils.DEFAULT_VARIABLES) - del variables["RP_API_KEY"] - variables.update({"RP_UUID": api_key}.items()) - - with warnings.catch_warnings(record=True) as w: - result = utils.run_robot_tests(["examples/simple.robot"], variables=variables) - assert int(result) == 0, "Exit code should be 0 (no errors)" - - assert mock_client_init.call_count == 1 - - constructor_args = mock_client_init.call_args_list[0][1] - assert constructor_args["api_key"] == api_key - assert len(filter_agent_calls(w)) == 1 - - -@mock.patch(REPORT_PORTAL_SERVICE) -def test_rp_api_key_priority(mock_client_init): - api_key = "rp_api_key" - variables = dict(utils.DEFAULT_VARIABLES) - variables.update({"RP_API_KEY": api_key, "RP_UUID": "rp_uuid"}.items()) - - with warnings.catch_warnings(record=True) as w: - result = utils.run_robot_tests(["examples/simple.robot"], variables=variables) - assert int(result) == 0, "Exit code should be 0 (no errors)" + assert int(result) == 0, "Exit code should be 0 (no errors)" - assert mock_client_init.call_count == 1 + assert mock_client_init.call_count == 1 - constructor_args = mock_client_init.call_args_list[0][1] - assert constructor_args["api_key"] == api_key - assert len(filter_agent_calls(w)) == 0 + constructor_args = mock_client_init.call_args_list[0][1] + assert constructor_args["api_key"] == api_key + assert len(w) == 0 -@mock.patch(REPORT_PORTAL_SERVICE) -def test_rp_api_key_empty(mock_client_init): +def test_rp_api_key_empty(): api_key = "" variables = dict(utils.DEFAULT_VARIABLES) variables.update({"RP_API_KEY": api_key}.items()) - with warnings.catch_warnings(record=True) as w: - result = utils.run_robot_tests(["examples/simple.robot"], variables=variables) - assert int(result) == 0, "Exit code should be 0 (no errors)" - - assert mock_client_init.call_count == 0 - assert len(filter_agent_calls(w)) == 2 + with pytest.warns(Warning) as w: + utils.run_robot_tests(["examples/simple.robot"], variables=variables) + assert len(w) == 1 + assert "Authentication credentials are required." in str(w[0].message) @mock.patch(REPORT_PORTAL_SERVICE) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 8b7b852..0999db8 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -15,8 +15,8 @@ """ import os -from unittest import mock +# noinspection PyPackageRequirements import pytest from robotframework_reportportal.listener import listener @@ -29,8 +29,6 @@ def visitor(): return RobotResultsVisitor() -@mock.patch("robotframework_reportportal.variables.strtobool", mock.Mock()) -@mock.patch("robotframework_reportportal.variables.get_variable", mock.Mock()) @pytest.fixture def mock_variables(): mock_variables = Variables() @@ -38,7 +36,7 @@ def mock_variables(): mock_variables.launch_name = "Robot" mock_variables.project = "default_personal" mock_variables.api_key = "test_api_key" - mock_variables.launch_attributes = "" + mock_variables.launch_attributes = [] mock_variables.launch_id = None mock_variables.launch_doc = None mock_variables.log_batch_size = 1 @@ -47,7 +45,6 @@ def mock_variables(): mock_variables.skip_analytics = None mock_variables.test_attributes = [] mock_variables.skip_analytics = True - mock_variables._pabot_used = False mock_variables.skipped_issue = True mock_variables.enabled = True return mock_variables