diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fff4c6..1b7f8e5 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 to [5.6.6](https://github.com/reportportal/client-Python/releases/tag/5.6.6), by @HardNorth +### Fixed +- Some configuration parameter names, which are different in the client, by @HardNorth +### Removed +- `token` param support, as it was deprecated pretty while ago, by @HardNorth + +## [5.0.0] +### Added - Support for `Python 3.13`, by @HardNorth ### Changed - Client version updated to [5.6.5](https://github.com/reportportal/client-Python/releases/tag/5.6.5), by @HardNorth diff --git a/README.md b/README.md index 12d5976..b6b11b1 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,6 @@ Behave extension for reporting test results of Behave to the ReportPortal. -- **Usage** -- **Installation** -- **Configuration** -- **Launching** -- **Test item attributes** -- **Logging** -- **Test case ID** -- **Integration with GA** -- **Copyright Notice** - ## Usage ### Installation @@ -44,7 +34,6 @@ behave -D config_file= The `behave.ini` file should have the following mandatory fields under `[report_portal]` section: -- `api_key` - value can be found in the User Profile section - `project` - name of project in ReportPortal - `endpoint` - address of ReportPortal Server @@ -62,10 +51,21 @@ launch_description = Smoke test The following parameters are optional: -- `client_type = SYNC` - Type of the under-the-hood ReportPortal client implementation. Possible - values: [SYNC, ASYNC_THREAD, ASYNC_BATCHED]. +- `enabled = True` - Enable / disable ReportPortal reporting. +- `api_key` - value can be found in the User Profile section. **Required** if OAuth 2.0 is not configured. +- `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. +- `oauth_username = my-username` - OAuth 2.0 username for password grant authentication. **Required** if OAuth 2.0 is + used. +- `oauth_password = my-password` - OAuth 2.0 password for password grant authentication. **Required** if OAuth 2.0 is + used. +- `oauth_client_id = client-id` - OAuth 2.0 client identifier. **Required** if OAuth 2.0 is used. +- `oauth_client_secret = client-id-secret` - OAuth 2.0 client secret. +- `oauth_scope = offline_access` - OAuth 2.0 access token scope. +- `client_type = SYNC` - Type of the under-the-hood ReportPortal client implementation. Possible values: + \[SYNC, ASYNC_THREAD, ASYNC_BATCHED]. - `launch_name = AnyLaunchName` - launch name (default value is 'Python Behave Launch') -- `launch_id = xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` - id of the existing launch (the session will not handle the +- `launch_uuid = xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` - UUID of the existing launch (the session will not handle the lifecycle of the given launch) - `launch_attributes = Smoke Env:Python3` - list of attributes for launch - `launch_description = Smoke test` - launch description @@ -81,7 +81,7 @@ The following parameters are optional: - `connect_timeout = 15` - Connection timeout to ReportPortal server. Default value is "10.0". - `read_timeout = 15` - Response read timeout for ReportPortal connection. Default value is "10.0". - `log_batch_size = 20` - maximum number of log entries which will be sent by the agent at once -- `log_batch_payload_size = 65000000` - maximum payload size of a log batch which will be sent by the agent at once +- `log_batch_payload_limit = 65000000` - maximum payload size of a log batch which will be sent by the agent at once If you would like to override the above parameters from command line, or from CI environment based on your build, then pass: diff --git a/behave_reportportal/behave_agent.py b/behave_reportportal/behave_agent.py index 02b68fc..34a3257 100644 --- a/behave_reportportal/behave_agent.py +++ b/behave_reportportal/behave_agent.py @@ -25,6 +25,9 @@ from behave.runner import Context from prettytable import MARKDOWN, PrettyTable from reportportal_client import RP, create_client + +# noinspection PyProtectedMember +from reportportal_client._internal.static.defines import NOT_SET from reportportal_client.helpers import ( dict_to_payload, gen_attributes, @@ -61,14 +64,21 @@ def create_rp_service(cfg: Config) -> Optional[RP]: project=cfg.project, api_key=cfg.api_key, is_skipped_an_issue=cfg.is_skipped_an_issue, - launch_id=cfg.launch_id, + launch_uuid=cfg.launch_uuid, retries=cfg.retries, mode="DEBUG" if cfg.debug_mode else "DEFAULT", log_batch_size=cfg.log_batch_size, - log_batch_payload_size=cfg.log_batch_payload_size, + log_batch_payload_limit=cfg.log_batch_payload_limit, launch_uuid_print=cfg.launch_uuid_print, print_output=cfg.launch_uuid_print_output, http_timeout=cfg.http_timeout, + # OAuth 2.0 parameters + oauth_uri=cfg.oauth_uri, + oauth_username=cfg.oauth_username, + oauth_password=cfg.oauth_password, + oauth_client_id=cfg.oauth_client_id, + oauth_client_secret=cfg.oauth_client_secret, + oauth_scope=cfg.oauth_scope, ) return None @@ -88,9 +98,12 @@ class BehaveAgent(metaclass=Singleton): _log_item_id: Optional[str] _ignore_tag_prefixes: List[str] - def __init__(self, cfg: Config, rp_service: Optional[RP] = None) -> None: + def __init__(self, cfg: Config, rp_service: Optional[RP] = NOT_SET) -> None: """Initialize instance attributes.""" - self._rp = rp_service + if rp_service is NOT_SET: + self._rp = create_rp_service(cfg) + else: + self._rp = rp_service self._cfg = cfg self._handle_lifecycle = True self._launch_id = None diff --git a/behave_reportportal/config.py b/behave_reportportal/config.py index 8bc1a7e..6e83502 100644 --- a/behave_reportportal/config.py +++ b/behave_reportportal/config.py @@ -52,7 +52,7 @@ class Config(object): project: Optional[str] api_key: Optional[str] enabled: bool - launch_id: Optional[str] + launch_uuid: Optional[str] launch_name: str launch_description: Optional[str] launch_attributes: Optional[List[str]] @@ -62,19 +62,27 @@ class Config(object): rerun: bool rerun_of: Optional[str] log_batch_size: int - log_batch_payload_size: int + log_batch_payload_limit: int log_layout: LogLayout launch_uuid_print: bool launch_uuid_print_output: Optional[OutputType] client_type: ClientType http_timeout: Optional[Union[Tuple[float, float], float]] + # 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] + def __init__( self, endpoint: Optional[str] = None, project: Optional[str] = None, api_key: Optional[str] = None, - launch_id: Optional[str] = None, + launch_uuid: Optional[str] = None, launch_name: Optional[str] = None, launch_description: Optional[str] = None, launch_attributes: Optional[str] = None, @@ -86,18 +94,38 @@ def __init__( rerun: Optional[Union[str, bool]] = None, rerun_of: Optional[str] = None, log_batch_size: Optional[str] = None, - log_batch_payload_size: Optional[str] = None, + log_batch_payload_limit: Optional[str] = None, launch_uuid_print: Optional[str] = None, launch_uuid_print_output: Optional[str] = None, client_type: Optional[str] = None, connect_timeout: Optional[Union[str, float]] = None, read_timeout: Optional[Union[str, float]] = None, + # OAuth 2.0 parameters + oauth_uri: Optional[str] = None, + oauth_username: Optional[str] = None, + oauth_password: Optional[str] = None, + oauth_client_id: Optional[str] = None, + oauth_client_secret: Optional[str] = None, + oauth_scope: Optional[str] = None, + enabled: bool = True, **kwargs, ): """Initialize instance attributes.""" + self.enabled = enabled self.endpoint = endpoint self.project = project - self.launch_id = launch_id + self.launch_uuid = launch_uuid + if not self.launch_uuid: + if "launch_id" in kwargs: + warn( + message="Argument `launch_id` is deprecated since 5.0.1 and " + "will be subject for removing in the next major " + "version. Use `api_key` argument instead.", + category=DeprecationWarning, + stacklevel=2, + ) + self.launch_uuid = kwargs["launch_id"] + self.launch_name = launch_name or DEFAULT_LAUNCH_NAME self.launch_description = launch_description self.launch_attributes = launch_attributes and launch_attributes.split() @@ -107,8 +135,8 @@ def __init__( self.rerun = to_bool(rerun or "False") self.rerun_of = rerun_of self.log_batch_size = (log_batch_size and int(log_batch_size)) or 20 - self.log_batch_payload_size = ( - log_batch_payload_size and int(log_batch_payload_size) + self.log_batch_payload_limit = ( + log_batch_payload_limit and int(log_batch_payload_limit) ) or MAX_LOG_BATCH_PAYLOAD_SIZE if step_based and not log_layout: @@ -122,26 +150,15 @@ def __init__( self.log_layout = LogLayout(log_layout) self.api_key = api_key - if not self.api_key: - if "token" in kwargs: - warn( - message="Argument `token` is deprecated since 2.0.4 and " - "will be subject for removing in the next major " - "version. Use `api_key` argument instead.", - category=DeprecationWarning, - stacklevel=2, - ) - self.api_key = kwargs["token"] - if not self.api_key: - warn( - message="Argument `api_key` is `None` or empty string, " - "this is unexpected because ReportPortal usually requires an authorization key. " - "Please check your configuration.", - category=RuntimeWarning, - stacklevel=2, - ) - self.enabled = all([self.endpoint, self.project, self.api_key]) + # OAuth 2.0 parameters + self.oauth_uri = oauth_uri + self.oauth_username = oauth_username + self.oauth_password = oauth_password + self.oauth_client_id = oauth_client_id + self.oauth_client_secret = oauth_client_secret + self.oauth_scope = oauth_scope + self.launch_uuid_print = to_bool(launch_uuid_print or "False") launch_uuid_print_output_strip = launch_uuid_print_output.strip() if launch_uuid_print_output else "" self.launch_uuid_print_output = ( diff --git a/requirements.txt b/requirements.txt index 0c54688..642056a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ behave>=1.3.3,<2.0 prettytable -reportportal-client~=5.6.5 +reportportal-client~=5.6.6 diff --git a/setup.py b/setup.py index 9371ab4..29e0648 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ from setuptools import setup -__version__ = "5.0.0" +__version__ = "5.0.1" def read_file(fname): diff --git a/tests/features/environment.py b/tests/features/environment.py index cf6d703..26f1a9a 100644 --- a/tests/features/environment.py +++ b/tests/features/environment.py @@ -11,20 +11,17 @@ # See the License for the specific language governing permissions and # limitations under the License -from behave_reportportal.behave_agent import BehaveAgent, create_rp_service +from behave_reportportal.behave_agent import BehaveAgent from behave_reportportal.config import read_config def before_all(context): - cfg = read_config(context) - context.rp_client = create_rp_service(cfg) - context.rp_agent = BehaveAgent(cfg, context.rp_client) + context.rp_agent = BehaveAgent(read_config(context)) context.rp_agent.start_launch(context) def after_all(context): context.rp_agent.finish_launch(context) - context.rp_client.close() def before_feature(context, feature): diff --git a/tests/units/test_config.py b/tests/units/test_config.py index ce9881b..22a8aae 100644 --- a/tests/units/test_config.py +++ b/tests/units/test_config.py @@ -182,7 +182,7 @@ def test_read_config_default_values(mock_cp): expect(cfg.retries is None) expect(cfg.rerun is False) expect(cfg.rerun_of is None) - expect(cfg.enabled is False) + expect(cfg.enabled is True) expect(cfg.launch_uuid_print is False) expect(cfg.launch_uuid_print_output is None) expect(cfg.client_type is ClientType.SYNC) @@ -216,41 +216,6 @@ def test_deprecated_step_based(mock_cp): assert len(w) == 1 -@mock.patch("behave_reportportal.config.ConfigParser", autospec=True) -def test_deprecated_token_param(mock_cp): - mock_context = mock.Mock() - mock_context._config.userdata = UserData.make({"config_file": "some_path"}) - mock_cp().__getitem__.return_value = { - "token": "api_key", - "endpoint": "endpoint", - "project": "project", - "launch_name": "launch_name", - } - - with warnings.catch_warnings(record=True) as w: - cfg = read_config(mock_context) - assert cfg.api_key == "api_key" - assert len(w) == 1 - - -@mock.patch("behave_reportportal.config.ConfigParser", autospec=True) -def test_api_key_token_param_priority(mock_cp): - mock_context = mock.Mock() - mock_context._config.userdata = UserData.make({"config_file": "some_path"}) - mock_cp().__getitem__.return_value = { - "api_key": "api_key", - "token": "token", - "endpoint": "endpoint", - "project": "project", - "launch_name": "launch_name", - } - - with warnings.catch_warnings(record=True) as w: - cfg = read_config(mock_context) - assert cfg.api_key == "api_key" - assert len(w) == 0 - - @mock.patch("behave_reportportal.config.ConfigParser", autospec=True) def test_empty_api_key(mock_cp): mock_context = mock.Mock() @@ -262,11 +227,9 @@ def test_empty_api_key(mock_cp): "launch_name": "launch_name", } - with warnings.catch_warnings(record=True) as w: - cfg = read_config(mock_context) - assert cfg.api_key == "" - assert cfg.enabled is False - assert len(w) == 1 + cfg = read_config(mock_context) + assert cfg.api_key == "" + assert cfg.enabled is True @mock.patch("behave_reportportal.config.ConfigParser", autospec=True) diff --git a/tests/units/test_rp_agent.py b/tests/units/test_rp_agent.py index 8082b37..a02866e 100644 --- a/tests/units/test_rp_agent.py +++ b/tests/units/test_rp_agent.py @@ -159,7 +159,9 @@ def test_get_parameters(): def test_create_rp_service_disabled_rp(): - assert create_rp_service(Config()) is None, "Service is not None for disabled integration with RP in config" + assert ( + create_rp_service(Config(enabled=False)) is None + ), "Service is not None for disabled integration with RP in config" def test_create_rp_service_enabled_rp(config): @@ -177,14 +179,20 @@ def test_create_rp_service_init(mock_rps): "C", api_key="B", is_skipped_an_issue=False, - launch_id=None, + launch_uuid=None, retries=None, mode="DEFAULT", log_batch_size=20, - log_batch_payload_size=MAX_LOG_BATCH_PAYLOAD_SIZE, + log_batch_payload_limit=MAX_LOG_BATCH_PAYLOAD_SIZE, launch_uuid_print=False, print_output=None, http_timeout=None, + oauth_uri=None, + oauth_username=None, + oauth_password=None, + oauth_client_id=None, + oauth_client_secret=None, + oauth_scope=None, ) ], any_order=True, @@ -211,8 +219,9 @@ def test_create_rp_service_init_type(client_type, client_class): def test_init_invalid_config(): - ba = BehaveAgent(Config()) - assert ba._rp is None, "Incorrect initialization of agent" + with pytest.raises(ValueError) as exc_info: + BehaveAgent(Config()) + assert "Authentication credentials are required" in str(exc_info.value) def test_init_valid_config(config): @@ -338,7 +347,7 @@ def test_finish_launch(mock_timestamp, config): def test_skip_finish_launch(mock_timestamp, config): mock_timestamp.return_value = 123 mock_rps = mock.create_autospec(RPClient) - mock_rps.launch_id = "abc" + mock_rps.launch_uuid = "abc" mock_context = mock.Mock() ba = BehaveAgent(config, mock_rps) ba.start_launch(mock_context, some_key="some_value")